diff --git a/CHANGELOG.md b/CHANGELOG.md index d52c8e1..9a197d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## Unreleased + +- Fixed a bug where the `milliseconds` function could return an incorrect value + for negative numbers. +- The `duration` module gains the `Unit` type and the `approximate`, `minutes` + and `hours` functions. +- The `calendar` module gains the `month_to_int` and `month_from_int` + functions. + ## v1.1.0 - 2025-03-29 - The `calendar` module gains the `month_to_string` function. diff --git a/src/gleam/time/calendar.gleam b/src/gleam/time/calendar.gleam index 3af2a25..22d29c3 100644 --- a/src/gleam/time/calendar.gleam +++ b/src/gleam/time/calendar.gleam @@ -83,6 +83,12 @@ pub const utc_offset = duration.empty /// For example, if you are making a web application that runs on a server you /// want _their_ computer's time zone, not yours. /// +/// This is the _current local_ offset, not the current local time zone. This +/// means that while it will result in the expected outcome for the current +/// time, it may result in unexpected output if used with other timestamps. For +/// example: a timestamp that would locally be during daylight savings time if +/// is it not currently daylight savings time when this function is called. +/// pub fn local_offset() -> duration.Duration { duration.seconds(local_time_offset_seconds()) } @@ -115,3 +121,54 @@ pub fn month_to_string(month: Month) -> String { December -> "December" } } + +/// Returns the number for the month, where January is 1 and December is 12. +/// +/// # Examples +/// +/// ```gleam +/// month_to_int(January) +/// // -> 1 +/// ``` +pub fn month_to_int(month: Month) -> Int { + case month { + January -> 1 + February -> 2 + March -> 3 + April -> 4 + May -> 5 + June -> 6 + July -> 7 + August -> 8 + September -> 9 + October -> 10 + November -> 11 + December -> 12 + } +} + +/// Returns the month for a given number, where January is 1 and December is 12. +/// +/// # Examples +/// +/// ```gleam +/// month_from_int(1) +/// // -> Ok(January) +/// ``` +pub fn month_from_int(month: Int) -> Result(Month, Nil) { + case month { + 1 -> Ok(January) + 2 -> Ok(February) + 3 -> Ok(March) + 4 -> Ok(April) + 5 -> Ok(May) + 6 -> Ok(June) + 7 -> Ok(July) + 8 -> Ok(August) + 9 -> Ok(September) + 10 -> Ok(October) + 11 -> Ok(November) + 12 -> Ok(December) + _ -> Error(Nil) + } +} diff --git a/src/gleam/time/duration.gleam b/src/gleam/time/duration.gleam index 0263381..5aae5ed 100644 --- a/src/gleam/time/duration.gleam +++ b/src/gleam/time/duration.gleam @@ -24,6 +24,82 @@ pub opaque type Duration { Duration(seconds: Int, nanoseconds: Int) } +/// A division of time. +/// +/// Note that not all months and years are the same length, so a reasonable +/// average length is used by this module. +/// +pub type Unit { + Nanosecond + /// 1000 nanoseconds. + Microsecond + /// 1000 microseconds. + Millisecond + /// 1000 microseconds. + Second + /// 60 seconds. + Minute + /// 60 minutes. + Hour + /// 24 hours. + Day + /// 7 days. + Week + /// About 30.4375 days. Real calendar months vary in length. + Month + /// About 365.25 days. Real calendar years vary in length. + Year +} + +/// Convert a duration to a number of the largest number of a unit, serving as +/// a rough description of the duration that a human can understand. +/// +/// The size used for each unit are described in the documentation for the +/// `Unit` type. +/// +/// ```gleam +/// seconds(125) +/// |> approximate +/// // -> #(2, Minute) +/// ``` +/// +/// This function rounds _towards zero_. This means that if a duration is just +/// short of 2 days then it will approximate to 1 day. +/// +/// ```gleam +/// hours(47) +/// |> approximate +/// // -> #(1, Day) +/// ``` +/// +pub fn approximate(duration: Duration) -> #(Int, Unit) { + let Duration(seconds: s, nanoseconds: ns) = duration + let minute = 60 + let hour = minute * 60 + let day = hour * 24 + let week = day * 7 + let year = day * 365 + hour * 6 + let month = year / 12 + let microsecond = 1000 + let millisecond = microsecond * 1000 + case Nil { + _ if s < 0 -> { + let #(amount, unit) = Duration(-s, -ns) |> normalise |> approximate + #(-amount, unit) + } + _ if s >= year -> #(s / year, Year) + _ if s >= month -> #(s / month, Month) + _ if s >= week -> #(s / week, Week) + _ if s >= day -> #(s / day, Day) + _ if s >= hour -> #(s / hour, Hour) + _ if s >= minute -> #(s / minute, Minute) + _ if s > 0 -> #(s, Second) + _ if ns >= millisecond -> #(ns / millisecond, Millisecond) + _ if ns >= microsecond -> #(ns / microsecond, Microsecond) + _ -> #(ns, Nanosecond) + } +} + /// Ensure the duration is represented with `nanoseconds` being positive and /// less than 1 second. /// @@ -162,6 +238,16 @@ pub fn seconds(amount: Int) -> Duration { Duration(amount, 0) } +/// Create a duration of a number of minutes. +pub fn minutes(amount: Int) -> Duration { + seconds(amount * 60) +} + +/// Create a duration of a number of hours. +pub fn hours(amount: Int) -> Duration { + seconds(amount * 60 * 60) +} + /// Create a duration of a number of milliseconds. pub fn milliseconds(amount: Int) -> Duration { let remainder = amount % 1000 @@ -169,6 +255,7 @@ pub fn milliseconds(amount: Int) -> Duration { let nanoseconds = remainder * 1_000_000 let seconds = overflow / 1000 Duration(seconds, nanoseconds) + |> normalise } /// Create a duration of a number of nanoseconds. diff --git a/test/gleam/time/duration_test.gleam b/test/gleam/time/duration_test.gleam index 641094f..ea17a21 100644 --- a/test/gleam/time/duration_test.gleam +++ b/test/gleam/time/duration_test.gleam @@ -361,3 +361,147 @@ pub fn difference_2_test() { duration.difference(duration.seconds(2), duration.milliseconds(3500)) |> should.equal(duration.milliseconds(1500)) } + +pub fn approximate_0_test() { + duration.minutes(10) + |> duration.approximate + |> should.equal(#(10, duration.Minute)) +} + +pub fn approximate_1_test() { + duration.seconds(30) + |> duration.approximate + |> should.equal(#(30, duration.Second)) +} + +pub fn approximate_2_test() { + duration.hours(23) + |> duration.approximate + |> should.equal(#(23, duration.Hour)) +} + +pub fn approximate_3_test() { + duration.hours(24) + |> duration.approximate + |> should.equal(#(1, duration.Day)) +} + +pub fn approximate_4_test() { + duration.hours(48) + |> duration.approximate + |> should.equal(#(2, duration.Day)) +} + +pub fn approximate_5_test() { + duration.hours(47) + |> duration.approximate + |> should.equal(#(1, duration.Day)) +} + +pub fn approximate_6_test() { + duration.hours(24 * 7) + |> duration.approximate + |> should.equal(#(1, duration.Week)) +} + +pub fn approximate_7_test() { + duration.hours(24 * 30) + |> duration.approximate + |> should.equal(#(4, duration.Week)) +} + +pub fn approximate_8_test() { + duration.hours(24 * 31) + |> duration.approximate + |> should.equal(#(1, duration.Month)) +} + +pub fn approximate_9_test() { + duration.hours(24 * 66) + |> duration.approximate + |> should.equal(#(2, duration.Month)) +} + +pub fn approximate_10_test() { + duration.hours(24 * 365) + |> duration.approximate + |> should.equal(#(11, duration.Month)) +} + +pub fn approximate_11_test() { + duration.hours(24 * 365 + 5) + |> duration.approximate + |> should.equal(#(11, duration.Month)) +} + +pub fn approximate_12_test() { + duration.hours(24 * 365 + 6) + |> duration.approximate + |> should.equal(#(1, duration.Year)) +} + +pub fn approximate_13_test() { + duration.hours(5 * 24 * 365 + 6) + |> duration.approximate + |> should.equal(#(4, duration.Year)) +} + +pub fn approximate_14_test() { + duration.hours(-5 * 24 * 365 + 6) + |> duration.approximate + |> should.equal(#(-4, duration.Year)) +} + +pub fn approximate_15_test() { + duration.milliseconds(1) + |> duration.approximate + |> should.equal(#(1, duration.Millisecond)) +} + +pub fn approximate_16_test() { + duration.milliseconds(-1) + |> duration.approximate + |> should.equal(#(-1, duration.Millisecond)) +} + +pub fn approximate_17_test() { + duration.milliseconds(999) + |> duration.approximate + |> should.equal(#(999, duration.Millisecond)) +} + +pub fn approximate_18_test() { + duration.nanoseconds(1000) + |> duration.approximate + |> should.equal(#(1, duration.Microsecond)) +} + +pub fn approximate_19_test() { + duration.nanoseconds(-1000) + |> duration.approximate + |> should.equal(#(-1, duration.Microsecond)) +} + +pub fn approximate_20_test() { + duration.nanoseconds(23_000) + |> duration.approximate + |> should.equal(#(23, duration.Microsecond)) +} + +pub fn approximate_21_test() { + duration.nanoseconds(999) + |> duration.approximate + |> should.equal(#(999, duration.Nanosecond)) +} + +pub fn approximate_22_test() { + duration.nanoseconds(-999) + |> duration.approximate + |> should.equal(#(-999, duration.Nanosecond)) +} + +pub fn approximate_23_test() { + duration.nanoseconds(0) + |> duration.approximate + |> should.equal(#(0, duration.Nanosecond)) +}