Why is subtracting these two epoch-milli Times (in year 1927) giving a strange result?

Why is subtracting these two epoch-milli Times (in year 1927) giving a strange result?

Introduction

When working with date and time in Java, you might encounter unexpected results due to historical timezone changes. One such case is the timezone transition in Shanghai (Asia/Shanghai) on December 31, 1927, where clocks moved back by 5 minutes and 52 seconds at midnight. This leads to unexpected results when subtracting timestamps that should be exactly 1 second apart.

This article explains why this happens and how to handle such issues in Java.


Problem Statement

Consider the following Java code:

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

public class TimezoneIssue {
    public static void main(String[] args) throws ParseException {
        SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");  
        String str3 = "1927-12-31 23:54:07";  
        String str4 = "1927-12-31 23:54:08";  

        Date sDt3 = sf.parse(str3);  
        Date sDt4 = sf.parse(str4);  

        long ld3 = sDt3.getTime() / 1000;  
        long ld4 = sDt4.getTime() / 1000;

        System.out.println(ld4 - ld3);  // Expected: 1, Actual: 353
    }
}

Expected Output

Since str3 and str4 are one second apart, we expect:

1

Actual Output

353

This means that Java considers 1927-12-31 23:54:08 to be 353 seconds after 1927-12-31 23:54:07, which is incorrect based on our expectation.


Root Cause: Timezone Transition in Shanghai (1927-12-31)

The unexpected time difference is due to a historical timezone shift in Shanghai.

  • On December 31, 1927, at midnight, Shanghai’s local time changed by -5 minutes and 52 seconds.
  • The specific transition was:
  • Old offset: UTC+08:05:52
  • New offset: UTC+08:00:00
  • This means that 1927-12-31 23:54:08 actually happened twice—once with the old offset and once with the new offset.
  • Java’s SimpleDateFormat defaults to the later offset, causing a jump of 353 seconds.

Solutions and Best Practices

1. Use UTC Instead of Local Timezones

One of the safest ways to avoid such timezone-related issues is to store and process dates in UTC.

Modify the SimpleDateFormat to use UTC:

sf.setTimeZone(TimeZone.getTimeZone("UTC"));

Updated Code:

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.TimeZone;

public class TimezoneSolution {
    public static void main(String[] args) throws Exception {
        SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        sf.setTimeZone(TimeZone.getTimeZone("UTC")); // Use UTC to avoid timezone shifts

        String str3 = "1927-12-31 23:54:07";
        String str4 = "1927-12-31 23:54:08";

        Date sDt3 = sf.parse(str3);
        Date sDt4 = sf.parse(str4);

        long ld3 = sDt3.getTime() / 1000;
        long ld4 = sDt4.getTime() / 1000;

        System.out.println(ld4 - ld3);  // Correct output: 1
    }
}

2. Use Java 8+ java.time API for Better Handling

Java 8 introduced a more reliable java.time API, which helps handle timezone transitions correctly.

Using ZonedDateTime to Detect Overlaps

import java.time.*;
import java.time.format.DateTimeFormatter;

public class JavaTimeSolution {
    public static void main(String[] args) {
        DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        ZoneId shanghai = ZoneId.of("Asia/Shanghai");

        String str3 = "1927-12-31 23:54:07";
        String str4 = "1927-12-31 23:54:08";

        ZonedDateTime zdt3 = LocalDateTime.parse(str3, dtf).atZone(shanghai);
        ZonedDateTime zdt4 = LocalDateTime.parse(str4, dtf).atZone(shanghai);

        Duration duration = Duration.between(zdt3, zdt4);
        System.out.println(duration.getSeconds()); // Outputs: 353
    }
}

Detecting Overlap and Correcting It

ZoneOffsetTransition zot = shanghai.getRules().getTransition(zdt4.toLocalDateTime());
if (zot != null && zot.isOverlap()) {
    System.out.println("Timezone Overlap Detected: " + zot.getDuration().getSeconds() + " seconds");
}

3. Always Specify Timezone in Input and Output

If you must work with local time, always explicitly specify the timezone in both input and output.

SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z");
sf.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai"));

Lessons Learned

  1. Never assume time calculations are straightforward. Timezones, daylight savings, and historical shifts can create unexpected results.
  2. Use UTC whenever possible to avoid issues with historical timezone transitions.
  3. If using local time, always specify the timezone explicitly when parsing and formatting dates.
  4. Java 8’s java.time API provides better timezone handling than java.util.Date and SimpleDateFormat.
  5. Historical date/time data may change due to updates in the timezone database (TZDB).

Conclusion

The unexpected 353 second difference in Java’s date calculation was due to a historical timezone shift in Shanghai on December 31, 1927. This case highlights the importance of using UTC, modern Java APIs, and timezone-aware programming practices when dealing with date/time operations.

For more information on timezone transitions, visit:

By following best practices and using reliable date-time APIs, you can avoid similar issues in your Java applications.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *