Skip to content

Fix :calendar crash on OTP 29 for IANA "24:00" transition times (fixes #166)#169

Merged
lau merged 1 commit into
lau:masterfrom
patrols:otp29-calendar-24h-fix
Jun 16, 2026
Merged

Fix :calendar crash on OTP 29 for IANA "24:00" transition times (fixes #166)#169
lau merged 1 commit into
lau:masterfrom
patrols:otp29-calendar-24h-fix

Conversation

@patrols

@patrols patrols commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Fixes #166.

Problem

Erlang/OTP 29 tightened :calendar.time_to_seconds/1 to reject hours outside 0..23. The IANA tz database legally uses "24:00" for end-of-day transition boundaries, which Tzdata.Util.time_for_rule/2 parses into an hour-24 datetime (e.g. {{Y, M, D}, {24, 0, 0}}).

Tzdata.PeriodBuilder.datetime_to_utc/3 and Tzdata.Util.datetime_to_utc/3 pass that datetime to :calendar.datetime_to_gregorian_seconds/1 (which calls time_to_seconds/1 internally), so on OTP 29 the :tzdata_release_updater crashes during period building (as reported in #166):

** (FunctionClauseError) no function clause matching in :calendar.time_to_seconds/1
   (stdlib 8.0) calendar.erl:873: :calendar.time_to_seconds({24, 0, 0})
   (stdlib 8.0) calendar.erl:275: :calendar.datetime_to_gregorian_seconds/1
   (tzdata 1.1.3) lib/tzdata/period_builder.ex:381: Tzdata.PeriodBuilder.datetime_to_utc/3

OTP ≤ 28 accepted {24, 0, 0} and returned the value for the equivalent next-day 00:00, so this only surfaces after upgrading to OTP 29.

Fix

Add Tzdata.Util.datetime_to_gregorian_seconds/1, which computes the value directly:

:calendar.date_to_gregorian_days(date) * 86_400 + hour * 3600 + minute * 60 + second

This handles any hour (including 24) and is numerically identical to :calendar.datetime_to_gregorian_seconds/1 for hours 0–23 — 24:00:00 on day D ≡ 00:00:00 on day D+1. Both datetime_to_utc/3 implementations route through it.

Tests

  • Added a regression test in test/tz_util_test.exs covering :utc / :standard / :wall modifiers with a 24:00 datetime.
  • Full suite green on OTP 29.0.1 / Elixir 1.20.0 and OTP 28.5 / Elixir 1.19.5.

🤖 Generated with Claude Code

Erlang/OTP 29 tightened :calendar.time_to_seconds/1 to reject hours
outside 0..23. The IANA tz database legally uses "24:00" for end-of-day
transition boundaries, which time_for_rule/transform_until_datetime parse
to an hour-24 datetime (e.g. {{2024, 3, 31}, {24, 0, 0}}). Passing that to
:calendar.datetime_to_gregorian_seconds/1 — which calls time_to_seconds/1
internally — crashed period building on OTP 29 with a FunctionClauseError.
OTP <= 28 accepted it and returned the value for the equivalent next-day
00:00.

Add Tzdata.Util.datetime_to_gregorian_seconds/1, which computes the value
directly (date_to_gregorian_days * 86400 + h*3600 + m*60 + s) so any hour
(including 24) is handled, matching the pre-OTP-29 result. Route both
Tzdata.Util.datetime_to_utc/3 and Tzdata.PeriodBuilder.datetime_to_utc/3
through it. Add a regression test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@grempe grempe mentioned this pull request Jun 12, 2026
@lau lau merged commit d7aa2d8 into lau:master Jun 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Erlang 29 issue

2 participants