Temporal formatting
Parsing a String to get a date/time objects
(Too) Generic way :
DateTimeFormatter df = ...; TemporalAccessor d = df.parse("dd/mmmm/...)"; |
Good for generic processing :
DateTimeFormatter df = ...; LocalDateTime d = df.parse("dd/mmmm/...", LocalDateTime::from); |
Good for one shot processing :
DateTimeFormatter df = ...; LocalDateTime d = LocalDateTime.parse("dd/mmmm/...", df); |
Specify constant/not evaluated characters in the formatter
Any unrecognized letter is an error. It is recommended to use single quotes around all characters that you want to output directly (to ensure that future changes do not break your application).
For example to specify the an ISO date time format (T
char between date and time) :
Example :
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssXXXX"); ZonedDateTime zdt = ZonedDateTime.parse("2021-01-25T00:05:45+0100", dtf); System.out.println(zdt); // 2021-01-25T00:05:45+01:00 |
Specify the offset of the timezone in the formatter
X and x :
We use X(XXX)
to render/use Z
for zulu timezone (UTC+0) while we use
x(xxx)
to render/use explicit offset +00(:)00
for zulu timezone (UTC+0).
From the javadoc :
Offset X and x: This formats the offset based on the number of pattern letters. One letter outputs just the hour, such as ‘+01’, unless the minute is non-zero in which case the minute is also output, such as ‘+0130’. Two letters outputs the hour and minute, without a colon, such as ‘+0130’. Three letters outputs the hour and minute, with a colon, such as ‘+01:30’. Four letters outputs the hour and minute and optional second, without a colon, such as ‘+013015’. Five letters outputs the hour and minute and optional second, with a colon, such as ‘+01:30:15’.
Six or more letters throws IllegalArgumentException.
Pattern letter ‘X’ (upper case) will output ‘Z’ when the offset to be output would be zero, whereas pattern letter ‘x’ (lower case) will output ‘+00’, ‘+0000’, or ‘+00:00’.
Add an offset to a zonedatetime or datetime
Suppose we have a USA/Ny zdt and we want to get the zdt as USA/Ny minus the USA/Ny offset
public static void main(String[] args) { ZoneId usaNyZoneId = ZoneId.of("America/New_York"); LocalDateTime localDateTime = LocalDateTime.of(2021, 6, 20, 10, 30, 20, 100); ZonedDateTime zdtUsaSearch = ZonedDateTime.of(localDateTime, usaNyZoneId); ZoneOffset offsetUsa = usaNyZoneId.getRules() .getOffset(localDateTime); ZonedDateTime zdtUsaMinusUsaOffset = zdtUsaSearch.minusSeconds(offsetUsa.getTotalSeconds()); System.out.println("Usa Ny zone date time " + zdtUsaSearch); System.out.println("offsetUsa " + offsetUsa); System.out.println("Usa Ny zone date time minus Usa Ny Offset " + zdtUsaMinusUsaOffset); } |
Output
Usa Ny zone date time 2021-06-20T10:30:20.000000100-04:00[America/New_York] offsetUsa -04:00 Usa Ny zone date time minus Usa Ny Offset 2021-06-20T14:30:20.000000100-04:00[America/New_York] |
Jackson hints
Specify a datetime formatter for serialization
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ssXXXX") private ZonedDateTime zdt; |
Specify the default timezone formatting
With Jackson, there exists a TimeZone for date formatting. The default value used is UTC (NOT default TimeZone of JVM).
To specify a different default timeZone :
import java.time.ZoneId; import java.util.TimeZone; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; @Configuration public class MyJacksonConfig { @Autowired public void config(ObjectMapper objectMapper) { objectMapper.setTimeZone(TimeZone.getTimeZone(ZoneId.of("Europe/Paris"))); } } |
Self explanatory unit tests
package davidxxx.dateandtime.example; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import javax.sound.midi.Soundbank; import java.time.Instant; import java.time.LocalDateTime; import java.time.ZoneId; import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.time.format.FormatStyle; import java.time.temporal.ChronoField; import java.time.temporal.UnsupportedTemporalTypeException; import java.util.Locale; public class DateAndTimeTest { @Test void localDateTime_has_no_timezone() { LocalDateTime localDateTime = LocalDateTime.of(2018, 11, 30, 15, 10, 50); Assertions.assertThat(localDateTime).isEqualTo("2018-11-30T15:10:50"); Assertions.assertThat(localDateTime.get(ChronoField.DAY_OF_MONTH)).isEqualTo(30); // No timezone assertions Assertions.assertThat(localDateTime.isSupported(ChronoField.OFFSET_SECONDS)).isFalse(); Assertions.assertThatExceptionOfType(UnsupportedTemporalTypeException.class) .isThrownBy(() -> localDateTime.get(ChronoField.OFFSET_SECONDS)); Assertions.assertThat(localDateTime.isSupported(ChronoField.INSTANT_SECONDS)).isFalse(); Assertions.assertThatExceptionOfType(UnsupportedTemporalTypeException.class) .isThrownBy(() -> localDateTime.getLong(ChronoField.INSTANT_SECONDS)); } @Test void localDateTime_needs_an_offset_or_zone_param_to_be_converted_to_zone_aware_temporal() { LocalDateTime localDateTime = LocalDateTime.of(2018, 11, 30, 15, 10, 50); // to Instant with +2h offset Assertions.assertThat(localDateTime.toInstant(ZoneOffset.of("+02:00"))).isEqualTo("2018-11-30T13:10:50Z"); //to epoch sec with +1h offset Assertions.assertThat(localDateTime.toEpochSecond(ZoneOffset.of("+01:00"))).isEqualTo(1543587050L); //to zonedDateTime with Europe/Paris zone (~= +1h offset in winter) Assertions.assertThat(localDateTime.atZone(ZoneId.of("Europe/Paris"))) .isEqualTo("2018-11-30T15:10:50+01:00[Europe/Paris]"); } @Test void zoneDateTime_has_timezone() { ZonedDateTime zonedDateTime = ZonedDateTime.of(2018, 11, 30, 15, 10, 50, 0, ZoneId.of("Europe/Paris")); Assertions.assertThat(zonedDateTime).isEqualTo("2018-11-30T15:10:50+01:00[Europe/Paris]"); // offset of 1h // timezone assertions for (ChronoField f : ChronoField.values()) { Assertions.assertThat(zonedDateTime.isSupported(f)); } Assertions.assertThat(zonedDateTime.get(ChronoField.OFFSET_SECONDS)).isEqualTo(3600); // offset of 1h = 60 * 60 Assertions.assertThat(zonedDateTime.getLong(ChronoField.INSTANT_SECONDS)) .isEqualTo(1543587050L); // epoch time (nb of second since 1970) } @Test void zoneDateTime_can_be_converted_to_instant() { ZonedDateTime zonedDateTime = ZonedDateTime.of(2018, 11, 30, 15, 10, 50, 0, ZoneId.of("Europe/Paris")); Assertions.assertThat(zonedDateTime.toInstant()) .isEqualTo("2018-11-30T14:10:50Z"); } @Test void instant_can_be_formatted_thanks_to_a_timezone_param() { Instant instant = Instant.ofEpochSecond(1543587050L); // eq to 2018-11-30T14:10:50Z // According to a locale formatting (+1h for Paris) Assertions.assertThat( DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM).withLocale(Locale.FRENCH) .withZone(ZoneId.of("Europe/Paris")).format(instant)) .isEqualTo("30 nov. 2018 à 15:10:50"); // According to a custom pattern and a zone (here Paris) Assertions.assertThat( DateTimeFormatter.ofPattern("yyyy-MM-dd'TTTT'HH:mm:ssXXXX") .withZone(ZoneId.of("Europe/Paris")).format(instant)) .isEqualTo("2018-11-30TTTT15:10:50+0100"); } } |