Initial commit
This commit is contained in:
commit
a6272848f9
379 changed files with 74829 additions and 0 deletions
101
build/packages/gleam_time/README.md
Normal file
101
build/packages/gleam_time/README.md
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
# Time 🕰️
|
||||
|
||||
Work with time in Gleam!
|
||||
|
||||
[](https://hex.pm/packages/gleam_time)
|
||||
[](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.
|
||||
19
build/packages/gleam_time/gleam.toml
Normal file
19
build/packages/gleam_time/gleam.toml
Normal 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"
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
-record(date, {
|
||||
year :: integer(),
|
||||
month :: gleam@time@calendar:month(),
|
||||
day :: integer()
|
||||
}).
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
-record(time_of_day, {
|
||||
hours :: integer(),
|
||||
minutes :: integer(),
|
||||
seconds :: integer(),
|
||||
nanoseconds :: integer()
|
||||
}).
|
||||
|
|
@ -0,0 +1 @@
|
|||
-record(duration, {seconds :: integer(), nanoseconds :: integer()}).
|
||||
|
|
@ -0,0 +1 @@
|
|||
-record(timestamp, {seconds :: integer(), nanoseconds :: integer()}).
|
||||
346
build/packages/gleam_time/src/gleam/time/calendar.gleam
Normal file
346
build/packages/gleam_time/src/gleam/time/calendar.gleam
Normal 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) })
|
||||
}
|
||||
297
build/packages/gleam_time/src/gleam/time/duration.gleam
Normal file
297
build/packages/gleam_time/src/gleam/time/duration.gleam
Normal 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)
|
||||
899
build/packages/gleam_time/src/gleam/time/timestamp.gleam
Normal file
899
build/packages/gleam_time/src/gleam/time/timestamp.gleam
Normal 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)
|
||||
}
|
||||
468
build/packages/gleam_time/src/gleam@time@calendar.erl
Normal file
468
build/packages/gleam_time/src/gleam@time@calendar.erl
Normal 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
|
||||
).
|
||||
381
build/packages/gleam_time/src/gleam@time@duration.erl
Normal file
381
build/packages/gleam_time/src/gleam@time@duration.erl
Normal 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
|
||||
).
|
||||
1188
build/packages/gleam_time/src/gleam@time@timestamp.erl
Normal file
1188
build/packages/gleam_time/src/gleam@time@timestamp.erl
Normal file
File diff suppressed because it is too large
Load diff
12
build/packages/gleam_time/src/gleam_time.app.src
Normal file
12
build/packages/gleam_time/src/gleam_time.app.src
Normal 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, []}
|
||||
]}.
|
||||
12
build/packages/gleam_time/src/gleam_time_ffi.erl
Normal file
12
build/packages/gleam_time/src/gleam_time_ffi.erl
Normal 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.
|
||||
11
build/packages/gleam_time/src/gleam_time_ffi.mjs
Normal file
11
build/packages/gleam_time/src/gleam_time_ffi.mjs
Normal 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;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue