Skip to content

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

Open
patrols wants to merge 1 commit into
lau:masterfrom
patrols:otp29-calendar-24h-fix
Open

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

Conversation

@patrols

@patrols patrols commented Jun 4, 2026

Copy link
Copy Markdown

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
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

1 participant