Chapter 8. The java.time Package

Friends don’t let friends use java.util.Date.

Tim Yates

From the beginning of the language, the standard edition library has included two classes for handling dates and times: java.util.Date and java.util.Calendar. The former is a classic example of how not to design a class. If you check the public API, practically all the methods are deprecated, and have been since Java 1.1 (roughly 1997). The deprecations recommend using Calendar instead, which isn’t much fun either.

Both predate1 the addition of enums into the language, so they use integer constants for fields like months. Both are mutable, and therefore not thread safe. To handle some issues, the library later added the java.sql.Date class as a subclass of the version in java.util, but that didn’t really address the fundamental problems.

Finally, in Java SE 8, a completely new package has been added that addressed everything. The java.time package is based on the Joda-Time library, which has been used as a free, open source alternative for years. In fact, the designers of Joda-Time helped design and build the new package, and recommend that future development take advantage of it.

The new package was developed under JSR-310: Date and Time API, and supports the ISO 8601 standard. It correctly adjusts for leap years and daylight savings rules in individual regions.

This chapter contains recipes that illustrate the usefulness of the java.time package. Hopefully they will address basic questions you may have, and point you to further information wherever needed.

As a reference, the Java Tutorial online has an excellent section on the Date-Time library. See https://docs.oracle.com/javase/tutorial/datetime/TOC.html for details.

8.1 Using the Basic Date-Time Classes

Problem

You want to use the new date and time classes in the java.time package.

Solution

Work with the factory methods in classes like Instant, Duration, Period, LocalDate, LocalTime, LocalDateTime, ZonedDateTime, and others.

Discussion

The classes in Date-Time all produce immutable instances, so they are thread safe. They also do not have public constructors, so each is instantiated using factory methods.

Two static factory methods are of particular note: now and of. The now method is used to create an instance based on the current date or time. Example 8-1 shows the sample code.

Example 8-1. The now factory method
System.out.println("Instant.now():       " + Instant.now());
System.out.println("LocalDate.now():     " + LocalDate.now());
System.out.println("LocalTime.now():     " + LocalTime.now());
System.out.println("LocalDateTime.now(): " + LocalDateTime.now());
System.out.println("ZonedDateTime.now(): " + ZonedDateTime.now());

A sample set of results are shown in Example 8-2.

Example 8-2. The results of calling the now method
Instant.now():       2017-06-20T17:27:08.184Z
LocalDate.now():     2017-06-20
LocalTime.now():     13:27:08.318
LocalDateTime.now(): 2017-06-20T13:27:08.319
ZonedDateTime.now(): 2017-06-20T13:27:08.319-04:00[America/New_York]

All output values are using the ISO 8601 standard formatting. For dates, the basic format is yyyy-MM-dd. For times, the format is hh:mm:ss.sss. The format for LocalDateTime combines the two, using a capital T as a separator. Date/times with a time zone append a numerical offset (here, -04:00) using UTC as a base, as well as a so-called region name (here, America/New_York). The output of the toString method in Instant shows the time to nanosecond precision, in Zulu time.

The now method also appears in the classes Year, YearMonth, and ZoneId.

The static of factory method is used to produce new values. For LocalDate, the arguments are the year, month (either the enum or an int), and the day of month.

Warning

The month field in all the of methods is overloaded to accept a Month enum, like Month.JANUARY, or an integer that starts at 1. Since integer constants in Calendar start at 0 (that is, Cale⁠ndar.JANUARY is 0), watch out for off-by-one errors. Use the Month enum wherever possible.

For LocalTime, there are several overloads, depending on how many values of the set of hour, minute, second, and nanosecond are available. The of method on LocalDateTime combines the others. Some examples are shown in Example 8-3.

Example 8-3. The of method for the date/time classes
System.out.println("First landing on the Moon:");
LocalDate moonLandingDate = LocalDate.of(1969, Month.JULY, 20);
LocalTime moonLandingTime = LocalTime.of(20, 18);
System.out.println("Date: " + moonLandingDate);
System.out.println("Time: " + moonLandingTime);

System.out.println("Neil Armstrong steps onto the surface: ");
LocalTime walkTime = LocalTime.of(20, 2, 56, 150_000_000);
LocalDateTime walk = LocalDateTime.of(moonLandingDate, walkTime);
System.out.println(walk);

The output of the demo in Example 8-3 is:

First landing on the Moon:
Date: 1969-07-20
Time: 20:18
Neil Armstrong steps onto the surface:
1969-07-20T20:02:56.150

The last argument to the LocalTime.of method is nanoseconds, so this example used a feature from Java 7 where you can insert an underscore inside a numerical value for readability.

The Instant class models a single, instantaneous point along the time line.

The ZonedDateTime class combines dates and times with time zone information from the ZoneId class. Time zones are expressed relative to UTC.

There are two types of zone IDs:

  • Fixed offsets, relative to UTC/Greenwich, like -05:00

  • Geographical regions, like America/Chicago

Technically there’s a third type of ID, which is an offset that is assumed to be from Zulu time. It includes a Z along with the numerical value.

The rules for offset changes come from the ZoneRules class, where the rules are loaded from a ZoneRulesProvider. The ZoneRules class has methods such as isD⁠aylightSavings(Instant).

You can get the current value of the ZoneId from the static systemDefault method. The complete list of available region IDs comes from the static getAvailableZoneIds method:

Set<String> regionNames = ZoneId.getAvailableZoneIds();
System.out.println("There are " + regionNames.size() + " region names");

For jdk1.8.0_131, there are 600 region names.2

The Date-Time API uses standard prefixes for method names. If you are familiar with the prefixes in Table 8-1, you can usually guess what a method does.3

Table 8-1. Prefixes used on Date-Time methods
Method Type Use

of

Static factory

Creates an instance

from

Static factory

Converts input parameters to target class

parse

Static factory

Parses an input string

format

Instance

Produces formatted output

get

Instance

Returns part of an object

is

Instance

Queries the state of the object

with

Instance

Creates a new object by changing one element of an existing one

plus, minus

Instance

Creates a new object by adding or subtracting from an existing one

to

Instance

Converts an object to another type

at

Instance

Combines this object with another

The of method was shown earlier. The parse and format methods are discussed in Recipe 8.5. The with method is covered in Recipe 8.2, and is the immutable equivalent of a set method. Using plus and minus and their variations are part of Recipe 8.2 as well.

An example of using the at method is to add a time zone to a local date and time, as in Example 8-4.

Example 8-4. Applying a time zone to a LocalDateTime
LocalDateTime dateTime = LocalDateTime.of(2017, Month.JULY, 4, 13, 20, 10);
ZonedDateTime nyc = dateTime.atZone(ZoneId.of("America/New_York"));
System.out.println(nyc);

ZonedDateTime london = nyc.withZoneSameInstant(ZoneId.of("Europe/London"));
System.out.println(london);

This prints:

2017-07-04T13:20:10-04:00[America/New_York]
2017-07-04T18:20:10+01:00[Europe/London]

As the result shows, the withZoneSameInstant method allows you to take one ZonedDateTime and find out what it would be in another time zone.

There are two enums in the package: Month and DayOfWeek. Month has constants for each month in the standard calendar (JANUARY through DECEMBER). Month also has many convenient methods, as shown in Example 8-5.

Example 8-5. Some methods in the Month enum
System.out.println("Days in Feb in a leap year: " +
    Month.FEBRUARY.length(true));        1
System.out.println("Day of year for first day of Aug (leap year): " +
    Month.AUGUST.firstDayOfYear(true));  1
System.out.println("Month.of(1): " + Month.of(1));
System.out.println("Adding two months: " + Month.JANUARY.plus(2));
System.out.println("Subtracting a month: " + Month.MARCH.minus(1));
1

Argument is boolean leapYear

The output of Example 8-5 is:

Days in Feb in a leap year: 29
Day of year for first day of Aug (leap year): 214
Month.of(1): JANUARY
Adding two months: MARCH
Subtracting a month: FEBRUARY

The last two examples, which use the plus and minus methods, create new instances.

Note

Because the java.time classes are immutable, any instance method that seems to modify one, like plus, minus, or with, produces a new instance.

The DayOfWeek enum has constants representing the seven weekdays, from MONDAY through SUNDAY. Again the int value for each follows the ISO standard, so that MONDAY is 1 and SUNDAY is 7.

See Also

Parsing and formatting methods are discussed in Recipe 8.5. Converting existing dates and times to new ones is covered in Recipe 8.2. The Duration and Period classes are discussed in Recipe 8.8.

8.2 Creating Dates and Times from Existing Instances

Problem

You want to modify an existing instance of one of the Date-Time classes.

Solution

If you need a simple addition or subtraction, use one of the plus or minus methods. Otherwise use the with method.

Discussion

One of the features of the new Date-Time API is that all of the instances are immutable. Once you’ve created a LocalDate, LocalTime, LocalDateTime, or ZonedDateTime, it can no longer be changed. This has the great advantage of making them thread safe, but what if you want to make a new instance based on the existing one?

The LocalDate class has several methods for adding and subtracting values from dates. Specifically, there are:

  • LocalDate plusDays(long daysToAdd)

  • LocalDate plusWeeks(long weeksToAdd)

  • LocalDate plusMonths(long monthsToAdd)

  • LocalDate plusYears(long yearsToAdd)

Each method returns a new LocalDate, which is a copy of the current date with the specified value added to it.

The LocalTime class has similar methods:

  • LocalTime plusNanos(long nanosToAdd)

  • LocalTime plusSeconds(long secondsToAdd)

  • LocalTime plusMinutes(long minutesToAdd)

  • LocalTime plusHours(long hoursToAdd)

Again, each returns a new instance, which is a copy of the original with the added amount. LocalDateTime has all the methods for both LocalDate and LocalTime. For instance, the various plus methods for LocalDate and LocalTime are shown in Example 8-6.

Example 8-6. Using plus methods on LocalDate and LocalTime
@Test
public void localDatePlus() throws Exception {
    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
    LocalDate start = LocalDate.of(2017, Month.FEBRUARY, 2);

    LocalDate end = start.plusDays(3);
    assertEquals("2017-02-05", end.format(formatter));

    end = start.plusWeeks(5);
    assertEquals("2017-03-09", end.format(formatter));

    end = start.plusMonths(7);
    assertEquals("2017-09-02", end.format(formatter));

    end = start.plusYears(2);
    assertEquals("2019-02-02", end.format(formatter));
}

@Test
public void localTimePlus() throws Exception {
    DateTimeFormatter formatter = DateTimeFormatter.ISO_LOCAL_TIME;

    LocalTime start = LocalTime.of(11, 30, 0, 0);

    LocalTime end = start.plusNanos(1_000_000);
    assertEquals("11:30:00.001", end.format(formatter));

    end = start.plusSeconds(20);
    assertEquals("11:30:20", end.format(formatter));

    end = start.plusMinutes(45);
    assertEquals("12:15:00", end.format(formatter));

    end = start.plusHours(5);
    assertEquals("16:30:00", end.format(formatter));
}

The classes also have two additional plus and minus methods. Here are the signatures for those methods in LocalDateTime:

LocalDateTime plus(long amountToAdd, TemporalUnit unit)
LocalDateTime plus(TemporalAmount amountToAdd)

LocalDateTime minus(long amountToSubtract, TemporalUnit unit)
LocalDateTime minus(TemporalAmount amountToSubtract)

The corresponding methods in LocalDate and LocalTime are the same, with the corresponding return types. Interestingly enough, the minus versions just call the plus versions with the amounts negated.

For the methods that take a TemporalAmount, the argument is usually a Period or a Duration, but may be any type implementing the TemporalAmount interface. That interface has methods called addTo and subtractFrom:

Temporal addTo(Temporal temporal)
Temporal subtractFrom(Temporal temporal)

If you follow the call stack, invoking minus delegates to plus with a negated argument, which delegates to TemporalAmount.addTo(Temporal), which calls back to plus(long, TemporalUnit), which actually does the work.4

Some examples with the plus and minus methods are shown in Example 8-7.

Example 8-7. The plus and minus methods
@Test
public void plus_minus() throws Exception {
    Period period = Period.of(2, 3, 4); // 2 years, 3 months, 4 days
    LocalDateTime start = LocalDateTime.of(2017, Month.FEBRUARY, 2, 11, 30);
    LocalDateTime end = start.plus(period);
    assertEquals("2019-05-06T11:30:00",
        end.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));

    end = start.plus(3, ChronoUnit.HALF_DAYS);
    assertEquals("2017-02-03T23:30:00",
        end.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));

    end = start.minus(period);
    assertEquals("2014-10-29T11:30:00",
        end.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));

    end = start.minus(2, ChronoUnit.CENTURIES);
    assertEquals("1817-02-02T11:30:00",
        end.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));

    end = start.plus(3, ChronoUnit.MILLENNIA);
    assertEquals("5017-02-02T11:30:00",
        end.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
}
Tip

When the API calls for TemporalUnit, remember that the provided implementation class is ChronoUnit, which has many convenient constants.

Finally, there are a series of with methods on each class that can be used to change one field at a time.

The signatures range from withNano to withYear, with a few interesting ones thrown in. Here is the set from LocalDateTime:

LocalDateTime withNano(int nanoOfSecond)
LocalDateTime withSecond(int second)
LocalDateTime withMinute(int minute)
LocalDateTime withHour(int hour)
LocalDateTime withDayOfMonth(int dayOfMonth)
LocalDateTime withDayOfYear(int dayOfYear)
LocalDateTime withMonth(int month)
LocalDateTime withYear(int year)

The code in Example 8-8 puts these methods through their paces.

Example 8-8. Using with methods on LocalDateTime
@Test
public void with() throws Exception {
    LocalDateTime start = LocalDateTime.of(2017, Month.FEBRUARY, 2, 11, 30);
    LocalDateTime end = start.withMinute(45);
    assertEquals("2017-02-02T11:45:00",
        end.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));

    end = start.withHour(16);
    assertEquals("2017-02-02T16:30:00",
        end.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));

    end = start.withDayOfMonth(28);
    assertEquals("2017-02-28T11:30:00",
        end.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));

    end = start.withDayOfYear(300);
    assertEquals("2017-10-27T11:30:00",
        end.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));

    end = start.withYear(2020);
    assertEquals("2020-02-02T11:30:00",
        end.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
}

@Test(expected = DateTimeException.class)
public void withInvalidDate() throws Exception {
    LocalDateTime start = LocalDateTime.of(2017, Month.FEBRUARY, 2, 11, 30);
    start.withDayOfMonth(29);
}

Since 2017 is not a leap year, you can’t set the date to February 29. The result is a DateTimeException, as the last test shows.

There are also with methods that take a TemporalAdjuster or a TemporalField:

LocalDateTime with(TemporalAdjuster adjuster)
LocalDateTime with(TemporalField field, long newValue)

The version with TemporalField lets the field resolve the date to make it valid. For instance, Example 8-9 takes the last day of January and tries to change the month to February. According to the Javadocs, the system chooses the previous valid date, which in this case is the last day of February.

Example 8-9. Adjusting the month to an invalid value
@Test
public void temporalField() throws Exception {
    LocalDateTime start = LocalDateTime.of(2017, Month.JANUARY, 31, 11, 30);
    LocalDateTime end = start.with(ChronoField.MONTH_OF_YEAR, 2);
    assertEquals("2017-02-28T11:30:00",
        end.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
}

As you might imagine, there are some fairly complicated rules involved, but they’re well documented in the Javadocs.

The with method taking a TemporalAdjuster is discussed in Recipe 8.3.

See Also

See Recipe 8.3 for information about TemporalAdjuster and TemporalQuery.

8.3 Adjusters and Queries

Problem

Given a temporal value, you want to adjust it to a new one based on your own logic, or you want to retrieve information about it.

Solution

Create a TemporalAdjuster or formulate a TemporalQuery.

Discussion

The TemporalAdjuster and TemporalQuery classes provide interesting ways to work with the Date-Time classes. They provide useful built-in methods and ways to implement your own. This recipe will illustrate both possibilities.

Using TemporalAdjuster

The TemporalAdjuster interface provides methods that take a Temporal value and return an adjusted one. The TemporalAdjusters class contains a set of adjusters as static methods you might find convenient.

You use a TemporalAdjuster via the with method on a temporal object, as in this version from LocalDateTime:

LocalDateTime with(TemporalAdjuster adjuster)

The TemporalAdjuster class has an adjustInto method that also works, but the one listed here is preferred.

Looking first at the TemporalAdjusters class methods, there are many convenience methods:

static TemporalAdjuster firstDayOfNextMonth()
static TemporalAdjuster firstDayOfNextYear()
static TemporalAdjuster firstDayOfYear()

static TemporalAdjuster firstInMonth(DayOfWeek dayOfWeek)
static TemporalAdjuster lastDayOfMonth()
static TemporalAdjuster lastDayOfYear()
static TemporalAdjuster lastInMonth(DayOfWeek dayOfWeek)

static TemporalAdjuster next(DayOfWeek dayOfWeek)
static TemporalAdjuster nextOrSame(DayOfWeek dayOfWeek)
static TemporalAdjuster previous(DayOfWeek dayOfWeek)
static TemporalAdjuster previousOrSame(DayOfWeek dayOfWeek)

The test case in Example 8-10 shows a couple of those methods in action.

Example 8-10. Using static methods in TemporalAdjusters
@Test
public void adjusters() throws Exception {
    LocalDateTime start = LocalDateTime.of(2017, Month.FEBRUARY, 2, 11, 30);
    LocalDateTime end = start.with(TemporalAdjusters.firstDayOfNextMonth());
    assertEquals("2017-03-01T11:30", end.toString());

    end = start.with(TemporalAdjusters.next(DayOfWeek.THURSDAY));
    assertEquals("2017-02-09T11:30", end.toString());

    end = start.with(TemporalAdjusters.previousOrSame(DayOfWeek.THURSDAY));
    assertEquals("2017-02-02T11:30", end.toString());
}

The fun comes when you write your own adjuster. TemporalAdjuster is a functional interface, whose single abstract method is:

Temporal adjustInto(Temporal temporal)

For example, the Java Tutorial for the Date-Time package has an example of a PaydayAdjuster, which assumes that an employee is being paid twice a month. The rules are that payment occurs on the 15th of the month and again on the last day of the month, but if either occurs on a weekend, the previous Friday is used.

The code from the online example is reproduced in Example 8-11 for reference. Note that in this case, the method has been added to a class that implements Temporal​Adjuster.

Example 8-11. PaydayAdjuster (from the Java Tutorial)
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.temporal.Temporal;
import java.time.temporal.TemporalAdjuster;
import java.time.temporal.TemporalAdjusters;

public class PaydayAdjuster implements TemporalAdjuster {
    public Temporal adjustInto(Temporal input) {
        LocalDate date = LocalDate.from(input);  1
        int day;
        if (date.getDayOfMonth() < 15) {
            day = 15;
        } else {
            day = date.with(TemporalAdjusters.lastDayOfMonth())
                      .getDayOfMonth();
        }
        date = date.withDayOfMonth(day);
        if (date.getDayOfWeek() == DayOfWeek.SATURDAY ||
                date.getDayOfWeek() == DayOfWeek.SUNDAY) {
            date = date.with(TemporalAdjusters.previous(DayOfWeek.FRIDAY));
        }

        return input.with(date);
    }
}
1

Useful way to convert any Temporal to a LocalDate

In July 2017, the 15th occured on a Saturday and the 31st was on a Monday. The test in Example 8-12 shows that it works correctly for July 2017.

Example 8-12. Testing the adjuster for July 2017
@Test
public void payDay() throws Exception {
  TemporalAdjuster adjuster = new PaydayAdjuster();
  IntStream.rangeClosed(1, 14)
           .mapToObj(day -> LocalDate.of(2017, Month.JULY, day))
           .forEach(date ->
               assertEquals(14, date.with(adjuster).getDayOfMonth()));

  IntStream.rangeClosed(15, 31)
           .mapToObj(day -> LocalDate.of(2017, Month.JULY, day))
           .forEach(date ->
               assertEquals(31, date.with(adjuster).getDayOfMonth()));
}

This works, but there are a couple of minor irritations. First of all, as of Java 8, you can’t create a stream of dates without going through another mechanism, like counting days as shown. That changes in Java 9, which includes a method that return a stream of dates. See Recipe 10.7 for details.

The other issue with the preceding code is that a class was created to implement the interface. Because TemporalAdjuster is a functional interface, you can provide a lambda expression or a method reference as an implementation instead.

You can now make a utility class called Adjusters that has static methods for whatever you want to do, as in Example 8-13.

Example 8-13. Utility class with adjusters
public class Adjusters {                                 1
    public static Temporal adjustInto(Temporal input) {  2
        LocalDate date = LocalDate.from(input);
        // ... implementation as before ...
        return input.with(date);
    }
}
1

Does not implement TemporalAdjuster

2

Static method, so no instantiation required

Now the comparable test is shown in Example 8-14.

Example 8-14. Using a method reference for the temporal adjuster
@Test
public void payDayWithMethodRef() throws Exception {
    IntStream.rangeClosed(1, 14)
        .mapToObj(day -> LocalDate.of(2017, Month.JULY, day))
        .forEach(date ->
              assertEquals(14,
                  date.with(Adjusters::adjustInto).getDayOfMonth())); 1

    IntStream.rangeClosed(15, 31)
        .mapToObj(day -> LocalDate.of(2017, Month.JULY, day))
        .forEach(date ->
              assertEquals(31,
                  date.with(Adjusters::adjustInto).getDayOfMonth()));
}
1

Method reference to adjustInto

You may find this approach more versatile if you have multiple temporal adjusters in mind.

Using TemporalQuery

The TemporalQuery interface is used as the argument to the query method on temporal objects. For example, on LocalDate, the signature of the query method is:

<R> R query(TemporalQuery<R> query)

This method invokes TemporalQuery.queryFrom(TemporalAccessor) with this as an argument and returns whatever the query is designed to do. All the methods on TemporalAccessor are available for performing the calculation.

The API includes a class called TemporalQueries, which includes constants defining many common queries:

static TemporalQuery<Chronology>   chronology()
static TemporalQuery<LocalDate>    localDate()
static TemporalQuery<LocalTime>    localTime()
static TemporalQuery<ZoneOffset>   offset()
static TemporalQuery<TemporalUnit> precision()
static TemporalQuery<ZoneId>       zone()
static TemporalQuery<ZoneId>       zoneId()

A simple test to show how some work is given in Example 8-15.

Example 8-15. Using the methods from TemporalQueries
@Test
public void queries() throws Exception {
    assertEquals(ChronoUnit.DAYS,
        LocalDate.now().query(TemporalQueries.precision()));
    assertEquals(ChronoUnit.NANOS,
        LocalTime.now().query(TemporalQueries.precision()));
    assertEquals(ZoneId.systemDefault(),
        ZonedDateTime.now().query(TemporalQueries.zone()));
    assertEquals(ZoneId.systemDefault(),
        ZonedDateTime.now().query(TemporalQueries.zoneId()));
}

Like with TemporalAdjuster, however, the interesting part comes when you write your own. The TemporalQuery interface has only a single abstract method:

R queryFrom(TemporalAccessor temporal)

Say we have a method that, given a TemporalAccessor, computes the number of days between the argument and International Talk Like A Pirate Day, September 19.5 Such a method is shown in Example 8-16.

Example 8-16. Method to calculate days until Talk Like A Pirate Day
private long daysUntilPirateDay(TemporalAccessor temporal) {
    int day = temporal.get(ChronoField.DAY_OF_MONTH);
    int month = temporal.get(ChronoField.MONTH_OF_YEAR);
    int year = temporal.get(ChronoField.YEAR);
    LocalDate date = LocalDate.of(year, month, day);
    LocalDate tlapd = LocalDate.of(year, Month.SEPTEMBER, 19);
    if (date.isAfter(tlapd)) {
        tlapd = tlapd.plusYears(1);
    }
    return ChronoUnit.DAYS.between(date, tlapd);
}

Since that method has a signature that is compatible with the single abstract method in the TemporalQuery interface, you can use a method reference to invoke it, as in Example 8-17.

Example 8-17. Using a TemporalQuery via a method reference
@Test
public void pirateDay() throws Exception {
    IntStream.range(10, 19)
             .mapToObj(n -> LocalDate.of(2017, Month.SEPTEMBER, n))
             .forEach(date ->
                assertTrue(date.query(this::daysUntilPirateDay) <= 9));
    IntStream.rangeClosed(20, 30)
             .mapToObj(n -> LocalDate.of(2017, Month.SEPTEMBER, n))
             .forEach(date -> {
                Long days = date.query(this::daysUntilPirateDay);
                assertTrue(days >= 354 && days < 365);
            });
}

You can use this approach to define your own custom queries.

8.4 Convert from java.util.Date to java.time.LocalDate

Problem

You want to convert from java.util.Date or java.util.Calendar to the new classes in the java.time package.

Solution

Use the Instant class as a bridge, or use java.sql.Date and java.sql.Timestamp methods, or even strings or integers for the conversion.

Discussion

When looking at the new classes in java.time, you may be surprised to find that there aren’t a lot of built-in mechanisms for converting from the standard date and time classes in java.util to the new preferred classes.

One approach to convert a java.util.Date to a java.time.LocalDate is to invoke the toInstant method to create an Instant. Then you can apply the default ZoneId and extract a LocalDate from the resulting ZonedDateTime, as in Example 8-18.

Example 8-18. Converting java.util.Date to java.time.LocalDate via Instant
public LocalDate convertFromUtilDateUsingInstant(Date date) {
    return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
}

Since java.util.Date includes date and time information but no time zone,6 it represents an Instant in the new API. Applying the atZone method on the system default time zone reapplies the time zone. Then you can extract the LocalDate from the resulting ZonedDateTime.

Another approach to changing from util dates to Date-Time dates is to notice that there are convenient conversion methods in java.sql.Date (see Example 8-19) and java.sql.Timestamp (see Example 8-20).

Example 8-19. Conversion methods in java.sql.Date
LocalDate   toLocalDate()
static Date valueOf(LocalDate date)
Example 8-20. Conversion methods in java.sql.Timestamp
LocalDateTime    toLocalDateTime()
static Timestamp valueOf(LocalDateTime dateTime)

Creating a class to do the conversion is easy enough, as in Example 8-21.

Example 8-21. Converting java.util classes to java.time classes (more to come)
package datetime;

import java.sql.Timestamp;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Date;

public class ConvertDate {
    public LocalDate convertFromSqlDatetoLD(java.sql.Date sqlDate) {
        return sqlDate.toLocalDate();
    }

    public java.sql.Date convertToSqlDateFromLD(LocalDate localDate) {
        return java.sql.Date.valueOf(localDate);
    }

    public LocalDateTime convertFromTimestampToLDT(Timestamp timestamp) {
        return timestamp.toLocalDateTime();
    }

    public Timestamp convertToTimestampFromLDT(LocalDateTime localDateTime) {
        return Timestamp.valueOf(localDateTime);
    }
}

Since the methods you need are based on java.sql.Date, the question then becomes, how do you convert from java.util.Date (which most developers use) and java.sql.Date? One way is to use the constructor from SQL date that takes a long representing the milliseconds elapsed in the current epoch.

The java.util.Date class has a method called getTime that returns the long value, and the java.sql.Date class has a constructor that takes this long as an argument.9

This means another way to convert from a java.util.Date instance to a java.time.LocalDate is to go through the java.sql.Date class, as in Example 8-22.

Example 8-22. Converting a java.util.Date to a java.time.LocalDate
public LocalDate convertUtilDateToLocalDate(java.util.Date date) {
    return new java.sql.Date(date.getTime()).toLocalDate()
}

Way back in Java 1.1, virtually the entire java.util.Date class was deprecated in favor of java.util.Calendar. Converting between calendar instances and the new java.time package can be done with the toInstant method, adjusting for the time zone (Example 8-23).

Example 8-23. Converting from java.util.Calendar to java.time.ZonedDateTime
public ZonedDateTime convertFromCalendar(Calendar cal) {
    return ZonedDateTime.ofInstant(cal.toInstant(), cal.getTimeZone().toZoneId());
}

This method uses the ZonedDateTime class. The LocalDateTime class also has an ofInstant method, but for some reason it also takes a ZoneId second argument. This is strange because a LocalDateTime doesn’t contain time zone information. It seems more intuitive, therefore, to use the method from ZonedDateTime instead.

You can also use the various getter methods on Calendar explicitly and go directly to LocalDateTime (Example 8-24), if you want to bypass the time zone information entirely.

Example 8-24. Using getter methods from Calendar to LocalDateTime
public LocalDateTime convertFromCalendarUsingGetters(Calendar cal) {
    return LocalDateTime.of(cal.get(Calendar.YEAR),
        cal.get(Calendar.MONTH),
        cal.get(Calendar.DAY_OF_MONTH),
        cal.get(Calendar.HOUR),
        cal.get(Calendar.MINUTE),
        cal.get(Calendar.SECOND));
}

Another mechanism is to generate a formatted string from the calendar, which can then be parsed into the new class (Example 8-25).

Example 8-25. Generating and parsing a timestamp string
public LocalDateTime convertFromUtilDateToLDUsingString(Date date) {
    DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
    return LocalDateTime.parse(df.format(date),
        DateTimeFormatter.ISO_LOCAL_DATE_TIME);
}

That’s not really an advantage, but it’s nice to know you can do it. Finally, although Calenda⁠r doesn’t have a direct conversion method, it turns out GregorianCalendar does (Example 8-26).

Example 8-26. Converting a GregorianCalendar to a ZonedDateTime
public ZonedDateTime convertFromGregorianCalendar(Calendar cal) {
    return ((GregorianCalendar) cal).toZonedDateTime();
}

That works, but it does assume you’re using a Gregorian calendar. Since that’s the only Calendar implementation in the standard library, it’s probably true, but not necessarily so.

Finally, Java 9 added the ofInstant method to LocalDate, making the conversion simpler, as in Example 8-27.

Example 8-27. Converting java.util.Dat to java.time.LocalDate (JAVA 9 ONLY)
public LocalDate convertFromUtilDateJava9(Date date) {
    return LocalDate.ofInstant(date.toInstant(), ZoneId.systemDefault());
}

That approach is more direct, but is restricted to Java 9.

8.5 Parsing and Formatting

Problem

You want to parse and/or format the new date-time classes.

Solution

The DateTimeFormatter class creates date-time formats, which can be used for both parsing and formatting.

Discussion

The DateTimeFormatter class has a wide variety of options, from constants like ISO_LOCAL_DATE to pattern letters like uuuu-MMM-dd to localized styles for any given Locale.

Fortunately, the process of parsing and formatting is almost trivially easy. All the main date-time classes have a format and a parse method. Example 8-28 shows the signatures for LocalDate:

Example 8-28. Methods to parse and format LocalDate instances
static LocalDate parse(CharSequence text)  1
static LocalDate parse(CharSequence text, DateTimeFormatter formatter)
       String    format(DateTimeFormatter formatter)
1

Uses ISO_LOCAL_DATE

Parsing and formatting are shown in Example 8-29.

Example 8-29. Parsing and formatting a LocalDate
LocalDateTime now = LocalDateTime.now();
String text = now.format(DateTimeFormatter.ISO_DATE_TIME);  1
LocalDateTime dateTime = LocalDateTime.parse(text);         2
1

Format from LocalDateTime to string

2

Parse from string to LocalDateTime

With that in mind, the real fun comes from playing with various date-time formats, locales, and so on. The code in Example 8-30 shows some examples.

Example 8-30. Formatting dates
LocalDate date = LocalDate.of(2017, Month.MARCH, 13);

System.out.println("Full   : " +
    date.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL)));
System.out.println("Long   : " +
    date.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG)));
System.out.println("Medium : " +
    date.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM)));
System.out.println("Short  : " +
    date.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT)));

System.out.println("France : " +
    date.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL)
        .withLocale(Locale.FRANCE)));
System.out.println("India  : " +
    date.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL)
        .withLocale(new Locale("hin", "IN"))));
System.out.println("Brazil : " +
    date.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL)
        .withLocale(new Locale("pt", "BR"))));
System.out.println("Japan  : " +
    date.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL)
        .withLocale(Locale.JAPAN)));

Locale loc = new Locale.Builder()
        .setLanguage("sr")
        .setScript("Latn")
        .setRegion("RS")
        .build();
System.out.println("Serbian: " +
    date.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL)
        .withLocale(loc)));

The output looks something like:10

Full   : Monday, March 13, 2017
Long   : March 13, 2017
Medium : Mar 13, 2017
Short  : 3/13/17

France : lundi 13 mars 2017
India  : Monday, March 13, 2017
Brazil : Segunda-feira, 13 de Março de 2017
Japan  : 2017年3月13日
Serbian: ponedeljak, 13. mart 2017.

The parse and format methods throw a DateTimeParseException and DateTime​Exception, respectively, so you might want to consider catching them in your own code.

If you have your own format in mind, use the ofPattern method to create it. All the legal values are described in detail in the Javadocs. As an example of what’s possible, see Example 8-31.

Example 8-31. Defining your own format pattern
ZonedDateTime moonLanding = ZonedDateTime.of(
        LocalDate.of(1969, Month.JULY, 20),
        LocalTime.of(20, 18),
        ZoneId.of("UTC")
);
System.out.println(moonLanding.format(DateTimeFormatter.ISO_ZONED_DATE_TIME));

DateTimeFormatter formatter =
    DateTimeFormatter.ofPattern("uuuu/MMMM/dd hh:mm:ss a zzz GG");
System.out.println(moonLanding.format(formatter));

formatter = DateTimeFormatter.ofPattern("uuuu/MMMM/dd hh:mm:ss a VV xxxxx");
System.out.println(moonLanding.format(formatter));

These produce:

1969-07-20T20:18:00Z[UTC]
1969/July/20 08:18:00 PM UTC AD
1969/July/20 08:18:00 PM UTC +00:00

Again, to see what’s possible and what all the different formatting letters mean, see the Javadocs for DateTimeFormatter. The process is always as simple as shown.

To show an example of a localized date-time formatter, consider the daylight savings time issue. In the United States, daylight savings moves the clocks forward at 2 A.M. on March 11, 2018, in the Eastern time zone. What happens when you ask for a zoned date time at 2:30 A.M. on that day? See Example 8-32.

Example 8-32. Move the clocks forward
ZonedDateTime zdt = ZonedDateTime.of(2018, 3, 11, 2, 30, 0, 0,
    ZoneId.of("America/New_York"));
System.out.println(
    zdt.format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.FULL)));

This uses an overload of the of method that takes the year, month, dayOfMonth, hours, minutes, seconds, nanoOfSecond, and ZoneId. Note that all the fields (other than the ZoneId) are of type int, which means you can’t use the Month enum.

The output of this code is:

Sunday, March 11, 2018 3:30:00 AM EDT

So the method correctly changed the time from 2:30 A.M. (which doesn’t exist) to 3:30 A.M.

8.6 Finding Time Zones with Unusual Offsets

Problem

You want to find all the time zones with non-integral hour offsets.

Solution

Get the time zone offset for each time zone and determine its remainder when dividing the total seconds by 3,600.

Discussion

Most time zones are offset from UTC by an integral number of hours. For example, what we normally called Eastern Time is UTC-05:00 and metropolitan France (CET) is UTC+01:00. There are time zones, however, that are offset by the half-hour, like Indian Standard Time (IST), which is UTC+05:30, or even 45 minutes, like the Chatham Islands in New Zealand, which is UTC+12:45. This recipe demonstrates how you can use the java.time package to find all the time zones that are off by nonintegral amounts.

Example 8-33 demonstrates how to find the ZoneOffset for each regional zone ID, and compare its total seconds to the number of seconds in an hour.

Example 8-33. Finding the offset seconds for each zone ID
public class FunnyOffsets {
    public static void main(String[] args) {
        Instant instant = Instant.now();
        ZonedDateTime current = instant.atZone(ZoneId.systemDefault());
        System.out.printf("Current time is %s%n%n", current);

        System.out.printf("%10s %20s %13s%n", "Offset", "ZoneId", "Time");
        ZoneId.getAvailableZoneIds().stream()
                .map(ZoneId::of)  1
                .filter(zoneId -> {
                    ZoneOffset offset = instant.atZone(zoneId).getOffset(); 2
                    return offset.getTotalSeconds() % (60 * 60) != 0; 3
                })
                .sorted(comparingInt(zoneId ->
                        instant.atZone(zoneId).getOffset().getTotalSeconds()))
                .forEach(zoneId -> {
                    ZonedDateTime zdt = current.withZoneSameInstant(zoneId);
                    System.out.printf("%10s %25s %10s%n",
                        zdt.getOffset(), zoneId,
                        zdt.format(DateTimeFormatter.ofLocalizedTime(
                            FormatStyle.SHORT)));
                });
    }
}
1

Map the string region IDs to zone IDs

2

Calculate the offset

3

Only use zone IDs whose offsets are not divisible by 3,600

The static ZoneId.getAvailableZoneIds method returns a Set<String> representing all the region IDs in the world. Using the ZoneId.of method, the resulting stream of strings is transformed into a stream of ZoneId instances.

The lambda expression in the filter first applies the atZone method to an Instant in order to create a ZonedDateTime, which then has a getOffset method. Finally, the ZoneOffset class provides a getTotalSeconds method. The Javadocs for that method describe it as “the primary way to access the offset amount. It returns the total of the hours, minutes and seconds fields as a single offset that can be added to a time.” The Predicate in the filter then returns true only for those total seconds amounts that aren’t evenly divisible by 3,600 (60 sec/min * 60 min/hour).

Before printing, the resulting ZoneId instances are sorted. The sorted method takes a Comparator. Here, the static Comparator.comparingInt method is used, which generates a Comparator that will sort by a given integer key. In this case, the same calculation is used to determine the total seconds in the offsets. The result is that the ZoneId instances are sorted by the offset amounts.

Then, to print the results, the current ZonedDateTime in the default time zone is evaluated for each ZoneId using the withZoneSameInstant method. The printed string then shows the offset, the regional zone ID, and a formatted, localized version of the local time in that zone.

The result is shown in Example 8-34.

Example 8-34. Time zones offset by non-hour amounts
Current time is 2016-08-08T23:12:44.264-04:00[America/New_York]

    Offset               ZoneId          Time
    -09:30         Pacific/Marquesas    5:42 PM
    -04:30           America/Caracas   10:42 PM
    -02:30          America/St_Johns   12:42 AM
    -02:30       Canada/Newfoundland   12:42 AM
    +04:30                      Iran    7:42 AM
    +04:30               Asia/Tehran    7:42 AM
    +04:30                Asia/Kabul    7:42 AM
    +05:30              Asia/Kolkata    8:42 AM
    +05:30              Asia/Colombo    8:42 AM
    +05:30             Asia/Calcutta    8:42 AM
    +05:45            Asia/Kathmandu    8:57 AM
    +05:45             Asia/Katmandu    8:57 AM
    +06:30              Asia/Rangoon    9:42 AM
    +06:30              Indian/Cocos    9:42 AM
    +08:45           Australia/Eucla   11:57 AM
    +09:30           Australia/North   12:42 PM
    +09:30      Australia/Yancowinna   12:42 PM
    +09:30        Australia/Adelaide   12:42 PM
    +09:30     Australia/Broken_Hill   12:42 PM
    +09:30           Australia/South   12:42 PM
    +09:30          Australia/Darwin   12:42 PM
    +10:30       Australia/Lord_Howe    1:42 PM
    +10:30             Australia/LHI    1:42 PM
    +11:30           Pacific/Norfolk    2:42 PM
    +12:45                   NZ-CHAT    3:57 PM
    +12:45           Pacific/Chatham    3:57 PM

This example shows how several of the classes in java.time can be combined to solve an interesting problem.

8.7 Finding Region Names from Offsets

Problem

You want to know the ISO 8601 region name given an offset from UTC.

Solution

Filter all the available zone IDs by the given offset.

Discussion

While time zone names like “Eastern Daylight Time” or “Indian Standard Time” are well-known, they are unofficial and their abbreviations like EDT and IST are sometimes not even unique. The ISO 8601 specification defines time zone IDs two ways:

  • By region name, like “America/Chicago”

  • By offset from UTC in hours and minutes, like “+05:30”

Say you want to know what the region name is for a given offset from UTC. Many regions share the same UTC offset at any given time, but you can calculate a List of region names that have a given offset easily.

The ZoneOffset class specifies a time zone offset from Greenwich/UTC time. If you already have a value for the offset, you can filter the complete list of region names using it, as in Example 8-35.

Example 8-35. Getting region names given an offset
public static List<String> getRegionNamesForOffset(ZoneOffset offset) {
    LocalDateTime now = LocalDateTime.now();
    return ZoneId.getAvailableZoneIds().stream()
            .map(ZoneId::of)
            .filter(zoneId -> now.atZone(zoneId).getOffset().equals(offset))
            .map(ZoneId::toString)
            .sorted()
            .collect(Collectors.toList());
}

The ZoneId.getAvailableZoneIds method returns a List of strings. Each one can be mapped to a ZoneId using the static ZoneId.of method. Then, after determining the corresponding ZonedDateTime for that ZoneId using the atZone method in LocalDateTime, you can get the ZoneOffset for each and filter the set by only those that match it. The result is then mapped to strings, which are sorted and collected into a List.

How do you get a ZoneOffset? One way is to use a given ZoneId, as shown in Example 8-36.

Example 8-36. Get region names for a given offset
public static List<String> getRegionNamesForZoneId(ZoneId zoneId) {
    LocalDateTime now = LocalDateTime.now();
    ZonedDateTime zdt = now.atZone(zoneId);
    ZoneOffset offset = zdt.getOffset();

    return getRegionNamesForOffset(offset);
}

This works for any given ZoneId.

For example, if you want to determine the list of region names that correspond to your current location, use the code in Example 8-37.

Example 8-37. Getting the current region names
@Test
public void getRegionNamesForSystemDefault() throws Exception {
    ZonedDateTime now = ZonedDateTime.now();
    ZoneId zoneId = now.getZone();
    List<String> names = getRegionNamesForZoneId(zoneId);

    assertTrue(names.contains(zoneId.getId()));
}

If you don’t know a region name but you do know the hours and minutes it is offset from GMT, the ZoneOffset class has a convenient method called ofHoursMinutes for that as well. The overload in Example 8-38 shows how to do that.

Example 8-38. Getting region names given an hour and minute offset
public static List<String> getRegionNamesForOffset(int hours, int minutes) {
    ZoneOffset offset = ZoneOffset.ofHoursMinutes(hours, minutes);
    return getRegionNamesForOffset(offset);
}

The tests in Example 8-39 demonstrate how the given code works.

Example 8-39. Testing region names for a given offset
@Test
public void getRegionNamesForGMT() throws Exception {
    List<String> names = getRegionNamesForOffset(0, 0);

    assertTrue(names.contains("GMT"));
    assertTrue(names.contains("Etc/GMT"));
    assertTrue(names.contains("Etc/UTC"));
    assertTrue(names.contains("UTC"));
    assertTrue(names.contains("Etc/Zulu"));
}

@Test
public void getRegionNamesForNepal() throws Exception {
    List<String> names = getRegionNamesForOffset(5, 45);

    assertTrue(names.contains("Asia/Kathmandu"));
    assertTrue(names.contains("Asia/Katmandu"));
}

@Test
public void getRegionNamesForChicago() throws Exception {
    ZoneId chicago = ZoneId.of("America/Chicago");
    List<String> names = RegionIdsByOffset.getRegionNamesForZoneId(chicago);

    assertTrue(names.contains("America/Chicago"));
    assertTrue(names.contains("US/Central"));
    assertTrue(names.contains("Canada/Central"));
    assertTrue(names.contains("Etc/GMT+5") || names.contains("Etc/GMT+6"));
}

A complete list of region names can be found in Wikipedia at https://en.wikipedia.org/wiki/List_of_tz_database_time_zones.

8.8 Time Between Events

Problem

You need to know the amount of time between two events.

Solution

If you want times readable by people, use the between or until methods on the temporal classes or between method on Period to generate a Period object. Otherwise use the Duration class for seconds and nanoseconds on the timeline.

Discussion

The Date-Time API includes the interface java.time.temporal.TemporalUnit, which is implemented by the enum ChronoUnit in the same package. The between method on that interface takes two TemporalUnit instances and returns a long:

long between(Temporal temporal1Inclusive,
             Temporal temporal2Exclusive)

The start and end times must be of compatible types. The implementation converts the second argument to be an instance of the first type before calculating the amount. The result is negative if the second argument occurs before the first argument.

The return value is the number of “units” between the arguments. This becomes convenient when using the constants in the ChronoUnit enum.

For example, say you want to know how many days you need to wait until a particular date. Since you’re interested in days, use the ChronoUnit.DAYS constant from the enum, as in Example 8-40.

Example 8-40. Days to Election Day
LocalDate electionDay = LocalDate.of(2020, Month.NOVEMBER, 3);
LocalDate today = LocalDate.now();

System.out.printf("%d day(s) to go...%n",
    ChronoUnit.DAYS.between(today, electionDay));

Since the between method is invoked on the DAYS enum value, this will return the number of days. Other constants in ChronoUnit include HOURS, WEEKS, MONTHS, YEARS, DECADES, CENTURIES, and more.11

Using the Period class

If you’re interested in a breakdown into years, months, and days, use the Period class. The until method in many of the basic classes has an overload that returns a Period:

// In java.time.LocalDate
Period until(ChronoLocalDate endDateExclusive)

This example can be rewritten as in Example 8-41.

Example 8-41. Using Period to get days, months, and years
LocalDate electionDay = LocalDate.of(2020, Month.NOVEMBER, 3);
LocalDate today = LocalDate.now();

Period until = today.until(electionDay); 1

years  = until.getYears();
months = until.getMonths();
days   = until.getDays();
System.out.printf("%d year(s), %d month(s), and %d day(s)%n",
        years, months, days);
1

Equivalent to Period.between(today, electionDay)

As the comment states, the Period class also has a static method called between that works the same way. The recommendation is to use whichever style makes the code more readable.

The Period class is used when you need to deal with human-readable times, like days, months, and years.

Using the Duration class

The Duration class represents an amount of time in terms of seconds and nanoseconds, which makes it suitable for working with Instant. The result can be converted to many other types. The class stores a long representing seconds and an int representing nanoseconds, and can be negative if the end point comes before the starting point.

A primitive timing mechanism using Duration is shown in Example 8-42.

Example 8-42. Timing a method
public static double getTiming(Instant start, Instant end) {
    return Duration.between(start, end).toMillis() / 1000.0;
}

Instant start = Instant.now();
// ... call method to be timed ...
Instant end = Instant.now();
System.out.println(getTiming(start, end) + " seconds");

This is a “poor developer’s” approach to timing a method, but it is easy.

The Duration class has conversion methods: toDays, toHours, toMillis, toMinutes, and toNanos, which is why the getTiming method in Example 8-42 used toMillis and divided by 1,000.

1 No pun intended.

2 Maybe it’s just me, but that seems like a lot.

3 Based on a similar table in the Java Tutorial, https://docs.oracle.com/javase/tutorial/datetime/overview/naming.html.

4 Holy Layers of Indirection, Batman!

5 For example, “Ahoy, matey, I’d like t’ add ye t’ me professional network on LinkedIn.”

6 When you print a java.util.Date, it uses Java’s default time zone to format the string.

7 See https://en.wikipedia.org/wiki/Year_2038_problem for details.

8 While I expect to be safely retired by that point, I can imagine being on a respirator somewhere when the failover occurs.

9 In fact, this is the only nondeprecated constructor in the java.sql.Date class, though you can also use the setTime method to adjust the value of an existing java.sql.Date.

10 There’s no truth to the rumor that I deliberately chose unusual languages and output formats just to challenge O’Reilly Media’s ability to print the results correctly, at least as far as you know.

11 Including, believe it or not, FOREVER. If you ever need that value, please send me a message. I’d love to know what the use case was.