Initial commit

This commit is contained in:
Hugo Mårdbrink 2025-11-30 15:44:22 +01:00
commit a6272848f9
379 changed files with 74829 additions and 0 deletions

View file

@ -0,0 +1,101 @@
# Time 🕰️
Work with time in Gleam!
[![Package Version](https://img.shields.io/hexpm/v/gleam_time)](https://hex.pm/packages/gleam_time)
[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/gleam_time/)
```sh
gleam add gleam_time
```
This package is the foundation of all code that works with time in Gleam. If
your program uses time then you should be using the types in this package, and
you might choose to add other packages to provide additional functionality.
## How not to have time related bugs
Time is famously difficult to work with! It's a very complex area, and there's
many approaches that seem reasonable or obvious, but then commonly result in
bugs. This package is carefully designed to help you avoid these problems, so
it is wise to read this documentation before continuing.
It is important to understand there are two main ways that time is represented:
- **Calendar time**: This is how humans commonly think and communicate about
time. For example, "10pm on the 5th of January". This is easy for a human to
read, but is it typically ambiguous and hard to work with! 10pm in
Killorglin, Ireland is not the same point in time as 10pm in Harare,
Zimbabwe. The exact meaning of calendar time depends on daylight savings
time, leap years, leap seconds, and continuously changing national and
political declarations. To make calendar time unambiguous you will need to
know what time zone it is for, and to have an up-to-date time zone database.
- **Epoch time**: Epoch time is defined as an exact amount of since some fixed
point in time. It is always unambiguous as is not impacted by geopolitics,
time zones, etc. It is efficient for computers to work with, and it is less
likely to result in buggy code.
In this package epoch time is provided by the `gleam/time/timestamp` module,
and calendar time is provided by the `gleam/time/calendar` module.
Time zone information has to be loaded from elsewhere, but which approch is
best will depend on your application. User interfaces may want to read current
time zone information from the user's web browser or operating system. Server
side applications may want to embed or downloads a full copy of the time zone
database and then ask clients which time zone they want to use.
For an entertaining overview of some of the problems of calendar time view this
video: ["The Problem with Time & Timezones" by Computerphile](https://www.youtube.com/watch?v=-5wpm-gesOY).
### Which time representation should you use?
> **tldr**: Use `gleam/time/timestamp`.
The longer, more detailed answer:
- Default to `gleam/time/timestamp`, which is epoch time. It is
unambiguous, efficient, and significantly less likely to result in logic
bugs.
- When writing time to a database or other data storage use epoch time,
using whatever epoch format it supports. For example, PostgreSQL
`timestamp` and `timestampz` are both epoch time, and `timestamp` is
preferred as it is more straightforward to use as your application is
also using epoch time.
- When communicating with other computer systems continue to use epoch
time. For example, when sending times to another program you could
encode time as UNIX timestamps (seconds since 00:00:00 UTC on 1 January
1970).
- When communicating with humans use epoch time internally, and convert
to-and-from calendar time at the last moment, when iteracting with the
human user. It may also help the users to also show the time as a fuzzy
duration from the present time, such as "about 4 days ago".
- When representing "fuzzy" human time concepts that don't exact periods
in time, such as "one month" (varies depending on which month, which
year, and in which time zone) and "Christmas Day" (varies depending on
which year and time zone) then use calendar time.
Any time you do use calendar time you should be extra careful! It is very
easy to make mistake with. Avoid it where possible.
## Special thanks
This package was created with great help from several kind contributors. In
alphabetical order:
- [Hayleigh Thompson](https://github.com/hayleigh-dot-dev)
- [John Strunk](https://github.com/jrstrunk)
- [Ryan Moore](https://github.com/mooreryan)
- [Shayan Javani](https://github.com/massivefermion)
These non-Gleam projects where highly influential on the design of this
package:
- Elm's `elm/time` package.
- Go's `time` module.
- Rust's `std::time` module.
- Elixir's standard library time modules and `timex` package.

View file

@ -0,0 +1,19 @@
name = "gleam_time"
version = "1.6.0"
description = "Work with time in Gleam!"
gleam = ">= 1.11.0"
licences = ["Apache-2.0"]
repository = { type = "github", user = "gleam-lang", repo = "time" }
links = [
{ title = "Sponsor", href = "https://github.com/sponsors/lpil" }
]
[dependencies]
gleam_stdlib = ">= 0.44.0 and < 2.0.0"
[dev-dependencies]
gleeunit = ">= 1.0.0 and < 2.0.0"
qcheck = ">= 1.0.0 and < 2.0.0"
simplifile = ">= 2.2.0 and < 3.0.0"
gleam_regexp = ">= 1.0.0 and < 2.0.0"
prng = ">= 4.0.1 and < 5.0.0"

View file

@ -0,0 +1,5 @@
-record(date, {
year :: integer(),
month :: gleam@time@calendar:month(),
day :: integer()
}).

View file

@ -0,0 +1,6 @@
-record(time_of_day, {
hours :: integer(),
minutes :: integer(),
seconds :: integer(),
nanoseconds :: integer()
}).

View file

@ -0,0 +1 @@
-record(duration, {seconds :: integer(), nanoseconds :: integer()}).

View file

@ -0,0 +1 @@
-record(timestamp, {seconds :: integer(), nanoseconds :: integer()}).

View file

@ -0,0 +1,346 @@
//// This module is for working with the Gregorian calendar, established by
//// Pope Gregory XIII in 1582!
////
//// ## When should you use this module?
////
//// > **tldr:** You probably want to use [`gleam/time/timestamp`](./timestamp.html)
//// > instead!
////
//// Calendar time is difficult to work with programmatically, it is the source
//// of most time-related bugs in software. Compared to _epoch time_, which the
//// `gleam/time/timestamp` module uses, there are many disadvantages to
//// calendar time:
////
//// - They are ambiguous if you don't know what time-zone is being used.
////
//// - A time-zone database is required to understand calendar time even when
//// you have the time zone. These are large and your program has to
//// continously be updated as new versions of the database are published.
////
//// - The type permits invalid states. e.g. `days` could be set to the number
//// 32, but this should not be possible!
////
//// - There is not a single unique canonical value for each point in time,
//// thanks to time zones. Two different `Date` + `TimeOfDay` value pairs
//// could represent the same point in time. This means that you can't check
//// for time equality with `==` when using calendar types.
////
//// - They are computationally complex, using a more memory to represent and
//// requiring a lot more CPU time to manipulate.
////
//// There are also advantages to calendar time:
////
//// - Calendar time is how human's talk about time, so if you want to show a
//// time or take a time from a human user then calendar time will make it
//// easier for them.
////
//// - They can represent more abstract time periods such as "New Year's Day".
//// This may seem like an exact window of time at first, but really the
//// definition of "New Year's Day" is more fuzzy than that. When it starts
//// and ends will depend where in the world you are, so if you want to refer
//// to a day as a global concept instead of a fixed window of time for that
//// day in a specific location, then calendar time can represent that.
////
//// So when should you use calendar time? These are our recommendations:
////
//// - Default to `gleam/time/timestamp`, which is epoch time. It is
//// unambiguous, efficient, and significantly less likely to result in logic
//// bugs.
////
//// - When writing time to a database or other data storage use epoch time,
//// using whatever epoch format it supports. For example, PostgreSQL
//// `timestamp` and `timestampz` are both epoch time, and `timestamp` is
//// preferred as it is more straightforward to use as your application is
//// also using epoch time.
////
//// - When communicating with other computer systems continue to use epoch
//// time. For example, when sending times to another program you could
//// encode time as UNIX timestamps (seconds since 00:00:00 UTC on 1 January
//// 1970).
////
//// - When communicating with humans use epoch time internally, and convert
//// to-and-from calendar time at the last moment, when iteracting with the
//// human user. It may also help the users to also show the time as a fuzzy
//// duration from the present time, such as "about 4 days ago".
////
//// - When representing "fuzzy" human time concepts that don't exact periods
//// in time, such as "one month" (varies depending on which month, which
//// year, and in which time zone) and "Christmas Day" (varies depending on
//// which year and time zone) then use calendar time.
////
//// Any time you do use calendar time you should be extra careful! It is very
//// easy to make mistake with. Avoid it where possible.
////
//// ## Time zone offsets
////
//// This package includes the `utc_offset` value and the `local_offset`
//// function, which are the offset for the UTC time zone and get the time
//// offset the computer running the program is configured to respectively.
////
//// If you need to use other offsets in your program then you will need to get
//// them from somewhere else, such as from a package which loads the
//// [IANA Time Zone Database](https://www.iana.org/time-zones), or from the
//// website visitor's web browser, which your frontend can send for you.
////
//// ## Use in APIs
////
//// If you are making an API such as a HTTP JSON API you are encouraged to use
//// Unix timestamps instead of calendar times.
import gleam/int
import gleam/order.{type Order}
import gleam/time/duration
/// The Gregorian calendar date. Ambiguous without a time zone.
///
/// Prefer to represent your time using the `Timestamp` type, and convert it
/// only to calendar types when you need to display them. See the documentation
/// for this module for more information.
///
pub type Date {
Date(year: Int, month: Month, day: Int)
}
/// The time of day. Ambiguous without a date and time zone.
///
pub type TimeOfDay {
TimeOfDay(hours: Int, minutes: Int, seconds: Int, nanoseconds: Int)
}
/// The 12 months of the year.
pub type Month {
January
February
March
April
May
June
July
August
September
October
November
December
}
/// The offset for the [Coordinated Universal Time (UTC)](https://en.wikipedia.org/wiki/Coordinated_Universal_Time)
/// time zone.
///
/// The utc zone has no time adjustments, it is always zero. It never observes
/// daylight-saving time and it never shifts around based on political
/// restructuring.
///
pub const utc_offset = duration.empty
/// Get the offset for the computer's currently configured time zone.
///
/// Note this may not be the time zone that is correct to use for your user.
/// 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())
}
@external(erlang, "gleam_time_ffi", "local_time_offset_seconds")
@external(javascript, "../../gleam_time_ffi.mjs", "local_time_offset_seconds")
fn local_time_offset_seconds() -> Int
/// Returns the English name for a month.
///
/// # Examples
///
/// ```gleam
/// month_to_string(April)
/// // -> "April"
/// ```
pub fn month_to_string(month: Month) -> String {
case month {
January -> "January"
February -> "February"
March -> "March"
April -> "April"
May -> "May"
June -> "June"
July -> "July"
August -> "August"
September -> "September"
October -> "October"
November -> "November"
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)
}
}
/// Checks if a given date is valid.
///
/// This function properly accounts for leap years when validating February days.
/// A leap year occurs every 4 years, except for years divisible by 100,
/// unless they are also divisible by 400.
///
/// # Examples
///
/// ```gleam
/// is_valid_date(Date(2023, April, 15))
/// // -> True
/// ```
///
/// ```gleam
/// is_valid_date(Date(2023, April, 31))
/// // -> False
/// ```
///
/// ```gleam
/// is_valid_date(Date(2024, February, 29))
/// // -> True (2024 is a leap year)
/// ```
///
pub fn is_valid_date(date: Date) -> Bool {
let Date(year:, month:, day:) = date
case day < 1 {
True -> False
False ->
case month {
January | March | May | July | August | October | December -> day <= 31
April | June | September | November -> day <= 30
February -> {
let max_february_days = case is_leap_year(year) {
True -> 29
False -> 28
}
day <= max_february_days
}
}
}
}
/// Determines if a given year is a leap year.
///
/// A leap year occurs every 4 years, except for years divisible by 100,
/// unless they are also divisible by 400.
///
/// # Examples
///
/// ```gleam
/// is_leap_year(2024)
/// // -> True
/// ```
///
/// ```gleam
/// is_leap_year(2023)
/// // -> False
/// ```
///
pub fn is_leap_year(year: Int) -> Bool {
case year % 400 == 0 {
True -> True
False ->
case year % 100 == 0 {
True -> False
False -> year % 4 == 0
}
}
}
/// Checks if a time of day is valid.
///
/// Validates that hours are 0-23, minutes are 0-59, seconds are 0-59,
/// and nanoseconds are 0-999,999,999.
///
/// # Examples
///
/// ```gleam
/// is_valid_time_of_day(TimeOfDay(12, 30, 45, 123456789))
/// // -> True
/// ```
///
pub fn is_valid_time_of_day(time: TimeOfDay) -> Bool {
let TimeOfDay(hours:, minutes:, seconds:, nanoseconds:) = time
hours >= 0
&& hours <= 23
&& minutes >= 0
&& minutes <= 59
&& seconds >= 0
&& seconds <= 59
&& nanoseconds >= 0
&& nanoseconds <= 999_999_999
}
/// Naively compares two dates without any time zone information, returning an
/// order.
///
/// ## Correctness
///
/// This function compares dates without any time zone information, only using
/// the rules for the gregorian calendar. This is typically sufficient, but be
/// aware that in reality some time zones will change their calendar date
/// occasionally. This can result in days being skipped, out of order, or
/// happening multiple times.
///
/// If you need real-world correct time ordering then use the
/// `gleam/time/timestamp` module instead.
///
pub fn naive_date_compare(one: Date, other: Date) -> Order {
int.compare(one.year, other.year)
|> order.lazy_break_tie(fn() {
int.compare(month_to_int(one.month), month_to_int(other.month))
})
|> order.lazy_break_tie(fn() { int.compare(one.day, other.day) })
}

View file

@ -0,0 +1,297 @@
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)

View file

@ -0,0 +1,899 @@
//// Welcome to the timestamp module! This module and its `Timestamp` type are
//// what you will be using most commonly when working with time in Gleam.
////
//// A timestamp represents a moment in time, represented as an amount of time
//// since the calendar time 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. It's uncommon that you
//// would need monotonic time, one example might be if you're making a
//// benchmarking framework.
////
//// 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.
//// <https://www.erlang.org/doc/apps/erts/time_correction>.
////
//// # Converting to local calendar 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 the offset for the time
//// zone you wish to use, likely from a time zone database. See the
//// `gleam/time/calendar` module for more information.
////
import gleam/bit_array
import gleam/float
import gleam/int
import gleam/list
import gleam/order
import gleam/result
import gleam/string
import gleam/time/calendar
import gleam/time/duration.{type Duration}
const seconds_per_day: Int = 86_400
const seconds_per_hour: Int = 3600
const seconds_per_minute: Int = 60
const nanoseconds_per_second: Int = 1_000_000_000
/// The `:` character as a byte
const byte_colon: Int = 0x3A
/// The `-` character as a byte
const byte_minus: Int = 0x2D
/// The `0` character as a byte
const byte_zero: Int = 0x30
/// The `9` character as a byte
const byte_nine: Int = 0x39
/// The `t` character as a byte
const byte_t_lowercase: Int = 0x74
/// The `T` character as a byte
const byte_t_uppercase: Int = 0x54
/// The `T` character as a byte
const byte_space: Int = 0x20
/// The Julian seconds of the UNIX epoch (Julian day is 2_440_588)
const julian_seconds_unix_epoch: Int = 210_866_803_200
/// The main time type, which you should favour over other types such as
/// calendar time types. It is efficient, unambiguous, and it is not possible
/// to construct an invalid timestamp.
///
/// The most common situation in which you may need a different time data
/// structure is when you need to display time to human for them to read. When
/// you need to do this convert the timestamp to calendar time when presenting
/// it, but internally always keep the time as a timestamp.
///
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)
}
/// The epoch of Unix time, which is 00:00:00 UTC on 1 January 1970.
pub const unix_epoch = Timestamp(0, 0)
/// 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 as an additional argument.
///
/// The output of this function is also ISO 8601 compatible so long as the
/// offset not negative. Offsets have at-most minute precision, so an offset
/// with higher precision will be rounded to the nearest minute.
///
/// If you are making an API such as a HTTP JSON API you are encouraged to use
/// Unix timestamps instead of this format or ISO 8601. Unix timestamps are a
/// better choice as they don't contain offset information. Consider:
///
/// - UTC offsets are not time zones. This does not and cannot tell us the time
/// zone in which the date was recorded. So what are we supposed to do with
/// this information?
/// - Users typically want dates formatted according to their local time zone.
/// What if the provided UTC offset is different from the current user's time
/// zone? What are we supposed to do with it then?
/// - Despite it being useless (or worse, a source of bugs), the UTC offset
/// creates a larger payload to transfer.
///
/// They also uses more memory than a unix timestamp. The way they are better
/// than Unix timestamp is that it is easier for a human to read them, but
/// this is a hinderance that tooling can remedy, and APIs are not primarily
/// for humans.
///
/// # Examples
///
/// ```gleam
/// timestamp.from_unix_seconds_and_nanoseconds(1000, 123_000_000)
/// |> to_rfc3339(calendar.utc_offset)
/// // -> "1970-01-01T00:16:40.123Z"
/// ```
///
/// ```gleam
/// timestamp.from_unix_seconds(1000)
/// |> to_rfc3339(duration.seconds(3600))
/// // -> "1970-01-01T01:16:40+01:00"
/// ```
///
pub fn to_rfc3339(timestamp: Timestamp, offset: Duration) -> String {
let offset = duration_to_minutes(offset)
let #(years, months, days, hours, minutes, seconds) =
to_calendar_from_offset(timestamp, offset)
let offset_minutes = modulo(offset, 60)
let offset_hours = int.absolute_value(floored_div(offset, 60.0))
let n2 = pad_digit(_, to: 2)
let n4 = pad_digit(_, to: 4)
let out = ""
let out = out <> n4(years) <> "-" <> n2(months) <> "-" <> n2(days)
let out = out <> "T"
let out = out <> n2(hours) <> ":" <> n2(minutes) <> ":" <> n2(seconds)
let out = out <> show_second_fraction(timestamp.nanoseconds)
case int.compare(offset, 0) {
order.Eq -> out <> "Z"
order.Gt -> out <> "+" <> n2(offset_hours) <> ":" <> n2(offset_minutes)
order.Lt -> out <> "-" <> n2(offset_hours) <> ":" <> n2(offset_minutes)
}
}
fn pad_digit(digit: Int, to desired_length: Int) -> String {
int.to_string(digit) |> string.pad_start(desired_length, "0")
}
/// Convert a `Timestamp` to calendar time, suitable for presenting to a human
/// to read.
///
/// If you want a machine to use the time value then you should not use this
/// function and should instead keep it as a timestamp. See the documentation
/// for the `gleam/time/calendar` module for more information.
///
/// # Examples
///
/// ```gleam
/// timestamp.from_unix_seconds(0)
/// |> timestamp.to_calendar(calendar.utc_offset)
/// // -> #(Date(1970, January, 1), TimeOfDay(0, 0, 0, 0))
/// ```
///
pub fn to_calendar(
timestamp: Timestamp,
offset: Duration,
) -> #(calendar.Date, calendar.TimeOfDay) {
let offset = duration_to_minutes(offset)
let #(year, month, day, hours, minutes, seconds) =
to_calendar_from_offset(timestamp, offset)
let month = case month {
1 -> calendar.January
2 -> calendar.February
3 -> calendar.March
4 -> calendar.April
5 -> calendar.May
6 -> calendar.June
7 -> calendar.July
8 -> calendar.August
9 -> calendar.September
10 -> calendar.October
11 -> calendar.November
_ -> calendar.December
}
let nanoseconds = timestamp.nanoseconds
let date = calendar.Date(year:, month:, day:)
let time = calendar.TimeOfDay(hours:, minutes:, seconds:, nanoseconds:)
#(date, time)
}
fn duration_to_minutes(duration: duration.Duration) -> Int {
float.round(duration.to_seconds(duration) /. 60.0)
}
fn to_calendar_from_offset(
timestamp: Timestamp,
offset: Int,
) -> #(Int, Int, Int, Int, Int, Int) {
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 #(year, month, day) = to_civil(total_minutes)
#(year, month, day, hours, minutes, seconds)
}
/// Create a `Timestamp` from a human-readable calendar time.
///
/// # Examples
///
/// ```gleam
/// timestamp.from_calendar(
/// date: calendar.Date(2024, calendar.December, 25),
/// time: calendar.TimeOfDay(12, 30, 50, 0),
/// offset: calendar.utc_offset,
/// )
/// |> timestamp.to_rfc3339(calendar.utc_offset)
/// // -> "2024-12-25T12:30:50Z"
/// ```
///
pub fn from_calendar(
date date: calendar.Date,
time time: calendar.TimeOfDay,
offset offset: Duration,
) -> Timestamp {
let month = case date.month {
calendar.January -> 1
calendar.February -> 2
calendar.March -> 3
calendar.April -> 4
calendar.May -> 5
calendar.June -> 6
calendar.July -> 7
calendar.August -> 8
calendar.September -> 9
calendar.October -> 10
calendar.November -> 11
calendar.December -> 12
}
from_date_time(
year: date.year,
month:,
day: date.day,
hours: time.hours,
minutes: time.minutes,
seconds: time.seconds,
second_fraction_as_nanoseconds: time.nanoseconds,
offset_seconds: float.round(duration.to_seconds(offset)),
)
}
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)
}
/// Converts nanoseconds into a `String` representation of fractional seconds.
///
/// Assumes that `nanoseconds < 1_000_000_000`, which will be true for any
/// normalised timestamp.
///
fn show_second_fraction(nanoseconds: Int) -> String {
case int.compare(nanoseconds, 0) {
// Zero fractional seconds are not shown.
order.Lt | order.Eq -> ""
order.Gt -> {
let second_fraction_part = {
nanoseconds
|> get_zero_padded_digits
|> remove_trailing_zeros
|> list.map(int.to_string)
|> string.join("")
}
"." <> second_fraction_part
}
}
}
/// Given a list of digits, return new list with any trailing zeros removed.
///
fn remove_trailing_zeros(digits: List(Int)) -> List(Int) {
let reversed_digits = list.reverse(digits)
do_remove_trailing_zeros(reversed_digits)
}
fn do_remove_trailing_zeros(reversed_digits) {
case reversed_digits {
[] -> []
[digit, ..digits] if digit == 0 -> do_remove_trailing_zeros(digits)
reversed_digits -> list.reverse(reversed_digits)
}
}
/// Returns the list of digits of `number`. If the number of digits is less
/// than 9, the result is zero-padded at the front.
///
fn get_zero_padded_digits(number: Int) -> List(Int) {
do_get_zero_padded_digits(number, [], 0)
}
fn do_get_zero_padded_digits(
number: Int,
digits: List(Int),
count: Int,
) -> List(Int) {
case number {
number if number <= 0 && count >= 9 -> digits
number if number <= 0 ->
// Zero-pad the digits at the front until we have at least 9 digits.
do_get_zero_padded_digits(number, [0, ..digits], count + 1)
number -> {
let digit = number % 10
let number = floored_div(number, 10.0)
do_get_zero_padded_digits(number, [digit, ..digits], count + 1)
}
}
}
/// Parses an [RFC 3339 formatted time string][spec] into a `Timestamp`.
///
/// [spec]: https://datatracker.ietf.org/doc/html/rfc3339#section-5.6
///
/// # Examples
///
/// ```gleam
/// let assert Ok(ts) = timestamp.parse_rfc3339("1970-01-01T00:00:01Z")
/// timestamp.to_unix_seconds_and_nanoseconds(ts)
/// // -> #(1, 0)
/// ```
///
/// Parsing an invalid timestamp returns an error.
///
/// ```gleam
/// let assert Error(Nil) = timestamp.parse_rfc3339("1995-10-31")
/// ```
///
/// ## Time zones
///
/// It may at first seem that the RFC 3339 format includes timezone
/// information, as it can specify an offset such as `Z` or `+3`, so why does
/// this function not return calendar time with a time zone? There are multiple
/// reasons:
///
/// - RFC 3339's timestamp format is based on calendar time, but it is
/// unambigous, so it can be converted into epoch time when being parsed. It
/// is always better to internally use epoch time to represent unambiguous
/// points in time, so we perform that conversion as a convenience and to
/// ensure that programmers with less time experience don't accidentally use
/// a less suitable time representation.
///
/// - RFC 3339's contains _calendar time offset_ information, not time zone
/// information. This is enough to convert it to an unambiguous timestamp,
/// but it is not enough information to reliably work with calendar time.
/// Without the time zone and the time zone database it's not possible to
/// know what time period that offset is valid for, so it cannot be used
/// without risk of bugs.
///
/// ## Behaviour details
///
/// - Follows the grammar specified in section 5.6 Internet Date/Time Format of
/// RFC 3339 <https://datatracker.ietf.org/doc/html/rfc3339#section-5.6>.
/// - The `T` and `Z` characters may alternatively be lower case `t` or `z`,
/// respectively.
/// - Full dates and full times must be separated by `T` or `t`. A space is also
/// permitted.
/// - Leap seconds rules are not considered. That is, any timestamp may
/// specify digts `00` - `60` for the seconds.
/// - Any part of a fractional second that cannot be represented in the
/// nanosecond precision is tructated. That is, for the time string,
/// `"1970-01-01T00:00:00.1234567899Z"`, the fractional second `.1234567899`
/// will be represented as `123_456_789` in the `Timestamp`.
///
pub fn parse_rfc3339(input: String) -> Result(Timestamp, Nil) {
let bytes = bit_array.from_string(input)
// Date
use #(year, bytes) <- result.try(parse_year(from: bytes))
use bytes <- result.try(accept_byte(from: bytes, value: byte_minus))
use #(month, bytes) <- result.try(parse_month(from: bytes))
use bytes <- result.try(accept_byte(from: bytes, value: byte_minus))
use #(day, bytes) <- result.try(parse_day(from: bytes, year:, month:))
use bytes <- result.try(accept_date_time_separator(from: bytes))
// Time
use #(hours, bytes) <- result.try(parse_hours(from: bytes))
use bytes <- result.try(accept_byte(from: bytes, value: byte_colon))
use #(minutes, bytes) <- result.try(parse_minutes(from: bytes))
use bytes <- result.try(accept_byte(from: bytes, value: byte_colon))
use #(seconds, bytes) <- result.try(parse_seconds(from: bytes))
use #(second_fraction_as_nanoseconds, bytes) <- result.try(
parse_second_fraction_as_nanoseconds(from: bytes),
)
// Offset
use #(offset_seconds, bytes) <- result.try(parse_offset(from: bytes))
// Done
use Nil <- result.try(accept_empty(bytes))
Ok(from_date_time(
year:,
month:,
day:,
hours:,
minutes:,
seconds:,
second_fraction_as_nanoseconds:,
offset_seconds:,
))
}
fn parse_year(from bytes: BitArray) -> Result(#(Int, BitArray), Nil) {
parse_digits(from: bytes, count: 4)
}
fn parse_month(from bytes: BitArray) -> Result(#(Int, BitArray), Nil) {
use #(month, bytes) <- result.try(parse_digits(from: bytes, count: 2))
case 1 <= month && month <= 12 {
True -> Ok(#(month, bytes))
False -> Error(Nil)
}
}
fn parse_day(
from bytes: BitArray,
year year,
month month,
) -> Result(#(Int, BitArray), Nil) {
use #(day, bytes) <- result.try(parse_digits(from: bytes, count: 2))
use max_day <- result.try(case month {
1 | 3 | 5 | 7 | 8 | 10 | 12 -> Ok(31)
4 | 6 | 9 | 11 -> Ok(30)
2 -> {
case is_leap_year(year) {
True -> Ok(29)
False -> Ok(28)
}
}
_ -> Error(Nil)
})
case 1 <= day && day <= max_day {
True -> Ok(#(day, bytes))
False -> Error(Nil)
}
}
// Implementation from RFC 3339 Appendix C
fn is_leap_year(year: Int) -> Bool {
year % 4 == 0 && { year % 100 != 0 || year % 400 == 0 }
}
fn parse_hours(from bytes: BitArray) -> Result(#(Int, BitArray), Nil) {
use #(hours, bytes) <- result.try(parse_digits(from: bytes, count: 2))
case 0 <= hours && hours <= 23 {
True -> Ok(#(hours, bytes))
False -> Error(Nil)
}
}
fn parse_minutes(from bytes: BitArray) -> Result(#(Int, BitArray), Nil) {
use #(minutes, bytes) <- result.try(parse_digits(from: bytes, count: 2))
case 0 <= minutes && minutes <= 59 {
True -> Ok(#(minutes, bytes))
False -> Error(Nil)
}
}
fn parse_seconds(from bytes: BitArray) -> Result(#(Int, BitArray), Nil) {
use #(seconds, bytes) <- result.try(parse_digits(from: bytes, count: 2))
// Max of 60 for leap seconds. We don't bother to check if this leap second
// actually occurred in the past or not.
case 0 <= seconds && seconds <= 60 {
True -> Ok(#(seconds, bytes))
False -> Error(Nil)
}
}
// Truncates any part of the fraction that is beyond the nanosecond precision.
fn parse_second_fraction_as_nanoseconds(from bytes: BitArray) {
case bytes {
<<".", byte, remaining_bytes:bytes>>
if byte_zero <= byte && byte <= byte_nine
-> {
do_parse_second_fraction_as_nanoseconds(
from: <<byte, remaining_bytes:bits>>,
acc: 0,
power: nanoseconds_per_second,
)
}
// bytes starts with a ".", which should introduce a fraction, but it does
// not, and so it is an ill-formed input.
<<".", _:bytes>> -> Error(Nil)
// bytes does not start with a "." so there is no fraction. Call this 0
// nanoseconds.
_ -> Ok(#(0, bytes))
}
}
fn do_parse_second_fraction_as_nanoseconds(
from bytes: BitArray,
acc acc: Int,
power power: Int,
) -> Result(#(Int, BitArray), a) {
// Each digit place to the left in the fractional second is 10x fewer
// nanoseconds.
let power = power / 10
case bytes {
<<byte, remaining_bytes:bytes>>
if byte_zero <= byte && byte <= byte_nine && power < 1
-> {
// We already have the max precision for nanoseconds. Truncate any
// remaining digits.
do_parse_second_fraction_as_nanoseconds(
from: remaining_bytes,
acc:,
power:,
)
}
<<byte, remaining_bytes:bytes>> if byte_zero <= byte && byte <= byte_nine -> {
// We have not yet reached the precision limit. Parse the next digit.
let digit = byte - 0x30
do_parse_second_fraction_as_nanoseconds(
from: remaining_bytes,
acc: acc + digit * power,
power:,
)
}
_ -> Ok(#(acc, bytes))
}
}
fn parse_offset(from bytes: BitArray) -> Result(#(Int, BitArray), Nil) {
case bytes {
<<"Z", remaining_bytes:bytes>> | <<"z", remaining_bytes:bytes>> ->
Ok(#(0, remaining_bytes))
_ -> parse_numeric_offset(bytes)
}
}
fn parse_numeric_offset(from bytes: BitArray) -> Result(#(Int, BitArray), Nil) {
use #(sign, bytes) <- result.try(parse_sign(from: bytes))
use #(hours, bytes) <- result.try(parse_hours(from: bytes))
use bytes <- result.try(accept_byte(from: bytes, value: byte_colon))
use #(minutes, bytes) <- result.try(parse_minutes(from: bytes))
let offset_seconds = offset_to_seconds(sign, hours:, minutes:)
Ok(#(offset_seconds, bytes))
}
fn parse_sign(from bytes) {
case bytes {
<<"+", remaining_bytes:bytes>> -> Ok(#("+", remaining_bytes))
<<"-", remaining_bytes:bytes>> -> Ok(#("-", remaining_bytes))
_ -> Error(Nil)
}
}
fn offset_to_seconds(sign, hours hours, minutes minutes) {
let abs_seconds = hours * seconds_per_hour + minutes * seconds_per_minute
case sign {
"-" -> -abs_seconds
_ -> abs_seconds
}
}
/// Parse and return the given number of digits from the given bytes.
///
fn parse_digits(
from bytes: BitArray,
count count: Int,
) -> Result(#(Int, BitArray), Nil) {
do_parse_digits(from: bytes, count:, acc: 0, k: 0)
}
fn do_parse_digits(
from bytes: BitArray,
count count: Int,
acc acc: Int,
k k: Int,
) -> Result(#(Int, BitArray), Nil) {
case bytes {
_ if k >= count -> Ok(#(acc, bytes))
<<byte, remaining_bytes:bytes>> if byte_zero <= byte && byte <= byte_nine ->
do_parse_digits(
from: remaining_bytes,
count:,
acc: acc * 10 + { byte - 0x30 },
k: k + 1,
)
_ -> Error(Nil)
}
}
/// Accept the given value from `bytes` and move past it if found.
///
fn accept_byte(from bytes: BitArray, value value: Int) -> Result(BitArray, Nil) {
case bytes {
<<byte, remaining_bytes:bytes>> if byte == value -> Ok(remaining_bytes)
_ -> Error(Nil)
}
}
fn accept_date_time_separator(from bytes: BitArray) -> Result(BitArray, Nil) {
case bytes {
<<byte, remaining_bytes:bytes>>
if byte == byte_t_uppercase
|| byte == byte_t_lowercase
|| byte == byte_space
-> Ok(remaining_bytes)
_ -> Error(Nil)
}
}
fn accept_empty(from bytes: BitArray) -> Result(Nil, Nil) {
case bytes {
<<>> -> Ok(Nil)
_ -> Error(Nil)
}
}
/// Note: The caller of this function must ensure that all inputs are valid.
///
fn from_date_time(
year year: Int,
month month: Int,
day day: Int,
hours hours: Int,
minutes minutes: Int,
seconds seconds: Int,
second_fraction_as_nanoseconds second_fraction_as_nanoseconds: Int,
offset_seconds offset_seconds: Int,
) -> Timestamp {
let julian_seconds =
julian_seconds_from_parts(year:, month:, day:, hours:, minutes:, seconds:)
let julian_seconds_since_epoch = julian_seconds - julian_seconds_unix_epoch
Timestamp(
seconds: julian_seconds_since_epoch - offset_seconds,
nanoseconds: second_fraction_as_nanoseconds,
)
|> normalise
}
/// `julian_seconds_from_parts(year, month, day, hours, minutes, seconds)`
/// returns the number of Julian
/// seconds represented by the given arguments.
///
/// Note: It is the callers responsibility to ensure the inputs are valid.
///
/// See https://www.tondering.dk/claus/cal/julperiod.php#formula
///
fn julian_seconds_from_parts(
year year: Int,
month month: Int,
day day: Int,
hours hours: Int,
minutes minutes: Int,
seconds seconds: Int,
) {
let julian_day_seconds =
julian_day_from_ymd(year:, month:, day:) * seconds_per_day
julian_day_seconds
+ { hours * seconds_per_hour }
+ { minutes * seconds_per_minute }
+ seconds
}
/// Note: It is the callers responsibility to ensure the inputs are valid.
///
/// See https://www.tondering.dk/claus/cal/julperiod.php#formula
///
fn julian_day_from_ymd(year year: Int, month month: Int, day day: Int) -> Int {
let adjustment = { 14 - month } / 12
let adjusted_year = year + 4800 - adjustment
let adjusted_month = month + 12 * adjustment - 3
day
+ { { 153 * adjusted_month } + 2 }
/ 5
+ 365
* adjusted_year
+ { adjusted_year / 4 }
- { adjusted_year / 100 }
+ { adjusted_year / 400 }
- 32_045
}
/// 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.
///
/// # JavaScript int limitations
///
/// Remember that JavaScript can only perfectly represent ints between positive
/// and negative 9,007,199,254,740,991! If you only use the nanosecond field
/// then you will almost certainly not get the date value you want due to this
/// loss of precision. Always use seconds primarily and then use nanoseconds
/// for the final sub-second adjustment.
///
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)
}

View file

@ -0,0 +1,468 @@
-module(gleam@time@calendar).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/gleam/time/calendar.gleam").
-export([local_offset/0, month_to_string/1, month_to_int/1, month_from_int/1, is_leap_year/1, is_valid_date/1, is_valid_time_of_day/1, naive_date_compare/2]).
-export_type([date/0, time_of_day/0, month/0]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
?MODULEDOC(
" This module is for working with the Gregorian calendar, established by\n"
" Pope Gregory XIII in 1582!\n"
"\n"
" ## When should you use this module?\n"
"\n"
" > **tldr:** You probably want to use [`gleam/time/timestamp`](./timestamp.html)\n"
" > instead!\n"
"\n"
" Calendar time is difficult to work with programmatically, it is the source\n"
" of most time-related bugs in software. Compared to _epoch time_, which the\n"
" `gleam/time/timestamp` module uses, there are many disadvantages to\n"
" calendar time:\n"
"\n"
" - They are ambiguous if you don't know what time-zone is being used.\n"
"\n"
" - A time-zone database is required to understand calendar time even when\n"
" you have the time zone. These are large and your program has to\n"
" continously be updated as new versions of the database are published.\n"
"\n"
" - The type permits invalid states. e.g. `days` could be set to the number\n"
" 32, but this should not be possible!\n"
"\n"
" - There is not a single unique canonical value for each point in time,\n"
" thanks to time zones. Two different `Date` + `TimeOfDay` value pairs\n"
" could represent the same point in time. This means that you can't check\n"
" for time equality with `==` when using calendar types.\n"
"\n"
" - They are computationally complex, using a more memory to represent and\n"
" requiring a lot more CPU time to manipulate.\n"
"\n"
" There are also advantages to calendar time:\n"
"\n"
" - Calendar time is how human's talk about time, so if you want to show a\n"
" time or take a time from a human user then calendar time will make it\n"
" easier for them.\n"
"\n"
" - They can represent more abstract time periods such as \"New Year's Day\".\n"
" This may seem like an exact window of time at first, but really the\n"
" definition of \"New Year's Day\" is more fuzzy than that. When it starts\n"
" and ends will depend where in the world you are, so if you want to refer\n"
" to a day as a global concept instead of a fixed window of time for that\n"
" day in a specific location, then calendar time can represent that.\n"
"\n"
" So when should you use calendar time? These are our recommendations:\n"
"\n"
" - Default to `gleam/time/timestamp`, which is epoch time. It is\n"
" unambiguous, efficient, and significantly less likely to result in logic\n"
" bugs.\n"
"\n"
" - When writing time to a database or other data storage use epoch time,\n"
" using whatever epoch format it supports. For example, PostgreSQL\n"
" `timestamp` and `timestampz` are both epoch time, and `timestamp` is\n"
" preferred as it is more straightforward to use as your application is\n"
" also using epoch time.\n"
"\n"
" - When communicating with other computer systems continue to use epoch\n"
" time. For example, when sending times to another program you could\n"
" encode time as UNIX timestamps (seconds since 00:00:00 UTC on 1 January\n"
" 1970).\n"
"\n"
" - When communicating with humans use epoch time internally, and convert\n"
" to-and-from calendar time at the last moment, when iteracting with the\n"
" human user. It may also help the users to also show the time as a fuzzy\n"
" duration from the present time, such as \"about 4 days ago\".\n"
"\n"
" - When representing \"fuzzy\" human time concepts that don't exact periods\n"
" in time, such as \"one month\" (varies depending on which month, which\n"
" year, and in which time zone) and \"Christmas Day\" (varies depending on\n"
" which year and time zone) then use calendar time.\n"
"\n"
" Any time you do use calendar time you should be extra careful! It is very\n"
" easy to make mistake with. Avoid it where possible.\n"
"\n"
" ## Time zone offsets\n"
"\n"
" This package includes the `utc_offset` value and the `local_offset`\n"
" function, which are the offset for the UTC time zone and get the time\n"
" offset the computer running the program is configured to respectively.\n"
"\n"
" If you need to use other offsets in your program then you will need to get\n"
" them from somewhere else, such as from a package which loads the\n"
" [IANA Time Zone Database](https://www.iana.org/time-zones), or from the\n"
" website visitor's web browser, which your frontend can send for you.\n"
"\n"
" ## Use in APIs\n"
"\n"
" If you are making an API such as a HTTP JSON API you are encouraged to use\n"
" Unix timestamps instead of calendar times.\n"
).
-type date() :: {date, integer(), month(), integer()}.
-type time_of_day() :: {time_of_day, integer(), integer(), integer(), integer()}.
-type month() :: january |
february |
march |
april |
may |
june |
july |
august |
september |
october |
november |
december.
-file("src/gleam/time/calendar.gleam", 147).
?DOC(
" Get the offset for the computer's currently configured time zone.\n"
"\n"
" Note this may not be the time zone that is correct to use for your user.\n"
" For example, if you are making a web application that runs on a server you\n"
" want _their_ computer's time zone, not yours.\n"
"\n"
" This is the _current local_ offset, not the current local time zone. This\n"
" means that while it will result in the expected outcome for the current\n"
" time, it may result in unexpected output if used with other timestamps. For\n"
" example: a timestamp that would locally be during daylight savings time if\n"
" is it not currently daylight savings time when this function is called.\n"
).
-spec local_offset() -> gleam@time@duration:duration().
local_offset() ->
gleam@time@duration:seconds(gleam_time_ffi:local_time_offset_seconds()).
-file("src/gleam/time/calendar.gleam", 163).
?DOC(
" Returns the English name for a month.\n"
"\n"
" # Examples\n"
"\n"
" ```gleam\n"
" month_to_string(April)\n"
" // -> \"April\"\n"
" ```\n"
).
-spec month_to_string(month()) -> binary().
month_to_string(Month) ->
case Month of
january ->
<<"January"/utf8>>;
february ->
<<"February"/utf8>>;
march ->
<<"March"/utf8>>;
april ->
<<"April"/utf8>>;
may ->
<<"May"/utf8>>;
june ->
<<"June"/utf8>>;
july ->
<<"July"/utf8>>;
august ->
<<"August"/utf8>>;
september ->
<<"September"/utf8>>;
october ->
<<"October"/utf8>>;
november ->
<<"November"/utf8>>;
december ->
<<"December"/utf8>>
end.
-file("src/gleam/time/calendar.gleam", 188).
?DOC(
" Returns the number for the month, where January is 1 and December is 12.\n"
"\n"
" # Examples\n"
"\n"
" ```gleam\n"
" month_to_int(January)\n"
" // -> 1\n"
" ```\n"
).
-spec month_to_int(month()) -> integer().
month_to_int(Month) ->
case Month of
january ->
1;
february ->
2;
march ->
3;
april ->
4;
may ->
5;
june ->
6;
july ->
7;
august ->
8;
september ->
9;
october ->
10;
november ->
11;
december ->
12
end.
-file("src/gleam/time/calendar.gleam", 213).
?DOC(
" Returns the month for a given number, where January is 1 and December is 12.\n"
"\n"
" # Examples\n"
"\n"
" ```gleam\n"
" month_from_int(1)\n"
" // -> Ok(January)\n"
" ```\n"
).
-spec month_from_int(integer()) -> {ok, month()} | {error, nil}.
month_from_int(Month) ->
case Month of
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}
end.
-file("src/gleam/time/calendar.gleam", 290).
?DOC(
" Determines if a given year is a leap year.\n"
"\n"
" A leap year occurs every 4 years, except for years divisible by 100,\n"
" unless they are also divisible by 400.\n"
"\n"
" # Examples\n"
"\n"
" ```gleam\n"
" is_leap_year(2024)\n"
" // -> True\n"
" ```\n"
"\n"
" ```gleam\n"
" is_leap_year(2023)\n"
" // -> False\n"
" ```\n"
).
-spec is_leap_year(integer()) -> boolean().
is_leap_year(Year) ->
case (Year rem 400) =:= 0 of
true ->
true;
false ->
case (Year rem 100) =:= 0 of
true ->
false;
false ->
(Year rem 4) =:= 0
end
end.
-file("src/gleam/time/calendar.gleam", 254).
?DOC(
" Checks if a given date is valid.\n"
"\n"
" This function properly accounts for leap years when validating February days.\n"
" A leap year occurs every 4 years, except for years divisible by 100,\n"
" unless they are also divisible by 400.\n"
"\n"
" # Examples\n"
"\n"
" ```gleam\n"
" is_valid_date(Date(2023, April, 15))\n"
" // -> True\n"
" ```\n"
"\n"
" ```gleam\n"
" is_valid_date(Date(2023, April, 31))\n"
" // -> False\n"
" ```\n"
"\n"
" ```gleam\n"
" is_valid_date(Date(2024, February, 29))\n"
" // -> True (2024 is a leap year)\n"
" ```\n"
).
-spec is_valid_date(date()) -> boolean().
is_valid_date(Date) ->
{date, Year, Month, Day} = Date,
case Day < 1 of
true ->
false;
false ->
case Month of
january ->
Day =< 31;
march ->
Day =< 31;
may ->
Day =< 31;
july ->
Day =< 31;
august ->
Day =< 31;
october ->
Day =< 31;
december ->
Day =< 31;
april ->
Day =< 30;
june ->
Day =< 30;
september ->
Day =< 30;
november ->
Day =< 30;
february ->
Max_february_days = case is_leap_year(Year) of
true ->
29;
false ->
28
end,
Day =< Max_february_days
end
end.
-file("src/gleam/time/calendar.gleam", 313).
?DOC(
" Checks if a time of day is valid.\n"
"\n"
" Validates that hours are 0-23, minutes are 0-59, seconds are 0-59,\n"
" and nanoseconds are 0-999,999,999.\n"
"\n"
" # Examples\n"
"\n"
" ```gleam\n"
" is_valid_time_of_day(TimeOfDay(12, 30, 45, 123456789))\n"
" // -> True\n"
" ```\n"
).
-spec is_valid_time_of_day(time_of_day()) -> boolean().
is_valid_time_of_day(Time) ->
{time_of_day, Hours, Minutes, Seconds, Nanoseconds} = Time,
(((((((Hours >= 0) andalso (Hours =< 23)) andalso (Minutes >= 0)) andalso (Minutes
=< 59))
andalso (Seconds >= 0))
andalso (Seconds =< 59))
andalso (Nanoseconds >= 0))
andalso (Nanoseconds =< 999999999).
-file("src/gleam/time/calendar.gleam", 340).
?DOC(
" Naively compares two dates without any time zone information, returning an\n"
" order.\n"
"\n"
" ## Correctness\n"
"\n"
" This function compares dates without any time zone information, only using\n"
" the rules for the gregorian calendar. This is typically sufficient, but be\n"
" aware that in reality some time zones will change their calendar date\n"
" occasionally. This can result in days being skipped, out of order, or\n"
" happening multiple times.\n"
"\n"
" If you need real-world correct time ordering then use the\n"
" `gleam/time/timestamp` module instead.\n"
).
-spec naive_date_compare(date(), date()) -> gleam@order:order().
naive_date_compare(One, Other) ->
_pipe = gleam@int:compare(erlang:element(2, One), erlang:element(2, Other)),
_pipe@1 = gleam@order:lazy_break_tie(
_pipe,
fun() ->
gleam@int:compare(
month_to_int(erlang:element(3, One)),
month_to_int(erlang:element(3, Other))
)
end
),
gleam@order:lazy_break_tie(
_pipe@1,
fun() ->
gleam@int:compare(erlang:element(4, One), erlang:element(4, Other))
end
).

View file

@ -0,0 +1,381 @@
-module(gleam@time@duration).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/gleam/time/duration.gleam").
-export([approximate/1, compare/2, difference/2, add/2, seconds/1, minutes/1, hours/1, milliseconds/1, nanoseconds/1, to_seconds/1, to_seconds_and_nanoseconds/1, to_iso8601_string/1]).
-export_type([duration/0, unit/0]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
-opaque duration() :: {duration, integer(), integer()}.
-type unit() :: nanosecond |
microsecond |
millisecond |
second |
minute |
hour |
day |
week |
month |
year.
-file("src/gleam/time/duration.gleam", 110).
?DOC(
" Ensure the duration is represented with `nanoseconds` being positive and\n"
" less than 1 second.\n"
"\n"
" This function does not change the amount of time that the duratoin refers\n"
" to, it only adjusts the values used to represent the time.\n"
).
-spec normalise(duration()) -> duration().
normalise(Duration) ->
Multiplier = 1000000000,
Nanoseconds = case Multiplier of
0 -> 0;
Gleam@denominator -> erlang:element(3, Duration) rem Gleam@denominator
end,
Overflow = erlang:element(3, Duration) - Nanoseconds,
Seconds = erlang:element(2, Duration) + (case Multiplier of
0 -> 0;
Gleam@denominator@1 -> Overflow div Gleam@denominator@1
end),
case Nanoseconds >= 0 of
true ->
{duration, Seconds, Nanoseconds};
false ->
{duration, Seconds - 1, Multiplier + Nanoseconds}
end.
-file("src/gleam/time/duration.gleam", 76).
?DOC(
" Convert a duration to a number of the largest number of a unit, serving as\n"
" a rough description of the duration that a human can understand.\n"
"\n"
" The size used for each unit are described in the documentation for the\n"
" `Unit` type.\n"
"\n"
" ```gleam\n"
" seconds(125)\n"
" |> approximate\n"
" // -> #(2, Minute)\n"
" ```\n"
"\n"
" This function rounds _towards zero_. This means that if a duration is just\n"
" short of 2 days then it will approximate to 1 day.\n"
"\n"
" ```gleam\n"
" hours(47)\n"
" |> approximate\n"
" // -> #(1, Day)\n"
" ```\n"
).
-spec approximate(duration()) -> {integer(), unit()}.
approximate(Duration) ->
{duration, S, Ns} = Duration,
Minute = 60,
Hour = Minute * 60,
Day = Hour * 24,
Week = Day * 7,
Year = (Day * 365) + (Hour * 6),
Month = Year div 12,
Microsecond = 1000,
Millisecond = Microsecond * 1000,
case nil of
_ when S < 0 ->
{Amount, Unit} = begin
_pipe = {duration, - S, - Ns},
_pipe@1 = normalise(_pipe),
approximate(_pipe@1)
end,
{- Amount, Unit};
_ when S >= Year ->
{case Year of
0 -> 0;
Gleam@denominator -> S div Gleam@denominator
end, year};
_ when S >= Month ->
{case Month of
0 -> 0;
Gleam@denominator@1 -> S div Gleam@denominator@1
end, month};
_ when S >= Week ->
{case Week of
0 -> 0;
Gleam@denominator@2 -> S div Gleam@denominator@2
end, week};
_ when S >= Day ->
{case Day of
0 -> 0;
Gleam@denominator@3 -> S div Gleam@denominator@3
end, day};
_ when S >= Hour ->
{case Hour of
0 -> 0;
Gleam@denominator@4 -> S div Gleam@denominator@4
end, hour};
_ when S >= Minute ->
{case Minute of
0 -> 0;
Gleam@denominator@5 -> S div Gleam@denominator@5
end, minute};
_ when S > 0 ->
{S, second};
_ when Ns >= Millisecond ->
{case Millisecond of
0 -> 0;
Gleam@denominator@6 -> Ns div Gleam@denominator@6
end, millisecond};
_ when Ns >= Microsecond ->
{case Microsecond of
0 -> 0;
Gleam@denominator@7 -> Ns div Gleam@denominator@7
end, microsecond};
_ ->
{Ns, nanosecond}
end.
-file("src/gleam/time/duration.gleam", 140).
?DOC(
" Compare one duration to another, indicating whether the first spans a\n"
" larger amount of time (and so is greater) or smaller amount of time (and so\n"
" is lesser) than the second.\n"
"\n"
" # Examples\n"
"\n"
" ```gleam\n"
" compare(seconds(1), seconds(2))\n"
" // -> order.Lt\n"
" ```\n"
"\n"
" Whether a duration is negative or positive doesn't matter for comparing\n"
" them, only the amount of time spanned matters.\n"
"\n"
" ```gleam\n"
" compare(seconds(-2), seconds(1))\n"
" // -> order.Gt\n"
" ```\n"
).
-spec compare(duration(), duration()) -> gleam@order:order().
compare(Left, Right) ->
Parts = fun(X) -> case erlang:element(2, X) >= 0 of
true ->
{erlang:element(2, X), erlang:element(3, X)};
false ->
{(erlang:element(2, X) * -1) - 1,
1000000000 - erlang:element(3, X)}
end end,
{Ls, Lns} = Parts(Left),
{Rs, Rns} = Parts(Right),
_pipe = gleam@int:compare(Ls, Rs),
gleam@order:break_tie(_pipe, gleam@int:compare(Lns, Rns)).
-file("src/gleam/time/duration.gleam", 164).
?DOC(
" Calculate the difference between two durations.\n"
"\n"
" This is effectively substracting the first duration from the second.\n"
"\n"
" # Examples\n"
"\n"
" ```gleam\n"
" difference(seconds(1), seconds(5))\n"
" // -> seconds(4)\n"
" ```\n"
).
-spec difference(duration(), duration()) -> duration().
difference(Left, Right) ->
_pipe = {duration,
erlang:element(2, Right) - erlang:element(2, Left),
erlang:element(3, Right) - erlang:element(3, Left)},
normalise(_pipe).
-file("src/gleam/time/duration.gleam", 178).
?DOC(
" Add two durations together.\n"
"\n"
" # Examples\n"
"\n"
" ```gleam\n"
" add(seconds(1), seconds(5))\n"
" // -> seconds(6)\n"
" ```\n"
).
-spec add(duration(), duration()) -> duration().
add(Left, Right) ->
_pipe = {duration,
erlang:element(2, Left) + erlang:element(2, Right),
erlang:element(3, Left) + erlang:element(3, Right)},
normalise(_pipe).
-file("src/gleam/time/duration.gleam", 225).
-spec nanosecond_digits(integer(), integer(), binary()) -> binary().
nanosecond_digits(N, Position, Acc) ->
case Position of
9 ->
Acc;
_ when (Acc =:= <<""/utf8>>) andalso ((N rem 10) =:= 0) ->
nanosecond_digits(N div 10, Position + 1, Acc);
_ ->
Acc@1 = <<(erlang:integer_to_binary(N rem 10))/binary, Acc/binary>>,
nanosecond_digits(N div 10, Position + 1, Acc@1)
end.
-file("src/gleam/time/duration.gleam", 239).
?DOC(" Create a duration of a number of seconds.\n").
-spec seconds(integer()) -> duration().
seconds(Amount) ->
{duration, Amount, 0}.
-file("src/gleam/time/duration.gleam", 244).
?DOC(" Create a duration of a number of minutes.\n").
-spec minutes(integer()) -> duration().
minutes(Amount) ->
seconds(Amount * 60).
-file("src/gleam/time/duration.gleam", 249).
?DOC(" Create a duration of a number of hours.\n").
-spec hours(integer()) -> duration().
hours(Amount) ->
seconds((Amount * 60) * 60).
-file("src/gleam/time/duration.gleam", 254).
?DOC(" Create a duration of a number of milliseconds.\n").
-spec milliseconds(integer()) -> duration().
milliseconds(Amount) ->
Remainder = Amount rem 1000,
Overflow = Amount - Remainder,
Nanoseconds = Remainder * 1000000,
Seconds = Overflow div 1000,
_pipe = {duration, Seconds, Nanoseconds},
normalise(_pipe).
-file("src/gleam/time/duration.gleam", 273).
?DOC(
" Create a duration of a number of nanoseconds.\n"
"\n"
" # JavaScript int limitations\n"
"\n"
" Remember that JavaScript can only perfectly represent ints between positive\n"
" and negative 9,007,199,254,740,991! If you use a single call to this\n"
" function to create durations larger than that number of nanoseconds then\n"
" you will likely not get exactly the value you expect. Use `seconds` and\n"
" `milliseconds` as much as possible for large durations.\n"
).
-spec nanoseconds(integer()) -> duration().
nanoseconds(Amount) ->
_pipe = {duration, 0, Amount},
normalise(_pipe).
-file("src/gleam/time/duration.gleam", 283).
?DOC(
" Convert the duration to a number of seconds.\n"
"\n"
" There may be some small loss of precision due to `Duration` being\n"
" nanosecond accurate and `Float` not being able to represent this.\n"
).
-spec to_seconds(duration()) -> float().
to_seconds(Duration) ->
Seconds = erlang:float(erlang:element(2, Duration)),
Nanoseconds = erlang:float(erlang:element(3, Duration)),
Seconds + (Nanoseconds / 1000000000.0).
-file("src/gleam/time/duration.gleam", 292).
?DOC(
" Convert the duration to a number of seconds and nanoseconds. There is no\n"
" loss of precision with this conversion on any target.\n"
).
-spec to_seconds_and_nanoseconds(duration()) -> {integer(), integer()}.
to_seconds_and_nanoseconds(Duration) ->
{erlang:element(2, Duration), erlang:element(3, Duration)}.
-file("src/gleam/time/duration.gleam", 192).
?DOC(
" Convert the duration to an [ISO8601][1] formatted duration string.\n"
"\n"
" The ISO8601 duration format is ambiguous without context due to months and\n"
" years having different lengths, and because of leap seconds. This function\n"
" encodes the duration as days, hours, and seconds without any leap seconds.\n"
" Be sure to take this into account when using the duration strings.\n"
"\n"
" [1]: https://en.wikipedia.org/wiki/ISO_8601#Durations\n"
).
-spec to_iso8601_string(duration()) -> binary().
to_iso8601_string(Duration) ->
gleam@bool:guard(
Duration =:= {duration, 0, 0},
<<"PT0S"/utf8>>,
fun() ->
Split = fun(Total, Limit) ->
Amount = case Limit of
0 -> 0;
Gleam@denominator -> Total rem Gleam@denominator
end,
Remainder = case Limit of
0 -> 0;
Gleam@denominator@1 -> (Total - Amount) div Gleam@denominator@1
end,
{Amount, Remainder}
end,
{Seconds, Rest} = Split(erlang:element(2, Duration), 60),
{Minutes, Rest@1} = Split(Rest, 60),
{Hours, Rest@2} = Split(Rest@1, 24),
Days = Rest@2,
Add = fun(Out, Value, Unit) -> case Value of
0 ->
Out;
_ ->
<<<<Out/binary,
(erlang:integer_to_binary(Value))/binary>>/binary,
Unit/binary>>
end end,
Output = begin
_pipe = <<"P"/utf8>>,
_pipe@1 = Add(_pipe, Days, <<"D"/utf8>>),
_pipe@2 = gleam@string:append(_pipe@1, <<"T"/utf8>>),
_pipe@3 = Add(_pipe@2, Hours, <<"H"/utf8>>),
Add(_pipe@3, Minutes, <<"M"/utf8>>)
end,
case {Seconds, erlang:element(3, Duration)} of
{0, 0} ->
Output;
{_, 0} ->
<<<<Output/binary,
(erlang:integer_to_binary(Seconds))/binary>>/binary,
"S"/utf8>>;
{_, _} ->
F = nanosecond_digits(
erlang:element(3, Duration),
0,
<<""/utf8>>
),
<<<<<<<<Output/binary,
(erlang:integer_to_binary(Seconds))/binary>>/binary,
"."/utf8>>/binary,
F/binary>>/binary,
"S"/utf8>>
end
end
).

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,12 @@
{application, gleam_time, [
{vsn, "1.6.0"},
{applications, [gleam_stdlib]},
{description, "Work with time in Gleam!"},
{modules, [gleam@time@calendar,
gleam@time@duration,
gleam@time@timestamp,
gleam_time@@main,
gleam_time_ffi,
gleam_time_test_ffi]},
{registered, []}
]}.

View file

@ -0,0 +1,12 @@
-module(gleam_time_ffi).
-export([system_time/0, local_time_offset_seconds/0]).
system_time() ->
{0, erlang:system_time(nanosecond)}.
local_time_offset_seconds() ->
Utc = calendar:universal_time(),
Local = calendar:local_time(),
UtcSeconds = calendar:datetime_to_gregorian_seconds(Utc),
LocalSeconds = calendar:datetime_to_gregorian_seconds(Local),
LocalSeconds - UtcSeconds.

View file

@ -0,0 +1,11 @@
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];
}
export function local_time_offset_seconds() {
return new Date().getTimezoneOffset() * -60;
}