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
1 change: 1 addition & 0 deletions gleam.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
2 changes: 2 additions & 0 deletions manifest.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
Expand All @@ -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" }
59 changes: 59 additions & 0 deletions src/birl.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
}
23 changes: 23 additions & 0 deletions src/birl/duration.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
}
85 changes: 85 additions & 0 deletions test/birl_test.gleam
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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)
}