Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Date.toString() adds an hour around epoch in London

Tags:

java

date

How can I get Date.toString() to produce an output that SimpleDateFormat can parse correctly for Dates around 1 Jan 1970 (I assume this applies to winter of 1968 and 1969 as well)

If I run the following,

System.out.println(TimeZone.getDefault());
Date date = new Date(0);
SimpleDateFormat sdf = new SimpleDateFormat("EEE MMM dd HH:mm:ss zzz yyyy");
Date date2 = sdf.parse(date.toString());
System.out.println("date: " + date);
System.out.println("date2: " + date2);
Date date3 = sdf.parse(date2.toString());
System.out.println("date3: " + date3);

This prints

sun.util.calendar.ZoneInfo[id="Europe/London",offset=0,dstSavings=3600000,useDaylight=true,transitions=242,lastRule=java.util.SimpleTimeZone[id=Europe/London,offset=0,dstSavings=3600000,useDaylight=true,startYear=0,startMode=2,startMonth=2,startDay=-1,startDayOfWeek=1,startTime=3600000,startTimeMode=2,endMode=2,endMonth=9,endDay=-1,endDayOfWeek=1,endTime=3600000,endTimeMode=2]]
date: Thu Jan 01 01:00:00 GMT 1970
date2: Thu Jan 01 02:00:00 GMT 1970
date3: Thu Jan 01 03:00:00 GMT 1970

The problem is that London was in BST on 1 Jan 1970. So the correct date should be either

date: Thu Jan 01 01:00:00 BST 1970

or

date: Thu Jan 01 00:00:00 GMT 1970

but it seems a confusion of the two.

And while I would love to not support java.util.Date, it's not an option for me.

like image 470
Peter Lawrey Avatar asked Dec 17 '22 18:12

Peter Lawrey


2 Answers

tl;dr

Your input is invalid as BST (British Summer Time) was not in effect during the winter.

BST cannot be reliably parsed, as it is a non-standard non-unique pseudo-zone.

There is no need to mess around with SimpleDateFormat. Let the modern java.time classes do the heavy lifting.

And while I would love to not support java.util.Date, it's not an option for me.

At the edges of your code, convert to-from the legacy and modern classes.

// Convert from legacy to modern.
Instant instant = myJavaUtilDate.toInstant() ;

// Convert from modern to legacy.
java.util.Date myJavaUtilDate = Date.from( instant ) ;

No BST in winter

Apparently the “B” in your BST is meant to be British. But BST in that context means British Summer Time. This means Daylight Saving Time (DST) which is engaged in the summer time, not the winter. So your input string of a January date combined with BST is nonsensical.

Double-Summertime

There is a further complication to your example of a moment in 1970 with a British time zone.

The practice of DST in Britain using an offset of one hour ahead of UTC (+01:00) in summer, and an offset of zero (+00:00) in the winter for Standard Time is current practice. That has not always been the case.

Back in 1970, Britain was trialling a “double-summertime”. In that experiment of 1968-1971, winter time was one hour ahead of UTC rather than zero, and summer time was two hours ahead of UTC instead of the one hour used nowadays. This put British time more in common with continental Europe and was hoped to reduce accidents.

So if we adjust a moment in January of 1970, we expect to jump to one hour ahead for time zone Europe/London. Whereas a moment in January of 2019, we expect no jump, the time-of-day in Britain will be the same as UTC (an offset-from-UTC of zero hours-minutes-seconds).

Avoid pseudo-zones

Avoid these 2-4 character pseudo-zones such as BST. They are not standardized. They are not even unique! So BST can be interpreted to be the time zone Pacific/Bougainville just as well as British Summer Time.

Specify a proper time zone name in the format of Continent/Region, such as America/Montreal, Africa/Casablanca, or Pacific/Auckland. Never use the 2-4 letter abbreviation such as BST or EST or IST as they are not true time zones, not standardized, and not even unique(!).

Convert

You can convert between the legacy and modern date-time classes easily. New conversion methods have been added to the old classes. Look for from, to, and valueOf methods, per the naming conventions.

  • java.util.Datejava.time.Instant
  • java.util.GregorianCalendarjava.time.ZonedDateTime
  • java.sql.Datejava.time.LocalDate
  • java.sql.Timestampjava.time.Instant

Converting

Your input string of 00:00 on January 1, 1970 happens to be the epoch reference date used by both the legacy and modern date-time classes. We have a constant for that value.

Instant epoch = Instant.EPOCH ;

instant.toString(): 1970-01-01T00:00:00Z

See that same moment through your time zone of Europe/London.

ZoneId z = ZoneId.of( "Europe/London" ) ;
ZonedDateTime zdt = epoch.atZone( z ) ;

zdt.toString(): 1970-01-01T01:00+01:00[Europe/London]

Notice that above-mentioned Double-Summertime experiment in effect then. If we try the same code for 2019, we get an offset-from-UTC of zero.

ZonedDateTime zdt2019 = 
    Instant
    .parse( "2019-01-01T00:00Z" )
    .atZone( ZoneId.of( "Europe/London" ) )
;

zdt2019.toString(): 2019-01-01T00:00Z[Europe/London]

To convert to a java.util.Date, we need an java.time.Instant object. An Instant represents a moment in UTC. We can extract an Instant from our ZonedDateTime object, effectively adjusting from a zone to UTC. Same moment, different wall-clock time.

Instant instant = zdt.toInstant(): 

We should now be back where we started, at the epoch reference date of 1970-01-01T00:00:00Z.

instant.toString(): 1970-01-01T00:00:00Z

To get the java.util.Date object you may need to interoperate with old code not yet updated to java.time classes, use the new Date.from method added to the old class.

java.util.Date d = Date.from( instant ) ; // Same moment, but with possible data-loss as nanoseconds are truncated to milliseconds.

d.toString(): Thu Jan 01 00:00:00 GMT 1970

By the way, be aware of possible data-loss when converting from Instant to Date. The modern classes have a resolution of nanoseconds while the legacy classes use milliseconds. So part of your fractional second may be truncated.

See all the code above run live at IdeOne.com.

To convert the other direction, use the Date::toInstant method.

Instant instant = d.toInstant() ;

ISO 8601

Avoid using text in custom formats for exchanging date-time values. When serializing date-time values as human-readable text, use only the standard ISO 8601 formats. The java.time classes use these formats by default.

Those strings you were experimenting with parsing are a terrible format and should never be used for data-exchange.


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?

  • Java SE 8, Java SE 9, Java SE 10, Java SE 11, and later - Part of the standard Java API with a bundled implementation.
  • Java 9 adds some minor features and fixes.
  • Java SE 6 and Java SE 7
  • Most of the java.time functionality is back-ported to Java 6 & 7 in ThreeTen-Backport.
  • Android
  • Later versions of Android bundle implementations of the java.time classes.
  • For earlier Android (<26), the ThreeTenABP project adapts ThreeTen-Backport (mentioned above). See How to use ThreeTenABP….

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.

like image 78
Basil Bourque Avatar answered Dec 31 '22 08:12

Basil Bourque


I may cite https://bugs.openjdk.java.net/browse/JDK-6609362?jql=text%20~%20%22epoch%20gmt%22:

Please use Z to format and parse historical time zone offset changes to avoid confusions with historical time zone name changes.


public class Test {
    public static void main(String[] args) throws ParseException {
        TimeZone.setDefault(TimeZone.getTimeZone("Europe/London"));
        SimpleDateFormat f = new SimpleDateFormat("EEE MMM dd HH:mm:ss Z yyyy");
        Date d = new Date(0);
        for (int i = 0; i < 10; i++) {
            String s = f.format(d);
            System.out.println(s);
            d = f.parse(s);
        }
    } } ```
like image 26
Jens Dibbern Avatar answered Dec 31 '22 10:12

Jens Dibbern