The Ultimate Guide For Handling Date And Time In Java

Vinod Madubashana
unibench
Published in
25 min readJan 12, 2024

Handling time feels simple in any programming language since standard time handling API is baked into the language. Most developers start to experience the complexity of date and time in a production incident. I try to make you not get into such a case with the help of this blog post.

Table Of Content

· What makes handling date and time complex?
· Java date time API before Java 8
· The design and basic building blocks of the new date-time API
· Design patterns of method names
· Instant
· Clock
· LocalDate, LocalTime and LocalDateTime
· TemporalAdjuster
· Duration and Period
· Date time with timezones
· OffsetDateTime
· ZonedDateTime
· Regional Calendars
· DateTimeFormatter
· Accuracy
· Persistence
· External Libraries
· References

What makes handling date and time complex?

People are used to doing their work in the daytime and sleeping at night (except for the developers😅) and also need to anticipate the seasons to occur at the correct periods of the year. Hence it is necessary to define the hours and days based on natural events like sunrise and seasons. On the other hand, standard units of measuring time need to be fixed units. The problem is natural events are not happening at a fixed rate for example the time taken by Earth to complete a cycle itself and around the sun changes slowly. So without corrections to the standard units, we will observe the drift of natural events from the normal time of the day.

The primary time standard used to govern clocks and time worldwide is Coordinated Universal Time or UTC. It is within the mean solar time of roughly one second. The standard time definitions (which are based on atomic clocks which measure the time by monitoring resonant frequency of atoms) and clock synchronization are a broad topic that you can dig deeper with online resources if you are interested.

As developers, we might not need to worry much about the above complications. However, I would like to highlight three points that every developer should be aware of because they are visible in the timeline.

Leap Year — This correction aligns seasons to stay the same (For example winter should start at the end of the year). In a normal year, there are 365 days. But the time takes Earth to rotate around the sun is a bit less than 365.25 days. If it is not corrected over time seasons will be drifted. Hence after 4 years additional day is added in February(29th). This day also adds an over-correction and hence every 25th leap year this additional day is going to skip (which means in every 100th year). There is a final correction which is again in every 400th year an additional day is added and that is why the year 2000 is a leap year and the year 2100 will not be a leap year. The summary is all years that are exactly divisible by four are leap years, except centurial years, which are leap years if they are exactly divisible by 400.

Time Zone — A time zone is an area where a uniform standard time can be found. Across time zones we find different times. This is because we all like to start our work in the morning. Sunrise is not going to happen for the whole world at the same time (the world is not flat😁). Hence different time zones have different offsets. If we are talking about offsets we need an offset zero time zone that aligns with the UTC which is the standard time. UTC is independent of all time zones, it is a standard. But Java treats this as same as GMT which is the timezone for London and UK. Let’s take an example. Considering that GMT (also aligns with UTC) time is 06 am now, my time zone offset(For Sri Lanka) is +5.30 which means it's 11.30 am in my time. For the same time zone, the offset might change due to daylight savings for different periods of the year.

These areas are not uniformly distributed and are highly affected by political decisions and the ease of communication hence this time might not match the desire for human working hours sometimes. For example, it is common to see a single time zone for a large country which might lead the standard time of some places to deviate from the natural time. But this is not the case always and some countries span across many time zones.

Daylight Saving — To maximize the amount of natural daylight available, Daylight Saving Time (DST) is the practice of moving the clocks forward one hour from standard time in the summer and backward one hour in the autumn. I will explain this in detail with examples later.

Oh!!! I almost forgot, that all the explanations above are based on the Gregorian calendar which is the modern standard calendar. That is not the only calendar available, for example, there is an Islamic calendar called the Hijri calendar and many more.

Lucky for us we don't need to implement all these things from scratch. As mentioned date time API is available in any programming language or there might be many third-party libraries that provide high-level abstract API to tackle the above-mentioned complexities.

Let’s understand the Java solutions for handling date and time. Before Java 8 the date time API provided had many downsides. Let’s understand briefly the old Java date time API before Java 8 to understand why the new date time API was introduced in Java 8.

Java date time API before Java 8

This API was packaged in java.util package. First, it has a Date class. Although its name is date what it represents is a point in time with millisecond precision. But in real-world scenarios, it is required to deal with dates and times separately and also needs to consider the time zones. For all these cases the pre-Java 8 solution was to use the Calendar class. Since it is a very generic class to handle all these scenarios it might easily lead to error-prone code.

Another drawback is these objects are mutable which makes them not thread safe which can lead to very hard-to-find bugs. If you need to do a modification without changing the original one you need to make a clone manually.

The DateTimeForma is the class used for formatting. This is also not thread-safe, which means it can not be reused in multiple threads. Another downside is it only works with the Date class and also parses date and time in a language-independent manner.

Since these two classes need to be used interchangeably it becomes very confusing to use them. Hence lots of developers used third-party libraries like Joda-time. Due to these reasons Java 8 made high-quality Java date time API natively available.

If you are still using these old classes most probably it is something you copied from stack overflow or any AI service like Chat GPT. Don’t use these old APIs anymore. If you have old code below table might help to migrate.

If you are incorporating with a library that you don't have permission to change which returns old Date and Calendar objects convert them before using them in your system to handle them easily. Java has added methods for conversions from and to, new types which makes our lives easier. (For example Date.toInstant() to convert Date to Instant and Date.from(Instant) to convert Instant to Date)

The story is incomplete if I didn't talk about database storage of time objects like java.sql.Date and java.sql.Timestamp. (Yess!! The class name is Date but different package check your imports again if you are still using Date objects 😅😅. It is actually a thin wrapper around java.util.Date class). These types are mostly used with JPA (Java Persistence API) implementations where database rows are mapped into Java objects which is called Entity. (It is very rare to find people using raw JDBC APIs unless there is a real valid reason). The newer JPA implementations support natively for the new date time constructs and hence it is not required to use them anymore. By any chance, if you using an older JPA implementation version you might need to use JPA attribute converters which you don't need to implement by yourself, check this third-party library at https://github.com/marschall/threeten-jpa

By the way, all the above migrations are possible only if you are using a Java version greater than 8. (Just kidding there's no way you are using Java 7 in your production, Java 21 is also out there. Are you????😯)

The design and basic building blocks of the new date-time API

Let’s start with machine time. Machines see time as a long number. So that means we need a starting point. This starting point is chosen as, midnight of 1st January 1970 UTC. So the machine time also called epoch time is the number of seconds elapsed from this starting time. If you want to deal with machine time the type Instant can be used. More details later.

The most important form of date time is the human-readable date time format. ISO 8601 is the standard date time format used across the globe. (Example 2024–01–01T00:00:00Z). The date part can only be modeled using LocalDate, the time part using LocalTime and the combination is LocalDateTime. With these three constructs, you can only represent local time which is specific to the defined system. The offset from UTC is modeled by ZoneOffset. The combination of LocalDateTime plus ZoneOffset is represented by OffsetDateTime. But to represent a time in a timezone zone offset is not enough because of daylight saving. For that Java has provided ZoneId to represent a time zone and with the help of that type, ZonedDateTime can be used to represent time in a specific timezone.

All of the above-mentioned classes inherit from an interface called Temporal. Another two important interfaces are TemporalUnit and TemporalField. TemporalUnit is implemented by enum ChronoUnit which has different time units like seconds, and milliseconds which are used full in date manipulations. TemporoalField is implemented by enum ChronoField which is helpful when extracting part of date time like hour of the day, day of the week, etc. Check these enums to see the full lists available.

Next, the piece is the dealing with time durations. The interface TemporalAmount is a model to define time durations. There are two implementations Duration and Period. Duration is for nano second level durations which are aligned with the real flow of time. The period is used for model differences in months, days, and years.

The interfaces Temporal, TemporalAmount, TemporalField, and TemporalUnit are framework-level components you might not refer to in your code, but understanding their relationship is key to understanding the API well.

The implementation of Temporal represents points in the timeline. These implementations internally manage a set of fields that are represented by the TemporalField interface. The implementation enum of TemporalField, ChonoField has fields like HOUR_OF_DAY, DAY_OF_MONTH, and MONTH_OF_YEAR. So you see those are not quantities like seconds and hours, they are one of several values like MONTH_OF_YEAR has 12 values and they also can be indexed and named(January, February….). So you can think of the Temporal as a mapping of its fields to a numeric value that represents a point in time.

The implementations of TemporalAmount represent the distance between two points in time. These implementations internally manage a set of units that are represented by the TemporalUnit interface. The implementation enum of TemporalUnit and ChronoUnit has fields like SECONDS, MINUTES, and HOURS. So these are quantitative values.

It is very common to reference instances in the form of their superclass in Java, but it is not valid for date-time API. Don't reference date time instances by the above interfaces unless you are not developing a date time framework or dealing with some framework components like temporal adjusters.

The high-level story is still incomplete without TemporalAdjusters which is used to create new temporal objects from existing ones. Also formatting and parsing strings into dates. But I will explain them later in this article.

The new design has addressed issues like the mutability of the old implementation. It also provides fluent methods for clean code and importantly the API is self-explanatory. The API is also calendar-neutral which means you should not use assumptions when you are performing date-time arithmetic. For example, don’t assume the number of days of a month and use that number when manipulating dates. Always use temporal units provided by the API. The methods are provided to the arithmetics in a calendar-neutral way.

I know, I know… This is a very high-level walkthrough. Before digging deeper into these components I would like to talk about method names of these components in java.time package.

Design patterns of method names

This API is well crafted by giving methods consistent namings across the different Temporal implementations. Mainly I found three categories.

Factory methods — in the form of ofXxx(), which can be used to create components

Adjustment methods — in the form of withXxx(), plusXxx(), minusXxx(), which creates a new object from an existing one by changing the value of different fields

Accessors — in the form of getXxx(), which can be used to extract different fields or units. For the implementations of Temporal, you can extract fields and for the implementations of TemporalAmount, you can extract units.

This understanding is very helpful for self-exploring the API and I can go through each component without going through the whole API of each component which I also don't know😅. The point I want to highlight is you also don't need to memorize any API, the understanding of the design principles is the most important. This is true for anything you are learning because API might have breaking changes but underlying design principles hardly changed. you can find details from Java docs or just press “.” in your ide and it will list the available methods the names are very intuitive and now you know how to categorize them too.

So much background, Now let's see these things in action with actual code.

Instant

The java.time.Instant class represents machine time which internally represents epoch time. The constant Instant.EPOCH represents the start of epoch time.

system.out.println(Instant.EPOCH);
// output - 1970-01-01T00:00:00Z

Since it is the fundamental block that models the machine time, the usability of this construct is a bit less. There are a couple of ofXxx() methods available to create an object that has Nano second-level precision. We can extract time parts using the get() method

Instant.ofEpochSecond(3);
Instant.ofEpochSecond(3, 10); // The second parameter for nano seconds
// (3 seconds and 10 nano seconds after epoch start time)

Instant instant = Instant.parse("2024-01-01T10:00:00.001Z");
System.out.println(instant.get(ChronoField.NANO_OF_SECOND));
// Output - 1000000

I will talk about the accuracy of epoch time and standard time later in this post.

Since most applications deal with human data time it might not appropriate the use of Instant in such cases. Double-check your usage of Instant with the other constructs which I will go through later, and change them if applicable which gives you convenient APIs to deal with human time.

Clock

Before starting to see the human date-related constructs let's understand java.time.Clock class.

I'm sure that you have got in a position where your logic depends on the current time. Think you need the local date and time, simple LocalDateTime.now() will give you a LocalDateTime object. But concerning what? The system clock time, right? All good from the development perspective.

Now think about testability. Your application has an implicit dependency on the system that it runs. How do you tackle external dependencies in unit tests? Simple right, mock them. But here what should we mock? LocalDateTime class static factory methods? or any bytecode manipulation tricks. What if that implicit dependency on time can be converted into an explicit dependency? Then you can mock it easily in a single place where you construct the object you are testing. The java.time.Clock makes this possible.

The Clock was introduced in Java 8 and offers access to the best system clock that is currently available. It combines Instant with a timezone. It can also be used as a time provider that can be successfully stubbed for testing needs. All the temporal classes have an overloaded version of now method which accepts a Clock instance.

There are a couple of ways to get a Clock instance.

Clock clock = Clock.systemUTC(); // current Instant in the UTC zone
Clock clock = Clock.system(ZoneId.of("Australia/Sydney")); // with specified zone
Clock clock = Clock.systemDefaultZone(); // system default time

// Create a fixed clock which can be used in testing
Clock fixedClock = Clock.fixed(Instant.parse("2024-01-01T04:00:00.00Z"),
ZoneId.of("Asia/Calcutta"));

Now let’s see the usage

Instant instant = clock.instant();
LocalDate.now(clock);
// and all the now methods in different temporal classes

This is a missing gem that is lacking in most Java developers. It is always a best practice to consider time as an explicit dependency which it is actually.

LocalDate, LocalTime and LocalDateTime

As the name says these classes can create instances that represent system default local times (or the time provided by the clock). The important thing is that when you are dealing with these instances it will not consider anything about time zone information and daylight-saving stuff. It simply represents date time in normal flow which means 24 hours each day. Date manipulation feels natural. Let’s see some scenarios in which these constructs are helpful.

LocalDate — represents only the date. This is good for tracking special days like birthdays or Christmas Day which are celebrated on the same day across the globe.

LocalTime — represents only the time of day. This is helpful in scenarios where you execute some logic for a fixed date.

LocalDateTime — represents date and time.

All of these are useful when we can tally our system time with the customer time which makes our lives so much easier when performing simple date arithmatics. In most cases, these are easy to use.

I will not try to go through the whole API, because it self self-explanatory. It adheres to the naming patterns I described earlier. Let’s consider the below examples with the LocalDate class, it is similar to the other classes too.

LocalDate now = LocalDate.now(clock); // pass clock explictly
LocalDate date1 = LocalDate.of(2023, 01, 11); // creating new instance
// Change the year this will create new instance,
// remember all manipulations create new objects since all temporal objects
// are immutable
LocalDate date2 = date1.withYear(2013);
LocalDate date3 = date2.withDayOfMonth(25);
LocalDate date4 = date3.with(ChronoField.MONTH_OF_YEAR, 2);
LocalDate date5 = date4.plusDays(1);
LocalDate date6 = date4.plus(1, ChronoUnit.DAYS);

Remember you should use the API in a calendar-neutral way. For example, if you want to add one month to the current date don't add the number of days using plusDays method instead use plusMonths method.

Also, we have the capability of combining date with time using atXxx() methods. See the below example

LocalDate date = LocalDate.of(2024, 01, 01);
LocalTime time = LocalTime.of(10, 0, 0);
LocalDateTime localDateTime = date.atTime(time);
LocalDateTime startOfDay = date.atStartOfDay();

The LocalDateTime instance methods toLocalDate and toLocalTime can be used to extract date and time separately.

Another generic pattern available is creating a component using fields of an existing component using the static from() method. See the example below.

LocalDateTime nowTime = LocalDateTime.now();
LocalDate from = LocalDate.from(nowTime);

The above manipulations are straightforward. Even the with method allows to simply change using TemporalField as the first argument and value as the second argument. There is another overloaded method available that takes TemporalAdjuster as its first argument. Now it's time to understand what temporal adjusters are.

TemporalAdjuster

There might be cases where some manipulations are not straightforward. For example, say you have the current date and now you need the date that represents the end of the month. Don’t even think of doing the math and adding days😁. In the Temporal interface, there is another overloaded method that takes TemporalAdjuster as an argument. Let’s see the source code of TemporalAdjuster.

@FunctionalInterface
public interface TemporalAdjuster {
Temporal adjustInto(Temporal temporal);
}

It takes a temporal as an input which will become the Temporal object that calls with method and returns a Temporal which will be the final return Temporal object. Since this is a functional interface it can be implemented as a lambda function too. There is a utility class called TemporalAdjusters which has some static factory method that provides some common TemporalAdjuster implementations. See the below example.

LocalDate now = LocalDate.now(clock);
LocalDate lastDayOfMonth = now.with(TemporalAdjusters.lastDayOfMonth());

Let’s see the source code of this lastDayOfMonth method

public static TemporalAdjuster lastDayOfMonth() {
return (temporal) -> temporal.with(
DAY_OF_MONTH,
temporal.range(DAY_OF_MONTH).getMaximum()
);
}

So you see you can implement your custom adjusters easily. The main reason I’m showing this example I want to emphasize again the fact that your logic should use the provided API rather than making assumptions and writing code based on the calendar.

Duration and Period

Duration represents a fixed distance along the epoch timeline which internally represents a whole number of seconds plus the remaining part in nanoseconds. The creation using factory methods might be when you want to add or subtract durations from any temporal element.

LocalDateTime startOfDay = LocalDateTime.now(clock).atStartOfDay();
Duration duration = Duration.ofHours(2);
LocalDateTime afterTwoHours = startOfDay.plus(duration);

It is useful when we need to calculate the difference between two Temporal elements and extract those differences into seconds and nanoseconds, etc. I think you can understand this API also easily. See some examples

Duration duration = Duration.between(localDateTime, startOfDay);
long seconds = duration.toSeconds();
long nanos = duration.toNanos();
long days = duration.toDaysPart(); // These toUnitPart methods added in Java 9

The Duration, between also not allowed to pass one in human human-based temporal element and the other as an Instant. It’s mainly because they are two different purpose solutions which can bring ambiguity when calculating the duration.

Duration duration = Duration.between(localDateTime, instant);
// Exception in thread "main" java.time.DateTimeException: Unable to obtain LocalDateTime from TemporalAccessor: 2023-12-29T15:45:58.919990500Z of type java.time.Instant

The last point I want to highlight is the fact that Duration is time based not date based. The methods that give date values do the calculation based on the assumption that the date is 86400 seconds (24*60*60). This fact you can see clearly if you use the get method to access date value using date-based CghronoUnit it will throw an exception.

long numDays = duration.get(ChronoUnit.DAYS);
// Exception in thread "main" java.time.temporal.UnsupportedTemporalTypeException: Unsupported unit: Days

With that info, if you guess that Period is the date-based solution, you are right. The Duration instance can’t be used to calculate duration as months because the number of days changes from month to month. The Period has fewer methods than the duration because it only deals with days, months, and years, but it is the one that should be used to represent these intervals. I’m not going to go into more details, since you can compare this with the Duration examples

Period periodOfFiveDays = Period.between(LocalDate.of(2024, 1, 1),
LocalDate.of(2024, 1, 6));

Note that there is a between method also available in the TemporalUnit class, For example, you can use this like ChronoUnit.DAYS.between(temporal 1, temporal2). Temporal also has a method called until which takes another Temporal as the first argument and TemporalUnit as the second argument. These methods also can be used to extract duration in both a date-based manner and a time-based manner.

Date time with timezones

So far we considered constructs that represent relative points in time (except for Instant which represents points in time in GMT/UTC). Of course, people could use the same time across the globe, but people's desire to align them works with the sun's movement creates time zones. Hence the local times of different zones are not the same.

There are two ways we can refer to time zones. One way is using the offset time from GMT. The other way is to use {area}/{city} which represents a region. Java represents these two cases using ZoneOffset and ZoneRegion classes which are subclasses of the abstract class ZoneId.

With ZoneOffset we can only model time with fixed zone offset which is not the case in most cases. Let’s consider OffsetDateTime first

OffsetDateTime

This is a bit easy to use since it has a fixed offset from GMT. The OfffsetDateTime object is constructed from LocalDateTime + ZoneOffset. These instances are capable of pointing to absolute positions in the timeline.

The relationship of this with Instant is an OffsetDateTime instance always points to a single Instant which means one point in timeline. But a single Instance can be pointed by multiple OffsetDateTimes. For example, when GMT is 5 am then the time with offset 1 is 6 am and with offset -1 is 4 am, It's the same Instant but two OffsetDateTimes (Two local date times in different zones). With this idea, I will explain two important methods that are used to convert time between two offsets. We can convert one OffsetDateTIme to another one with a different offset which will point to the same Instant by calling the withOffsetSameInstant(ZoneOffset) method. We can keep one OffsetDateTime instance local date time fixed and change the offset by using withOffsetSameLocal which will point to a different point in the timeline(Instant).

Let’s see an example to clarify this concept more. Let’s assume the zone offset of Sri Lanka is +5 and in Australia +10.30. Now I want to convert 10 am local time in Australia on January 1st, 2024 into local date time in Sri Lanka.

LocalDateTime localDateTime = LocalDateTime.of(LocalDate.of(2024,1,1),
LocalTime.of(10, 0, 0));
OffsetDateTime timeInAustralia = OffsetDateTime
.of(localDateTime, ZoneOffset.of("+10:30"));
OffsetDateTime timeInSriLanka = timeInAustralia
.withOffsetSameInstant(ZoneOffset.of("+05:30"));
LocalDateTime localDateTimeInSriLanka = timeInSriLanka.toLocalDateTime();
System.out.println(localDateTimeInSriLanka); // 2024-01-01T05:00

Let’s see an example of keeping the local date-time same and changing the offset

LocalDateTime localDateTime = LocalDateTime.of(LocalDate.of(2024,1,1),
LocalTime.of(10, 0, 0));
OffsetDateTime timeInAustralia = OffsetDateTime
.of(localDateTime, ZoneOffset.of("+10:30"));
OffsetDateTime timeInSriLanka = timeInAustralia
.withOffsetSameLocal(ZoneOffset.of("+05:30"));
System.out.println(timeInSriLanka); // 2024-01-01T10:00+05:30

ZonedDateTime

This can be seen as a combination of LocatDateTime + ZoneRegion + ZoneOffset. The ZoneOffset is not fixed when daylight saving happens but it is needed to map a local date time into an absolute point in time. ZoneRegion represents a geographical region with the same timezone rules applied. This is more precise than OffsetDateTime because for some zones the offset changes due to the daylight saving effect. The ZoneRegion class is a package private class. Hence to create one we need to use the factory method of ZoneId class. The ZoneId.of method returns ZoneOffset if we place offset like “+10:30” or ZoneRegion if we place region id like “Australia/Sydney”. This region id must match with the ones available in the internet timezone database which shipped with JDK which is normally in the form of {area}/{city}.

These ZoneRegions have associated ZoneRules instance which defines the offset changing rules. The ZoneRules instance has the historical data on a region’s offset changes and it also predicts future daylight transitions. The actual decisions of changing offsets to facilitate depend on the political decisions of that particular region. Now we have a problem what if our application highly depends on these rules which are changed based on political decisions? The answer is JDK has a timezone database which I already mentioned. This database can updated isolately without updating your JDK. You can find documentation on how to update this database online for your JDK distribution easily, for example, azul JDK one is available at https://docs.azul.com/core/timezone-updater.

Now let’s understand the daylight saving effect. What happens here is some countries forward their clock by one hour to maximize the use of daylight. It will be adjusted back again after some period. Let’s see the rules with the help of ZoneRules for two different time zones.

ZoneId zoneId = ZoneId.of("Australia/Sydney");
ZoneRules rules = zoneId.getRules();
rules.getTransitionRules().forEach(System.out::println);

// TransitionRule[Overlap +11:00 to +10:00, SUNDAY on or after APRIL 1 at 02:00 STANDARD, standard offset +10:00]
// TransitionRule[Gap +10:00 to +11:00, SUNDAY on or after OCTOBER 1 at 02:00 STANDARD, standard offset +10:00]

ZoneId zoneId = ZoneId.of("Europe/London");
ZoneRules rules = zoneId.getRules();
rules.getTransitionRules().forEach(System.out::println);

// TransitionRule[Gap Z to +01:00, SUNDAY on or after MARCH 25 at 01:00 UTC, standard offset Z]
// TransitionRule[Overlap +01:00 to Z, SUNDAY on or after OCTOBER 25 at 01:00 UTC, standard offset Z]

Let’s take the Europe/London zone which is aligned with the UTC in normal cases. You can see that the rule says it will change offset to +1 on the Sunday after 25th March at 1 am UTC. It’s also called a Gap because it creates a gap in the local date time. Let’s see this in action the Sunday after March 25 is March 31st.

LocalDateTime secondBeforeOneAM = LocalDateTime.of(LocalDate.of(2024, 3, 31),
LocalTime.of(0, 59, 59));
ZonedDateTime secondBeforeClockForwards = ZonedDateTime
.of(secondBeforeOneAM, ZoneId.of("Europe/London"));
ZonedDateTime theMomentClockForwards = secondBeforeClockForwards.plusSeconds(1);
System.out.println(secondBeforeClockForwards); // 2024-03-31T00:59:59Z[Europe/London]
System.out.println(theMomentClockForwards); // 2024-03-31T02:00+01:00[Europe/London]

LocalDateTime localDateTime = secondBeforeOneAM.plusSeconds(1);
ZonedDateTime zonedDateTime = ZonedDateTime.of(localDateTime, ZoneId.of("Europe/London"));
System.out.println(localDateTime); // 2024-03-31T01:00
System.out.println(zonedDateTime); // 2024-03-31T02:00+01:00[Europe/London]

For example, we can observe that there is a missing hour in the local date time for 31st March 2024. Even if you try to pass a local date-time value that relies on this gap and convert it to this zone it will simply forward that by one hour with a new offset of +1 which is already shown in the above example. But this can lead to some edge cases, See the example below

LocalDateTime oneThirtyAM = LocalDateTime.of(LocalDate.of(2024, 03, 31),
LocalTime.of(1, 30, 0));
LocalDateTime twoAM = oneThirtyAM.plusMinutes(30);


System.out.println(oneThirtyAM.isBefore(twoAM)); // true

ZoneId europeZoneId = ZoneId.of("Europe/London");
ZonedDateTime oneThirtyZoned = ZonedDateTime.of(oneThirtyAM, europeZoneId); // 2024-03-31T02:30+01:00[Europe/London]
ZonedDateTime towZoned = ZonedDateTime.of(twoAM, europeZoneId); // 2024-03-31T02:00+01:00[Europe/London]

System.out.println(oneThirtyZoned.isBefore(towZoned)); // false

Here what happens is when converting 1.30 am local time into ZonedDateTime it converts 2.30 am in +1 offset because there is no local time of 1.30 am in this gap and the two am one converts to the same with +1 offset and it leads the second comparison to become false. This can be a case that causes a big production incident. Most developers understand this kind of edge case only from such an incident. Even if you are not lucky enough to be in such a production incident😋now you know this fact to write defect-free code.

The last thing I want to point out is that the date where a gap occurs is a 23-hour date. Look at the example below

LocalDateTime dateTime = LocalDateTime.of(LocalDate.of(2024, 03, 31),
LocalTime.of(0, 0, 0));
ZonedDateTime zonedDateTime = ZonedDateTime.of(dateTime, ZoneId.of("Europe/London"));
ZonedDateTime plusDays = zonedDateTime.plusDays(1);
ZonedDateTime plusHours = zonedDateTime.plusHours(24);
System.out.println(plusDays); // 2024-04-01T00:00+01:00[Europe/London]
System.out.println(plusHours); // 2024-04-01T01:00+01:00[Europe/London]

Next, consider the overlap. This timeline again aligns with the offset zero on Sunday after 25th October which is on 27th October. Now things are a bit trickier because one hour in local time is two hours (from 1 am to 2 am). These are referred to two time zone offsets. For example, let’s consider the time 1.30 am. If you convert it to zoned date time by default it uses the previous offset before the overlap begins. But now in the local time, there is another 1.30 am with offset zero. We can reference that using ofLocal method. See the below example

LocalDateTime oneThirtyAM = LocalDateTime.of(LocalDate.of(2024, 10, 27),
LocalTime.of(1, 30, 0));
ZonedDateTime oneThirtyAMWIthOffsetOne = ZonedDateTime.of(oneThirtyAM,
ZoneId.of("Europe/London"));
System.out.println(oneThirtyAMWIthOffsetOne); // 2024-10-27T01:30+01:00[Europe/London]
ZonedDateTime oneThirtyInOffsetZero = ZonedDateTime.ofLocal(oneThirtyAM,
ZoneId.of("Europe/London"), ZoneOffset.of("+0"));
System.out.println(oneThirtyInOffsetZero); // 2024-10-27T01:30Z[Europe/London]

The day that overlap happens is 25 hours long.

Regional Calendars

The all examples above were based on the standard calendar. But there might be cases where you want to use other calendars. I will not dig deeper into this subject. I will provide an example where we can use the Islamic calendar to identify the start and end dates of the Ramadan festival.

HijrahDate ramadanStartDate =
HijrahDate.now().with(ChronoField.DAY_OF_MONTH, 1)
.with(ChronoField.MONTH_OF_YEAR, 9);
System.out.println("Ramadan starts on " +
IsoChronology.INSTANCE.date(ramadanStartDate) +
" and ends on " +
IsoChronology.INSTANCE.date(ramadanStartDate.with(TemporalAdjusters.lastDayOfMonth()))
);

// Ramadan starts on 2024-03-11 and ends on 2024-04-09

DateTimeFormatter

Simple conversions from date to string using the toString method and String to date using the parse method are available. But the string representation is not customizable

ZonedDateTime now = ZonedDateTime.now();
System.out.println(now.toString()); // 2023-12-30T15:33:29.075470400+05:30[Asia/Colombo]

String time = "2023-12-30T15:32:12.0+05:30[Asia/Colombo]";
ZonedDateTime parse = ZonedDateTime.parse(time);
System.out.println(parse);

But this is not the case in the real world, we have strings in different formats and we also need to convert them into different string representations. That's where we can use DateTimeFormatter.

Let’s talk about parse. But before that, I will introduce a couple of framework-level concepts which I didn't go through earlier. The Temporal interface extends another interface called TemporalAccessor which contains the mappings from TemporalFileds to numbers. The Temporal interface adds the adjustment methods (for example with methods). now you might puzzled as to why I am talking about these internals. So think what do you want by parsing a string? you temporal field mappings, for example, consider the “2024–01–01” string to parse this to a LoalDate you need to extract temporal field mapping values by splitting year, month, and date which is represented by a TemporalAccessor. That’s what DateTimeFormatter does convert string into a TemporalAccessor(there are internal implementations that are not exposed. check java.time.format.Parsed) and uses a query method that takes an argument of type TemporalQuery to build the Temporal object. TemporalQuery is a functional interface.

@FunctionalInterface
public interface TemporalQuery<R> {
R queryFrom(TemporalAccessor temporal);
}

You already know an example of TemporalQuery Which is LocalDate::from. If you check the argument of this from the method you will notice that it is a TemporalAccesor, what this method does is query the provided accessor to extract the date parts and create a LocalDate object.

See the below example to understand this in the context of parsing

DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
TemporalAccessor accessor = dateTimeFormatter.parse("2024-01-01");
LocalDate localDate = LocalDate.from(accessor);
System.out.println(localDate);

// Another ways is pass the TemporalQueryDirectly to formatter
LocalDate localDate1 = dateTimeFormatter.parse("2024-01-01", LocalDate::from);
System.out.println(localDate1);

I think this understanding is enough for you to self-explore the DateTimeFormater, but I didn’t talk about how to create one. Let’s see the available options to create a DateTimeFormatter.

If the format you use is a standard one then there are pre-define formaters available as static fields in the DateTimeFormatter class and you can directly use them, for example, DateTimeFormatter.ISO_LOCAL_DATE which is for the format “yyyy-MM-dd”.

There are also a couple of predefined date time formats presented using the enum FormatStyle

public enum FormatStyle {
// ordered from large to small

/**
* Full text style, with the most detail.
* For example, the format might be 'Tuesday, April 12, 1952 AD' or '3:30:42pm PST'.
*/
FULL,
/**
* Long text style, with lots of detail.
* For example, the format might be 'January 12, 1952'.
*/
LONG,
/**
* Medium text style, with some detail.
* For example, the format might be 'Jan 12, 1952'.
*/
MEDIUM,
/**
* Short text style, typically numeric.
* For example, the format might be '12.13.52' or '3:30pm'.
*/
SHORT;

}

If the string you are parsing matches these formats or a combination of these formats you can use ofLocalizedXxx() methods, for example

DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM);
DateTimeFormatter.ofLocalizedDateTime(FormatStyle.FULL, FormatStyle.MEDIUM);

The next option is when your format matches a pattern like yyyy-MM-dd. Then you can use of factory method as I showed in the first example. Refer to the Java doc to learn about available symbols.

If none of the above matches your requirement which is very rare we can create a custom DateTimeFormatter with the help of DateTimeFormatterBuilder. It can be configured with some advanced features like setting default values and considering case sensitivity. I will show an example that is used by the DateTimeFormater itself to define the ISO_LOCAL_DATE formatter.

DateTimeFormatter ISO_LOCAL_DATE = new DateTimeFormatterBuilder()
.appendValue(YEAR, 4, 10, SignStyle.EXCEEDS_PAD)
.appendLiteral('-')
.appendValue(MONTH_OF_YEAR, 2)
.appendLiteral('-')
.appendValue(DAY_OF_MONTH, 2)
.toFormatter(ResolverStyle.STRICT, IsoChronology.INSTANCE);

Formatting is the opposite of parsing which converts the temporal object to a string. I think now you can self-explore this format option. The thing I want to highlight is by default it uses the system locale and you can change it, see this example

String format = DateTimeFormatter
.ofLocalizedDateTime(FormatStyle.FULL, FormatStyle.MEDIUM)
.withLocale(Locale.GERMAN).format(LocalDateTime.now());
System.out.println(format); // Samstag, 30. Dezember 2023, 19:59:28

You can’t use DateTimeFormatters to format Duration and Period instances. Use the parse and format instance methods that adhere to the ISO 8601 standards.

Accuracy

Here I want to highlight one factor the standard time units are fixed for example days are considered as 86400 seconds. But, the time taken by Earth to complete the cycle around itself is slightly more than that. UTC does a correction by adding seconds at some points which are known as “Leap Seconds”. But these are not reflected in machine time. So if you are building applications that leap second accuracy is critical you might need to build your solution to tackle that. I’m not aware of such a solution but like to know from you guys if you have implemented such a solution.

Persistence

This is another broad topic that I will not dig deeper into, But I like to point out an article by Vlad Mihalcea that might be helpful to dig into this subject based on Hibernate on relational databases.

External Libraries

I want to highlight https://www.threeten.org/threeten-extra/ which has some helpful temporal adjusters and also some extended types like MutableClock that can be used to change Clock when a single method invocation in testing (especially to simulate long-running tasks). It is better to explore this library if you work with complex time-related tasks and some constructs of this library can make your life easier.

--

--

Vinod Madubashana
unibench

Full-stack Developer, Java, Spring Boot, React, Angular, AWS ...