diff --git a/gleam.toml b/gleam.toml index 2d95074..78677f5 100644 --- a/gleam.toml +++ b/gleam.toml @@ -12,6 +12,7 @@ links = [{ title = "Gleam", href = "https://gleam.run" }] ranger = ">= 1.4.0 and < 2.0.0" gleam_stdlib = ">= 0.48.0 and < 2.0.0" gleam_regexp = ">= 1.0.0 and < 2.0.0" +gleam_time = ">= 1.0.0 and < 2.0.0" [dev-dependencies] gleeunit = ">= 1.2.0 and < 2.0.0" diff --git a/manifest.toml b/manifest.toml index 7dd1448..a9ecdec 100644 --- a/manifest.toml +++ b/manifest.toml @@ -4,6 +4,7 @@ packages = [ { name = "gleam_regexp", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_regexp", source = "hex", outer_checksum = "A3655FDD288571E90EE9C4009B719FEF59FA16AFCDF3952A76A125AF23CF1592" }, { name = "gleam_stdlib", version = "0.48.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "6C7799F315EB3AC53271078685297579183A287F2E65C6DD36C6583C76F12BBE" }, + { name = "gleam_time", version = "1.7.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_time", source = "hex", outer_checksum = "56DB0EF9433826D3B99DB0B4AF7A2BFED13D09755EC64B1DAAB46F804A9AD47D" }, { name = "gleam_yielder", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_yielder", source = "hex", outer_checksum = "8E4E4ECFA7982859F430C57F549200C7749823C106759F4A19A78AEA6687717A" }, { name = "gleeunit", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "F7A7228925D3EE7D0813C922E062BFD6D7E9310F0BEE585D3A42F3307E3CFD13" }, { name = "ranger", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "gleam_yielder"], otp_app = "ranger", source = "hex", outer_checksum = "C8988E8F8CDBD3E7F4D8F2E663EF76490390899C2B2885A6432E942495B3E854" }, @@ -12,5 +13,6 @@ packages = [ [requirements] gleam_regexp = { version = ">= 1.0.0 and < 2.0.0" } gleam_stdlib = { version = ">= 0.48.0 and < 2.0.0" } +gleam_time = { version = ">= 1.0.0 and < 2.0.0" } gleeunit = { version = ">= 1.2.0 and < 2.0.0" } ranger = { version = ">= 1.4.0 and < 2.0.0" } diff --git a/src/birl.gleam b/src/birl.gleam index fda106d..eaf62b0 100644 --- a/src/birl.gleam +++ b/src/birl.gleam @@ -9,6 +9,8 @@ import gleam/order import gleam/regexp import gleam/result import gleam/string +import gleam/time/calendar +import gleam/time/timestamp import ranger @@ -1659,3 +1661,60 @@ fn ffi_weekday(a: Int, b: Int) -> Int @external(erlang, "birl_ffi", "local_timezone") @external(javascript, "./birl_ffi.mjs", "local_timezone") fn local_timezone() -> option.Option(String) + +// --------------------------------------------------------------------------- +// gleam_time interoperability +// --------------------------------------------------------------------------- + +/// Convert birl Time to gleam_time Timestamp. +/// +/// Note: This conversion loses offset/timezone information since Timestamp +/// represents an absolute point in time (like UTC). +pub fn to_timestamp(value: Time) -> timestamp.Timestamp { + let Time(wall_time: t, ..) = value + // birl uses microseconds, gleam_time uses seconds + nanoseconds + let seconds = t / 1_000_000 + let nanoseconds = { t % 1_000_000 } * 1000 + timestamp.from_unix_seconds_and_nanoseconds(seconds, nanoseconds) +} + +/// Convert gleam_time Timestamp to birl Time. +/// +/// The resulting Time will be in UTC with no timezone information. +pub fn from_timestamp(ts: timestamp.Timestamp) -> Time { + let #(seconds, nanoseconds) = timestamp.to_unix_seconds_and_nanoseconds(ts) + let microseconds = seconds * 1_000_000 + nanoseconds / 1000 + Time(microseconds, 0, option.Some("Etc/UTC"), option.None) +} + +/// Convert birl Day to gleam_time calendar.Date. +pub fn day_to_date(day: Day) -> calendar.Date { + let assert Ok(month) = calendar.month_from_int(day.month) + calendar.Date(day.year, month, day.date) +} + +/// Convert gleam_time calendar.Date to birl Day. +pub fn date_to_day(date: calendar.Date) -> Day { + Day(date.year, calendar.month_to_int(date.month), date.day) +} + +/// Convert birl TimeOfDay to gleam_time calendar.TimeOfDay. +/// +/// Note: birl stores milliseconds while gleam_time stores nanoseconds, +/// so some precision may be gained (with zeros in the nanosecond places). +pub fn time_of_day_to_calendar(tod: TimeOfDay) -> calendar.TimeOfDay { + calendar.TimeOfDay( + tod.hour, + tod.minute, + tod.second, + tod.milli_second * 1_000_000, + ) +} + +/// Convert gleam_time calendar.TimeOfDay to birl TimeOfDay. +/// +/// Note: gleam_time stores nanoseconds while birl stores milliseconds, +/// so sub-millisecond precision will be lost. +pub fn calendar_to_time_of_day(tod: calendar.TimeOfDay) -> TimeOfDay { + TimeOfDay(tod.hours, tod.minutes, tod.seconds, tod.nanoseconds / 1_000_000) +} diff --git a/src/birl/duration.gleam b/src/birl/duration.gleam index d278369..b2961d0 100644 --- a/src/birl/duration.gleam +++ b/src/birl/duration.gleam @@ -5,6 +5,7 @@ import gleam/order import gleam/regexp import gleam/result import gleam/string +import gleam/time/duration as time_duration pub type Duration { Duration(Int) @@ -390,3 +391,25 @@ pub fn parse(expression: String) -> Result(Duration, Nil) { fn extract(duration: Int, unit_value: Int) -> #(Int, Int) { #(duration / unit_value, duration % unit_value) } + +// --------------------------------------------------------------------------- +// gleam_time interoperability +// --------------------------------------------------------------------------- + +/// Convert birl Duration to gleam_time Duration. +/// +/// birl uses microseconds internally, while gleam_time uses nanoseconds. +pub fn to_gleam_duration(d: Duration) -> time_duration.Duration { + let Duration(microseconds) = d + // Convert microseconds to nanoseconds + time_duration.nanoseconds(microseconds * 1000) +} + +/// Convert gleam_time Duration to birl Duration. +/// +/// gleam_time uses nanoseconds internally, while birl uses microseconds. +/// Sub-microsecond precision will be lost. +pub fn from_gleam_duration(d: time_duration.Duration) -> Duration { + let #(seconds, nanoseconds) = time_duration.to_seconds_and_nanoseconds(d) + Duration(seconds * 1_000_000 + nanoseconds / 1000) +} diff --git a/test/birl_test.gleam b/test/birl_test.gleam index 049a4a6..9b40459 100644 --- a/test/birl_test.gleam +++ b/test/birl_test.gleam @@ -1,4 +1,7 @@ import gleam/order +import gleam/time/calendar +import gleam/time/duration as time_duration +import gleam/time/timestamp import gleeunit import gleeunit/should @@ -272,3 +275,85 @@ pub fn blur_test() { |> duration.blur |> should.equal(#(1, duration.Year)) } + +// `gleam_time` interoperability tests + +pub fn timestamp_roundtrip_test() { + let now = birl.utc_now() + let ts = birl.to_timestamp(now) + let back = birl.from_timestamp(ts) + // Should be equal within microsecond precision + birl.to_unix_micro(now) + |> should.equal(birl.to_unix_micro(back)) +} + +pub fn timestamp_epoch_test() { + let ts = birl.to_timestamp(birl.unix_epoch) + let #(seconds, nanoseconds) = timestamp.to_unix_seconds_and_nanoseconds(ts) + seconds + |> should.equal(0) + nanoseconds + |> should.equal(0) +} + +pub fn duration_roundtrip_test() { + let d = duration.hours(2) |> duration.add(duration.minutes(30)) + let gleam_d = duration.to_gleam_duration(d) + let back = duration.from_gleam_duration(gleam_d) + d + |> should.equal(back) +} + +pub fn duration_conversion_test() { + // 1 second = 1_000_000 microseconds in birl + // 1 second = 1_000_000_000 nanoseconds in gleam_time + let d = duration.seconds(1) + let gleam_d = duration.to_gleam_duration(d) + let #(seconds, nanoseconds) = + time_duration.to_seconds_and_nanoseconds(gleam_d) + seconds + |> should.equal(1) + nanoseconds + |> should.equal(0) +} + +pub fn day_date_roundtrip_test() { + let day = birl.Day(2024, 6, 15) + let date = birl.day_to_date(day) + let back = birl.date_to_day(date) + day + |> should.equal(back) +} + +pub fn day_to_date_test() { + let day = birl.Day(2024, 1, 15) + let date = birl.day_to_date(day) + date.year + |> should.equal(2024) + date.month + |> should.equal(calendar.January) + date.day + |> should.equal(15) +} + +pub fn time_of_day_roundtrip_test() { + let tod = birl.TimeOfDay(14, 30, 45, 123) + let calendar_tod = birl.time_of_day_to_calendar(tod) + let back = birl.calendar_to_time_of_day(calendar_tod) + tod + |> should.equal(back) +} + +pub fn time_of_day_to_calendar_test() { + let tod = birl.TimeOfDay(14, 30, 45, 123) + let calendar_tod = birl.time_of_day_to_calendar(tod) + calendar_tod.hours + |> should.equal(14) + calendar_tod.minutes + |> should.equal(30) + calendar_tod.seconds + |> should.equal(45) + // 123 milliseconds = 123_000_000 nanoseconds + calendar_tod.nanoseconds + |> should.equal(123_000_000) +}