tl;dr
LocalDateTime.of( // A `LocalDateTime` represents a set of *potential* moments along a range of about 26-27 hours. Not an actual moment, not a point on the timeline.
LocalDate.systemDefault() , // Get the JVM’s current default time zone. Can change at any moment *during* runtime. When crucial, always confirm with the user.
LocalTime.parse( "14:57" ) // Parse a string in standard ISO 8601 format as a time-of-day without regard for time zone or offset-from-UTC.
) // Returns a `LocalDateTime` object.
.atZone( // Determine an actual moment, a point on the timeline.
ZoneId( "Africa/Tunis" ) // Specify a time zone as `Continent/Region`, never as 3-4 letter pseudo-zones such as `PST`, `CST`, or `IST`.
) // Returns a `ZonedDateTime` object.
.toInstant() // Extracts a `Instant` object from the `ZonedDateTime` object, always in UTC by default.
Details
At the moment it is a bit confusing
Date-time handling is very confusing work.
Tips:
- Forget about your own time zone. Think in terms of UTC rather than your own parochial time zone.
- Learn the difference between real moments (points on the timeline), and date-time approximations that are not on the timeline, often-called “local” values.
- Be careful when reading Stack Overflow or other places on the internet about date-time. You will encounter poor advice and many wrong solutions.
with all the choices; Timestamp, Date, Localtime etc.
Never use the troublesome old legacy date-time classes bundled with the earliest versions of Java. Never use java.sql.Timestamp
, java.util.Date
, java.util.Calendar
, and so on.
➡ Use only classes in the java.time package.
The java.time classes are an industry-leading date-time framework. Extremely well-designed and thought-through, with lessons learned from the Joda-Time project it succeeds.
Anyone has a clue how to do this or could provide some tips / best practices?
You might be sorry you asked. Read on.
At the moment I got a class 'Flight' with Date datatypes; departure and arrival datetime.
So define a Flight
class.
In real-life, flights happen far enough out in the future that we risk politicians changing the definition of the time zone. Most commonly these changes are adopting/dropping/altering Daylight Saving Time (DST). But arbitrary changes are made periodically for all kinds of reasons. We could debate the wisdom/sanity of such changes, but the fact is they happen. They happen quite frequently as politicians seemly oddly prone to making these changes around the world in many countries. And nearly all of them do so with little forewarning, sometimes just weeks. Or even with no warning at all, as North Korea did this week.
I have no understanding of how airlines actually work, but from poking around airline schedules and various readings, it seems they try to maintain their schedules using the zoned time of the departing locality. So if a flight is scheduled to depart LAX at 6 AM, they keep that flight schedule on the day before, the day of, and the day after a DST change-over. If this is indeed the general intent, that means sitting-around killing time on one DST cut-over while trying to save an hour on the opposite DST cut-over. Apparently, Amtrak adopts this practice for its trains. Let’s proceed with this approach.
Using this “imaginary” schedule approach means we cannot determine for certain the exact moment when 6 AM will occur in the future. So we need to record our desire for that date and that time-of-day without applying a time zone. But we must record the desired time zone so we know in what context we can later determine the exact moment, when close enough in time that we needn’t worry about zone changes.
So we use LocalDate
and LocalTime
types, as they purposely lack any concept of time zone (a name in Continent/Region
format) or offset-from-UTC (a number of hours-minutes-seconds).
The ZoneId
class represents a time zone.
I am using the word Unzoned
in the names to remind us that these values do not represent actual moments on the timeline. The word “local” tends to confuse beginners.
public class Flight {
private String flightNumber;
private LocalDate departureDateUnzoned;
private LocalTime departureTimeUnzoned;
private ZoneId departureZoneId ;
}
As for arrival, store the span-of-time expected for that flight rather than the arrival date-time. You can calculate the arrival, so no need to store it. The Duration
class tracks a number of hours, minutes, seconds, and fractional second.
To calculate the arrival, let’s return a single value using the LocalDateTime
class, which simply combines a LocalDate
with a LocalTime
. We could have used this type to make a single departureUnzoned
member variable in our class definition. I went with separate LocalDate
and LocalTime
as building blocks so you would understand the pieces. So many programmers use their intuition rather than the documentation to assume that LocalDateTime
means a specific moment in a locality when actually it means just the opposite. (You will find many incorrect Answers on Stack Overflow advising LocalDateTime
when actually Instant
or ZonedDateTime
should be used.)
Let's add a method to calculate that arrival.
public class Flight {
private String flightNumber;
private LocalDate departureDateUnzoned;
private LocalTime departureTimeUnzoned;
private ZoneId departureZoneId;
private Duration duration;
public LocalDateTime arrivalDateTimeUnzoned () {
LocalDateTime departureUnzoned = LocalDateTime.of( this.departureDateUnzoned , this.departureTimeUnzoned );
LocalDateTime ldt = departureUnzoned.plus( this.duration );
return ldt;
}
}
But this returned LocalDateTime
fails to account for time zone. Usually, airlines and train report to customers the expected arrival time adjusted into the time zone of that region. So we need an arrival time zone. And we can use that zone when calculating the arrival, thereby producing a ZonedDateTime
. A ZonedDateTime
is a specific moment, it is a point on the timeline, unlike LocalDateTime
. But remember, if we are scheduling flights out into the future, the calculated ZonedDateTime
will change if our code is run after politicians redefine the time zone.
public class Flight {
private String flightNumber;
private LocalDate departureDateUnzoned;
private LocalTime departureTimeUnzoned;
private ZoneId departureZoneId;
private Duration duration;
private ZoneId arrivalZoneId;
public ZonedDateTime arrivalDateTimeZoned () {
ZonedDateTime departureZoned = ZonedDateTime.of( this.departureDateUnzoned , this.departureTimeUnzoned , this.departureZoneId );
ZonedDateTime zdt = departureZoned.plus( this.duration );
return zdt;
}
}
Back to the part of your Question about determining the date automatically. That requires a time zone. For any given moment, the date varies around the globe. Think about that. A few minutes after midnight in Paris France is a new day, while still “yesterday” in Montréal Québec.
We can ask for the JVM’s current default time zone.
ZoneId userZoneId = ZoneId.systemDefault() ;
But when crucial, you must confirm with the user.
ZoneId userZoneId = ZoneId.of( "America/Montreal" ) ;
So now we can add the constructor you asked for, passing the time-of-day (a LocalTime
, and guessing the time zone by using the JVM’s current default.
But we still need all the other pieces. So defaulting the date does not save us much.
public class Flight {
private String flightNumber;
private LocalDate departureDateUnzoned;
private LocalTime departureTimeUnzoned;
private ZoneId departureZoneId;
private Duration duration;
private ZoneId arrivalZoneId;
// Constructor
public Flight ( String flightNumber , LocalTime departureTimeUnzoned , ZoneId departureZoneId , Duration duration , ZoneId arrivalZoneId ) {
this.flightNumber = flightNumber;
this.departureTimeUnzoned = departureTimeUnzoned;
this.departureZoneId = departureZoneId;
this.duration = duration;
this.arrivalZoneId = arrivalZoneId;
// Determine today’s date using JVM’s current default time zone. Not advisable in many business scenarios, but specified by our Question at hand.
ZoneId z = ZoneId.systemDefault();
LocalDate today = LocalDate.now( z );
this.departureDateUnzoned = today;
}
public ZonedDateTime arrivalDateTimeZoned () {
ZonedDateTime departureZoned = ZonedDateTime.of( this.departureDateUnzoned , this.departureTimeUnzoned , this.departureZoneId );
ZonedDateTime zdt = departureZoned.plus( this.duration );
return zdt;
}
}
Let’s add a toString
method for reporting.
We represent the date-time values as strings in standard ISO 8601 formats. The java.time classes use these standard formats when parsing/generating strings. The Z
on the end is pronounced Zulu
and means UTC
.
While airlines and trains report date-times to their customers in the regions’ time zones, we can assume they use only UTC internally. The Instant
class represents values in UTC specifically. So our toString
extracts Instant
objects from the ZonedDateTime
objects.
And we add a main
method for demonstration. Here is the complete class, with import
etc.
package com.basilbourque.example;
import java.time.*;
public class Flight {
private String flightNumber;
private LocalDate departureDateUnzoned;
private LocalTime departureTimeUnzoned;
private ZoneId departureZoneId;
private Duration duration;
private ZoneId arrivalZoneId;
// Constructor
public Flight ( String flightNumber , LocalTime departureTimeUnzoned , ZoneId departureZoneId , Duration duration , ZoneId arrivalZoneId ) {
this.flightNumber = flightNumber;
this.departureTimeUnzoned = departureTimeUnzoned;
this.departureZoneId = departureZoneId;
this.duration = duration;
this.arrivalZoneId = arrivalZoneId;
// Determine today’s date using JVM’s current default time zone. Not advisable in many business scenarios, but specified by our Question at hand.
ZoneId z = ZoneId.systemDefault();
LocalDate today = LocalDate.now( z );
this.departureDateUnzoned = today;
}
public ZonedDateTime arrivalDateTimeZoned () {
ZonedDateTime departureZoned = ZonedDateTime.of( this.departureDateUnzoned , this.departureTimeUnzoned , this.departureZoneId );
ZonedDateTime zdt = departureZoned.plus( this.duration );
return zdt;
}
@Override
public String toString () {
ZonedDateTime departureZoned = ZonedDateTime.of( this.departureDateUnzoned , this.departureTimeUnzoned , this.departureZoneId );
String flightInUtc = departureZoned.toInstant().toString() + "/" + this.arrivalDateTimeZoned().toInstant().toString();
return "Flight{ " +
"flightNumber='" + this.flightNumber + '\'' +
" | departureDateUnzoned=" + this.departureDateUnzoned +
" | departureTimeUnzoned=" + this.departureTimeUnzoned +
" | departureZoneId=" + this.departureZoneId +
" | departureZoned=" + departureZoned +
" | duration=" + this.duration +
" | arrivalZoneId=" + this.arrivalZoneId +
" | calculatedArrival=" + this.arrivalDateTimeZoned() +
" | flightInUtc=" + flightInUtc +
" }";
}
public static void main ( String[] args ) {
LocalTime lt = LocalTime.of( 6 , 0 ); // 6 AM.
Flight f = new Flight( "A472" , lt , ZoneId.of( "America/Los_Angeles" ) , Duration.parse( "PT6H37M" ) , ZoneId.of( "America/Montreal" ) );
String output = f.toString();
System.out.println( output );
}
}
When run.
Flight{ flightNumber='A472' | departureDateUnzoned=2018-05-06 | departureTimeUnzoned=06:00 | departureZoneId=America/Los_Angeles | departureZoned=2018-05-06T06:00-07:00[America/Los_Angeles] | duration=PT6H37M | arrivalZoneId=America/Montreal | calculatedArrival=2018-05-06T12:37-07:00[America/Los_Angeles] | flightInUtc=2018-05-06T13:00:00Z/2018-05-06T19:37:00Z }
To use this from the console, ask the user for the time-of-day in 24-hour clock. Parse the input string.
String input = "14:56" ; // 24-hour clock.
LocalTime lt = LocalTime.parse( input ) ;
This is far from complete for real-world work. But hopefully it makes for an educational example.
About java.time
The java.time framework is built into Java 8 and later. These classes supplant the troublesome old legacy date-time classes such as java.util.Date
, Calendar
, & SimpleDateFormat
.
The Joda-Time project, now in maintenance mode, advises migration to the java.time classes.
To learn more, see the Oracle Tutorial. And search Stack Overflow for many examples and explanations. Specification is JSR 310.
You may exchange java.time objects directly with your database. Use a JDBC driver compliant with JDBC 4.2 or later. No need for strings, no need for java.sql.*
classes.
Where to obtain the java.time classes?
The ThreeTen-Extra project extends java.time with additional classes. This project is a proving ground for possible future additions to java.time. You may find some useful classes here such as Interval
, YearWeek
, YearQuarter
, and more.