GLM 4.5 Air

Java Date Subtraction Unexpected Results Explained

Learn why Java date subtraction gives unexpected results when working with 1927 timestamps due to historical timezone changes in Shanghai. Complete guide to handling historical dates in Java.

Question

Why does subtracting epoch-millisecond timestamps from 1927 give an unexpected result in Java?

When I parse two date strings from 1927 that are 1 second apart and calculate their difference in seconds, I get 353 instead of 1. Here’s the Java code I’m using:

java
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);
}

The output is:

353

Why is ld4-ld3 not 1 (as I would expect from the one-second difference in the times), but 353?

Interestingly, if I change the dates to times 1 second later:

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

Then ld4-ld3 will be 1.

Environment details:

  • Java version: 1.6.0_22
  • Timezone: Asia/Shanghai (offset=28800000, dstSavings=0, useDaylight=false)
  • Locale: zh_CN
GLM 4.5 Air

Why does subtracting epoch-millisecond timestamps from 1927 give an unexpected result in Java?

The unexpected result you’re seeing is due to a historical timezone change in Shanghai in 1927. When you parse dates from this period, Java applies the timezone rules that were in effect at those times, which can lead to unexpected differences in timestamps.

Contents

The Core Issue: Historical Timezone Change

The unexpected result of 353 seconds instead of 1 occurs because on December 31, 1927, Shanghai (and China) implemented a significant timezone change. At midnight, the clocks were advanced by 5 minutes and 52 seconds (352 seconds) to align with China Standard Time (CST, UTC+8).

When your code parses:

  • 1927-12-31 23:54:07 - This is before the timezone change
  • 1927-12-31 23:54:08 - This is after the timezone change

Java correctly applies different timezone offsets to these two dates, resulting in a timestamp difference of 353 seconds (1 second actual time difference + 352 seconds timezone change).

Understanding the 1927 Shanghai Timezone Change

In 1927, China standardized its time to a single timezone (China Standard Time, CST) to replace the previous system where different regions used different local times. The transition occurred on December 31, 1927, at midnight:

Before the change:

  • Shanghai Local Time: UTC+8:05:52
  • This was the local time used in Shanghai before standardization

After the change:

  • China Standard Time: UTC+8
  • Applied uniformly across China

This meant that when the clock struck midnight on December 31, 1927, it effectively jumped forward by 5 minutes and 52 seconds (352 seconds). This is why your calculation shows a 353-second difference instead of the expected 1 second.

Why Your Code Produces Different Results

Let’s break down exactly why your code behaves differently with these two time pairs:

java
// First case - crosses the timezone boundary
String str3 = "1927-12-31 23:54:07";  // Before change
String str4 = "1927-12-31 23:54:08";  // After change
// Result: 353 seconds (1 second + 352 seconds timezone change)

// Second case - both times after the change
String str3 = "1927-12-31 23:54:08";  
String str4 = "1927-12-31 23:54:09";  
// Result: 1 second (no timezone change between these times)

The key difference is that your first example crosses the timezone change boundary at midnight on December 31, 1927. When SimpleDateFormat converts these dates to epoch milliseconds, it applies the appropriate timezone offset for each date:

  1. 1927-12-31 23:54:07 gets the old timezone offset (UTC+8:05:52)
  2. 1927-12-31 23:54:08 gets the new timezone offset (UTC+8)

This creates a discontinuity in the timeline, which is reflected in your calculation.

Best Practices for Handling Historical Dates

When working with historical dates, especially around timezone changes, consider these best practices:

  1. Be aware of historical timezone changes: Different regions have implemented various timezone changes throughout history, not just daylight saving time adjustments.

  2. Use modern date/time APIs: In Java 8 and later, use java.time package classes like ZonedDateTime, which handle historical timezone transitions more robustly.

  3. Explicitly specify timezones: Always specify the timezone when parsing dates to avoid ambiguity.

  4. Consider using UTC: For critical date calculations, especially when dealing with historical data, consider working in UTC and converting to local time only for display purposes.

  5. Test edge cases: When working with historical dates, specifically test around known timezone change dates.

Here’s how you might handle this using Java 8’s java.time:

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

public class HistoricalDateExample {
    public static void main(String[] args) {
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        ZoneId shanghaiZone = ZoneId.of("Asia/Shanghai");
        
        // First case - crosses the timezone boundary
        LocalDateTime dt3 = LocalDateTime.parse("1927-12-31 23:54:07", formatter);
        LocalDateTime dt4 = LocalDateTime.parse("1927-12-31 23:54:08", formatter);
        
        ZonedDateTime zdt3 = dt3.atZone(shanghaiZone);
        ZonedDateTime zdt4 = dt4.atZone(shanghaiZone);
        
        System.out.println("Difference in seconds: " + (zdt4.toEpochSecond() - zdt3.toEpochSecond()));
        
        // Second case - both times after the change
        LocalDateTime dt5 = LocalDateTime.parse("1927-12-31 23:54:08", formatter);
        LocalDateTime dt6 = LocalDateTime.parse("1927-12-31 23:54:09", formatter);
        
        ZonedDateTime zdt5 = dt5.atZone(shanghaiZone);
        ZonedDateTime zdt6 = dt6.atZone(shanghaiZone);
        
        System.out.println("Difference in seconds: " + (zdt6.toEpochSecond() - zdt5.toEpochSecond()));
    }
}

Alternative Approaches for Date Arithmetic

When you need to calculate differences between dates, especially around timezone changes, consider these approaches:

  1. Use Duration or Period: These classes in java.time are designed for date arithmetic and handle edge cases better than manual timestamp calculations.

  2. Work with LocalDateTime if timezone isn’t needed: If you’re only interested in the local time differences without timezone considerations, use LocalDateTime.

  3. Be explicit about timezone interpretations: When parsing dates, you can specify how to handle ambiguous or non-existent times.

  4. Use Joda-Time for older Java versions: If you’re stuck with Java 6 or 7, consider using Joda-Time, which provides more robust date/time handling than the legacy java.util.Date.

Here’s an example using Duration:

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

public class DurationExample {
    public static void main(String[] args) {
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        
        // Parse dates as LocalDateTime (timezone not considered)
        LocalDateTime dt3 = LocalDateTime.parse("1927-12-31 23:54:07", formatter);
        LocalDateTime dt4 = LocalDateTime.parse("1927-12-31 23:54:08", formatter);
        
        Duration duration = Duration.between(dt3, dt4);
        System.out.println("Duration in seconds: " + duration.getSeconds());
    }
}

This would give you the expected result of 1 second because it’s calculating the difference between two local times without considering timezone changes.

Conclusion and Recommendations

The unexpected result you encountered occurs because Java correctly accounts for the historical timezone change that happened in Shanghai on December 31, 1927. When your parsed dates cross this boundary, the timezone difference is factored into the timestamp calculation.

To avoid such issues:

  1. Be mindful of historical timezone changes when working with dates from before the modern timezone system was standardized.

  2. Upgrade to modern Java date/time APIs if possible - the java.time package in Java 8+ handles these scenarios much better.

  3. Consider your use case: If you’re calculating differences between local times (without needing to relate to absolute points in time), working with LocalDateTime might be more appropriate.

  4. Document your assumptions: When working with historical dates, clearly document how you’re handling timezone considerations.

The 353-second difference you observed isn’t a bug in Java - it’s the correct behavior when accounting for the historical timezone change that occurred at midnight on December 31, 1927, in Shanghai.