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
- Never assume time calculations are straightforward. Timezones, daylight savings, and historical shifts can create unexpected results.
- Use UTC whenever possible to avoid issues with historical timezone transitions.
- If using local time, always specify the timezone explicitly when parsing and formatting dates.
- Java 8’s
java.time
API provides better timezone handling thanjava.util.Date
andSimpleDateFormat
. - 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.