Skip to content

Unit, approximate, minutes, month_to_int, month_from_int #22

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Apr 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
57 changes: 57 additions & 0 deletions src/gleam/time/calendar.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
Expand Down Expand Up @@ -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)
}
}
87 changes: 87 additions & 0 deletions src/gleam/time/duration.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand Down Expand Up @@ -162,13 +238,24 @@ 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
let overflow = amount - remainder
let nanoseconds = remainder * 1_000_000
let seconds = overflow / 1000
Duration(seconds, nanoseconds)
|> normalise
}

/// Create a duration of a number of nanoseconds.
Expand Down
144 changes: 144 additions & 0 deletions test/gleam/time/duration_test.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}