From f5309e75ad185a49a543eff2cbb917441e7d3c4b Mon Sep 17 00:00:00 2001 From: Louis Pilfold Date: Mon, 23 Dec 2024 11:55:41 +0000 Subject: [PATCH 01/21] Duration, timestamp --- src/gleam/time/duration.gleam | 41 ++++++++ src/gleam/time/timestamp.gleam | 97 +++++++++++++++++++ ..._time_test.gleam => gleam_time_test.gleam} | 0 3 files changed, 138 insertions(+) create mode 100644 src/gleam/time/duration.gleam create mode 100644 src/gleam/time/timestamp.gleam rename test/{leam_time_test.gleam => gleam_time_test.gleam} (100%) diff --git a/src/gleam/time/duration.gleam b/src/gleam/time/duration.gleam new file mode 100644 index 0000000..f02d109 --- /dev/null +++ b/src/gleam/time/duration.gleam @@ -0,0 +1,41 @@ +import gleam/order + +// TODO: document +pub type Duration { + Duration(seconds: Int, nanoseconds: Int) +} + +// TODO: test +/// Ensure the duration is represented with `nanoseconds` being positive and +/// less than 1 second. +/// +/// This function does not change the amount of time that the duratoin refers +/// to, it only adjusts the values used to represent the time. +/// +pub fn normalise(duration: Duration) -> Duration { + todo +} + +// TODO: docs +// TODO: test +pub fn compare(left: Duration, right: Duration) -> order.Order { + todo +} + +// TODO: docs +// TODO: test +pub fn difference(left: Duration, right: Duration) -> Duration { + todo +} + +// TODO: docs +// TODO: test +pub fn add(left: Duration, right: Duration) -> Duration { + todo +} + +// TODO: docs +// TODO: test +pub fn to_iso8601_string(duration: Duration) -> String { + todo +} diff --git a/src/gleam/time/timestamp.gleam b/src/gleam/time/timestamp.gleam new file mode 100644 index 0000000..caf0a50 --- /dev/null +++ b/src/gleam/time/timestamp.gleam @@ -0,0 +1,97 @@ +import gleam/order +import gleam/time/duration.{type Duration} + +/// A timestamp represents a moment in time, represented as an amount of time +/// since 00:00:00 UTC on 1 January 1970, also known as the _Unix epoch_. +/// +/// # Wall clock time and monotonicity +/// +/// Time is very complicated, especially on computers! While they generally do +/// a good job of keeping track of what the time is, computers can get +/// out-of-sync and start to report a time that is too late or too early. Most +/// computers use "network time protocol" to tell each other what they think +/// the time is, and computers that realise they are running too fast or too +/// slow will adjust their clock to correct it. When this happens it can seem +/// to your program that the current time has changed, and it may have even +/// jumped backwards in time! +/// +/// This measure of time is called _wall clock time_, and it is what people +/// commonly think of when they think of time. It is important to be aware that +/// it can go backwards, and your program must not rely on it only ever going +/// forwards at a steady rate. For example, for tracking what order events happen +/// in. +/// +/// This module uses wall clock time. If your program needs time values to always +/// increase you will need a _monotonic_ time instead. +/// +/// The exact way that time works will depend on what runtime you use. The +/// Erlang documentation on time has a lot of detail about time generally as well +/// as how it works on the BEAM, it is worth reading. +/// . +/// +/// # Converting to local time +/// +/// Timestamps don't take into account time zones, so a moment in time will +/// have the same timestamp value regardless of where you are in the world. To +/// convert them to local time you will need to know details about the local +/// time zone, likely from a time zone database. +/// +/// The UTC time zone never has any adjustments, so you don't need a time zone +/// database to convert to UTC local time. +/// +/// # Representation +/// +/// When compiling to JavaScript ints have limited precision and size. This +/// means that if we were to store the the timestamp in a single int the +/// timestamp would not be able to represent times far in the future or in the +/// past, or distinguish between two times that are close together. Timestamps +/// are instead represented as a number of seconds and a number of nanoseconds. +/// +/// If you have manually adjusted the seconds and nanoseconds values the +/// `normalise` function can be used to ensure the time is represented the +/// intended way, with `nanoseconds` being positive and less than 1 second. +/// +pub type Timestamp { + Timestamp(seconds: Int, nanoseconds: Int) +} + +// TODO: test +/// Ensure the time is represented with `nanoseconds` being positive and less +/// than 1 second. +/// +/// This function does not change the time that the timestamp refers to, it +/// only adjusts the values used to represent the time. +/// +pub fn normalise(timestamp: Timestamp) -> Timestamp { + todo +} + +// TODO: docs +// TODO: test +pub fn compare(left: Timestamp, right: Timestamp) -> order.Order { + todo +} + +// TODO: docs +// TODO: test +pub fn now() -> Timestamp { + todo +} + +// TODO: docs +// TODO: test +pub fn difference(left: Timestamp, right: Timestamp) -> Duration { + todo +} + +// TODO: docs +// TODO: test +pub fn add(timetamp: Timestamp, duration: Duration) -> Duration { + todo +} + +// TODO: docs +// TODO: test +pub fn to_rfc3339_utc_string(timestamp: Timestamp) -> String { + todo +} diff --git a/test/leam_time_test.gleam b/test/gleam_time_test.gleam similarity index 100% rename from test/leam_time_test.gleam rename to test/gleam_time_test.gleam From 671b7d450a104a064d68fea169ecb9b9950e6f50 Mon Sep 17 00:00:00 2001 From: Louis Pilfold Date: Mon, 23 Dec 2024 12:21:04 +0000 Subject: [PATCH 02/21] Date parse function --- src/gleam/time/timestamp.gleam | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/gleam/time/timestamp.gleam b/src/gleam/time/timestamp.gleam index caf0a50..6ed0f2c 100644 --- a/src/gleam/time/timestamp.gleam +++ b/src/gleam/time/timestamp.gleam @@ -95,3 +95,9 @@ pub fn add(timetamp: Timestamp, duration: Duration) -> Duration { pub fn to_rfc3339_utc_string(timestamp: Timestamp) -> String { todo } + +// TODO: docs +// TODO: test +pub fn parse_rfc3339(input: String) -> Result(Timestamp, Nil) { + todo +} From 97de3c572812d0070c215797adbec7f8497b7786 Mon Sep 17 00:00:00 2001 From: Louis Pilfold Date: Mon, 23 Dec 2024 12:31:30 +0000 Subject: [PATCH 03/21] Duration constructors --- src/gleam/time/duration.gleam | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/gleam/time/duration.gleam b/src/gleam/time/duration.gleam index f02d109..ca49759 100644 --- a/src/gleam/time/duration.gleam +++ b/src/gleam/time/duration.gleam @@ -39,3 +39,21 @@ pub fn add(left: Duration, right: Duration) -> Duration { pub fn to_iso8601_string(duration: Duration) -> String { todo } + +// TODO: docs +// TODO: test +pub fn minutes(amount: Int) -> Duration { + todo +} + +// TODO: docs +// TODO: test +pub fn seconds(amount: Int) -> Duration { + todo +} + +// TODO: docs +// TODO: test +pub fn milliseconds(amount: Int) -> Duration { + todo +} From 9b3ce59d0523bf9c1242fbfb65dfcfc1063e3fb9 Mon Sep 17 00:00:00 2001 From: Louis Pilfold Date: Mon, 23 Dec 2024 13:12:53 +0000 Subject: [PATCH 04/21] Make opaque --- src/gleam/time/duration.gleam | 13 +++++++++++-- src/gleam/time/timestamp.gleam | 25 +++++++++++-------------- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/src/gleam/time/duration.gleam b/src/gleam/time/duration.gleam index ca49759..3228f20 100644 --- a/src/gleam/time/duration.gleam +++ b/src/gleam/time/duration.gleam @@ -1,7 +1,16 @@ import gleam/order // TODO: document -pub type Duration { +pub opaque type Duration { + // When compiling to JavaScript ints have limited precision and size. This + // means that if we were to store the the timestamp in a single int the + // duration would not be able to represent very large or small durations. + // Durations are instead represented as a number of seconds and a number of + // nanoseconds. + // + // If you have manually adjusted the seconds and nanoseconds values the + // `normalise` function can be used to ensure the time is represented the + // intended way, with `nanoseconds` being positive and less than 1 second. Duration(seconds: Int, nanoseconds: Int) } @@ -12,7 +21,7 @@ pub type Duration { /// This function does not change the amount of time that the duratoin refers /// to, it only adjusts the values used to represent the time. /// -pub fn normalise(duration: Duration) -> Duration { +fn normalise(duration: Duration) -> Duration { todo } diff --git a/src/gleam/time/timestamp.gleam b/src/gleam/time/timestamp.gleam index 6ed0f2c..8321a08 100644 --- a/src/gleam/time/timestamp.gleam +++ b/src/gleam/time/timestamp.gleam @@ -39,19 +39,16 @@ import gleam/time/duration.{type Duration} /// The UTC time zone never has any adjustments, so you don't need a time zone /// database to convert to UTC local time. /// -/// # Representation -/// -/// When compiling to JavaScript ints have limited precision and size. This -/// means that if we were to store the the timestamp in a single int the -/// timestamp would not be able to represent times far in the future or in the -/// past, or distinguish between two times that are close together. Timestamps -/// are instead represented as a number of seconds and a number of nanoseconds. -/// -/// If you have manually adjusted the seconds and nanoseconds values the -/// `normalise` function can be used to ensure the time is represented the -/// intended way, with `nanoseconds` being positive and less than 1 second. -/// -pub type Timestamp { +pub opaque type Timestamp { + // When compiling to JavaScript ints have limited precision and size. This + // means that if we were to store the the timestamp in a single int the + // timestamp would not be able to represent times far in the future or in the + // past, or distinguish between two times that are close together. Timestamps + // are instead represented as a number of seconds and a number of nanoseconds. + // + // If you have manually adjusted the seconds and nanoseconds values the + // `normalise` function can be used to ensure the time is represented the + // intended way, with `nanoseconds` being positive and less than 1 second. Timestamp(seconds: Int, nanoseconds: Int) } @@ -62,7 +59,7 @@ pub type Timestamp { /// This function does not change the time that the timestamp refers to, it /// only adjusts the values used to represent the time. /// -pub fn normalise(timestamp: Timestamp) -> Timestamp { +fn normalise(timestamp: Timestamp) -> Timestamp { todo } From d7abab4d42fe31d248594438d95eecf243f4389b Mon Sep 17 00:00:00 2001 From: Louis Pilfold Date: Mon, 23 Dec 2024 13:49:57 +0000 Subject: [PATCH 05/21] Constructors and getters --- src/gleam/time/duration.gleam | 12 ++++++++++++ src/gleam/time/timestamp.gleam | 27 +++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/src/gleam/time/duration.gleam b/src/gleam/time/duration.gleam index 3228f20..9358e30 100644 --- a/src/gleam/time/duration.gleam +++ b/src/gleam/time/duration.gleam @@ -66,3 +66,15 @@ pub fn seconds(amount: Int) -> Duration { pub fn milliseconds(amount: Int) -> Duration { todo } + +// TODO: docs +// TODO: test +pub fn nanoseconds(amount: Int) -> Duration { + todo +} + +// TODO: docs +// TODO: test +pub fn to_seconds(duration: Duration) -> Float { + todo +} diff --git a/src/gleam/time/timestamp.gleam b/src/gleam/time/timestamp.gleam index 8321a08..63bb831 100644 --- a/src/gleam/time/timestamp.gleam +++ b/src/gleam/time/timestamp.gleam @@ -98,3 +98,30 @@ pub fn to_rfc3339_utc_string(timestamp: Timestamp) -> String { pub fn parse_rfc3339(input: String) -> Result(Timestamp, Nil) { todo } + +// TODO: docs +// TODO: test +pub fn from_unix_seconds(seconds: Int) -> Timestamp { + todo +} + +// TODO: docs +// TODO: test +pub fn from_unix_seconds_and_nanoseconds( + seconds seconds: Int, + nanoseconds nanoseconds: Int, +) -> Timestamp { + todo +} + +// TODO: docs +// TODO: test +pub fn to_unix_seconds(input: String) -> Float { + todo +} + +// TODO: docs +// TODO: test +pub fn to_unix_seconds_and_nanoseconds(input: String) -> #(Int, Int) { + todo +} From 2b07766d66d5b55df6f94d229026b8d5a16d5718 Mon Sep 17 00:00:00 2001 From: Louis Pilfold Date: Mon, 23 Dec 2024 14:52:12 +0000 Subject: [PATCH 06/21] Implement some duration types --- src/gleam/time/duration.gleam | 42 ++++++------ test/gleam/time/duration_test.gleam | 99 +++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+), 19 deletions(-) create mode 100644 test/gleam/time/duration_test.gleam diff --git a/src/gleam/time/duration.gleam b/src/gleam/time/duration.gleam index 9358e30..f9f3fcc 100644 --- a/src/gleam/time/duration.gleam +++ b/src/gleam/time/duration.gleam @@ -1,3 +1,4 @@ +import gleam/int import gleam/order // TODO: document @@ -14,7 +15,6 @@ pub opaque type Duration { Duration(seconds: Int, nanoseconds: Int) } -// TODO: test /// Ensure the duration is represented with `nanoseconds` being positive and /// less than 1 second. /// @@ -22,13 +22,19 @@ pub opaque type Duration { /// to, it only adjusts the values used to represent the time. /// fn normalise(duration: Duration) -> Duration { - todo + let multiplier = 1_000_000_000 + let nanoseconds = duration.nanoseconds % multiplier + let overflow = duration.nanoseconds - nanoseconds + let seconds = duration.seconds + overflow / multiplier + Duration(seconds, nanoseconds) } // TODO: docs -// TODO: test pub fn compare(left: Duration, right: Duration) -> order.Order { - todo + order.break_tie( + int.compare(left.seconds, right.seconds), + int.compare(left.nanoseconds, right.nanoseconds), + ) } // TODO: docs @@ -38,9 +44,9 @@ pub fn difference(left: Duration, right: Duration) -> Duration { } // TODO: docs -// TODO: test pub fn add(left: Duration, right: Duration) -> Duration { - todo + Duration(left.seconds + right.seconds, left.nanoseconds + right.nanoseconds) + |> normalise } // TODO: docs @@ -50,31 +56,29 @@ pub fn to_iso8601_string(duration: Duration) -> String { } // TODO: docs -// TODO: test -pub fn minutes(amount: Int) -> Duration { - todo -} - -// TODO: docs -// TODO: test pub fn seconds(amount: Int) -> Duration { - todo + Duration(amount, 0) |> normalise } // TODO: docs // TODO: test pub fn milliseconds(amount: Int) -> Duration { - todo + let remainder = amount % 1000 + let overflow = amount - remainder + let nanoseconds = remainder * 1_000_000 + let seconds = overflow / 1000 + Duration(seconds, nanoseconds) } // TODO: docs -// TODO: test pub fn nanoseconds(amount: Int) -> Duration { - todo + Duration(0, amount) + |> normalise } // TODO: docs -// TODO: test pub fn to_seconds(duration: Duration) -> Float { - todo + let seconds = int.to_float(duration.seconds) + let nanoseconds = int.to_float(duration.nanoseconds) + seconds +. { nanoseconds /. 1_000_000_000.0 } } diff --git a/test/gleam/time/duration_test.gleam b/test/gleam/time/duration_test.gleam new file mode 100644 index 0000000..3f329f6 --- /dev/null +++ b/test/gleam/time/duration_test.gleam @@ -0,0 +1,99 @@ +import gleam/order +import gleam/time/duration +import gleeunit/should + +pub fn add_0_test() { + duration.nanoseconds(500_000_000) + |> duration.add(duration.nanoseconds(500_000_000)) + |> should.equal(duration.seconds(1)) +} + +pub fn add_1_test() { + duration.nanoseconds(-500_000_000) + |> duration.add(duration.nanoseconds(-500_000_000)) + |> should.equal(duration.seconds(-1)) +} + +pub fn add_2_test() { + duration.nanoseconds(-500_000_000) + |> duration.add(duration.nanoseconds(500_000_000)) + |> should.equal(duration.seconds(0)) +} + +pub fn add_3_test() { + duration.seconds(4) + |> duration.add(duration.nanoseconds(4_000_000_000)) + |> should.equal(duration.seconds(8)) +} + +pub fn add_4_test() { + duration.seconds(4) + |> duration.add(duration.nanoseconds(-5_000_000_000)) + |> should.equal(duration.seconds(-1)) +} + +pub fn add_5_test() { + duration.nanoseconds(4_000_000) + |> duration.add(duration.milliseconds(4)) + |> should.equal(duration.milliseconds(8)) +} + +pub fn to_seconds_0_test() { + duration.seconds(1) + |> duration.to_seconds + |> should.equal(1.0) +} + +pub fn to_seconds_1_test() { + duration.seconds(2) + |> duration.to_seconds + |> should.equal(2.0) +} + +pub fn to_seconds_2_test() { + duration.milliseconds(500) + |> duration.to_seconds + |> should.equal(0.5) +} + +pub fn to_seconds_3_test() { + duration.milliseconds(5100) + |> duration.to_seconds + |> should.equal(5.1) +} + +pub fn to_seconds_4_test() { + duration.nanoseconds(500) + |> duration.to_seconds + |> should.equal(0.0000005) +} + +pub fn compare_0_test() { + duration.compare(duration.seconds(1), duration.seconds(1)) + |> should.equal(order.Eq) +} + +pub fn compare_1_test() { + duration.compare(duration.seconds(2), duration.seconds(1)) + |> should.equal(order.Gt) +} + +pub fn compare_2_test() { + duration.compare(duration.seconds(0), duration.seconds(1)) + |> should.equal(order.Lt) +} + +pub fn compare_3_test() { + duration.compare(duration.nanoseconds(999_999_999), duration.seconds(1)) + |> should.equal(order.Lt) +} + +pub fn compare_4_test() { + duration.compare(duration.nanoseconds(1_000_000_001), duration.seconds(1)) + |> should.equal(order.Gt) +} + +pub fn compare_5_test() { + duration.compare(duration.nanoseconds(1_000_000_000), duration.seconds(1)) + |> should.equal(order.Eq) +} From d96607236fdb69b74fd40af64b234003736d07f3 Mon Sep 17 00:00:00 2001 From: Louis Pilfold Date: Mon, 23 Dec 2024 19:25:23 +0000 Subject: [PATCH 07/21] duration.to_iso8601_string --- src/gleam/time/duration.gleam | 25 +++++++++++++- test/gleam/time/duration_test.gleam | 52 +++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 1 deletion(-) diff --git a/src/gleam/time/duration.gleam b/src/gleam/time/duration.gleam index f9f3fcc..9fd326f 100644 --- a/src/gleam/time/duration.gleam +++ b/src/gleam/time/duration.gleam @@ -1,5 +1,7 @@ import gleam/int +import gleam/io import gleam/order +import gleam/string // TODO: document pub opaque type Duration { @@ -52,7 +54,28 @@ pub fn add(left: Duration, right: Duration) -> Duration { // TODO: docs // TODO: test pub fn to_iso8601_string(duration: Duration) -> String { - todo + let split = fn(total, limit) { + let amount = total % limit + let remainder = { total - amount } / limit + #(amount, remainder) + } + let #(seconds, rest) = split(duration.seconds, 60) + let #(minutes, rest) = split(rest, 60) + let #(hours, rest) = split(rest, 24) + let days = rest + + let add = fn(out, value, unit) { + case value { + 0 -> out + _ -> out <> int.to_string(value) <> unit + } + } + "P" + |> add(days, "D") + |> string.append("T") + |> add(hours, "H") + |> add(minutes, "M") + |> add(seconds, "S") } // TODO: docs diff --git a/test/gleam/time/duration_test.gleam b/test/gleam/time/duration_test.gleam index 3f329f6..8e87911 100644 --- a/test/gleam/time/duration_test.gleam +++ b/test/gleam/time/duration_test.gleam @@ -97,3 +97,55 @@ pub fn compare_5_test() { duration.compare(duration.nanoseconds(1_000_000_000), duration.seconds(1)) |> should.equal(order.Eq) } + +pub fn to_iso8601_string_0_test() { + duration.seconds(42) + |> duration.to_iso8601_string + |> should.equal("PT42S") +} + +pub fn to_iso8601_string_1_test() { + duration.seconds(60) + |> duration.to_iso8601_string + |> should.equal("PT1M") +} + +pub fn to_iso8601_string_2_test() { + duration.seconds(362) + |> duration.to_iso8601_string + |> should.equal("PT6M2S") +} + +pub fn to_iso8601_string_3_test() { + duration.seconds(60 * 60) + |> duration.to_iso8601_string + |> should.equal("PT1H") +} + +pub fn to_iso8601_string_4_test() { + duration.seconds(60 * 60 * 24) + |> duration.to_iso8601_string + |> should.equal("P1DT") +} + +pub fn to_iso8601_string_5_test() { + duration.seconds(60 * 60 * 24 * 50) + |> duration.to_iso8601_string + |> should.equal("P50DT") +} + +pub fn to_iso8601_string_6_test() { + // We don't use years because you can't tell how long a year is in seconds + // without context. _Which_ year? They have different lengths. + duration.seconds(60 * 60 * 24 * 365) + |> duration.to_iso8601_string + |> should.equal("P365DT") +} + +pub fn to_iso8601_string_7_test() { + let year = 60 * 60 * 24 * 365 + let hour = 60 * 60 + duration.seconds(year + hour * 3 + 66) + |> duration.to_iso8601_string + |> should.equal("P365DT3H1M6S") +} From 1c4203c4860e7870ba1206f3f11b195413e42928 Mon Sep 17 00:00:00 2001 From: Louis Pilfold Date: Mon, 23 Dec 2024 20:12:41 +0000 Subject: [PATCH 08/21] Include nanoseconds in duration --- src/gleam/time/duration.gleam | 38 +++++++++++---- test/gleam/time/duration_test.gleam | 72 +++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+), 9 deletions(-) diff --git a/src/gleam/time/duration.gleam b/src/gleam/time/duration.gleam index 9fd326f..ac4dadd 100644 --- a/src/gleam/time/duration.gleam +++ b/src/gleam/time/duration.gleam @@ -1,5 +1,7 @@ +import gleam/bool import gleam/int import gleam/io +import gleam/list import gleam/order import gleam/string @@ -52,7 +54,6 @@ pub fn add(left: Duration, right: Duration) -> Duration { } // TODO: docs -// TODO: test pub fn to_iso8601_string(duration: Duration) -> String { let split = fn(total, limit) { let amount = total % limit @@ -63,19 +64,39 @@ pub fn to_iso8601_string(duration: Duration) -> String { let #(minutes, rest) = split(rest, 60) let #(hours, rest) = split(rest, 24) let days = rest - let add = fn(out, value, unit) { case value { 0 -> out _ -> out <> int.to_string(value) <> unit } } - "P" - |> add(days, "D") - |> string.append("T") - |> add(hours, "H") - |> add(minutes, "M") - |> add(seconds, "S") + let output = + "P" + |> add(days, "D") + |> string.append("T") + |> add(hours, "H") + |> add(minutes, "M") + case seconds, duration.nanoseconds { + 0, 0 -> output + _, 0 -> output <> int.to_string(seconds) <> "S" + _, _ -> { + let f = nanosecond_digits(duration.nanoseconds, 0, "") + output <> int.to_string(seconds) <> "." <> f <> "S" + } + } +} + +fn nanosecond_digits(n: Int, position: Int, acc: String) -> String { + case position { + 9 -> acc + _ if acc == "" && n % 10 == 0 -> { + nanosecond_digits(n / 10, position + 1, acc) + } + _ -> { + let acc = int.to_string(n % 10) <> acc + nanosecond_digits(n / 10, position + 1, acc) + } + } } // TODO: docs @@ -84,7 +105,6 @@ pub fn seconds(amount: Int) -> Duration { } // TODO: docs -// TODO: test pub fn milliseconds(amount: Int) -> Duration { let remainder = amount % 1000 let overflow = amount - remainder diff --git a/test/gleam/time/duration_test.gleam b/test/gleam/time/duration_test.gleam index 8e87911..05caf65 100644 --- a/test/gleam/time/duration_test.gleam +++ b/test/gleam/time/duration_test.gleam @@ -149,3 +149,75 @@ pub fn to_iso8601_string_7_test() { |> duration.to_iso8601_string |> should.equal("P365DT3H1M6S") } + +pub fn to_iso8601_string_8_test() { + duration.milliseconds(1000) + |> duration.to_iso8601_string + |> should.equal("PT1S") +} + +pub fn to_iso8601_string_9_test() { + duration.milliseconds(100) + |> duration.to_iso8601_string + |> should.equal("PT0.1S") +} + +pub fn to_iso8601_string_10_test() { + duration.milliseconds(10) + |> duration.to_iso8601_string + |> should.equal("PT0.01S") +} + +pub fn to_iso8601_string_11_test() { + duration.milliseconds(1) + |> duration.to_iso8601_string + |> should.equal("PT0.001S") +} + +pub fn to_iso8601_string_12_test() { + duration.nanoseconds(1_000_000) + |> duration.to_iso8601_string + |> should.equal("PT0.001S") +} + +pub fn to_iso8601_string_13_test() { + duration.nanoseconds(100_000) + |> duration.to_iso8601_string + |> should.equal("PT0.0001S") +} + +pub fn to_iso8601_string_14_test() { + duration.nanoseconds(10_000) + |> duration.to_iso8601_string + |> should.equal("PT0.00001S") +} + +pub fn to_iso8601_string_15_test() { + duration.nanoseconds(1000) + |> duration.to_iso8601_string + |> should.equal("PT0.000001S") +} + +pub fn to_iso8601_string_16_test() { + duration.nanoseconds(100) + |> duration.to_iso8601_string + |> should.equal("PT0.0000001S") +} + +pub fn to_iso8601_string_17_test() { + duration.nanoseconds(10) + |> duration.to_iso8601_string + |> should.equal("PT0.00000001S") +} + +pub fn to_iso8601_string_18_test() { + duration.nanoseconds(1) + |> duration.to_iso8601_string + |> should.equal("PT0.000000001S") +} + +pub fn to_iso8601_string_19_test() { + duration.nanoseconds(123_456_789) + |> duration.to_iso8601_string + |> should.equal("PT0.123456789S") +} From ce9f8e5e18bcdf43d49e150e7b65ed9824ef0993 Mon Sep 17 00:00:00 2001 From: Louis Pilfold Date: Mon, 23 Dec 2024 20:25:04 +0000 Subject: [PATCH 09/21] timestamp.duration --- src/gleam/time/duration.gleam | 7 ++----- test/gleam/time/duration_test.gleam | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/gleam/time/duration.gleam b/src/gleam/time/duration.gleam index ac4dadd..e3733ec 100644 --- a/src/gleam/time/duration.gleam +++ b/src/gleam/time/duration.gleam @@ -1,7 +1,4 @@ -import gleam/bool import gleam/int -import gleam/io -import gleam/list import gleam/order import gleam/string @@ -42,9 +39,9 @@ pub fn compare(left: Duration, right: Duration) -> order.Order { } // TODO: docs -// TODO: test pub fn difference(left: Duration, right: Duration) -> Duration { - todo + Duration(right.seconds - left.seconds, right.nanoseconds - left.nanoseconds) + |> normalise } // TODO: docs diff --git a/test/gleam/time/duration_test.gleam b/test/gleam/time/duration_test.gleam index 05caf65..42465fd 100644 --- a/test/gleam/time/duration_test.gleam +++ b/test/gleam/time/duration_test.gleam @@ -221,3 +221,18 @@ pub fn to_iso8601_string_19_test() { |> duration.to_iso8601_string |> should.equal("PT0.123456789S") } + +pub fn difference_0_test() { + duration.difference(duration.seconds(100), duration.seconds(250)) + |> should.equal(duration.seconds(150)) +} + +pub fn difference_1_test() { + duration.difference(duration.seconds(250), duration.seconds(100)) + |> should.equal(duration.seconds(-150)) +} + +pub fn difference_2_test() { + duration.difference(duration.seconds(2), duration.milliseconds(3500)) + |> should.equal(duration.milliseconds(1500)) +} From 0dc9da19ed2ddd2c1f42201e5c76b2455fc3b256 Mon Sep 17 00:00:00 2001 From: Louis Pilfold Date: Fri, 27 Dec 2024 11:17:34 +0000 Subject: [PATCH 10/21] Documentation --- src/gleam/time/duration.gleam | 64 +++++++++++++++++++++++++---- src/gleam/time/timestamp.gleam | 16 ++++---- test/gleam/time/duration_test.gleam | 12 ++++++ 3 files changed, 76 insertions(+), 16 deletions(-) diff --git a/src/gleam/time/duration.gleam b/src/gleam/time/duration.gleam index e3733ec..bdfeb0f 100644 --- a/src/gleam/time/duration.gleam +++ b/src/gleam/time/duration.gleam @@ -2,7 +2,8 @@ import gleam/int import gleam/order import gleam/string -// TODO: document +/// An amount of time, with up to nanosecond precision. +/// pub opaque type Duration { // When compiling to JavaScript ints have limited precision and size. This // means that if we were to store the the timestamp in a single int the @@ -30,7 +31,16 @@ fn normalise(duration: Duration) -> Duration { Duration(seconds, nanoseconds) } -// TODO: docs +/// Compare one duration to another, indicating whether the first is greater or +/// smaller than the second. +/// +/// # Examples +/// +/// ```gleam +/// compare(seconds(1), seconds(2)) +/// // -> order.Lt +/// ``` +/// pub fn compare(left: Duration, right: Duration) -> order.Order { order.break_tie( int.compare(left.seconds, right.seconds), @@ -38,19 +48,45 @@ pub fn compare(left: Duration, right: Duration) -> order.Order { ) } -// TODO: docs +/// Calculate the difference between two durations. +/// +/// This is effectively substracting the first duration from the second. +/// +/// # Examples +/// +/// ```gleam +/// difference(seconds(1), seconds(5)) +/// // -> seconds(4) +/// ``` +/// pub fn difference(left: Duration, right: Duration) -> Duration { Duration(right.seconds - left.seconds, right.nanoseconds - left.nanoseconds) |> normalise } -// TODO: docs +/// Add two durations together. +/// +/// # Examples +/// +/// ```gleam +/// add(seconds(1), seconds(5)) +/// // -> seconds(6) +/// ``` +/// pub fn add(left: Duration, right: Duration) -> Duration { Duration(left.seconds + right.seconds, left.nanoseconds + right.nanoseconds) |> normalise } -// TODO: docs +/// Convert the duration to an [ISO8601][1] formatted duration string. +/// +/// The ISO8601 duration format is ambiguous without context due to months and +/// years having different lengths, and because of leap seconds. This function +/// encodes the duration as days, hours, and seconds without any leap seconds. +/// Be sure to take this into account when using the duration strings. +/// +/// [1]: https://en.wikipedia.org/wiki/ISO_8601#Durations +/// pub fn to_iso8601_string(duration: Duration) -> String { let split = fn(total, limit) { let amount = total % limit @@ -96,12 +132,12 @@ fn nanosecond_digits(n: Int, position: Int, acc: String) -> String { } } -// TODO: docs +/// Create a duration of a number of seconds. pub fn seconds(amount: Int) -> Duration { Duration(amount, 0) |> normalise } -// TODO: docs +/// Create a duration of a number of milliseconds. pub fn milliseconds(amount: Int) -> Duration { let remainder = amount % 1000 let overflow = amount - remainder @@ -110,15 +146,25 @@ pub fn milliseconds(amount: Int) -> Duration { Duration(seconds, nanoseconds) } -// TODO: docs +/// Create a duration of a number of nanoseconds. pub fn nanoseconds(amount: Int) -> Duration { Duration(0, amount) |> normalise } -// TODO: docs +/// Convert the duration to a number of seconds. +/// +/// There may be some small loss of precision due to `Duration` being +/// nanosecond accurate and `Float` not being able to represent this. +/// pub fn to_seconds(duration: Duration) -> Float { let seconds = int.to_float(duration.seconds) let nanoseconds = int.to_float(duration.nanoseconds) seconds +. { nanoseconds /. 1_000_000_000.0 } } + +/// Convert the duration to a number of seconds and nanoseconds. There is no +/// loss of precision with this conversion on any target. +pub fn to_seconds_and_nanoseconds(duration: Duration) -> #(Int, Int) { + #(duration.seconds, duration.nanoseconds) +} diff --git a/src/gleam/time/timestamp.gleam b/src/gleam/time/timestamp.gleam index 63bb831..b49c134 100644 --- a/src/gleam/time/timestamp.gleam +++ b/src/gleam/time/timestamp.gleam @@ -71,7 +71,7 @@ pub fn compare(left: Timestamp, right: Timestamp) -> order.Order { // TODO: docs // TODO: test -pub fn now() -> Timestamp { +pub fn system_time() -> Timestamp { todo } @@ -89,15 +89,17 @@ pub fn add(timetamp: Timestamp, duration: Duration) -> Duration { // TODO: docs // TODO: test -pub fn to_rfc3339_utc_string(timestamp: Timestamp) -> String { - todo -} +// TODO: implement +// pub fn to_rfc3339_utc_string(timestamp: Timestamp) -> String { +// todo +// } // TODO: docs // TODO: test -pub fn parse_rfc3339(input: String) -> Result(Timestamp, Nil) { - todo -} +// TODO: implement +// pub fn parse_rfc3339(input: String) -> Result(Timestamp, Nil) { +// todo +// } // TODO: docs // TODO: test diff --git a/test/gleam/time/duration_test.gleam b/test/gleam/time/duration_test.gleam index 42465fd..fe5e4e5 100644 --- a/test/gleam/time/duration_test.gleam +++ b/test/gleam/time/duration_test.gleam @@ -38,6 +38,18 @@ pub fn add_5_test() { |> should.equal(duration.milliseconds(8)) } +pub fn to_seconds_and_nanoseconds_0_test() { + duration.seconds(1) + |> duration.to_seconds_and_nanoseconds() + |> should.equal(#(1, 0)) +} + +pub fn to_seconds_and_nanoseconds_1_test() { + duration.milliseconds(1) + |> duration.to_seconds_and_nanoseconds() + |> should.equal(#(0, 1_000_000)) +} + pub fn to_seconds_0_test() { duration.seconds(1) |> duration.to_seconds From 716c30f19ed05af07851e25edaa0f5df99c6ed56 Mon Sep 17 00:00:00 2001 From: Louis Pilfold Date: Fri, 27 Dec 2024 13:59:43 +0000 Subject: [PATCH 11/21] Timestamp functions --- src/gleam/time/duration.gleam | 5 ++ src/gleam/time/timestamp.gleam | 118 ++++++++++++++++++++------- src/gleam_time_ffi.erl | 5 ++ src/gleam_time_ffi.mjs | 7 ++ test/gleam/time/timestamp_test.gleam | 107 ++++++++++++++++++++++++ 5 files changed, 213 insertions(+), 29 deletions(-) create mode 100644 src/gleam_time_ffi.erl create mode 100644 src/gleam_time_ffi.mjs create mode 100644 test/gleam/time/timestamp_test.gleam diff --git a/src/gleam/time/duration.gleam b/src/gleam/time/duration.gleam index bdfeb0f..d049e88 100644 --- a/src/gleam/time/duration.gleam +++ b/src/gleam/time/duration.gleam @@ -4,6 +4,11 @@ import gleam/string /// An amount of time, with up to nanosecond precision. /// +/// This type does not represent calendar periods such as "1 month" or "2 +/// days". Those periods will be different lengths of time depending on which +/// month or day they apply to. For example, January is longer than February. +/// A different type should be used for calendar periods. +/// pub opaque type Duration { // When compiling to JavaScript ints have limited precision and size. This // means that if we were to store the the timestamp in a single int the diff --git a/src/gleam/time/timestamp.gleam b/src/gleam/time/timestamp.gleam index b49c134..bdbcb51 100644 --- a/src/gleam/time/timestamp.gleam +++ b/src/gleam/time/timestamp.gleam @@ -1,3 +1,4 @@ +import gleam/int import gleam/order import gleam/time/duration.{type Duration} @@ -52,7 +53,6 @@ pub opaque type Timestamp { Timestamp(seconds: Int, nanoseconds: Int) } -// TODO: test /// Ensure the time is represented with `nanoseconds` being positive and less /// than 1 second. /// @@ -60,31 +60,81 @@ pub opaque type Timestamp { /// only adjusts the values used to represent the time. /// fn normalise(timestamp: Timestamp) -> Timestamp { - todo + let multiplier = 1_000_000_000 + let nanoseconds = timestamp.nanoseconds % multiplier + let overflow = timestamp.nanoseconds - nanoseconds + let seconds = timestamp.seconds + overflow / multiplier + Timestamp(seconds, nanoseconds) } -// TODO: docs -// TODO: test +/// Compare one timestamp to another, indicating whether the first is greater or +/// smaller than the second. +/// +/// # Examples +/// +/// ```gleam +/// compare(from_unix_seconds(1), from_unix_seconds(2)) +/// // -> order.Lt +/// ``` +/// pub fn compare(left: Timestamp, right: Timestamp) -> order.Order { - todo + order.break_tie( + int.compare(left.seconds, right.seconds), + int.compare(left.nanoseconds, right.nanoseconds), + ) } -// TODO: docs -// TODO: test +/// Get the current system time. +/// +/// Note this time is not unique or monotonic, it could change at any time or +/// even go backwards! The exact behaviour will depend on the runtime used. See +/// the module documentation for more information. +/// +/// On Erlang this uses [`erlang:system_time/1`][1]. On JavaScript this uses +/// [`Date.now`][2]. +/// +/// [1]: https://www.erlang.org/doc/apps/erts/erlang#system_time/1 +/// [2]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/now +/// pub fn system_time() -> Timestamp { - todo + let #(seconds, nanoseconds) = get_system_time() + normalise(Timestamp(seconds, nanoseconds)) } -// TODO: docs -// TODO: test +@external(erlang, "gleam_time_ffi", "system_time") +@external(javascript, "../../gleam_time_ffi.mjs", "system_time") +fn get_system_time() -> #(Int, Int) + +/// Calculate the difference between two timestamps. +/// +/// This is effectively substracting the first timestamp from the second. +/// +/// # Examples +/// +/// ```gleam +/// difference(from_unix_seconds(1), from_unix_seconds(5)) +/// // -> duration.seconds(4) +/// ``` +/// pub fn difference(left: Timestamp, right: Timestamp) -> Duration { - todo + let seconds = duration.seconds(right.seconds - left.seconds) + let nanoseconds = duration.nanoseconds(right.nanoseconds - left.nanoseconds) + duration.add(seconds, nanoseconds) } -// TODO: docs -// TODO: test -pub fn add(timetamp: Timestamp, duration: Duration) -> Duration { - todo +/// Add a duration to a timestamp. +/// +/// # Examples +/// +/// ```gleam +/// add(from_unix_seconds(1000), duration.seconds(5)) +/// // -> from_unix_seconds(1005) +/// ``` +/// +pub fn add(timestamp: Timestamp, duration: Duration) -> Timestamp { + let #(seconds, nanoseconds) = duration.to_seconds_and_nanoseconds(duration) + Timestamp(timestamp.seconds + seconds, timestamp.nanoseconds + nanoseconds) + |> normalise } // TODO: docs @@ -101,29 +151,39 @@ pub fn add(timetamp: Timestamp, duration: Duration) -> Duration { // todo // } -// TODO: docs -// TODO: test +/// Create a timestamp from a number of seconds since 00:00:00 UTC on 1 January +/// 1970. +/// pub fn from_unix_seconds(seconds: Int) -> Timestamp { - todo + Timestamp(seconds, 0) } -// TODO: docs -// TODO: test +/// Create a timestamp from a number of seconds and nanoseconds since 00:00:00 +/// UTC on 1 January 1970. +/// pub fn from_unix_seconds_and_nanoseconds( seconds seconds: Int, nanoseconds nanoseconds: Int, ) -> Timestamp { - todo + Timestamp(seconds, nanoseconds) + |> normalise } -// TODO: docs -// TODO: test -pub fn to_unix_seconds(input: String) -> Float { - todo +/// Convert the timestamp to a number of seconds since 00:00:00 UTC on 1 +/// January 1970. +/// +/// There may be some small loss of precision due to `Timestamp` being +/// nanosecond accurate and `Float` not being able to represent this. +/// +pub fn to_unix_seconds(timestamp: Timestamp) -> Float { + let seconds = int.to_float(timestamp.seconds) + let nanoseconds = int.to_float(timestamp.nanoseconds) + seconds +. { nanoseconds /. 1_000_000_000.0 } } -// TODO: docs -// TODO: test -pub fn to_unix_seconds_and_nanoseconds(input: String) -> #(Int, Int) { - todo +/// Convert the timestamp to a number of seconds and nanoseconds since 00:00:00 +/// UTC on 1 January 1970. There is no loss of precision with this conversion +/// on any target. +pub fn to_unix_seconds_and_nanoseconds(timestamp: Timestamp) -> #(Int, Int) { + #(timestamp.seconds, timestamp.nanoseconds) } diff --git a/src/gleam_time_ffi.erl b/src/gleam_time_ffi.erl new file mode 100644 index 0000000..2362c8b --- /dev/null +++ b/src/gleam_time_ffi.erl @@ -0,0 +1,5 @@ +-module(gleam_time_ffi). +-export([system_time/0]). + +system_time() -> + {0, erlang:system_time(nanosecond)}. diff --git a/src/gleam_time_ffi.mjs b/src/gleam_time_ffi.mjs new file mode 100644 index 0000000..92dcb2d --- /dev/null +++ b/src/gleam_time_ffi.mjs @@ -0,0 +1,7 @@ +export function system_time() { + const now = Date.now(); + const milliseconds = now % 1_000; + const nanoseconds = milliseconds * 1000_000; + const seconds = (now - milliseconds) / 1_000; + return [seconds, nanoseconds]; +} diff --git a/test/gleam/time/timestamp_test.gleam b/test/gleam/time/timestamp_test.gleam new file mode 100644 index 0000000..f01baa8 --- /dev/null +++ b/test/gleam/time/timestamp_test.gleam @@ -0,0 +1,107 @@ +import gleam/order +import gleam/time/duration +import gleam/time/timestamp +import gleeunit/should + +pub fn compare_0_test() { + timestamp.compare( + timestamp.from_unix_seconds(1), + timestamp.from_unix_seconds(1), + ) + |> should.equal(order.Eq) +} + +pub fn compare_1_test() { + timestamp.compare( + timestamp.from_unix_seconds(2), + timestamp.from_unix_seconds(1), + ) + |> should.equal(order.Gt) +} + +pub fn compare_2_test() { + timestamp.compare( + timestamp.from_unix_seconds(2), + timestamp.from_unix_seconds(3), + ) + |> should.equal(order.Lt) +} + +pub fn difference_0_test() { + timestamp.difference( + timestamp.from_unix_seconds(1), + timestamp.from_unix_seconds(1), + ) + |> should.equal(duration.seconds(0)) +} + +pub fn difference_1_test() { + timestamp.difference( + timestamp.from_unix_seconds(1), + timestamp.from_unix_seconds(5), + ) + |> should.equal(duration.seconds(4)) +} + +pub fn difference_2_test() { + timestamp.difference( + timestamp.from_unix_seconds_and_nanoseconds(1, 10), + timestamp.from_unix_seconds_and_nanoseconds(5, 20), + ) + |> should.equal(duration.seconds(4) |> duration.add(duration.nanoseconds(10))) +} + +pub fn add_0_test() { + timestamp.from_unix_seconds(0) + |> timestamp.add(duration.seconds(1)) + |> should.equal(timestamp.from_unix_seconds(1)) +} + +pub fn add_1_test() { + timestamp.from_unix_seconds(100) + |> timestamp.add(duration.seconds(-1)) + |> should.equal(timestamp.from_unix_seconds(99)) +} + +pub fn add_2_test() { + timestamp.from_unix_seconds(99) + |> timestamp.add(duration.nanoseconds(100)) + |> should.equal(timestamp.from_unix_seconds_and_nanoseconds(99, 100)) +} + +pub fn to_unix_seconds_0_test() { + timestamp.from_unix_seconds_and_nanoseconds(1, 0) + |> timestamp.to_unix_seconds + |> should.equal(1.0) +} + +pub fn to_unix_seconds_1_test() { + timestamp.from_unix_seconds_and_nanoseconds(1, 500_000_000) + |> timestamp.to_unix_seconds + |> should.equal(1.5) +} + +pub fn to_unix_seconds_and_nanoseconds_0_test() { + timestamp.from_unix_seconds_and_nanoseconds(1, 0) + |> timestamp.to_unix_seconds_and_nanoseconds + |> should.equal(#(1, 0)) +} + +pub fn to_unix_seconds_and_nanoseconds_1_test() { + timestamp.from_unix_seconds_and_nanoseconds(1, 2) + |> timestamp.to_unix_seconds_and_nanoseconds + |> should.equal(#(1, 2)) +} + +pub fn system_time_0_test() { + let #(now, _) = + timestamp.system_time() |> timestamp.to_unix_seconds_and_nanoseconds + + // This test will start to fail once enough time has passed. + // When that happens please update these values. + let when_this_test_was_last_updated = 1_735_307_287 + let christmas_day_2025 = 1_766_620_800 + + let assert True = now > when_this_test_was_last_updated + let assert True = now < christmas_day_2025 +} From 14696d395c7ffdebc91770d209608f68ea8d835b Mon Sep 17 00:00:00 2001 From: Louis Pilfold Date: Fri, 27 Dec 2024 15:45:05 +0000 Subject: [PATCH 12/21] rfc3339 --- src/gleam/time/timestamp.gleam | 70 ++++++++++++++++++++++++++-- test/gleam/time/timestamp_test.gleam | 42 +++++++++++++++++ 2 files changed, 107 insertions(+), 5 deletions(-) diff --git a/src/gleam/time/timestamp.gleam b/src/gleam/time/timestamp.gleam index bdbcb51..9e23ede 100644 --- a/src/gleam/time/timestamp.gleam +++ b/src/gleam/time/timestamp.gleam @@ -1,5 +1,8 @@ +import gleam/float import gleam/int import gleam/order +import gleam/result +import gleam/string import gleam/time/duration.{type Duration} /// A timestamp represents a moment in time, represented as an amount of time @@ -138,11 +141,68 @@ pub fn add(timestamp: Timestamp, duration: Duration) -> Timestamp { } // TODO: docs -// TODO: test -// TODO: implement -// pub fn to_rfc3339_utc_string(timestamp: Timestamp) -> String { -// todo -// } +// 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 } + let #(years, months, days) = to_civil(total_minutes) + 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" +} + +fn floored_div(numerator: Int, denominator: Float) -> Int { + let n = int.to_float(numerator) /. denominator + float.round(float.floor(n)) +} + +// Adapted from Elm's Time module +fn to_civil(minutes: Int) -> #(Int, Int, Int) { + let raw_day = floored_div(minutes, { 60.0 *. 24.0 }) + 719_468 + let era = case raw_day >= 0 { + True -> raw_day / 146_097 + False -> { raw_day - 146_096 } / 146_097 + } + let day_of_era = raw_day - era * 146_097 + let year_of_era = + { + day_of_era + - { day_of_era / 1460 } + + { day_of_era / 36_524 } + - { day_of_era / 146_096 } + } + / 365 + let year = year_of_era + era * 400 + let day_of_year = + day_of_era + - { 365 * year_of_era + { year_of_era / 4 } - { year_of_era / 100 } } + let mp = { 5 * day_of_year + 2 } / 153 + let month = case mp < 10 { + True -> mp + 3 + False -> mp - 9 + } + let day = day_of_year - { 153 * mp + 2 } / 5 + 1 + let year = case month <= 2 { + True -> year + 1 + False -> year + } + #(year, month, day) +} // TODO: docs // TODO: test diff --git a/test/gleam/time/timestamp_test.gleam b/test/gleam/time/timestamp_test.gleam index f01baa8..5e6eccc 100644 --- a/test/gleam/time/timestamp_test.gleam +++ b/test/gleam/time/timestamp_test.gleam @@ -105,3 +105,45 @@ pub fn system_time_0_test() { let assert True = now > when_this_test_was_last_updated let assert True = now < christmas_day_2025 } + +pub fn to_rfc3339_utc_0_test() { + timestamp.from_unix_seconds(1_735_309_467) + |> timestamp.to_rfc3339_utc + |> should.equal("2024-12-27T14:24:27Z") +} + +pub fn to_rfc3339_utc_1_test() { + timestamp.from_unix_seconds(1) + |> timestamp.to_rfc3339_utc + |> should.equal("1970-01-01T00:00:01Z") +} + +pub fn to_rfc3339_utc_2_test() { + timestamp.from_unix_seconds(0) + |> timestamp.to_rfc3339_utc + |> should.equal("1970-01-01T00:00:00Z") +} + +pub fn to_rfc3339_utc_3_test() { + timestamp.from_unix_seconds(123_456_789) + |> timestamp.to_rfc3339_utc + |> should.equal("1973-11-29T21:33:09Z") +} + +pub fn to_rfc3339_utc_4_test() { + timestamp.from_unix_seconds(31_560_000) + |> timestamp.to_rfc3339_utc + |> should.equal("1971-01-01T06:40:00Z") +} + +pub fn to_rfc3339_utc_5_test() { + timestamp.from_unix_seconds(-12_345_678) + |> timestamp.to_rfc3339_utc + |> should.equal("1969-08-11T02:38:42Z") +} + +pub fn to_rfc3339_utc_6_test() { + timestamp.from_unix_seconds(-1) + |> timestamp.to_rfc3339_utc + |> should.equal("1969-12-31T23:59:59Z") +} From 977ddf9c08245f3f9b3da57575e6bce405cffd91 Mon Sep 17 00:00:00 2001 From: Louis Pilfold Date: Fri, 27 Dec 2024 17:22:23 +0000 Subject: [PATCH 13/21] Run tests on JS also --- .github/workflows/test.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c3d0e65..67d5183 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,5 +19,6 @@ jobs: rebar3-version: "3" # elixir-version: "1.15.4" - run: gleam deps download - - run: gleam test + - run: gleam test --target erlang + - run: gleam test --target javascript - run: gleam format --check src test From 719f847417e6f3efd9bd7f3fe5ba53a6709b8a14 Mon Sep 17 00:00:00 2001 From: Louis Pilfold Date: Fri, 27 Dec 2024 18:26:29 +0000 Subject: [PATCH 14/21] Accept an offset in RFC function --- src/gleam/time/timestamp.gleam | 61 +++++++++++++++++----------- test/gleam/time/timestamp_test.gleam | 40 +++++++++++------- 2 files changed, 64 insertions(+), 37 deletions(-) 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") +} From 01f0945e9ab81cb38f63a3f46129517ed878a136 Mon Sep 17 00:00:00 2001 From: Louis Pilfold Date: Sun, 29 Dec 2024 21:41:33 +0000 Subject: [PATCH 15/21] Test for Jak --- test/gleam/time/duration_test.gleam | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/gleam/time/duration_test.gleam b/test/gleam/time/duration_test.gleam index fe5e4e5..c0b607d 100644 --- a/test/gleam/time/duration_test.gleam +++ b/test/gleam/time/duration_test.gleam @@ -38,6 +38,12 @@ pub fn add_5_test() { |> should.equal(duration.milliseconds(8)) } +pub fn add_6_test() { + duration.nanoseconds(-2) + |> duration.add(duration.nanoseconds(-3)) + |> should.equal(duration.nanoseconds(-5)) +} + pub fn to_seconds_and_nanoseconds_0_test() { duration.seconds(1) |> duration.to_seconds_and_nanoseconds() From 92ed148c1927cb8c193361e802b5cb6925b59eac Mon Sep 17 00:00:00 2001 From: Louis Pilfold Date: Sun, 29 Dec 2024 22:35:39 +0000 Subject: [PATCH 16/21] Fix duration normalise bug --- gleam.toml | 1 + manifest.toml | 7 +++++++ src/gleam/time/duration.gleam | 5 ++++- test/gleam/time/duration_test.gleam | 24 ++++++++++++++++++++++++ 4 files changed, 36 insertions(+), 1 deletion(-) diff --git a/gleam.toml b/gleam.toml index 9f76f01..1d01a9b 100644 --- a/gleam.toml +++ b/gleam.toml @@ -17,3 +17,4 @@ gleam_stdlib = ">= 0.44.0 and < 2.0.0" [dev-dependencies] gleeunit = ">= 1.0.0 and < 2.0.0" +qcheck = ">= 0.0.7 and < 1.0.0" diff --git a/manifest.toml b/manifest.toml index 0d7658e..ad1c496 100644 --- a/manifest.toml +++ b/manifest.toml @@ -2,10 +2,17 @@ # You typically do not need to edit this file packages = [ + { name = "exception", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "F5580D584F16A20B7FCDCABF9E9BE9A2C1F6AC4F9176FA6DD0B63E3B20D450AA" }, + { name = "gleam_bitwise", version = "1.3.1", build_tools = ["gleam"], requirements = [], otp_app = "gleam_bitwise", source = "hex", outer_checksum = "B36E1D3188D7F594C7FD4F43D0D2CE17561DE896202017548578B16FE1FE9EFC" }, + { 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.51.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "14AFA8D3DDD7045203D422715DBB822D1725992A31DF35A08D97389014B74B68" }, + { 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 = "prng", version = "4.0.0", build_tools = ["gleam"], requirements = ["gleam_bitwise", "gleam_stdlib", "gleam_yielder"], otp_app = "prng", source = "hex", outer_checksum = "E452F957D19CCDC1B4BD12AA6E1B33194B1EB9C2BC0B3449D96E3585602EE3AE" }, + { name = "qcheck", version = "0.0.7", build_tools = ["gleam"], requirements = ["exception", "gleam_regexp", "gleam_stdlib", "gleam_yielder", "prng"], otp_app = "qcheck", source = "hex", outer_checksum = "2363A970BB2E82623023154ED09EE48E3005229A869DA423B18468EA066A4CA9" }, ] [requirements] gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" } gleeunit = { version = ">= 1.0.0 and < 2.0.0" } +qcheck = { version = ">= 0.0.7 and < 1.0.0" } diff --git a/src/gleam/time/duration.gleam b/src/gleam/time/duration.gleam index d049e88..35e0579 100644 --- a/src/gleam/time/duration.gleam +++ b/src/gleam/time/duration.gleam @@ -33,7 +33,10 @@ fn normalise(duration: Duration) -> Duration { let nanoseconds = duration.nanoseconds % multiplier let overflow = duration.nanoseconds - nanoseconds let seconds = duration.seconds + overflow / multiplier - Duration(seconds, nanoseconds) + case nanoseconds >= 0 { + True -> Duration(seconds, nanoseconds) + False -> Duration(seconds - 1, multiplier + nanoseconds) + } } /// Compare one duration to another, indicating whether the first is greater or diff --git a/test/gleam/time/duration_test.gleam b/test/gleam/time/duration_test.gleam index c0b607d..6d34bc9 100644 --- a/test/gleam/time/duration_test.gleam +++ b/test/gleam/time/duration_test.gleam @@ -1,6 +1,18 @@ import gleam/order import gleam/time/duration import gleeunit/should +import qcheck + +pub fn add_property_test() { + use #(x, y) <- qcheck.given(qcheck.map2( + fn(x, y) { #(x, y) }, + qcheck.int_uniform(), + qcheck.int_uniform(), + )) + let expected = duration.nanoseconds(x + y) + let actual = duration.nanoseconds(x) |> duration.add(duration.nanoseconds(y)) + expected == actual +} pub fn add_0_test() { duration.nanoseconds(500_000_000) @@ -44,6 +56,18 @@ pub fn add_6_test() { |> should.equal(duration.nanoseconds(-5)) } +pub fn add_7_test() { + duration.nanoseconds(-1) + |> duration.add(duration.nanoseconds(-1_000_000_000)) + |> should.equal(duration.nanoseconds(-1_000_000_001)) +} + +pub fn add_8_test() { + duration.nanoseconds(1) + |> duration.add(duration.nanoseconds(-1_000_000_000)) + |> should.equal(duration.nanoseconds(-999_999_999)) +} + pub fn to_seconds_and_nanoseconds_0_test() { duration.seconds(1) |> duration.to_seconds_and_nanoseconds() From 9933ffc8c3f2f6c9b0a8058c759dce256e98988e Mon Sep 17 00:00:00 2001 From: Louis Pilfold Date: Sun, 29 Dec 2024 22:42:14 +0000 Subject: [PATCH 17/21] Fix timestamp normalise bug --- src/gleam/time/timestamp.gleam | 5 ++++- test/gleam/time/timestamp_test.gleam | 26 ++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/gleam/time/timestamp.gleam b/src/gleam/time/timestamp.gleam index ca97491..5803c01 100644 --- a/src/gleam/time/timestamp.gleam +++ b/src/gleam/time/timestamp.gleam @@ -66,7 +66,10 @@ fn normalise(timestamp: Timestamp) -> Timestamp { let nanoseconds = timestamp.nanoseconds % multiplier let overflow = timestamp.nanoseconds - nanoseconds let seconds = timestamp.seconds + overflow / multiplier - Timestamp(seconds, nanoseconds) + case nanoseconds >= 0 { + True -> Timestamp(seconds, nanoseconds) + False -> Timestamp(seconds - 1, multiplier + nanoseconds) + } } /// Compare one timestamp to another, indicating whether the first is greater or diff --git a/test/gleam/time/timestamp_test.gleam b/test/gleam/time/timestamp_test.gleam index 64a5ca2..afe56d7 100644 --- a/test/gleam/time/timestamp_test.gleam +++ b/test/gleam/time/timestamp_test.gleam @@ -2,6 +2,7 @@ import gleam/order import gleam/time/duration import gleam/time/timestamp import gleeunit/should +import qcheck pub fn compare_0_test() { timestamp.compare( @@ -51,6 +52,19 @@ pub fn difference_2_test() { |> should.equal(duration.seconds(4) |> duration.add(duration.nanoseconds(10))) } +pub fn add_property_test() { + use #(x, y) <- qcheck.given(qcheck.map2( + fn(x, y) { #(x, y) }, + qcheck.int_uniform(), + qcheck.int_uniform(), + )) + let expected = timestamp.from_unix_seconds_and_nanoseconds(0, x + y) + let actual = + timestamp.from_unix_seconds_and_nanoseconds(0, x) + |> timestamp.add(duration.nanoseconds(y)) + expected == actual +} + pub fn add_0_test() { timestamp.from_unix_seconds(0) |> timestamp.add(duration.seconds(1)) @@ -69,6 +83,18 @@ pub fn add_2_test() { |> should.equal(timestamp.from_unix_seconds_and_nanoseconds(99, 100)) } +pub fn add_3_test() { + timestamp.from_unix_seconds_and_nanoseconds(0, -1) + |> timestamp.add(duration.nanoseconds(-1_000_000_000)) + |> should.equal(timestamp.from_unix_seconds_and_nanoseconds(0, -1_000_000_001)) +} + +pub fn add_4_test() { + timestamp.from_unix_seconds_and_nanoseconds(0, 1) + |> timestamp.add(duration.nanoseconds(-1_000_000_000)) + |> should.equal(timestamp.from_unix_seconds_and_nanoseconds(0, -999_999_999)) +} + pub fn to_unix_seconds_0_test() { timestamp.from_unix_seconds_and_nanoseconds(1, 0) |> timestamp.to_unix_seconds From c20b9833dc0b98f57ed0ce8ed617f581be9e9b84 Mon Sep 17 00:00:00 2001 From: Louis Pilfold Date: Sun, 29 Dec 2024 22:52:39 +0000 Subject: [PATCH 18/21] Compare properties --- test/gleam/time/duration_test.gleam | 36 +++++++++++++++++++++++++++- test/gleam/time/timestamp_test.gleam | 27 ++++++++++++++++++++- 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/test/gleam/time/duration_test.gleam b/test/gleam/time/duration_test.gleam index 6d34bc9..960a7aa 100644 --- a/test/gleam/time/duration_test.gleam +++ b/test/gleam/time/duration_test.gleam @@ -1,9 +1,10 @@ +import gleam/int import gleam/order import gleam/time/duration import gleeunit/should import qcheck -pub fn add_property_test() { +pub fn add_property_0_test() { use #(x, y) <- qcheck.given(qcheck.map2( fn(x, y) { #(x, y) }, qcheck.int_uniform(), @@ -14,6 +15,17 @@ pub fn add_property_test() { expected == actual } +pub fn add_property_1_test() { + use #(x, y) <- qcheck.given(qcheck.map2( + fn(x, y) { #(x, y) }, + qcheck.int_uniform(), + qcheck.int_uniform(), + )) + let expected = duration.seconds(x + y) + let actual = duration.seconds(x) |> duration.add(duration.seconds(y)) + expected == actual +} + pub fn add_0_test() { duration.nanoseconds(500_000_000) |> duration.add(duration.nanoseconds(500_000_000)) @@ -110,6 +122,28 @@ pub fn to_seconds_4_test() { |> should.equal(0.0000005) } +pub fn compare_property_0_test() { + use #(x, y) <- qcheck.given(qcheck.map2( + fn(x, y) { #(x, y) }, + qcheck.int_uniform(), + qcheck.int_uniform(), + )) + let tx = duration.seconds(x) + let ty = duration.seconds(y) + duration.compare(tx, ty) == int.compare(x, y) +} + +pub fn compare_property_1_test() { + use #(x, y) <- qcheck.given(qcheck.map2( + fn(x, y) { #(x, y) }, + qcheck.int_uniform(), + qcheck.int_uniform(), + )) + let tx = duration.nanoseconds(x) + let ty = duration.nanoseconds(y) + duration.compare(tx, ty) == int.compare(x, y) +} + pub fn compare_0_test() { duration.compare(duration.seconds(1), duration.seconds(1)) |> should.equal(order.Eq) diff --git a/test/gleam/time/timestamp_test.gleam b/test/gleam/time/timestamp_test.gleam index afe56d7..6918105 100644 --- a/test/gleam/time/timestamp_test.gleam +++ b/test/gleam/time/timestamp_test.gleam @@ -1,9 +1,21 @@ +import gleam/int import gleam/order import gleam/time/duration import gleam/time/timestamp import gleeunit/should import qcheck +pub fn compare_property_0_test() { + use #(x, y) <- qcheck.given(qcheck.map2( + fn(x, y) { #(x, y) }, + qcheck.int_uniform(), + qcheck.int_uniform(), + )) + let tx = timestamp.from_unix_seconds(x) + let ty = timestamp.from_unix_seconds(y) + timestamp.compare(tx, ty) == int.compare(x, y) +} + pub fn compare_0_test() { timestamp.compare( timestamp.from_unix_seconds(1), @@ -52,7 +64,7 @@ pub fn difference_2_test() { |> should.equal(duration.seconds(4) |> duration.add(duration.nanoseconds(10))) } -pub fn add_property_test() { +pub fn add_property_0_test() { use #(x, y) <- qcheck.given(qcheck.map2( fn(x, y) { #(x, y) }, qcheck.int_uniform(), @@ -65,6 +77,19 @@ pub fn add_property_test() { expected == actual } +pub fn add_property_1_test() { + use #(x, y) <- qcheck.given(qcheck.map2( + fn(x, y) { #(x, y) }, + qcheck.int_uniform(), + qcheck.int_uniform(), + )) + let expected = timestamp.from_unix_seconds_and_nanoseconds(x + y, 0) + let actual = + timestamp.from_unix_seconds_and_nanoseconds(x, 0) + |> timestamp.add(duration.seconds(y)) + expected == actual +} + pub fn add_0_test() { timestamp.from_unix_seconds(0) |> timestamp.add(duration.seconds(1)) From 3bbdeb117b114e7221f7b8eec894f21d32567c71 Mon Sep 17 00:00:00 2001 From: Louis Pilfold Date: Sun, 29 Dec 2024 23:04:27 +0000 Subject: [PATCH 19/21] Better qcheck usage --- test/gleam/time/duration_test.gleam | 12 ++++-------- test/gleam/time/timestamp_test.gleam | 9 +++------ 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/test/gleam/time/duration_test.gleam b/test/gleam/time/duration_test.gleam index 960a7aa..c317b40 100644 --- a/test/gleam/time/duration_test.gleam +++ b/test/gleam/time/duration_test.gleam @@ -5,8 +5,7 @@ import gleeunit/should import qcheck pub fn add_property_0_test() { - use #(x, y) <- qcheck.given(qcheck.map2( - fn(x, y) { #(x, y) }, + use #(x, y) <- qcheck.given(qcheck.tuple2( qcheck.int_uniform(), qcheck.int_uniform(), )) @@ -16,8 +15,7 @@ pub fn add_property_0_test() { } pub fn add_property_1_test() { - use #(x, y) <- qcheck.given(qcheck.map2( - fn(x, y) { #(x, y) }, + use #(x, y) <- qcheck.given(qcheck.tuple2( qcheck.int_uniform(), qcheck.int_uniform(), )) @@ -123,8 +121,7 @@ pub fn to_seconds_4_test() { } pub fn compare_property_0_test() { - use #(x, y) <- qcheck.given(qcheck.map2( - fn(x, y) { #(x, y) }, + use #(x, y) <- qcheck.given(qcheck.tuple2( qcheck.int_uniform(), qcheck.int_uniform(), )) @@ -134,8 +131,7 @@ pub fn compare_property_0_test() { } pub fn compare_property_1_test() { - use #(x, y) <- qcheck.given(qcheck.map2( - fn(x, y) { #(x, y) }, + use #(x, y) <- qcheck.given(qcheck.tuple2( qcheck.int_uniform(), qcheck.int_uniform(), )) diff --git a/test/gleam/time/timestamp_test.gleam b/test/gleam/time/timestamp_test.gleam index 6918105..c49ceb8 100644 --- a/test/gleam/time/timestamp_test.gleam +++ b/test/gleam/time/timestamp_test.gleam @@ -6,8 +6,7 @@ import gleeunit/should import qcheck pub fn compare_property_0_test() { - use #(x, y) <- qcheck.given(qcheck.map2( - fn(x, y) { #(x, y) }, + use #(x, y) <- qcheck.given(qcheck.tuple2( qcheck.int_uniform(), qcheck.int_uniform(), )) @@ -65,8 +64,7 @@ pub fn difference_2_test() { } pub fn add_property_0_test() { - use #(x, y) <- qcheck.given(qcheck.map2( - fn(x, y) { #(x, y) }, + use #(x, y) <- qcheck.given(qcheck.tuple2( qcheck.int_uniform(), qcheck.int_uniform(), )) @@ -78,8 +76,7 @@ pub fn add_property_0_test() { } pub fn add_property_1_test() { - use #(x, y) <- qcheck.given(qcheck.map2( - fn(x, y) { #(x, y) }, + use #(x, y) <- qcheck.given(qcheck.tuple2( qcheck.int_uniform(), qcheck.int_uniform(), )) From 490d46b51e664fb6adb213d160c15d493cb457c7 Mon Sep 17 00:00:00 2001 From: Louis Pilfold Date: Mon, 30 Dec 2024 11:00:42 +0000 Subject: [PATCH 20/21] Adopt decided upon duration comparison semantics --- src/gleam/time/duration.gleam | 31 ++++++++++++---- src/gleam/time/timestamp.gleam | 7 +++- test/gleam/time/duration_test.gleam | 57 +++++++++++++++++++++++++++-- test/gleam_time_test.gleam | 7 ---- 4 files changed, 82 insertions(+), 20 deletions(-) diff --git a/src/gleam/time/duration.gleam b/src/gleam/time/duration.gleam index 35e0579..4c31406 100644 --- a/src/gleam/time/duration.gleam +++ b/src/gleam/time/duration.gleam @@ -19,6 +19,8 @@ pub opaque type Duration { // If you have manually adjusted the seconds and nanoseconds values the // `normalise` function can be used to ensure the time is represented the // intended way, with `nanoseconds` being positive and less than 1 second. + // + // The duration is the sum of the seconds and the nanoseconds. Duration(seconds: Int, nanoseconds: Int) } @@ -39,8 +41,9 @@ fn normalise(duration: Duration) -> Duration { } } -/// Compare one duration to another, indicating whether the first is greater or -/// smaller than the second. +/// Compare one duration to another, indicating whether the first spans a +/// larger amount of time (and so is greater) or smaller amount of time (and so +/// is lesser) than the second. /// /// # Examples /// @@ -49,11 +52,25 @@ fn normalise(duration: Duration) -> Duration { /// // -> order.Lt /// ``` /// +/// Whether a duration is negative or positive doesn't matter for comparing +/// them, only the amount of time spanned matters. +/// +/// ```gleam +/// compare(seconds(-2), seconds(1)) +/// // -> order.Gt +/// ``` +/// pub fn compare(left: Duration, right: Duration) -> order.Order { - order.break_tie( - int.compare(left.seconds, right.seconds), - int.compare(left.nanoseconds, right.nanoseconds), - ) + let parts = fn(x: Duration) { + case x.seconds >= 0 { + True -> #(x.seconds, x.nanoseconds) + False -> #(x.seconds * -1 - 1, 1_000_000_000 - x.nanoseconds) + } + } + let #(ls, lns) = parts(left) + let #(rs, rns) = parts(right) + int.compare(ls, rs) + |> order.break_tie(int.compare(lns, rns)) } /// Calculate the difference between two durations. @@ -142,7 +159,7 @@ fn nanosecond_digits(n: Int, position: Int, acc: String) -> String { /// Create a duration of a number of seconds. pub fn seconds(amount: Int) -> Duration { - Duration(amount, 0) |> normalise + Duration(amount, 0) } /// Create a duration of a number of milliseconds. diff --git a/src/gleam/time/timestamp.gleam b/src/gleam/time/timestamp.gleam index 5803c01..c2ed87d 100644 --- a/src/gleam/time/timestamp.gleam +++ b/src/gleam/time/timestamp.gleam @@ -52,6 +52,8 @@ pub opaque type Timestamp { // If you have manually adjusted the seconds and nanoseconds values the // `normalise` function can be used to ensure the time is represented the // intended way, with `nanoseconds` being positive and less than 1 second. + // + // The timestamp is the sum of the seconds and the nanoseconds. Timestamp(seconds: Int, nanoseconds: Int) } @@ -72,8 +74,9 @@ fn normalise(timestamp: Timestamp) -> Timestamp { } } -/// Compare one timestamp to another, indicating whether the first is greater or -/// smaller than the second. +/// Compare one timestamp to another, indicating whether the first is further +/// into the future (greater) or further into the past (lesser) than the +/// second. /// /// # Examples /// diff --git a/test/gleam/time/duration_test.gleam b/test/gleam/time/duration_test.gleam index c317b40..d22fdc4 100644 --- a/test/gleam/time/duration_test.gleam +++ b/test/gleam/time/duration_test.gleam @@ -122,9 +122,10 @@ pub fn to_seconds_4_test() { pub fn compare_property_0_test() { use #(x, y) <- qcheck.given(qcheck.tuple2( - qcheck.int_uniform(), - qcheck.int_uniform(), + qcheck.int_uniform_inclusive(0, 999_999_999), + qcheck.int_uniform_inclusive(0, 999_999_999), )) + // Durations of seconds let tx = duration.seconds(x) let ty = duration.seconds(y) duration.compare(tx, ty) == int.compare(x, y) @@ -132,14 +133,49 @@ pub fn compare_property_0_test() { pub fn compare_property_1_test() { use #(x, y) <- qcheck.given(qcheck.tuple2( - qcheck.int_uniform(), - qcheck.int_uniform(), + qcheck.int_uniform_inclusive(0, 999_999_999), + qcheck.int_uniform_inclusive(0, 999_999_999), )) + // Durations of nanoseconds let tx = duration.nanoseconds(x) let ty = duration.nanoseconds(y) duration.compare(tx, ty) == int.compare(x, y) } +pub fn compare_property_2_test() { + use x <- qcheck.given(qcheck.int_uniform()) + let tx = duration.nanoseconds(x) + duration.compare(tx, tx) == order.Eq +} + +pub fn compare_property_3_test() { + use #(x, y) <- qcheck.given(qcheck.tuple2( + qcheck.int_uniform_inclusive(0, 999_999_999), + qcheck.int_uniform_inclusive(0, 999_999_999), + )) + let tx = duration.nanoseconds(x) + // It doesn't matter if a duration is positive or negative, it's the amount + // of time spanned that matters. + // + // Second durations + duration.compare(tx, duration.seconds(y)) + == duration.compare(tx, duration.seconds(0 - y)) +} + +pub fn compare_property_4_test() { + use #(x, y) <- qcheck.given(qcheck.tuple2( + qcheck.int_uniform_inclusive(0, 999_999_999), + qcheck.int_uniform_inclusive(0, 999_999_999), + )) + let tx = duration.nanoseconds(x) + // It doesn't matter if a duration is positive or negative, it's the amount + // of time spanned that matters. + // + // Nanosecond durations + duration.compare(tx, duration.nanoseconds(y)) + == duration.compare(tx, duration.nanoseconds(y * -1)) +} + pub fn compare_0_test() { duration.compare(duration.seconds(1), duration.seconds(1)) |> should.equal(order.Eq) @@ -170,6 +206,19 @@ pub fn compare_5_test() { |> should.equal(order.Eq) } +pub fn compare_6_test() { + duration.compare(duration.seconds(-10), duration.seconds(-20)) + |> should.equal(order.Lt) +} + +pub fn compare_7_test() { + duration.compare( + duration.seconds(1) |> duration.add(duration.nanoseconds(1)), + duration.seconds(-1) |> duration.add(duration.nanoseconds(-1)), + ) + |> should.equal(order.Eq) +} + pub fn to_iso8601_string_0_test() { duration.seconds(42) |> duration.to_iso8601_string diff --git a/test/gleam_time_test.gleam b/test/gleam_time_test.gleam index 3831e7a..ecd12ad 100644 --- a/test/gleam_time_test.gleam +++ b/test/gleam_time_test.gleam @@ -1,12 +1,5 @@ import gleeunit -import gleeunit/should pub fn main() { gleeunit.main() } - -// gleeunit test functions end in `_test` -pub fn hello_world_test() { - 1 - |> should.equal(1) -} From 6efb6e1bc3a10c7847e807a7579849c7ba54ab7b Mon Sep 17 00:00:00 2001 From: Louis Pilfold Date: Mon, 30 Dec 2024 11:31:18 +0000 Subject: [PATCH 21/21] Work around qcheck bug See https://github.com/mooreryan/gleam_qcheck/issues/7 --- test/gleam/time/duration_test.gleam | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/test/gleam/time/duration_test.gleam b/test/gleam/time/duration_test.gleam index d22fdc4..d2612cd 100644 --- a/test/gleam/time/duration_test.gleam +++ b/test/gleam/time/duration_test.gleam @@ -122,8 +122,8 @@ pub fn to_seconds_4_test() { pub fn compare_property_0_test() { use #(x, y) <- qcheck.given(qcheck.tuple2( - qcheck.int_uniform_inclusive(0, 999_999_999), - qcheck.int_uniform_inclusive(0, 999_999_999), + qcheck.int_uniform_inclusive(0, 999_999), + qcheck.int_uniform_inclusive(0, 999_999), )) // Durations of seconds let tx = duration.seconds(x) @@ -133,8 +133,8 @@ pub fn compare_property_0_test() { pub fn compare_property_1_test() { use #(x, y) <- qcheck.given(qcheck.tuple2( - qcheck.int_uniform_inclusive(0, 999_999_999), - qcheck.int_uniform_inclusive(0, 999_999_999), + qcheck.int_uniform_inclusive(0, 999_999), + qcheck.int_uniform_inclusive(0, 999_999), )) // Durations of nanoseconds let tx = duration.nanoseconds(x) @@ -150,8 +150,8 @@ pub fn compare_property_2_test() { pub fn compare_property_3_test() { use #(x, y) <- qcheck.given(qcheck.tuple2( - qcheck.int_uniform_inclusive(0, 999_999_999), - qcheck.int_uniform_inclusive(0, 999_999_999), + qcheck.int_uniform_inclusive(0, 999_999), + qcheck.int_uniform_inclusive(0, 999_999), )) let tx = duration.nanoseconds(x) // It doesn't matter if a duration is positive or negative, it's the amount @@ -164,8 +164,8 @@ pub fn compare_property_3_test() { pub fn compare_property_4_test() { use #(x, y) <- qcheck.given(qcheck.tuple2( - qcheck.int_uniform_inclusive(0, 999_999_999), - qcheck.int_uniform_inclusive(0, 999_999_999), + qcheck.int_uniform_inclusive(0, 999_999), + qcheck.int_uniform_inclusive(0, 999_999), )) let tx = duration.nanoseconds(x) // It doesn't matter if a duration is positive or negative, it's the amount