Chapter 4
IN THIS CHAPTER
Understanding human and computer dates and times
Using java.time classes to represent dates and times
Comparing dates and times
Performing calculations with dates and times
Formatting dates and times
Does anybody really know what time it is? Does anybody really care about time?
So mused Robert Lamm of The Chicago Transit Authority (later known as simply Chicago) in 1969.
I'm not sure who cared much about time in 1969, but I do know that the people who designed the original version of Java in 1995 didn’t care much about it, at least as evidenced by the weak classes they provided for working with times and dates in the Java.util
package. Java programmers have long struggled with simple calculations involving dates and times, such as determining what the date will be 45 days from today or calculating the number of days between two given dates.
Java finally got with the times with the release of Java 8, which introduced an entirely new framework for working with dates and times, usually referred to as the Date-Time API. This new API is pretty complicated, involving about 50 new classes and interfaces and hundreds of new methods. In this chapter, I’ll introduce you to just a few of the most important and useful classes of the new Date-Time API. Then you can explore the rest online via Oracle’s documentation at https://docs.oracle.com/en/java/javase/14/docs/api/index.html
.
Before I launch into the details of the new Date-Time API’s classes, let’s review a few basic concepts about time. Probably the most important basic concept to understand about time (at least from a programming point of view) is that computers and humans use two entirely different methods of keeping track of time. Humans measure time using a system of progressively longer units, starting with seconds and increasing to minutes, hours, days, weeks, months, years, decades, and centuries.
Our human time units are intuitively familiar to us, but their precise definitions are more complicated than you might guess. All kinds of factors muck up the way we represent time: leap days, time zones, and daylight-saving time. And did you know that about once every 18 months, scientists pick a day (they usually choose June 30 or December 31) that they add one second to? This is necessary because the speed of the earth’s rotation varies ever so slightly, throwing our clocks off.
In contrast, the way computers keep track of time is much simpler: Computers simply count the number of units (typically milliseconds or nanoseconds) that have elapsed since a given start time. Thus, to a computer, a time is just a number.
In Java, machine time is set as the number of nanoseconds that have elapsed since midnight, January 1, 1970. Why January 1, 1970? There’s no particular reason other than historical: Java inherited that date from the Unix operating system, which was developed in the 1970s. (For more information, see the sidebar “And you thought Y2K was bad!”)
The difference between how humans and computers keep track of time makes any computer program that deals with dates and times a bit tricky. For example, suppose you want to schedule a phone call between two people, one in Los Angeles, the other in New York. Obviously, the time that you agree upon for the call must take into account the time zone of each participant. Thus you might agree to make the call at 1 p.m. local time for the West Coast participant and 4 p.m. local time for the East Coast participant. So, how would you represent that appointment time in a database?
Or suppose you want to calculate the due date for an invoice that is dated January 27, 2015, when the payment due date is 45 days after the invoice date. The algorithm that calculates the due date must be aware that January has 31 days and that February has 28 days in 2015.
Fortunately, the new Date-Time API is designed to handle all those nuances for you. The Date-Time API includes all the classes you need to represent dates and times in just about any imaginable context, for performing calculations and comparisons between date and time objects, and for converting dates and times to string representations in just about any imaginable format.
The first order of business when developing an application that must work with dates or times (or both) is picking the Date-Time class to represent your date and time values. The java.time
package defines ten distinct classes used to represent different types of times and dates, as described in Table 4-1.
Each of these classes has many different methods that let you create date and time objects, perform calculations on them, compare them, and convert them to strings that can be displayed and read by humans. You can find complete documentation of the methods for each of these classes online at https://docs.oracle.com/en/java/javase/14/docs/api/index.html
.
TABLE 4-1 Ten Date-Time Classes in java.time
Class |
What It Represents |
|
A time (hours, minutes, and seconds to nanosecond precision) without an associated time zone. |
|
A date (year, month, and day) without an associated time zone. |
|
A date and time without an associated time zone. |
|
A time and an offset from UTC (Coordinated Universal Time, also known as Greenwich Mean Time), such as 12:30:00-8.00, which means the time is 12:30 with an offset of -8 hours from UTC. |
|
A date and time with an offset value from UTC. |
|
A date and time with an associated time zone, such as |
|
A month and day without an associated year. You can use a MonthDay object to represent a date such as a birthday, anniversary, or holiday. |
|
A year and month, such as December, 2015. No day, time, or time zone values are associated with the year and month. |
|
A year, such as 2038. No month, day, time, or time zone values are associated with the year. |
|
A single point of time, represented internally as the number of nanoseconds that have elapsed since midnight, January 1, 1970. The value assumes a UTC/GMT time offset of 0. |
All Date-Time classes have a static now
method, which creates an object representing the current date and/or time. For example, to get the current date, you would use code similar to this:
LocalDate date = LocalDate.now();
To get the current date and time with time zone, use this code:
ZonedDateTime datetime = ZonedDateTime.now();
The following program displays the current time using all ten classes, creating an object of each class using now()
and printing it with toString()
:
import java.util.*;
import java.time.*;
public class TimeTester
{
public static void main(String[] args)
{
System.out.println("\nLocalTime: "
+ LocalTime.now().toString());
System.out.println("\nLocalDateTime: "
+ LocalDateTime.now().toString());
System.out.println("\nZonedDateTime: "
+ ZonedDateTime.now().toString());
System.out.println("\nOffsetTime: "
+ OffsetTime.now().toString());
System.out.println("\nOffsetDateTime: "
+ OffsetDateTime.now().toString());
System.out.println("\nMonthDay: "
+ MonthDay.now().toString());
System.out.println("\nYearMonth: "
+ YearMonth.now().toString());
System.out.println("\nInstant: "
+ Instant.now().toString());
}
}
If you compile and run this program, the output will appear something like this:
LocalTime: 20:56:26.325
LocalDateTime: 2013-10-07T20:56:26.388
ZonedDateTime: 2013-10-07T20:56:26.388-07:00[America/Los_Angeles]
OffsetTime: 20:56:26.388-07:00
OffsetDateTime: 2013-10-07T20:56:26.388-07:00
MonthDay: --10-07
YearMonth: 2013-10
Instant: 2013-10-08T03:56:26.388Z
From this output, you can get an idea of the information represented by the various Date-Time classes.
Another way to create a Date-Time object is to use the static parse
method, which creates a Date-Time object from a string that represents a specific date or time. For example, the following code creates a LocalDate
object representing December 15, 2014:
LocalDate d = LocalDate.parse("2014-12-15");
To create a LocalDateTime
object that represents a specific time on a specific date, use the parse
method. Here’s an example that sets the time to 3:45 p.m. on December 15, 2014:
LocalDateTime dt;
dt = LocalDateTime.parse("2014-12-15T15:45");
Note that the letter T
separates the date from the time, and the time is expressed in 24-hour clock format. If you need to be more precise, you can also specify seconds, as in this example:
dt = LocalDateTime.parse("2014-12-15T15:45:13.5");
Here the time is set to 13.5 seconds after 2:45 p.m.
If the string is not in the correct format, the parse
method throws a DateTimeParseException
. Whenever you use the parse
method, you should enclose it in a try
block and catch this exception, as in this example:
LocalDateTime dt;
try
{
dt = LocalDateTime.parse("2014-12-15T03:45PM");
}
catch (DateTimeParseException ex)
{
System.out.println(ex.toString());
}
The parse
method is especially useful for converting user input to a Date-Time object. For example, you might use it along with the Scanner
class to read a date from the console, or you can use parse
in a Swing application to read a date from a text box. When you do, you should prompt the user with the expected date format and catch DateTimeParseException
in case the user enters the date in the wrong format.
A third way to create Date-Time objects is to use the static of
method to create a Date-Time object from its constituent parts. For example, you can create a LocalDate
object by supplying integers that represent the year, month, and day like this:
LocalDate date = LocalDate.of(2014,12,15);
Each of the Date-Time classes has one or more variations of the of
method, as spelled out in Table 4-2.
TABLE 4-2 Date-Time of Methods
Class |
Method |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Note that several of the methods in Table 4-2 use the additional types Month
, ZoneOffset
, and ZoneId
. These types are described in the following sections.
Several of the methods listed in Table 4-2 let you specify the month as a Month
object. Month
is an enumeration that represents the twelve months of the year, as follows:
Month.JANUARY Month.MAY Month.SEPTEMBER
Month.FEBRUARY Month.JUNE Month.OCTOBER
Month.MARCH Month.JULY Month.NOVEMBER
Month.APRIL Month.AUGUST Month.DECEMBER
Thus you can create a date like this:
LocalDate date = LocalDate.of(2014,Month.DECEMBER,15);
Interestingly, the Month
enumeration has some interesting methods which you might find occasionally useful. For example, you can print the number of days in December like this:
System.out.println("December hath "
+ Month.DECEMBER.length(false) + " days.");
The boolean
argument in the length
method indicates whether the calculation should be for a leap year. Consult the online documentation for other useful methods of the Month
enumeration.
To create a ZonedDateTime
, you must first create a time zone object by using the ZoneId
class. To create a time zone, you must know the standard name of the time zone you want to create. Unfortunately, there are more than 500 distinct zone IDs, and they periodically change. So listing them here would be impractical, but you can easily list them all by using this handy bit of code:
for (String id : ZoneId.getAvailableZoneIds())
System.out.println(id);
This for
loop will write the names of each ZoneId
to the console.
Once you know the name of the ZoneId you want to use, you can create it using ZoneId.of
, then use it to create a ZonedDateTime
as in this example:
ZoneId z = ZoneId.of("America/Los_Angeles");
ZonedDateTime zdate;
zdate = ZonedDateTime.of(2014, 12, 15, 0, 0, 0, 0, z);
Or, if you prefer, you can create the ZoneId
directly when you create the ZonedDateTime
:
zdate = ZonedDateTime.of(2014, 12, 15, 0, 0, 0, 0,
ZoneId.of("America/Los_Angeles"));
The of
method OffsetTime
and OffsetDateTime
classes use an additional class named ZoneOffset
to indicate the offset from UTC. You can create a ZoneOffset
by using any of the following methods of the ZoneOffset
class:
of(String offsetId)
ofHours(int hours)
ofHoursMinutes(int hours, int minutes)
ofHoursMinutesSeconds(int hours, int minutes, int seconds)
ofTotalSeconds(int totalSeconds)
For example, you can create a ZoneOffset
of -8 hours like this:
ZoneOffset z = ZoneOffset.ofHours(-8);
Alternatively, you could specify the offset as a string, as in this example:
ZoneOffset z = ZoneOffset.of("-08:00");
Note that when you use a string offset, you must provide two digits for the hours, minutes, and (optionally) seconds.
Once you have a ZoneOffset
object, you can use it to create an OffsetTime
, as in this example:
OffsetTime time = OffsetTime.of(10, 30, 0, 0, z);
Or if you prefer, you can create the ZoneOffset
directly in the call to the OffsetTime
's of
method:
OffsetTime time = OffsetTime.of(10, 30, 0, 0,
ZoneOffset.ofHours(-8));
The basic java.time
classes are similar enough that once you learn how to use one of them, you’ll find it easy to learn how to use the rest. Thus, for the rest of this chapter, I’ll focus on just one: the LocalDate
class. This class represents a date (year, month, and day) without an associated time. In this section and in the sections that follow, you’ll learn how to use many of the methods of this class to extract information about a date, to compare two dates, and to perform calculations on a date.
Table 4-3 shows the most commonly used methods of the LocalDate
class. For your convenience, this table includes the methods used to create LocalDate
objects, even though those methods have already been covered earlier in this chapter.
TABLE 4-3 Methods of the LocalDate Class
Method |
Explanation |
Methods that create a |
|
|
Creates a |
|
Creates a |
|
Creates a |
|
Creates a |
Methods that extract information about a date |
|
|
Returns the year. |
|
Returns the month as a |
|
Returns the month as an int from 1 through 12. |
|
Returns the day of the month. |
|
Returns the day of the week as a |
|
Returns the day of the year. |
|
Returns the number of days in this month. |
|
Returns the number of days in this year. |
Methods that compare dates |
|
|
Returns |
|
Returns |
|
Returns |
Methods that perform date calculations |
|
|
Returns a copy of the |
|
Returns a copy of the |
|
Returns a copy of the |
|
Returns a copy of the |
|
Returns a copy of the |
|
Returns a copy of the |
|
Returns a copy of the |
|
Returns a copy of the |
|
Returns the difference between this date and the specified date measured in the specified units. |
Several methods of the LocalDate
class let you extract useful information about a given date. For instance, the following example shows how you can extract the current year, month, and day:
LocalDate date = LocalDate.now();
int year = date.getYear();
int month = date.getMonthValue();
int day = date.getDayOfMonth();
If you need to know how many days into the year a particular date is, you can use this code:
LocalDate date = LocalDate.parse("2016-04-09");
System.out.println(date.getDayOfYear());
This example will print the number 100
, as April 9 is the 100th day of 2016.
The getDayOfWeek
method returns a value of type DayOfWeek
, which is an enumeration with the following values:
SUNDAY THURSDAY
MONDAY FRIDAY
TUESDAY SATURDAY
WEDNESDAY
Here’s an example of how you might use this method:
LocalDate date = LocalDate.parse("2016-04-09");
System.out.println(date.getDayOfWeek());
In this example, the string SATURDAY
will be printed because in 2016, April 9 falls on a Saturday.
The lengthOfMonth
and lengthOfYear
are useful if you want to know the number of days in the month or year represented by a LocalDate
. Both methods take into account leap years.
You can’t compare Date-Time objects using Java’s standard comparison operators. Consider the following example:
if (LocalDate.now() == LocalDate.now())
System.out.println("All is right in the universe.");
else
System.out.println("There must be a disturbance " +
"in the space-time continuum!");
If you run this code, There must be a disturbance in the space-time continuum!
will be printed. That’s because when used on objects, the equality operator tests whether two expressions refer to the same object, not to objects with the same value.
To test the equality of two dates, you must use the isEqual
method, as in this example:
if (LocalDate.now().isEqual(LocalDate.now()))
System.out.println("All is right in the universe.");
Similarly, you must use either the isBefore
or the isAfter
method to determine whether one date falls before or after another date.
Note that you can use built-in operators with methods that return integer results. Thus, the following code will work just as you would expect:
if (LocalDate.now().getDayOfMonth() < 15)
System.out.println("It is not yet the 15th.");
Because the getDayOfMonth
method returns an integer, you can use the <
operator to determine if the 15th of the month has yet arrived.
Just as you cannot use Java's built-in comparison operators with dates, you also may not use built-in mathematical operators. Instead, you can perform addition and subtraction on dates using the various plus
and minus
methods, and you can determine the difference between two dates by using the until
method.
The plus
and minus
methods let you add various date and time units to a Date-Time object. Table 4-3 lists four variants of each for the LocalDate
class, allowing you to add or subtract years, months, weeks, and days to a LocalDate
object. The following code prints the current date, tomorrow’s date, and the date one week, one month, and one year from now:
System.out.println("Today: " + LocalDate.now());
System.out.println("Tomorrow: " + LocalDate.now().plusDays(1));
System.out.println("Next week: " + LocalDate.now().plusWeeks(1));
System.out.println("Next month: " + LocalDate.now().plusMonths(1));
System.out.println("Next year: " + LocalDate.now().plusYears(1));
LocalDate date1 = LocalDate.parse("2014-05-16");
LocalDate date2 = LocalDate.parse("2014-12-15");
System.out.println(date1.until(date2, ChronoUnit.DAYS));
Some date calculations can be a bit more complex. For example, consider a business that prepares invoices on the 15th of each month. The following snippet of code displays the number of days from the current date until the next invoicing date:
LocalDate today = LocalDate.now();
LocalDate invDate = LocalDate.of(today.getYear(),
today.getMonthValue(), 15);
if (today.getDayOfMonth() > 15)
invDate = invDate.plusMonths(1);
long daysToInvoice = today.until(invDate,
ChronoUnit.DAYS);
System.out.println(daysToInvoice
+ " until next invoice date.");
This example works by first getting the current date, then creating a new LocalDate
object that represents the 15th of the current month. Then, if the current day of the month is greater than 15, it adds one month to the invoicing date. In other words, if it is the 16th or later, invoicing occurs on the 15th of the following month, not of this month. Then it uses the until
method to determine the number of days between the current date and the next invoicing date.
CENTURIES
DAYS
DECADES
ERAS
FOREVER
HALF-DAYS
HOURS
MICROS
MILLENNIA
MILLIS
MINUTES
MONTHS
NANOS
SECONDS
WEEKS
YEARS
Most of these are self-explanatory, but two of them are a bit peculiar:
ERA
indicates whether the date refers to the Common Era (CE, also known as AD) or Before Era (BCE, also known as BC).FOREVER
represents the largest value that can be represented as a duration. Sadly, Java won’t let you live forever. The following code throws an exception:
LocalDate birthday = LocalDate.parse("1959-05-16);
birthday = birthday.plus(1,ChronoUnit.FOREVER);
Note that ChronoUnit
is in the java.time.temporal
package, so be sure to include the following statement at the top of any program that uses ChronoUnit
:
import java.time.temporal.*;
If you use the toString()
method to convert a LocalDate
to a string, you get a string such as 2014-10-31
. What if you want to display the date in a different format, such as 10-31-2014
or October 31, 2014
? To accomplish that, you can use the format
method of the LocalDate
class along with a custom formatter you create using the DateTimeFormatter
class. To specify the format you want to use, you pass the DateTimeFormatter
class a pattern string, using the formatting symbols listed in Table 4-4.
TABLE 4-4 Formatting Characters for the DateTimeFormatter Class
Format Pattern |
Explanation |
|
Year (two or four digits) |
|
Month (one or two digits or three or more letters) |
|
Day of month (such as |
|
Hour |
|
Minute |
|
Second ( |
|
Clock hour ( |
|
|
|
Time zone ID (such as |
|
Time zone name (such as |
The easiest way to create a DateTimeFormatter
object is to use the static ofPattern
method along with a pattern string. For example:
DateTimeFormatter formatter;
formatter = DateTimeFormatter.ofPattern("dd MMM YYYY");
This formatter produces dates formatted like 04 SEP 2014
. You can then use the formatter to produce a formatted date
string like this:
LocalDate date = LocalDate.now();
String formattedDate = date.format(formatter);
Here’s a simple program that prints the current date in several different formats:
import java.util.*;
import java.time.*;
import java.time.format.*;
public class FormatDateTime
{
public static void main(String[] args)
{
LocalDateTime now = LocalDateTime.now();
printDate(now, "YYYY-MM-dd");
printDate(now, "MM-dd-YYYY");
printDate(now, "dd MMM YYYY");
printDate(now, "MMMM d, YYYY");
printDate(now, "HH:mm");
printDate(now, "h:mm a");
}
public static void printDate(LocalDateTime date, String pattern)
{
DateTimeFormatter f;
f = DateTimeFormatter.ofPattern(pattern);
pattern = (pattern + " ").substring(0, 14);
System.out.println(pattern + " " + date.format(f));
}
}
When you run this program, you’ll get console output that resembles this:
YYYY-MM-dd 2013-10-09
MM-dd-YYYY 10-09-2013
dd MMM YYYY 09 Oct 2013
MMMM d, YYYY October 9, 2013
HH:mm 20:29
h:mm a 8:29 PM
pattern = (pattern + " ").substring(0, 14);
Here a string of 14 spaces is added to the pattern
string, then a 14-character-long substring is taken starting at the first character. I figured the nice spacing in the output would make it easier for you to see the effect of each of the pattern strings.
Now that you’ve seen the techniques for working with Date-Time objects, it’s time to look at a complete programming example. Listing 4-1 presents a program that prompts the user to enter his or her birthday and then prints a variety of interesting information deduced from the date, including:
Here’s an example of the BirthdayFun
application in action:
Today is October 9, 2013.
Please enter your birthdate (yyyy-mm-dd): 1959-12-15
December 15, 1959 was a very good day!
You were born on a TUESDAY.
You are 53 years young.
Your next birthday is December 15, 2013.
That's just 67 days from now!
Your half-birthday is June 15.
Another? (Y or N) N
LISTING 4-1 The BirthdayFun Application
import java.util.*;
import java.time.*;→2
import java.time.format.*;
import java.time.temporal.*;
public class BirthdayFun
{
static Scanner sc = new Scanner(System.in);→8
public static void main(String[] args)
{
do
{
LocalDate birthDate;→14
DateTimeFormatter fullFormat =→16
DateTimeFormatter.ofPattern("MMMM d, YYYY");
DateTimeFormatter monthDayFormat =
DateTimeFormatter.ofPattern("MMMM d");
System.out.println("Today is "→21
+ LocalDate.now().format(fullFormat) + ".");
System.out.println();
System.out.print("Please enter your birthdate "
+ "(yyyy-mm-dd): ");
String input = sc.nextLine();
try
{
birthDate = LocalDate.parse(input);→30
if (birthDate.isAfter(LocalDate.now()))→32
{
System.out.println("You haven't been born yet!");
continue;
}
System.out.println();
System.out.println(birthDate.format(fullFormat)→40
+ " was a very good day!");
DayOfWeek birthDayOfWeek = birthDate.getDayOfWeek();→43
System.out.println("You were born on a "
+ birthDayOfWeek + ".");
long years = birthDate.until(LocalDate.now(),→47
ChronoUnit.YEARS);
System.out.println("You are " + years + " years young.");
LocalDate nextBDay = birthDate.plusYears(years + 1);→51
System.out.println("Your next birthday is "
+ nextBDay.format(fullFormat) + ".");
long wait = LocalDate.now().until(nextBDay,→55
ChronoUnit.DAYS);
System.out.println("That's just " + wait
+ " days from now!");
LocalDate halfBirthday = birthDate.plusMonths(6);→60
System.out.println("Your half-birthday is "
+ halfBirthday.format(monthDayFormat) + ".");
}
catch (DateTimeParseException ex)
{
System.out.println("Sorry, that is not a valid date.");
}
} while(askAgain());
}
private static boolean askAgain()
{
System.out.println();
System.out.print("Another? (Y or N) ");
String reply = sc.nextLine();
if (reply.equalsIgnoreCase("Y"))
{
return true;
}
return false;
}
}
The following paragraphs explain the most important lines in this program:
java.time
, java.time.format
, and java.time.temporal
.Scanner
is used to get the user input. The Scanner
is defined as a class variable so that it can be accessed by both the main
and the askAgain
methods.birthdate
variable is used to store the birthdate entered by the user.fullFormat
formats the date in full-text format (such as December 15, 1959) and monthDay Format
formats the date as just a month and day.if
statement ensures that the user has not entered a date in the future.