Kazakhstan and the 25-Hour Leap Day
on
The Republic of Kazakhstan, the world’s largest landlocked country, has for decades been divided into two time zones. The Western part of the country has observed UTC+5 while the Eastern part, which contains major city Almaty and Kazakh capital Astana, has observed UTC+6.
This January, though, the Kazakh government announced a major change: the entire country would be unified under a single time zone, UTC+5. The cities in the East would need to set their clocks back an hour, at some designated point in time, in order to switch from UTC+6 to UTC+5.
The Kazakh government designated midnight on March 1st, 2024, as the time to make the switch. But this choice is an interesting one for two reasons, one bad and one, well, curious. First, the bad: this allows only two months for the world’s timekeeping software – in particular, the clocks in the smartphones and computers in Eastern Kazakhstan! – to “learn” about the time zone change. The mechanism by which software learns about it, the IANA time zone database, recommends at least a year lead time between an announced clock change and the date that it takes effect.
But there’s a more interesting facet of Kazakhstan’s time change: it’s this year’s leap day, February 29th, and it will be 25 hours long in Eastern Kazakhstan.
25-hour days aren’t all that special. Much of the world experiences them once every year:
it’s the annual day that daylight saving time ends, the “Fall back.” In the United States,
we “Fall back” (and “Spring forward”) at 2am local time. That means right before the clock
strikes 02:00
, it’s moved back to 01:00
, repeating the hour of time from 01:00
to
01:59:59...
. Likewise, “Spring forward” happens on the annual 23-hour day of the year.
The people in Almaty, Kazakhstan, for example, will change their clocks from 23:59:59...
this Thursday – the leap day – back to 23:00
and repeat the hour. That struck me as a
rather strange circumstance. Is this the first 25-hour leap day in history?
To answer that question, I turned to the Java programming language, its magnificent
java.time
library for date, time, and zone manipulation in particular. After updating
my Java installation to use the latest version tzdata, 2024a
(which includes the
Kazakhstan change), the snippet of code further down answered my question: no.
As it turns out, the following years saw 25-hour leap days somewhere in the world:
- *1932 (Mon): Argentina
- *1936 (Sat): Argentina
- *1940 (Thu): Argentina
- *1964 (Sat): Argentina, Brazil
- *1968 (Thu): Brazil
- 1976 (Sun): Paraguay
- 1992 (Sat): Argentina, Paraguay
- 1996 (Thu): Paraguay
- 2024 (Thu): Kazakhstan
The years before 1970 are marked with an * because, since they predate the range of time that tzdata aims to cover, the data is possibly inaccurate; one would need to research the historical references contained in tzdata’s source files. But the later years, like 1996, are far more likely to be correct. All of the other 25-hour leap days are actually instances where the end of daylight saving time, the “Fall back” – in the Southern hemisphere, mind you – happened to occur on March 1st at midnight.
Shucks, so Eastern Kazakhstan 2024 isn’t the first one in history. But it’s the first one this millennium anyway. To close out this magnificent piece of trivia, here’s that Java code to see for yourself:
final int minYear = 1900;
final MonthDay leapDay = MonthDay.of(Month.FEBRUARY, 29);
// All zone IDs known to Java
ZoneRulesProvider.getAvailableZoneIds().stream()
.map(
z -> {
// Get the stream of leap day transitions for this zone across all years
final ZoneRules rules = ZoneRulesProvider.getRules(z, false);
final List<ZoneOffsetTransition> leapDayOverlapTransitions =
// Need to assemble the transitions from...
Stream.concat(
// 1) the explicit zone offset transitions, and
rules.getTransitions().stream(),
// 2) the implicit transitions created from its rules (i.e. DST rules) for a wide range of years
IntStream.rangeClosed(minYear, Year.now().getValue())
.boxed()
.flatMap(
year -> rules.getTransitionRules().stream()
.map(zotr -> zotr.createTransition(year))))
// Filter to leap years within our bounds
.filter(zot -> {
final int year = zot.getDateTimeBefore().getYear();
return Year.isLeap(year) && year >= minYear;
})
// Filter to overlap transitions lasting one hour (which makes it a 25-hour day) ...
.filter(zot -> zot.isOverlap() && zot.getDuration().abs().equals(Duration.ofHours(1)))
// ... that occur on the leap day
.filter(zot -> {
// A transition happens on the leap day if its "before" time is either on the leap day, Feb 29,
// or at exactly midnight the day after. (The "before" time is never actually reached.) So just
// subtract a second from that "before" time and check for it being on the leap day.
return MonthDay.from(zot.getDateTimeBefore().minusSeconds(1))
.equals(leapDay);
})
// Order by time of the transition, i.e., effectively by year
.sorted(Comparator.comparingLong(ZoneOffsetTransition::toEpochSecond))
.toList();
return Map.entry(z, leapDayOverlapTransitions);
}
)
// For each zone entry, expand it to a flat `(year, (zone, leap transition))` stream
.flatMap(
e -> e.getValue().stream().map(
leapTransition -> {
final String zone = e.getKey();
final int year = leapTransition.getDateTimeBefore().getYear();
return Map.entry(year, Map.entry(zone, leapTransition));
}))
// Sort the entries by year
.sorted(Map.Entry.comparingByKey())
.forEach(System.out::println);