stellar_prune/build/packages/gleam_time/src/gleam/time/duration.gleam
2025-11-30 15:44:22 +01:00

297 lines
8.6 KiB
Gleam

import gleam/bool
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)
}
/// 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 milliseconds.
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.
///
/// 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 {
use <- bool.guard(duration == empty, "PT0S")
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 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.
///
/// # JavaScript int limitations
///
/// Remember that JavaScript can only perfectly represent ints between positive
/// and negative 9,007,199,254,740,991! If you use a single call to this
/// function to create durations larger than that number of nanoseconds then
/// you will likely not get exactly the value you expect. Use `seconds` and
/// `milliseconds` as much as possible for large durations.
///
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)
}
@internal
pub const empty = Duration(0, 0)