Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
6 changes: 5 additions & 1 deletion src/tzif/database.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
18 changes: 17 additions & 1 deletion src/tzif/tzcalendar.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -119,14 +119,30 @@ 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,
zone_name: String,
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(
Expand Down
31 changes: 31 additions & 0 deletions test/tzif/tzcalendar_test.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -292,3 +292,34 @@ 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))
}

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