From c5ef8c849bd600ff2297c170a7f63a7a615c9fe2 Mon Sep 17 00:00:00 2001 From: Christopher De Vries Date: Wed, 24 Sep 2025 09:47:09 -0400 Subject: [PATCH 1/4] Considering issues with times being in unix leap time. I put in a correction to the atomic_difference which fixes that, but I am wondering if I should always consider system_time to be unix time or maybe it should be unix leap time (if leap seconds are defined). The weird thing is that days are exactly every 24 hours in unix leap time, meaning that the unix leap day is 27 seconds before the unix day, but daylight savings shifts are at the same time, meaning that daylight shifts start at 2:00:27 am in unix leap time. Also, most time zone files ignore the existance of leap seconds. Maybe I should have a leaptimestamp? I am not exactly sure how to best proceed. --- src/tzif/database.gleam | 6 +++++- src/tzif/tzcalendar.gleam | 2 +- test/tzif/tzcalendar_test.gleam | 22 ++++++++++++++++++++++ 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/tzif/database.gleam b/src/tzif/database.gleam index 56954e3..3126f24 100644 --- a/src/tzif/database.gleam +++ b/src/tzif/database.gleam @@ -201,12 +201,16 @@ pub fn leap_seconds( let #(ts_seconds, _) = timestamp.to_unix_seconds_and_nanoseconds(ts) + // Converting the leapseconds to unix time rather than monotonic time, but the "right/" + // timezones assume monotonic time. I am assuming timestamp represents + // unix time. case list.length(tzdata.fields.leapsecond_values) { 0 -> Error(InfoNotFound) _ -> { tzdata.fields.leapsecond_values + |> list.map(fn(ls_tuple) { #(ls_tuple.0 - ls_tuple.1 + 1, ls_tuple.1) }) |> list.fold_until(Ok(0), fn(acc, leap_second_info) { - case leap_second_info.0 < ts_seconds { + case leap_second_info.0 <= ts_seconds { True -> list.Continue(Ok(leap_second_info.1)) False -> list.Stop(acc) } diff --git a/src/tzif/tzcalendar.gleam b/src/tzif/tzcalendar.gleam index b3a5825..395232a 100644 --- a/src/tzif/tzcalendar.gleam +++ b/src/tzif/tzcalendar.gleam @@ -126,7 +126,7 @@ pub fn from_calendar( db: TzDatabase, ) -> Result(List(Timestamp), database.TzDatabaseError) { // Assume no shift will be more than 24 hours - let ts_utc = timestamp.from_calendar(date, time, duration.seconds(0)) + let ts_utc = timestamp.from_calendar(date, time, calendar.utc_offset) // What are the offsets at +/- the 24 hour window use before_zone <- result.try( diff --git a/test/tzif/tzcalendar_test.gleam b/test/tzif/tzcalendar_test.gleam index 90e1265..eebf3cb 100644 --- a/test/tzif/tzcalendar_test.gleam +++ b/test/tzif/tzcalendar_test.gleam @@ -292,3 +292,25 @@ pub fn atomic_difference_test() { assert tzcalendar.atomic_difference(middle, late, "right/UTC", db) == Ok(duration.seconds(1_104_537_612)) } + +pub fn atomic_difference_one_second_test() { + let db = get_database() + + let before = + timestamp.from_calendar( + calendar.Date(2016, calendar.December, 31), + calendar.TimeOfDay(23, 59, 59, 0), + calendar.utc_offset, + ) + let after = + timestamp.from_calendar( + calendar.Date(2017, calendar.January, 1), + calendar.TimeOfDay(0, 0, 0, 0), + calendar.utc_offset, + ) + + assert timestamp.difference(before, after) == duration.seconds(1) + + assert tzcalendar.atomic_difference(before, after, "right/UTC", db) + == Ok(duration.seconds(2)) +} From 325ae77296fc6532afc27f68f446374a6440423d Mon Sep 17 00:00:00 2001 From: Christopher De Vries Date: Thu, 25 Sep 2025 09:14:53 -0400 Subject: [PATCH 2/4] add a negative atomic difference test --- test/tzif/tzcalendar_test.gleam | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/tzif/tzcalendar_test.gleam b/test/tzif/tzcalendar_test.gleam index eebf3cb..f6c4bf3 100644 --- a/test/tzif/tzcalendar_test.gleam +++ b/test/tzif/tzcalendar_test.gleam @@ -314,3 +314,12 @@ pub fn atomic_difference_one_second_test() { assert tzcalendar.atomic_difference(before, after, "right/UTC", db) == Ok(duration.seconds(2)) } + +pub fn atomic_difference_no_leap_second_test() { + let db = get_database() + let start = timestamp.from_unix_seconds(0) + let end = timestamp.from_unix_seconds(644_241_600) + + assert tzcalendar.atomic_difference(start, end, "UTC", db) + == Error(database.InfoNotFound) +} From 5f957942908d79644a75682e7d9c4df36615d446 Mon Sep 17 00:00:00 2001 From: Christopher De Vries Date: Mon, 29 Sep 2025 16:19:42 -0400 Subject: [PATCH 3/4] add example to from_calendar --- src/tzif/tzcalendar.gleam | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/tzif/tzcalendar.gleam b/src/tzif/tzcalendar.gleam index 395232a..3d9dee2 100644 --- a/src/tzif/tzcalendar.gleam +++ b/src/tzif/tzcalendar.gleam @@ -119,6 +119,22 @@ pub fn to_calendar( /// /// This /// returns a `TzDatabaseError` if there is an issue finding time zone information. +/// +/// # Example +/// +/// ```gleam +/// import gleam/time/calendar +/// import tzif/database +/// +/// let assert Ok(db) = database.load_from_os() +/// +/// from_calendar( +/// calendar.Date(2025, calendar.November, 2), +/// calendar.TimeOfDay(1, 30, 0, 0), +/// "America/New_York", +/// db, +/// ) +/// // Ok([Timestamp(1762061400, 0), Timestamp(1762065000, 0)]) pub fn from_calendar( date: Date, time: TimeOfDay, From b1428040ad5c44181403670352aeec5e7a94c844 Mon Sep 17 00:00:00 2001 From: Christopher De Vries Date: Mon, 29 Sep 2025 16:23:00 -0400 Subject: [PATCH 4/4] update changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 46ed3ac..b9fc8b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## Unreleased +- Fix issue with leap second tables being recorded in unix leap seconds + rather than unix seconds. +- Add tests to test exact placement of leap seconds + ## v1.1.0 - 2025-09-23 - Add `tzcalendar.atomic_difference` to calculate the actual difference between two timestamps including leap seconds.