Skip to content
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

Basic time types #1

Merged
merged 21 commits into from
Dec 30, 2024
Merged
Show file tree
Hide file tree
Changes from 15 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
3 changes: 2 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
175 changes: 175 additions & 0 deletions src/gleam/time/duration.gleam
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
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.
Duration(seconds: Int, nanoseconds: Int)
lpil marked this conversation as resolved.
Show resolved Hide resolved
}

/// 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
Duration(seconds, nanoseconds)
lpil marked this conversation as resolved.
Show resolved Hide resolved
}

/// 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),
int.compare(left.nanoseconds, right.nanoseconds),
)
}
lpil marked this conversation as resolved.
Show resolved Hide resolved

/// Calculate the difference between two durations.
///
/// This is effectively substracting the first duration from the second.
lpil marked this conversation as resolved.
Show resolved Hide resolved
///
/// # 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 {
lpil marked this conversation as resolved.
Show resolved Hide resolved
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
lpil marked this conversation as resolved.
Show resolved Hide resolved
let add = fn(out, value, unit) {
case value {
0 -> out
_ -> out <> int.to_string(value) <> unit
}
}
let output =
"P"
|> add(days, "D")
lpil marked this conversation as resolved.
Show resolved Hide resolved
|> 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) |> normalise
lpil marked this conversation as resolved.
Show resolved Hide resolved
}

/// 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)
}
Loading
Loading