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 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 new file mode 100644 index 0000000..4c31406 --- /dev/null +++ b/src/gleam/time/duration.gleam @@ -0,0 +1,195 @@ +import gleam/int +import gleam/order +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 + // 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. + // + // The duration is the sum of the seconds and the nanoseconds. + Duration(seconds: Int, nanoseconds: Int) +} + +/// 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. +/// +fn normalise(duration: Duration) -> Duration { + let multiplier = 1_000_000_000 + let nanoseconds = duration.nanoseconds % multiplier + let overflow = duration.nanoseconds - nanoseconds + let seconds = duration.seconds + overflow / multiplier + case nanoseconds >= 0 { + True -> Duration(seconds, nanoseconds) + False -> Duration(seconds - 1, multiplier + nanoseconds) + } +} + +/// 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 +/// +/// ```gleam +/// compare(seconds(1), seconds(2)) +/// // -> 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 { + 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. +/// +/// 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 +} + +/// 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 +} + +/// 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 + 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 + } + } + 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) + } + } +} + +/// Create a duration of a number of seconds. +pub fn seconds(amount: Int) -> Duration { + Duration(amount, 0) +} + +/// Create a duration of a number of milliseconds. +pub fn milliseconds(amount: Int) -> Duration { + let remainder = amount % 1000 + let overflow = amount - remainder + let nanoseconds = remainder * 1_000_000 + let seconds = overflow / 1000 + Duration(seconds, nanoseconds) +} + +/// Create a duration of a number of nanoseconds. +pub fn nanoseconds(amount: Int) -> Duration { + Duration(0, amount) + |> normalise +} + +/// 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 new file mode 100644 index 0000000..c2ed87d --- /dev/null +++ b/src/gleam/time/timestamp.gleam @@ -0,0 +1,270 @@ +import gleam/float +import gleam/int +import gleam/order +import gleam/string +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. +/// +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. + // + // The timestamp is the sum of the seconds and the nanoseconds. + Timestamp(seconds: Int, nanoseconds: Int) +} + +/// 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. +/// +fn normalise(timestamp: Timestamp) -> Timestamp { + let multiplier = 1_000_000_000 + let nanoseconds = timestamp.nanoseconds % multiplier + let overflow = timestamp.nanoseconds - nanoseconds + let seconds = timestamp.seconds + overflow / multiplier + case nanoseconds >= 0 { + True -> Timestamp(seconds, nanoseconds) + False -> Timestamp(seconds - 1, multiplier + nanoseconds) + } +} + +/// 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 +/// +/// ```gleam +/// compare(from_unix_seconds(1), from_unix_seconds(2)) +/// // -> order.Lt +/// ``` +/// +pub fn compare(left: Timestamp, right: Timestamp) -> order.Order { + order.break_tie( + int.compare(left.seconds, right.seconds), + int.compare(left.nanoseconds, right.nanoseconds), + ) +} + +/// 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 { + let #(seconds, nanoseconds) = get_system_time() + normalise(Timestamp(seconds, nanoseconds)) +} + +@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 { + let seconds = duration.seconds(right.seconds - left.seconds) + let nanoseconds = duration.nanoseconds(right.nanoseconds - left.nanoseconds) + duration.add(seconds, nanoseconds) +} + +/// 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 +} + +/// 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") } + 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 { + 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 +// TODO: implement +// pub fn parse_rfc3339(input: String) -> Result(Timestamp, Nil) { +// todo +// } + +/// 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 { + Timestamp(seconds, 0) +} + +/// 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 { + Timestamp(seconds, nanoseconds) + |> normalise +} + +/// 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 } +} + +/// 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/duration_test.gleam b/test/gleam/time/duration_test.gleam new file mode 100644 index 0000000..d2612cd --- /dev/null +++ b/test/gleam/time/duration_test.gleam @@ -0,0 +1,359 @@ +import gleam/int +import gleam/order +import gleam/time/duration +import gleeunit/should +import qcheck + +pub fn add_property_0_test() { + use #(x, y) <- qcheck.given(qcheck.tuple2( + 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_property_1_test() { + use #(x, y) <- qcheck.given(qcheck.tuple2( + 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)) + |> 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 add_6_test() { + duration.nanoseconds(-2) + |> duration.add(duration.nanoseconds(-3)) + |> 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() + |> 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 + |> 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_property_0_test() { + use #(x, y) <- qcheck.given(qcheck.tuple2( + qcheck.int_uniform_inclusive(0, 999_999), + qcheck.int_uniform_inclusive(0, 999_999), + )) + // Durations of seconds + 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.tuple2( + qcheck.int_uniform_inclusive(0, 999_999), + qcheck.int_uniform_inclusive(0, 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), + 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 + // 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), + 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 + // 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) +} + +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) +} + +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 + |> 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") +} + +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") +} + +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)) +} diff --git a/test/gleam/time/timestamp_test.gleam b/test/gleam/time/timestamp_test.gleam new file mode 100644 index 0000000..c49ceb8 --- /dev/null +++ b/test/gleam/time/timestamp_test.gleam @@ -0,0 +1,209 @@ +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.tuple2( + 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), + 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_property_0_test() { + use #(x, y) <- qcheck.given(qcheck.tuple2( + 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_property_1_test() { + use #(x, y) <- qcheck.given(qcheck.tuple2( + 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)) + |> 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 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 + |> 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 +} + +pub fn to_rfc3339_0_test() { + timestamp.from_unix_seconds(1_735_309_467) + |> timestamp.to_rfc3339(0) + |> should.equal("2024-12-27T14:24:27Z") +} + +pub fn to_rfc3339_1_test() { + timestamp.from_unix_seconds(1) + |> timestamp.to_rfc3339(0) + |> should.equal("1970-01-01T00:00:01Z") +} + +pub fn to_rfc3339_2_test() { + timestamp.from_unix_seconds(0) + |> timestamp.to_rfc3339(0) + |> should.equal("1970-01-01T00:00:00Z") +} + +pub fn to_rfc3339_3_test() { + timestamp.from_unix_seconds(123_456_789) + |> timestamp.to_rfc3339(0) + |> should.equal("1973-11-29T21:33:09Z") +} + +pub fn to_rfc3339_4_test() { + timestamp.from_unix_seconds(31_560_000) + |> timestamp.to_rfc3339(0) + |> should.equal("1971-01-01T06:40:00Z") +} + +pub fn to_rfc3339_5_test() { + timestamp.from_unix_seconds(-12_345_678) + |> timestamp.to_rfc3339(0) + |> should.equal("1969-08-11T02:38:42Z") +} + +pub fn to_rfc3339_6_test() { + timestamp.from_unix_seconds(-1) + |> 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") +} diff --git a/test/gleam_time_test.gleam b/test/gleam_time_test.gleam new file mode 100644 index 0000000..ecd12ad --- /dev/null +++ b/test/gleam_time_test.gleam @@ -0,0 +1,5 @@ +import gleeunit + +pub fn main() { + gleeunit.main() +} diff --git a/test/leam_time_test.gleam b/test/leam_time_test.gleam deleted file mode 100644 index 3831e7a..0000000 --- a/test/leam_time_test.gleam +++ /dev/null @@ -1,12 +0,0 @@ -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) -}