Skip to content

Commit 2d84092

Browse files
Merge pull request #53 from tylerbutler/feat/gleam-time-interop
feat: add gleam_time interoperability layer
2 parents 992acad + 2df6d12 commit 2d84092

File tree

5 files changed

+170
-0
lines changed

5 files changed

+170
-0
lines changed

gleam.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ links = [{ title = "Gleam", href = "https://gleam.run" }]
1212
ranger = ">= 1.4.0 and < 2.0.0"
1313
gleam_stdlib = ">= 0.48.0 and < 2.0.0"
1414
gleam_regexp = ">= 1.0.0 and < 2.0.0"
15+
gleam_time = ">= 1.0.0 and < 2.0.0"
1516

1617
[dev-dependencies]
1718
gleeunit = ">= 1.2.0 and < 2.0.0"

manifest.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
packages = [
55
{ name = "gleam_regexp", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_regexp", source = "hex", outer_checksum = "A3655FDD288571E90EE9C4009B719FEF59FA16AFCDF3952A76A125AF23CF1592" },
66
{ name = "gleam_stdlib", version = "0.48.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "6C7799F315EB3AC53271078685297579183A287F2E65C6DD36C6583C76F12BBE" },
7+
{ name = "gleam_time", version = "1.7.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_time", source = "hex", outer_checksum = "56DB0EF9433826D3B99DB0B4AF7A2BFED13D09755EC64B1DAAB46F804A9AD47D" },
78
{ name = "gleam_yielder", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_yielder", source = "hex", outer_checksum = "8E4E4ECFA7982859F430C57F549200C7749823C106759F4A19A78AEA6687717A" },
89
{ name = "gleeunit", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "F7A7228925D3EE7D0813C922E062BFD6D7E9310F0BEE585D3A42F3307E3CFD13" },
910
{ 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 = [
1213
[requirements]
1314
gleam_regexp = { version = ">= 1.0.0 and < 2.0.0" }
1415
gleam_stdlib = { version = ">= 0.48.0 and < 2.0.0" }
16+
gleam_time = { version = ">= 1.0.0 and < 2.0.0" }
1517
gleeunit = { version = ">= 1.2.0 and < 2.0.0" }
1618
ranger = { version = ">= 1.4.0 and < 2.0.0" }

src/birl.gleam

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import gleam/order
99
import gleam/regexp
1010
import gleam/result
1111
import gleam/string
12+
import gleam/time/calendar
13+
import gleam/time/timestamp
1214

1315
import ranger
1416

@@ -1659,3 +1661,60 @@ fn ffi_weekday(a: Int, b: Int) -> Int
16591661
@external(erlang, "birl_ffi", "local_timezone")
16601662
@external(javascript, "./birl_ffi.mjs", "local_timezone")
16611663
fn local_timezone() -> option.Option(String)
1664+
1665+
// ---------------------------------------------------------------------------
1666+
// gleam_time interoperability
1667+
// ---------------------------------------------------------------------------
1668+
1669+
/// Convert birl Time to gleam_time Timestamp.
1670+
///
1671+
/// Note: This conversion loses offset/timezone information since Timestamp
1672+
/// represents an absolute point in time (like UTC).
1673+
pub fn to_timestamp(value: Time) -> timestamp.Timestamp {
1674+
let Time(wall_time: t, ..) = value
1675+
// birl uses microseconds, gleam_time uses seconds + nanoseconds
1676+
let seconds = t / 1_000_000
1677+
let nanoseconds = { t % 1_000_000 } * 1000
1678+
timestamp.from_unix_seconds_and_nanoseconds(seconds, nanoseconds)
1679+
}
1680+
1681+
/// Convert gleam_time Timestamp to birl Time.
1682+
///
1683+
/// The resulting Time will be in UTC with no timezone information.
1684+
pub fn from_timestamp(ts: timestamp.Timestamp) -> Time {
1685+
let #(seconds, nanoseconds) = timestamp.to_unix_seconds_and_nanoseconds(ts)
1686+
let microseconds = seconds * 1_000_000 + nanoseconds / 1000
1687+
Time(microseconds, 0, option.Some("Etc/UTC"), option.None)
1688+
}
1689+
1690+
/// Convert birl Day to gleam_time calendar.Date.
1691+
pub fn day_to_date(day: Day) -> calendar.Date {
1692+
let assert Ok(month) = calendar.month_from_int(day.month)
1693+
calendar.Date(day.year, month, day.date)
1694+
}
1695+
1696+
/// Convert gleam_time calendar.Date to birl Day.
1697+
pub fn date_to_day(date: calendar.Date) -> Day {
1698+
Day(date.year, calendar.month_to_int(date.month), date.day)
1699+
}
1700+
1701+
/// Convert birl TimeOfDay to gleam_time calendar.TimeOfDay.
1702+
///
1703+
/// Note: birl stores milliseconds while gleam_time stores nanoseconds,
1704+
/// so some precision may be gained (with zeros in the nanosecond places).
1705+
pub fn time_of_day_to_calendar(tod: TimeOfDay) -> calendar.TimeOfDay {
1706+
calendar.TimeOfDay(
1707+
tod.hour,
1708+
tod.minute,
1709+
tod.second,
1710+
tod.milli_second * 1_000_000,
1711+
)
1712+
}
1713+
1714+
/// Convert gleam_time calendar.TimeOfDay to birl TimeOfDay.
1715+
///
1716+
/// Note: gleam_time stores nanoseconds while birl stores milliseconds,
1717+
/// so sub-millisecond precision will be lost.
1718+
pub fn calendar_to_time_of_day(tod: calendar.TimeOfDay) -> TimeOfDay {
1719+
TimeOfDay(tod.hours, tod.minutes, tod.seconds, tod.nanoseconds / 1_000_000)
1720+
}

src/birl/duration.gleam

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import gleam/order
55
import gleam/regexp
66
import gleam/result
77
import gleam/string
8+
import gleam/time/duration as time_duration
89

910
pub type Duration {
1011
Duration(Int)
@@ -390,3 +391,25 @@ pub fn parse(expression: String) -> Result(Duration, Nil) {
390391
fn extract(duration: Int, unit_value: Int) -> #(Int, Int) {
391392
#(duration / unit_value, duration % unit_value)
392393
}
394+
395+
// ---------------------------------------------------------------------------
396+
// gleam_time interoperability
397+
// ---------------------------------------------------------------------------
398+
399+
/// Convert birl Duration to gleam_time Duration.
400+
///
401+
/// birl uses microseconds internally, while gleam_time uses nanoseconds.
402+
pub fn to_gleam_duration(d: Duration) -> time_duration.Duration {
403+
let Duration(microseconds) = d
404+
// Convert microseconds to nanoseconds
405+
time_duration.nanoseconds(microseconds * 1000)
406+
}
407+
408+
/// Convert gleam_time Duration to birl Duration.
409+
///
410+
/// gleam_time uses nanoseconds internally, while birl uses microseconds.
411+
/// Sub-microsecond precision will be lost.
412+
pub fn from_gleam_duration(d: time_duration.Duration) -> Duration {
413+
let #(seconds, nanoseconds) = time_duration.to_seconds_and_nanoseconds(d)
414+
Duration(seconds * 1_000_000 + nanoseconds / 1000)
415+
}

test/birl_test.gleam

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
import gleam/order
2+
import gleam/time/calendar
3+
import gleam/time/duration as time_duration
4+
import gleam/time/timestamp
25
import gleeunit
36
import gleeunit/should
47

@@ -272,3 +275,85 @@ pub fn blur_test() {
272275
|> duration.blur
273276
|> should.equal(#(1, duration.Year))
274277
}
278+
279+
// `gleam_time` interoperability tests
280+
281+
pub fn timestamp_roundtrip_test() {
282+
let now = birl.utc_now()
283+
let ts = birl.to_timestamp(now)
284+
let back = birl.from_timestamp(ts)
285+
// Should be equal within microsecond precision
286+
birl.to_unix_micro(now)
287+
|> should.equal(birl.to_unix_micro(back))
288+
}
289+
290+
pub fn timestamp_epoch_test() {
291+
let ts = birl.to_timestamp(birl.unix_epoch)
292+
let #(seconds, nanoseconds) = timestamp.to_unix_seconds_and_nanoseconds(ts)
293+
seconds
294+
|> should.equal(0)
295+
nanoseconds
296+
|> should.equal(0)
297+
}
298+
299+
pub fn duration_roundtrip_test() {
300+
let d = duration.hours(2) |> duration.add(duration.minutes(30))
301+
let gleam_d = duration.to_gleam_duration(d)
302+
let back = duration.from_gleam_duration(gleam_d)
303+
d
304+
|> should.equal(back)
305+
}
306+
307+
pub fn duration_conversion_test() {
308+
// 1 second = 1_000_000 microseconds in birl
309+
// 1 second = 1_000_000_000 nanoseconds in gleam_time
310+
let d = duration.seconds(1)
311+
let gleam_d = duration.to_gleam_duration(d)
312+
let #(seconds, nanoseconds) =
313+
time_duration.to_seconds_and_nanoseconds(gleam_d)
314+
seconds
315+
|> should.equal(1)
316+
nanoseconds
317+
|> should.equal(0)
318+
}
319+
320+
pub fn day_date_roundtrip_test() {
321+
let day = birl.Day(2024, 6, 15)
322+
let date = birl.day_to_date(day)
323+
let back = birl.date_to_day(date)
324+
day
325+
|> should.equal(back)
326+
}
327+
328+
pub fn day_to_date_test() {
329+
let day = birl.Day(2024, 1, 15)
330+
let date = birl.day_to_date(day)
331+
date.year
332+
|> should.equal(2024)
333+
date.month
334+
|> should.equal(calendar.January)
335+
date.day
336+
|> should.equal(15)
337+
}
338+
339+
pub fn time_of_day_roundtrip_test() {
340+
let tod = birl.TimeOfDay(14, 30, 45, 123)
341+
let calendar_tod = birl.time_of_day_to_calendar(tod)
342+
let back = birl.calendar_to_time_of_day(calendar_tod)
343+
tod
344+
|> should.equal(back)
345+
}
346+
347+
pub fn time_of_day_to_calendar_test() {
348+
let tod = birl.TimeOfDay(14, 30, 45, 123)
349+
let calendar_tod = birl.time_of_day_to_calendar(tod)
350+
calendar_tod.hours
351+
|> should.equal(14)
352+
calendar_tod.minutes
353+
|> should.equal(30)
354+
calendar_tod.seconds
355+
|> should.equal(45)
356+
// 123 milliseconds = 123_000_000 nanoseconds
357+
calendar_tod.nanoseconds
358+
|> should.equal(123_000_000)
359+
}

0 commit comments

Comments
 (0)