A Ballerina wrapper library around java.time — providing LocalDate, LocalDateTime, LocalTime, and Period for working with dates and times without time zones.
📚 For a complete overview of all features and the full API reference, please visit:
central.ballerina.io/kruutteri1/java_time_utils/latest — detailed documentation and full functionality can be found there.
Note on scope: This library wraps only the members that are fully implemented. A few Java-side wrapper types (
Month,DayOfWeek,IsoEra,Chronology,IsoChronology,Class) are declared as Java bindings internally but have no public members yet, so any method that would return or accept one of those types (e.g.getMonth(),getDayOfWeek(),getEra(),getChronology(),getClass()) is intentionally left out of this documentation and out of the public API for now.
- Ballerina Swan Lake
2201.13.4or later - Java 21 runtime (bundled with the Ballerina distribution — no separate install needed)
# Ballerina.toml
[[dependency]]
org = "kruutteri1"
name = "java_time_utils"
version = "1.0.5" # check central.ballerina.io for the latest versionimport kruutteri1/java_time_utils as jt;- Quick Start
- LocalDate
- LocalDateTime
- LocalTime
- Period
- A Note on Method Naming
- Error Handling
- Inherited Java Object Methods
import ballerina/io;
import kruutteri1/java_time_utils as jt;
public function main() {
jt:LocalDate date = jt:ofDate(2026, 7, 15);
jt:LocalTime time = jt:ofTimeWithSecond(14, 28, 19);
jt:LocalDateTime dateTime = jt:ofLocalDateWithLocalTime(date, time);
io:println(date.toString());
io:println(time.toString());
io:println(dateTime.toString());
}A date without a time-of-day or time zone component, such as 2026-07-15.
getCurrentDate()returnsLocalDate— current date from the system clockofDate(int year, int month, int day)returnsLocalDate— panics on invalid inputofMonth(int year, Month month, int day)returnsLocalDate— using the Month enum, panics on invalid inputofEpochDay(int epochDay)returnsLocalDateofYearDay(int year, int dayOfYear)returnsLocalDate— panics if dayOfYear exceeds the year lengthgetMINDate()returnsLocalDate— minimum supported dategetMAXDate()returnsLocalDate— maximum supported dategetEPOCHDate()returnsLocalDate— 1970-01-01
jt:LocalDate d = jt:ofDate(2026, 7, 15);Output: toString() returns string
Component getters (no args, return int): getYear(), getMonthValue(), getDayOfMonth(), getDayOfYear()
Comparison: isEquals(Object other) returns boolean — returns false rather than panicking on a type mismatch. hashCode() also available (see Inherited Java Object Methods).
Checks: isLeapYear(), lengthOfMonth(), lengthOfYear(), toEpochDay()
Combining with time (produces LocalDateTime):
atStartOfDay()— 00:00atTime(int hour, int minute)atTimeDetailed(int hour, int minute, int second)atTimeFull(int hour, int minute, int second, int nano)atLocalTime(LocalTime time)— combines with an already-built LocalTime; never panics
Date ranges:
datesUntil(LocalDate endExclusive)returnsStream— every date up to (not including) endExclusive. TheStreamtype is a low-level binding not documented in depth here.
Arithmetic (immutable, returns new LocalDate):
- Add:
plusYears,plusMonths,plusWeeks,plusDays - Subtract:
minusYears,minusMonths,minusWeeks,minusDays plusMonths/plusYears/minusMonths/minusYearsclamp the day-of-month on overflow (e.g. Jan 31 + 1 month → Feb 28/29) — this does not panic.
Altering fields (with*, panics on invalid value instead of clamping):
withYear(int year), withMonth(int month), withDayOfMonth(int dayOfMonth), withDayOfYear(int dayOfYear)
import ballerina/io;
import kruutteri1/java_time_utils as jt;
public function main() {
jt:LocalDate d = jt:ofDate(2026, 7, 15);
jt:LocalDate nextWeek = d.plusWeeks(1);
jt:LocalDate newYearDay = d.withMonth(1).withDayOfMonth(1);
io:println("Year: ", d.getYear());
io:println("Leap year: ", d.isLeapYear());
io:println(d.toString());
io:println(nextWeek.toString());
jt:LocalDateTime dt = d.atTime(9, 0);
io:println(dt.toString());
jt:LocalDate|error invalid = trap jt:ofDate(2026, 2, 30);
if invalid is error {
io:println("Rejected as expected: ", invalid.message());
}
}A date and time without a time zone, such as 2026-07-15T14:28:19.
getCurrentDateTime()returnsLocalDateTimeofDateTime(int year, int month, int dayOfMonth, int hour, int minute)returnsLocalDateTimeofWithSeconds(int year, int month, int dayOfMonth, int hour, int minute, int second)returnsLocalDateTimeofWithNanos(int year, int month, int dayOfMonth, int hour, int minute, int second, int nanoOfSecond)returnsLocalDateTimeofMonthFullWithsecond(int year, Month month, int dayOfMonth, int hour, int minute, int second)returnsLocalDateTime— using the Month enumofMonthFullWithNano(int year, Month month, int dayOfMonth, int hour, int minute, int second, int nanoOfSecond)returnsLocalDateTime— using the Month enum and nanosecondsofLocalDateWithLocalTime(LocalDate date, LocalTime time)returnsLocalDateTime— never panics, both inputs already validatedgetMINDateTime()returnsLocalDateTimegetMAXDateTime()returnsLocalDateTime
jt:LocalDateTime dt = jt:ofDateTime(2026, 7, 15, 14, 28);Output: toString() returns string
Component getters: getYear(), getMonthValue(), getDayOfMonth(), getDayOfYear(), getHour(), getMinute(), getSecond(), getNano()
Comparison: isEquals(Object other) returns boolean. hashCode() also available.
Splitting: toLocalDate() returns LocalDate, toLocalTime() returns LocalTime
Arithmetic (immutable):
- Add:
plusYears,plusMonths,plusWeeks,plusDays,plusHours,plusMinutes,plusSeconds,plusNanos - Subtract:
minusYears,minusMonths,minusWeeks,minusDays,minusHours,minusMinutes,minusSeconds,minusNanos ⚠️ Rollover past midnight inplusHours/plusMinutes/etc. advances the calendar date, unlikeLocalTimewhich just wraps within the same day.
Altering fields (with*, panics on invalid value):
withYear, withMonth, withDayOfMonth, withDayOfYear, withHour, withMinute, withSecond, withNano
import ballerina/io;
import kruutteri1/java_time_utils as jt;
public function main() {
jt:LocalDateTime dt = jt:ofDateTime(2026, 7, 15, 14, 28);
jt:LocalDateTime tomorrow = dt.plusDays(1);
jt:LocalDate d = dt.toLocalDate();
jt:LocalTime t = dt.toLocalTime();
jt:LocalDateTime combined = jt:ofLocalDateWithLocalTime(d, t);
io:println(dt.toString());
io:println(tomorrow.toString());
// Midnight rollover advances the date
jt:LocalDateTime nearMidnight = jt:ofWithSeconds(2026, 7, 15, 23, 30, 0);
jt:LocalDateTime rolledOver = nearMidnight.plusHours(1);
io:println(rolledOver.toString()); // 2026-07-16T00:30
}A time of day without a date or time zone, such as 10:15:30.
LocalTimeis a value-based type — identity operators (==) are unreliable. UseisEquals,compareTo,isAfter, orisBefore.
getCurrentTime()returnsLocalTimeofTime(int hour, int minute)returnsLocalTimeofTimeWithSecond(int hour, int minute, int second)returnsLocalTimeofTimeWithSecondNano(int hour, int minute, int second, int nanoOfSecond)returnsLocalTimeofNanoOfDay(int nanoOfDay)returnsLocalTimeofSecondOfDay(int secondOfDay)returnsLocalTimegetMinTime()returnsLocalTime— minimum (00:00)getMAXTime()returnsLocalTime— maximum (23:59:59.999999999)getMIDNIGHT()returnsLocalTimegetNOON()returnsLocalTime
jt:LocalTime t = jt:ofTimeWithSecond(10, 15, 30);Output: toString() returns string
Component getters: getHour(), getMinute(), getSecond(), getNano()
Comparison — unlike LocalDate/LocalDateTime, these are all public here:
isEquals(Object other)returnsbooleancompareTo(LocalTime other)returnsintisAfter(LocalTime other)returnsbooleanisBefore(LocalTime other)returnsbooleanhashCode()also available
Combining with a date: atDate(LocalDate date) returns LocalDateTime — never panics
As seconds/nanoseconds of day: toSecondOfDay(), toNanoOfDay()
Arithmetic (immutable, wraps cyclically — never panics):
- Add:
plusHours,plusMinutes,plusSeconds,plusNanos - Subtract:
minusHours,minusMinutes,minusSeconds,minusNanos - e.g.
23:00+ 2 hours =01:00— stays within the same day, unlikeLocalDateTime.
Altering fields (with*, panics on invalid value instead of wrapping):
withHour, withMinute, withSecond, withNano
import ballerina/io;
import kruutteri1/java_time_utils as jt;
public function main() {
jt:LocalTime t = jt:ofTimeWithSecond(10, 15, 30);
jt:LocalTime inTwoHours = t.plusHours(2);
boolean isLater = inTwoHours.isAfter(t); // true
io:println(t.toString());
io:println(inTwoHours.toString());
jt:LocalDate d = jt:ofDate(2026, 7, 15);
jt:LocalDateTime dt = t.atDate(d);
io:println(dt.toString());
// Wraparound near midnight (stays within the same day)
jt:LocalTime lateNight = jt:ofTimeWithSecond(23, 0, 0);
jt:LocalTime wrapped = lateNight.plusHours(2);
io:println(wrapped.toString()); // 01:00:00
}A date-based amount of time in years, months, and days, such as "1 year, 2 months, and 3 days". Unlike LocalDate/LocalDateTime/LocalTime, a Period doesn't represent a point in time — it represents a quantity of time, and is most often produced by between or added to/subtracted from a LocalDate.
⚠️ Perioddoes not store weeks as a separate field.ofPeriodWeeks(n)is a convenience that internally storesn * 7as days —getDays()afterward returns the day count, not the week count.
ofPeriod(int years, int months, int days)returnsPeriodofPeriodDays(int days)returnsPeriodofPeriodMonths(int months)returnsPeriodofPeriodYears(int years)returnsPeriodofPeriodWeeks(int weeks)returnsPeriod— stored internally asweeks * 7daysbetween(LocalDate startDateInclusive, LocalDate endDateExclusive)returnsPeriod— the years/months/days between two dates; negative ifendDateExclusiveis beforestartDateInclusivegetZERO()returnsPeriod— a period of zero length
jt:Period p = jt:ofPeriod(1, 2, 3);
jt:Period between = jt:between(jt:ofDate(2026, 1, 1), jt:ofDate(2026, 7, 15));Output: toString() returns string — ISO-8601 period format, e.g. "P1Y2M3D"
Component getters (no args, return int): getYears(), getMonths(), getDays()
Comparison: isEquals(Object other) returns boolean — returns false rather than panicking on a type mismatch. hashCode() also available (see Inherited Java Object Methods).
Checks:
isZero()returnsboolean— true only if years, months, and days are all0isNegative()returnsboolean— true if any of the three fields is negative, even if the others are positive
Arithmetic (immutable, returns new Period) — panics on int32 overflow:
- Add:
plusYears,plusMonths,plusDays - Subtract:
minusYears,minusMonths,minusDays multipliedBy(int scalar)— scales all three fields; panics on overflownegated()— flips the sign of all three fields
Normalization:
normalized()returnsPeriod— carries excess months into years (e.g. 14 months → 1 year, 2 months). Does not touch the days field.
Converting to months:
toTotalMonths()returnsint—years * 12 + months, ignoring days. Useslongarithmetic internally, so unlike the other arithmetic methods this does not overflow atint32boundaries.
Altering fields (with*, replaces one field without touching the others):
withYears(int years), withMonths(int months), withDays(int days)
import ballerina/io;
import kruutteri1/java_time_utils as jt;
public function main() {
jt:LocalDate startDate = jt:ofDate(2026, 1, 1);
jt:LocalDate endDate = jt:ofDate(2026, 7, 15);
jt:Period p = jt:between(startDate, endDate);
io:println(p.toString()); // P0Y6M14D
io:println("Years: ", p.getYears());
io:println("Months: ", p.getMonths());
io:println("Days: ", p.getDays());
jt:Period doubled = p.multipliedBy(2);
io:println(doubled.toString());
jt:Period normalized = jt:ofPeriod(0, 14, 0).normalized();
io:println(normalized.toString()); // P1Y2M
// Safe arithmetic near int32 boundaries
jt:Period nearMax = jt:ofPeriodYears(2147483647);
jt:Period|error overflowed = trap nearMax.plusYears(1);
if overflowed is error {
io:println("Overflow rejected as expected: ", overflowed.message());
}
}Ballerina does not support method/function overloading, so this library's function names don't mirror java.time one-to-one:
- Creation functions carry a suffix indicating arity, since Ballerina can't have multiple functions named
ofin the same module.LocalDateusesofDate(...);LocalDateTimeusesofDateTime(...)/ofWithSeconds(...)/ofWithNanos(...);LocalTimeusesofTime(...)/ofTimeWithSecond(...)/ofTimeWithSecondNano(...);PeriodusesofPeriod(...)/ofPeriodDays(...)/ofPeriodMonths(...)/ofPeriodYears(...)/ofPeriodWeeks(...). equalsis exposed asisEquals. Ballerina reservesequalsas a keyword-adjacent identifier; this library uses the plain, unescaped nameisEquals(Object other)on all four types instead.wait/wait2/wait3are exposed asdoWait/waitWithTimeout/waitWithTimeoutAndNanos— same reasoning, see Inherited Java Object Methods.compareTo,isAfter,isBeforeare only public onLocalTime— onLocalDate,LocalDateTime, andPeriodthese either don't apply or exist internally but aren't part of the current public API.
Functions and methods that validate their input — creation functions like ofDate, and any with* method on LocalDate/LocalDateTime/LocalTime — panic on an out-of-range value (e.g. month 13, or February 30th). They return a plain T, not T|error; invalid input triggers a runtime panic from the underlying Java DateTimeException.
Period's arithmetic methods (plus*, minus*, multipliedBy) panic for a different reason — not invalid calendar values, but int32 overflow in the underlying Java ArithmeticException, since Period has no calendar validation of its own.
Either way, use trap to convert the panic into a catchable error:
import ballerina/io;
import kruutteri1/java_time_utils as jt;
public function main() {
jt:LocalDate|error d = trap jt:ofDate(2026, 2, 30);
if d is error {
io:println("Invalid date: ", d.message());
} else {
io:println("Created: ", d.toString());
}
}If you're confident the input is always valid (e.g. hardcoded constants), you can skip trap — but any dynamic or user-supplied input should go through this pattern to avoid an unhandled panic crashing your program.
This applies uniformly to LocalDate, LocalDateTime, LocalTime, and Period.
All four types (LocalDate, LocalDateTime, LocalTime, Period) inherit these from java.lang.Object, mainly for low-level thread synchronization — you almost certainly don't need them in ordinary business logic.
| Method | Parameters | Description |
|---|---|---|
notify() |
— | Wakes a single thread waiting on the object's monitor |
notifyAll() |
— | Wakes all threads waiting on the object's monitor |
doWait() |
— | Waits for a notification (may return InterruptedException) |
waitWithTimeout(int timeoutMillis) |
timeoutMillis | Waits with a millisecond timeout |
waitWithTimeoutAndNanos(int timeoutMillis, int nanos) |
timeoutMillis, nanos | Waits with a timeout (ms + ns) |
hashCode() |
— | Hash code, consistent with isEquals |
jt:InterruptedException? err = d.doWait();
if err is jt:InterruptedException {
// handle interruption
}Available identically on LocalDate, LocalDateTime, LocalTime, and Period.