Java 8 Date Time API Usage

Introduction

Every effort we put in to showcase the extent of Java 8 Date Time API usage leaves us only scratching the surface of it. This post walks through a couple of examples and some standard methods that can be helpful in daily use. Timezone aspects are not covered in this post. Refer to this post to for an introduction to Java 8 Date Time API.

Old And The New

Let us see how date formatting code changes with the new Java 8 Date Time API. If you landed directly on this post, we recommend reading this introductory post first.
public static final ThreadLocal<SimpleDateFormat> FILE_NAME_DATE_FORMAT =
	new ThreadLocal<SimpleDateFormat>() {
		@Override
		protected SimpleDateFormat initialValue() {
			final SimpleDateFormat format =
					new SimpleDateFormat("yyyyMMdd");
			return format;
		}
	};
	
String dateOfEntry = FILE_NAME_DATE_FORMAT.get().format(new Date());

String backupFile = "SysBackup-" + dateOfEntry + ".zip";
Goodbye ThreadLocals and complicated code. Welcome meaningful and fluent code. The DateTimeFormatterBuilder is a very powerful tool and we will look at the possibilities soon.
public static final DateTimeFormatter FILE_NAME_DATE_FORMAT = new DateTimeFormatterBuilder().appendPattern("yyyyMMdd").toFormatter();

String dateOfEntry = LocalDate.now().format(FILE_NAME_DATE_FORMAT);

Date Time API Usage

Let us look at a problem statement. A busy parking lot in the middle of the city wants to automate and charge its regulars, online. A parking entry ticket costs $2. The rates are as follows: 1) First half-hour in part or full costs $2 (excluding entry ticket cost) 2) Second hour and above the rate is $4 for each part or full hour Given the entry and exit time in HH:MM format, calculate the total amount to be charged to the customer
public class ParkingChargesCalculator {
   
    private static int getParkingCharges(String startTime, String endTime) throws IllegalArgumentException {

        LocalTime start = LocalTime.parse(startTime);
        LocalTime end = LocalTime.parse(endTime);

        if (start.isBefore(LocalTime.of(06, 00))) {
            throw new IllegalArgumentException("Parking lot opens at 06:00");
        } else if (end.isAfter(LocalTime.of(23, 30))) {
            throw new IllegalArgumentException("Parking lot closes at 23:30");
        } else if (start.isAfter(end)) {
            throw new IllegalArgumentException("Start time is after end time");
        }

        Duration duration = Duration.between(start, end);
        int fullHours = (int)Math.floor(duration.toMinutes()/60f);

        return 2 // entry ticket cost
                + 3 // first hour cost
                + (fullHours * 4); // subsequent hour costs
    }

    public static void main(String[] args) {
        System.out.println("$ " + getParkingCharges("10:20", "23:00")); // $ 53
        System.out.println("$ " + getParkingCharges("12:00", "15:00")); // $ 13
        System.out.println("$ " + getParkingCharges("18:30", "19:00")); // $ 5
        System.out.println("$ " + getParkingCharges("00:20", "10:00")); // Parking lot opens at 06:00
    }
}
Parsing HH:MM to a LocalTime instance is easy, but other formats can also be parsed easily with a formatter of your choice using LocalTime.parse(CharSequence text, DateTimeFormatter formatter) Utility methods like isBefore()  and isAfter() are used, in this case, to check for boundary conditions. LocalTime.of() shows the fluent API and is also available in LocalDate and other concerns. We now need to find the duration of hours that elapsed while the car was parked in the lot and rightly so, the class to be used is called Duration . Again, an immutable and thread-safe class, it allows modelling a quantity or amount of time in terms of seconds or nanoseconds. E.g. 120 seconds, 320.4 seconds. Similarly, date based modelling is allowed by the Period class. We use the between(Temporal startInclusive, Temporal endExclusive) method to obtain the duration between the car entering the lot and exiting it. Temporal (meaning: relating to time) is an object such as date, time, offset or some combination of these. For our charge calculation we need the total number of hours (part or full). So if you use the method toHours() you will get 12 based on our first sample (10:20 to 23:00). But we need the 40 mins remainder to be considered as an hour too. So we use the toMinutes() to get the total minutes in the duration. Although we said that Duration is time-based modelling, it allows to fetch number of days using toDays() . However, this assumes a 24 hr day thus ignoring day light savings effects. If you want to factor in day light savings then use the Period class. Fun Fact: A physical duration could be of infinite length. For practicality, the duration is stored with constraints similar to Instant. The duration uses nanosecond resolution with a maximum value of the seconds that can be held in a long. This is greater than the current estimated age of the universe.

Quick Reference

EPOCH constant referring to 1970 Jan 1st
System.out.println("EPOCH -> " + Instant.EPOCH);
EPOCH -> 1970-01-01T00:00:00Z
now() always returns the current time based on the System clock
System.out.println("Current time -> " + Instant.now());
Current time -> 2017-01-26T17:29:13.576Z
Get Instant using traditional millis from epoch
System.out.println("A day before (init from epoch millis) -> " + Instant.ofEpochMilli(System.currentTimeMillis()-(1000*60*60*24)));
A day before (init from epoch millis) -> 2017-01-25T17:29:13.576Z
Get Instant using seconds from Epoch too
System.out.println("The day after EPOCH (init from epoch secs) -> " + Instant.ofEpochSecond(60*60*24));
The day after EPOCH (init from epoch secs) -> 1970-01-02T00:00:00Z
Parse standard date formats to get an Instant
System.out.println("Instant from text -> " + Instant.parse("2017-02-28T00:00:00Z"));
Instant from text -> 2017-02-28T00:00:00Z
Unparseable date gives a DateTimeParseException
System.out.println("DateTimeParseException -> " + Instant.parse("2017-02-30T00:00:00Z"));
java.time.format.DateTimeParseException: Text '2017-02-30T00:00:00Z' could not be parsed at index 0
now() always returns the current time when invoked
Thread.sleep(5000);
System.out.println("Current time -> " + Instant.now());
Current time -> 2017-01-26T17:29:18.577Z
Get Instant from a custom clock
Clock birthdayClock = Clock.fixed(Instant.parse("2014-06-17T04:14:00Z"), ZoneId.of("UTC"));
System.out.println("Birthday clock's time -> " + Instant.now(birthdayClock));
Birthday clock's time -> 2014-06-17T04:14:00Z

Thread.sleep(1000*60*60);
System.out.println("Birthday clock's time (an hour later) -> " + Instant.now(birthdayClock));
Birthday clock's time (an hour later) -> 2014-06-17T04:14:00Z
As you can see, Instant.now() can also take a Clock reference. When invoked without parameters, Instant return current date/time based on the system clock in UTC zone. But when you pass a clock instance to it, it returns time based on that clock. In this example, we have initialized a fixed clock that always points to the birth-date of my daughter. Calling Instant.now() using this clock, an hour or a day later, will still return the same date and time my daughter was born. A fixed clock is not a true clock in the sense but is useful for testing purposes. You can use dependency injection to put in a fixed clock for testing. E.g. raise an alarm 10 secs after the test is started by initializing a clock with an instant set to 10 secs later than the current time.
Clock _10SecAlarmClock = Clock.fixed(Instant.now().plus(10, ChronoUnit.SECONDS), ZoneId.of("UTC"));

Playing with Date Time and Zones

Adding time component to LocalDate is as easy as
LocalDate date = LocalDate.now();
date.atTime(12, 10);
date.atTime(12, 10, 58);
date.atTime(12, 10, 58, 28283820);
date.atTime(LocalTime.NOON);
date.atTime(OffsetTime.MAX);
Consider this usage in a punch-in/punch-out time keeping application for a small store that does not reward an employee for being before time but punishes if they are late.
    LocalDateTime punchInTime = LocalDateTime.now();
    LocalTime standardPunchInTime = LocalTime.of(9, 30);
    if (punchInTime.toLocalTime().isBefore(standardPunchInTime)) {
        punchInTime = punchInTime.with(standardPunchInTime);
    //  punchInTime = (LocalDateTime) standardPunchInTime.adjustInto(punchInTime);
    }

    // All other employee unfriendly business practices follows...
The with() method returns an adjusted copy of the date-time. If the employee punches-in before start of day (e.g. 09:30 AM), his punch-in time is reset to 9:30 AM but not so if he comes in late. It is much clearer to use with(TemporalAdjuster) rather than adjustInto() as can be seen from the above example. Both do exactly the same thing, in fact with() calls adjustInto() internally.

Conclusion

It is very evident that a lot of effort has been put in, so that the API usage results in clear and concise code. The API offers tons of manipulation options between temporal objects. The API is well written and very extensive, please comment if there is a use-case that you think is not easily achievable. We would like to hear about it.