diff --git a/src/gleam/time/timestamp.gleam b/src/gleam/time/timestamp.gleam index 9e23ede..ca97491 100644 --- a/src/gleam/time/timestamp.gleam +++ b/src/gleam/time/timestamp.gleam @@ -1,7 +1,6 @@ import gleam/float import gleam/int import gleam/order -import gleam/result import gleam/string import gleam/time/duration.{type Duration} @@ -140,30 +139,46 @@ pub fn add(timestamp: Timestamp, duration: Duration) -> Timestamp { |> normalise } -// TODO: docs -// TODO: rename? -pub fn to_rfc3339_utc(timestamp: Timestamp) -> String { - let seconds = int.modulo(timestamp.seconds, 60) |> result.unwrap(0) - let total_minutes = floored_div(timestamp.seconds, 60.0) - let minutes = - { int.modulo(timestamp.seconds, 60 * 60) |> result.unwrap(0) } / 60 - let hours = - { int.modulo(timestamp.seconds, 24 * 60 * 60) |> result.unwrap(0) } - / { 60 * 60 } +/// Convert a timestamp to a RFC 3339 formatted time string, with an offset +/// supplied in minutes. +/// +/// The output of this function is also ISO 8601 compatible so long as the +/// offset not negative. +/// +/// # Examples +/// +/// ```gleam +/// to_rfc3339(from_unix_seconds(1000), 0) +/// // -> "1970-01-01T00:00:00Z" +/// ``` +/// +pub fn to_rfc3339(timestamp: Timestamp, offset_minutes offset: Int) -> String { + let total = timestamp.seconds - { offset * 60 } + let seconds = modulo(total, 60) + let total_minutes = floored_div(total, 60.0) + let minutes = modulo(total, 60 * 60) / 60 + let hours = modulo(total, 24 * 60 * 60) / { 60 * 60 } let #(years, months, days) = to_civil(total_minutes) + let offset_minutes = modulo(offset, 60) + let offset_hours = int.absolute_value(floored_div(offset, 60.0)) + let n = fn(n) { int.to_string(n) |> string.pad_start(2, "0") } - n(years) - <> "-" - <> n(months) - <> "-" - <> n(days) - <> "T" - <> n(hours) - <> ":" - <> n(minutes) - <> ":" - <> n(seconds) - <> "Z" + let out = "" + let out = out <> n(years) <> "-" <> n(months) <> "-" <> n(days) + let out = out <> "T" + let out = out <> n(hours) <> ":" <> n(minutes) <> ":" <> n(seconds) + case int.compare(offset, 0) { + order.Eq -> out <> "Z" + order.Gt -> out <> "+" <> n(offset_hours) <> ":" <> n(offset_minutes) + order.Lt -> out <> "-" <> n(offset_hours) <> ":" <> n(offset_minutes) + } +} + +fn modulo(n: Int, m: Int) -> Int { + case int.modulo(n, m) { + Ok(n) -> n + Error(_) -> 0 + } } fn floored_div(numerator: Int, denominator: Float) -> Int { diff --git a/test/gleam/time/timestamp_test.gleam b/test/gleam/time/timestamp_test.gleam index 5e6eccc..64a5ca2 100644 --- a/test/gleam/time/timestamp_test.gleam +++ b/test/gleam/time/timestamp_test.gleam @@ -106,44 +106,56 @@ pub fn system_time_0_test() { let assert True = now < christmas_day_2025 } -pub fn to_rfc3339_utc_0_test() { +pub fn to_rfc3339_0_test() { timestamp.from_unix_seconds(1_735_309_467) - |> timestamp.to_rfc3339_utc + |> timestamp.to_rfc3339(0) |> should.equal("2024-12-27T14:24:27Z") } -pub fn to_rfc3339_utc_1_test() { +pub fn to_rfc3339_1_test() { timestamp.from_unix_seconds(1) - |> timestamp.to_rfc3339_utc + |> timestamp.to_rfc3339(0) |> should.equal("1970-01-01T00:00:01Z") } -pub fn to_rfc3339_utc_2_test() { +pub fn to_rfc3339_2_test() { timestamp.from_unix_seconds(0) - |> timestamp.to_rfc3339_utc + |> timestamp.to_rfc3339(0) |> should.equal("1970-01-01T00:00:00Z") } -pub fn to_rfc3339_utc_3_test() { +pub fn to_rfc3339_3_test() { timestamp.from_unix_seconds(123_456_789) - |> timestamp.to_rfc3339_utc + |> timestamp.to_rfc3339(0) |> should.equal("1973-11-29T21:33:09Z") } -pub fn to_rfc3339_utc_4_test() { +pub fn to_rfc3339_4_test() { timestamp.from_unix_seconds(31_560_000) - |> timestamp.to_rfc3339_utc + |> timestamp.to_rfc3339(0) |> should.equal("1971-01-01T06:40:00Z") } -pub fn to_rfc3339_utc_5_test() { +pub fn to_rfc3339_5_test() { timestamp.from_unix_seconds(-12_345_678) - |> timestamp.to_rfc3339_utc + |> timestamp.to_rfc3339(0) |> should.equal("1969-08-11T02:38:42Z") } -pub fn to_rfc3339_utc_6_test() { +pub fn to_rfc3339_6_test() { timestamp.from_unix_seconds(-1) - |> timestamp.to_rfc3339_utc + |> timestamp.to_rfc3339(0) |> should.equal("1969-12-31T23:59:59Z") } + +pub fn to_rfc3339_7_test() { + timestamp.from_unix_seconds(60 * 60 + 60 * 5) + |> timestamp.to_rfc3339(65) + |> should.equal("1970-01-01T00:00:00+01:05") +} + +pub fn to_rfc3339_8_test() { + timestamp.from_unix_seconds(0) + |> timestamp.to_rfc3339(-120) + |> should.equal("1970-01-01T02:00:00-02:00") +}