Current section

Files

Jump to
gtempo src tempo datetime.gleam
Raw

src/tempo/datetime.gleam

//// Functions to use with the `DateTime` type in Tempo.
////
//// ## Example
////
//// ```gleam
//// import tempo/datetime
////
//// pub fn main() {
//// datetime.literal("2024-12-25T06:00:00+05:00")
//// |> datetime.format("ddd @ h:mm A, Z")
//// // -> "Fri @ 6:00 AM, +05:00"
////
//// datetime.parse("06:21:2024 23:17:07.123Z", "MM:DD:YYYY HH:mm:ss.SSSZ")
//// |> datetime.to_string
//// // -> "2024-06-21T23:17:07.123Z"
////
//// datetime.now_local()
//// |> datetime.to_string
//// // -> "2024-10-09T13:42:11.195Z"
//// }
//// ```
////
//// ```gleam
//// import tempo/datetime
////
//// pub fn is_30_mins_old(datetime_str: String) {
//// let my_dt = datetime.from_string(datetime_str)
////
//// my_dt
//// |> datetime.is_equal(
//// to: my_dt |> datetime.subtract(duration.minutes(30))
//// )
//// }
//// ```
////
//// ```gleam
//// import tempo/datetime
//// import tempo/period
////
//// pub fn get_every_friday_between(datetime1, datetime2) {
//// period.new(datetime1, datetime2)
//// |> period.comprising_dates
//// |> iterator.filter(fn(date) {
//// date |> date.to_day_of_week == date.Fri
//// })
//// |> iterator.to_list
//// // -> ["2024-06-21", "2024-06-28", "2024-07-05"]
//// }
//// ```
import gleam/bool
import gleam/dynamic
import gleam/list
import gleam/option.{None, Some}
import gleam/order
import gleam/regex
import gleam/result
import gleam/string
import tempo
import tempo/date
import tempo/naive_datetime
import tempo/offset
import tempo/time
/// Create a new datetime from a date, time, and offset.
///
/// ## Examples
///
/// ```gleam
/// datetime.new(
/// date.literal("2024-06-13"),
/// time.literal("23:04:00.009"),
/// offset.literal("+10:00"),
/// )
/// // -> datetime.literal("2024-06-13T23:04:00.009+10:00")
/// ```
pub fn new(
date date: tempo.Date,
time time: tempo.Time,
offset offset: tempo.Offset,
) -> tempo.DateTime {
tempo.datetime(naive_datetime.new(date, time), offset: offset)
}
/// Create a new datetime value from a string literal, but will panic if
/// the string is invalid. Accepted formats are `YYYY-MM-DDThh:mm:ss.sTZD` or
/// `YYYYMMDDThhmmss.sTZD`
///
/// Useful for declaring datetime literals that you know are valid within your
/// program.
///
/// ## Examples
///
/// ```gleam
/// datetime.literal("2024-06-13T23:04:00.009+10:00")
/// |> datetime.to_string
/// // -> "2024-06-13T23:04:00.009+10:00"
/// ```
pub fn literal(datetime: String) -> tempo.DateTime {
case from_string(datetime) {
Ok(datetime) -> datetime
Error(tempo.DateTimeInvalidFormat) ->
panic as "Invalid datetime literal format"
Error(tempo.DateOutOfBounds) ->
panic as "Invalid date in datetime literal value"
Error(tempo.TimeOutOfBounds) ->
panic as "Invalid time indatetime literal value"
Error(_) -> panic as "Invalid datetime literal"
}
}
/// Gets the current local datetime of the host. Always prefer using
/// `duration.start_monotonic` to record time passing and `time.now_unique`
/// to sort events by time.
///
/// ## Examples
///
/// ```gleam
/// datetime.now()
/// |> datetime.to_string
/// // -> "2024-06-14T04:19:20.006809349-04:00"
/// ```
pub fn now_local() -> tempo.DateTime {
// This should always be precise because it is the current time.
case now_utc() |> to_local {
tempo.Precise(datetime) -> datetime
tempo.Imprecise(datetime) -> datetime
}
}
/// Gets the current UTC datetime of the host.Always prefer using
/// `duration.start_monotonic` to record time passing and `time.now_unique`
/// to sort events by time.
///
/// ## Examples
///
/// ```gleam
/// datetime.now_utc()
/// |> datetime.to_string
/// // -> "2024-06-14T08:19:20.006809349Z"
/// ```
pub fn now_utc() -> tempo.DateTime {
let #(now_monotonic, now_unique) = tempo.now_monounique()
let now_ts_nano = tempo.now_utc()
new(
date.from_unix_utc(now_ts_nano / 1_000_000_000),
time.from_unix_nano_utc(now_ts_nano)
|> tempo.time_set_mono(Some(now_monotonic), Some(now_unique)),
tempo.utc,
)
}
/// Gets the current local datetime of the host as a string in milliseconds
/// precision. For easy reading by humans in text formats, like log
/// statements, etc.
///
/// ## Examples
///
/// ```gleam
/// datetime.now_text()
/// // -> "2024-06-14 04:19:20.349"
/// ```
pub fn now_text() -> String {
now_local()
|> drop_offset
|> naive_datetime.to_milli_precision
|> naive_datetime.to_string
|> string.replace("T", " ")
}
/// Parses a datetime string in the format `YYYY-MM-DDThh:mm:ss.sTZD`,
/// `YYYYMMDDThhmmss.sTZD`, `YYYY-MM-DD hh:mm:ss.sTZD`,
/// `YYYYMMDD hhmmss.sTZD`, `YYYY-MM-DD`, `YYYY-M-D`, `YYYY/MM/DD`,
/// `YYYY/M/D`, `YYYY.MM.DD`, `YYYY.M.D`, `YYYY_MM_DD`, `YYYY_M_D`,
/// `YYYY MM DD`, `YYYY M D`, or `YYYYMMDD`.
///
/// ## Examples
///
/// ```gleam
/// datetime.from_string("20240613T230400.009+00:00")
/// // -> datetime.literal("2024-06-13T23:04:00.009Z")
/// ```
pub fn from_string(datetime: String) -> Result(tempo.DateTime, tempo.Error) {
let split_dt = case string.contains(datetime, "T") {
True -> string.split(datetime, "T")
False -> string.split(datetime, " ")
}
case split_dt {
[date, time] -> {
use date: tempo.Date <- result.try(date.from_string(date))
use #(time, offset): #(String, String) <- result.try(
split_time_and_offset(time),
)
use time: tempo.Time <- result.try(time.from_string(time))
use offset: tempo.Offset <- result.map(offset.from_string(offset))
new(date, time, offset)
}
[date] ->
date.from_string(date)
|> result.map(new(
_,
tempo.time(0, 0, 0, 0, tempo.Sec, None, None),
tempo.utc,
))
_ -> Error(tempo.DateTimeInvalidFormat)
}
}
fn split_time_and_offset(
time_with_offset: String,
) -> Result(#(String, String), tempo.Error) {
case string.slice(time_with_offset, at_index: -1, length: 1) {
"Z" -> #(string.drop_right(time_with_offset, 1), "Z") |> Ok
"z" -> #(string.drop_right(time_with_offset, 1), "Z") |> Ok
_ ->
case string.split(time_with_offset, "-") {
[time, offset] -> #(time, "-" <> offset) |> Ok
_ ->
case string.split(time_with_offset, "+") {
[time, offset] -> #(time, "+" <> offset) |> Ok
_ -> Error(tempo.DateTimeInvalidFormat)
}
}
}
}
/// Returns a string representation of a datetime value in the ISO 8601
/// format.
///
/// ## Examples
///
/// ```gleam
/// datetime.now_utc()
/// |> datetime.to_string
/// // -> "2024-06-21T05:22:22.009Z"
/// ```
pub fn to_string(datetime: tempo.DateTime) -> String {
datetime |> tempo.datetime_get_naive |> naive_datetime.to_string
<> case datetime |> tempo.datetime_get_offset |> tempo.offset_get_minutes {
0 -> "Z"
_ -> datetime |> tempo.datetime_get_offset |> offset.to_string
}
}
/// Parses a datetime string in the provided format. Always prefer using
/// this over `parse_any`. All parsed formats must have all parts of a
/// datetime (date, time, offset). Use the other modules for parsing lesser
/// date time values.
///
/// Values can be escaped by putting brackets around them, like "[Hello!] YYYY".
///
/// Available directives: YY (two-digit year), YYYY (four-digit year), M (month),
/// MM (two-digit month), MMM (short month name), MMMM (full month name),
/// D (day of the month), DD (two-digit day of the month),
/// H (hour), HH (two-digit hour), h (12-hour clock hour), hh
/// (two-digit 12-hour clock hour), m (minute), mm (two-digit minute),
/// s (second), ss (two-digit second), SSS (millisecond), SSSS (microsecond),
/// SSSSS (nanosecond), Z (offset from UTC), ZZ (offset from UTC with no ":"),
/// z (short offset from UTC "-04", "Z"), A (AM/PM), a (am/pm).
///
/// ## Example
///
/// ```gleam
/// datetime.parse("2024/06/08, 13:42:11, -04:00", "YYYY/MM/DD, HH:mm:ss, Z")
/// // -> Ok(datetime.literal("2024-06-08T13:42:11-04"))
/// ```
///
/// ```gleam
/// datetime.parse("January 13, 2024. 3:42:11Z", "MMMM DD, YYYY. H:mm:ssz")
/// // -> Ok(datetime.literal("2024-01-13T03:42:11Z"))
/// ```
///
/// ```gleam
/// datetime.parse("Hi! 2024 11 13 12 2 am Z", "[Hi!] YYYY M D h m a z")
/// // -> Ok(datetime.literal("2024-11-13T00:02:00Z"))
/// ```
pub fn parse(str: String, in fmt: String) -> Result(tempo.DateTime, tempo.Error) {
use #(parts, _) <- result.try(tempo.consume_format(str, in: fmt))
use date <- result.try(tempo.find_date(in: parts))
use time <- result.try(tempo.find_time(in: parts))
use offset <- result.try(tempo.find_offset(in: parts))
Ok(new(date, time, offset))
}
/// Tries to parse a given date string without a known format. It will not
/// parse two digit years and will assume the month always comes before the
/// day in a date.
///
/// ## Example
///
/// ```gleam
/// parse_any.parse_any("2024.06.21 01:32 PM -0400")
/// // -> Ok(datetime.literal("2024-06-21T13:32:00-04:00"))
/// ```
///
/// ```gleam
/// parse_any.parse_any("2024.06.21 01:32 PM")
/// // -> Error(tempo.ParseMissingOffset)
/// ```
pub fn parse_any(str: String) -> Result(tempo.DateTime, tempo.Error) {
case tempo.parse_any(str) {
Ok(#(Some(date), Some(time), Some(offset))) -> Ok(new(date, time, offset))
Ok(#(_, _, None)) -> Error(tempo.ParseMissingOffset)
Ok(#(_, None, _)) -> Error(tempo.ParseMissingTime)
Ok(#(None, _, _)) -> Error(tempo.ParseMissingDate)
Error(err) -> Error(err)
}
}
/// Formats a datetime value into a string using the provided format string.
/// Implements the same formatting directives as the great Day.js
/// library: https://day.js.org/docs/en/display/format, plus short timezones
/// and nanosecond precision.
///
/// Values can be escaped by putting brackets around them, like "[Hello!] YYYY".
///
/// Available directives: YY (two-digit year), YYYY (four-digit year), M (month),
/// MM (two-digit month), MMM (short month name), MMMM (full month name),
/// D (day of the month), DD (two-digit day of the month), d (day of the week),
/// dd (min day of the week), ddd (short day of week), dddd (full day of the week),
/// H (hour), HH (two-digit hour), h (12-hour clock hour), hh
/// (two-digit 12-hour clock hour), m (minute), mm (two-digit minute),
/// s (second), ss (two-digit second), SSS (millisecond), SSSS (microsecond),
/// SSSSS (nanosecond), Z (offset from UTC), ZZ (offset from UTC with no ":"),
/// z (short offset from UTC "-04", "Z"), A (AM/PM), a (am/pm).
///
/// ## Example
///
/// ```gleam
/// datetime.literal("2024-06-21T13:42:11.314-04:00")
/// |> datetime.format("ddd @ h:mm A (z)")
/// // -> "Fri @ 1:42 PM (-04)"
/// ```
///
/// ```gleam
/// datetime.literal("2024-06-03T09:02:01-04:00")
/// |> datetime.format("YY YYYY M MM MMM MMMM D DD d dd ddd")
/// // --------------> "24 2024 6 06 Jun June 3 03 1 Mo Mon"
/// ```
///
/// ```gleam
/// datetime.literal("2024-06-03T09:02:01.014920202-00:00")
/// |> datetime.format("dddd SSS SSSS SSSSS Z ZZ z")
/// // -> "Monday 014 014920 014920202 -00:00 -0000 Z"
/// ```
///
/// ```gleam
/// datetime.literal("2024-06-03T13:02:01-04:00")
/// |> datetime.format("H HH h hh m mm s ss a A [An ant]")
/// // -------------> "13 13 1 01 2 02 1 01 pm PM An ant"
/// ```
pub fn format(datetime: tempo.DateTime, in fmt: String) -> String {
let assert Ok(re) = regex.from_string(tempo.format_regex)
regex.scan(re, fmt)
|> list.reverse
|> list.fold(from: [], with: fn(acc, match) {
case match {
regex.Match(content, []) -> [
content
|> date.replace_format(datetime |> get_date)
|> time.replace_format(datetime |> get_time)
|> replace_format(datetime),
..acc
]
// If there is a non-empty subpattern, then the escape
// character "[ ... ]" matched, so we should not change anything here.
regex.Match(_, [Some(sub)]) -> [sub, ..acc]
// This case is not expected, not really sure what to do with it
// so just prepend whatever was found
regex.Match(content, _) -> [content, ..acc]
}
})
|> string.join("")
}
fn replace_format(content: String, datetime) -> String {
let offset = datetime |> get_offset
case content {
"z" ->
case offset |> tempo.offset_get_minutes {
0 -> "Z"
_ -> {
let str_offset = offset |> offset.to_string
case str_offset |> string.split(":") {
[hours, "00"] -> hours
_ -> str_offset
}
}
}
"Z" -> offset |> offset.to_string
"ZZ" ->
offset
|> offset.to_string
|> string.replace(":", "")
_ -> content
}
}
/// Returns the UTC datetime of a unix timestamp.
///
/// ## Examples
///
/// ```gleam
/// datetime.from_unix_utc(1_718_829_191)
/// // -> datetime.literal("2024-06-17T12:59:51Z")
/// ```
pub fn from_unix_utc(unix_ts: Int) -> tempo.DateTime {
new(date.from_unix_utc(unix_ts), time.from_unix_utc(unix_ts), tempo.utc)
}
/// Returns the UTC unix timestamp of a datetime.
///
/// ## Examples
///
/// ```gleam
/// datetime.literal("2024-06-17T12:59:51Z")
/// |> datetime.to_unix_utc
/// // -> 1_718_829_191
/// ```
pub fn to_unix_utc(datetime: tempo.DateTime) -> Int {
let utc_dt = datetime |> apply_offset
date.to_unix_utc(utc_dt |> tempo.naive_datetime_get_date)
+ {
time.to_nanoseconds(utc_dt |> tempo.naive_datetime_get_time) / 1_000_000_000
}
}
/// Returns the UTC datetime of a unix timestamp in milliseconds.
///
/// ## Examples
///
/// ```gleam
/// datetime.from_unix_milli_utc(1_718_629_314_334)
/// // -> datetime.literal("2024-06-17T13:01:54.334Z")
/// ```
pub fn from_unix_milli_utc(unix_ts: Int) -> tempo.DateTime {
new(
date.from_unix_milli_utc(unix_ts),
time.from_unix_milli_utc(unix_ts),
tempo.utc,
)
}
/// Returns the UTC unix timestamp in milliseconds of a datetime.
///
/// ## Examples
///
/// ```gleam
/// datetime.literal("2024-06-17T13:01:54.334Z")
/// |> datetime.to_unix_milli_utc
/// // -> 1_718_629_314_334
/// ```
pub fn to_unix_milli_utc(datetime: tempo.DateTime) -> Int {
let utc_dt = datetime |> apply_offset
date.to_unix_milli_utc(utc_dt |> tempo.naive_datetime_get_date)
+ { time.to_nanoseconds(utc_dt |> tempo.naive_datetime_get_time) / 1_000_000 }
}
/// Returns the UTC datetime of a unix timestamp in microseconds.
///
/// ## Examples
///
/// ```gleam
/// datetime.from_unix_micro_utc(1_718_629_314_334_734)
/// // -> datetime.literal("2024-06-17T13:01:54.334734Z")
/// ```
pub fn from_unix_micro_utc(unix_ts: Int) -> tempo.DateTime {
new(
date.from_unix_micro_utc(unix_ts),
time.from_unix_micro_utc(unix_ts),
tempo.utc,
)
}
/// Returns the UTC unix timestamp in microseconds of a datetime.
///
/// ## Examples
///
/// ```gleam
/// datetime.literal("2024-06-17T13:01:54.334734Z")
/// |> datetime.to_unix_micro_utc
/// // -> 1_718_629_314_334_734
/// ```
pub fn to_unix_micro_utc(datetime: tempo.DateTime) -> Int {
let utc_dt = datetime |> apply_offset
date.to_unix_micro_utc(utc_dt |> tempo.naive_datetime_get_date)
+ { time.to_nanoseconds(utc_dt |> tempo.naive_datetime_get_time) / 1000 }
}
/// Checks if a dynamic value is a valid datetime string, and returns the
/// datetime if it is.
///
/// ## Examples
///
/// ```gleam
/// dynamic.from("2024-06-13T13:42:11.195Z")
/// |> datetime.from_dynamic_string
/// // -> Ok(datetime.literal("2024-06-13T13:42:11.195Z"))
/// ```
///
/// ```gleam
/// dynamic.from("24-06-13,13:42:11.195")
/// |> datetime.from_dynamic_string
/// // -> Error([
/// // dynamic.DecodeError(
/// // expected: "tempo.DateTime",
/// // found: "Invalid format: 24-06-13,13:42:11.195",
/// // path: [],
/// // ),
/// // ])
/// ```
pub fn from_dynamic_string(
dynamic_string: dynamic.Dynamic,
) -> Result(tempo.DateTime, List(dynamic.DecodeError)) {
use dt: String <- result.try(dynamic.string(dynamic_string))
case from_string(dt) {
Ok(datetime) -> Ok(datetime)
Error(tempo_error) ->
Error([
dynamic.DecodeError(
expected: "tempo.DateTime",
found: case tempo_error {
tempo.DateTimeInvalidFormat -> "Invalid format: "
tempo.NaiveDateTimeInvalidFormat -> "Invalid format: "
tempo.DateInvalidFormat -> "Invalid format: "
tempo.TimeInvalidFormat -> "Invalid format: "
tempo.OffsetInvalidFormat -> "Invalid format: "
tempo.DateOutOfBounds -> "Date out of bounds: "
tempo.MonthOutOfBounds -> "Month out of bounds: "
tempo.TimeOutOfBounds -> "Time out of bounds: "
tempo.OffsetOutOfBounds -> "Offset out of bounds: "
_ -> ""
}
<> dt,
path: [],
),
])
}
}
/// Checks if a dynamic value is a valid unix timestamp in seconds, and
/// returns the datetime if it is.
///
/// ## Examples
///
/// ```gleam
/// dynamic.from(1_718_629_314)
/// |> datetime.from_dynamic_unix_utc
/// // -> Ok(datetime.literal("2024-06-17T13:01:54Z"))
/// ```
///
/// ```gleam
/// dynamic.from("hello")
/// |> datetime.from_dynamic_unix_utc
/// // -> Error([
/// // dynamic.DecodeError(
/// // expected: "Int",
/// // found: "String",
/// // path: [],
/// // ),
/// // ])
/// ```
pub fn from_dynamic_unix_utc(
dynamic_ts: dynamic.Dynamic,
) -> Result(tempo.DateTime, List(dynamic.DecodeError)) {
use dt: Int <- result.map(dynamic.int(dynamic_ts))
from_unix_utc(dt)
}
/// Checks if a dynamic value is a valid unix timestamp in milliseconds, and
/// returns the datetime if it is.
///
/// ## Examples
///
/// ```gleam
/// dynamic.from(1_718_629_314_334)
/// |> datetime.from_dynamic_unix_utc
/// // -> Ok(datetime.literal("2024-06-17T13:01:54.334Z"))
/// ```
///
/// ```gleam
/// dynamic.from("hello")
/// |> datetime.from_dynamic_unix_utc
/// // -> Error([
/// // dynamic.DecodeError(
/// // expected: "Int",
/// // found: "String",
/// // path: [],
/// // ),
/// // ])
/// ```
pub fn from_dynamic_unix_milli_utc(
dynamic_ts: dynamic.Dynamic,
) -> Result(tempo.DateTime, List(dynamic.DecodeError)) {
use dt: Int <- result.map(dynamic.int(dynamic_ts))
from_unix_milli_utc(dt)
}
/// Checks if a dynamic value is a valid unix timestamp in microseconds, and
/// returns the datetime if it is.
///
/// ## Examples
///
/// ```gleam
/// dynamic.from(1_718_629_314_334_734)
/// |> datetime.from_dynamic_unix_utc
/// // -> Ok(datetime.literal("2024-06-17T13:01:54.334734Z"))
/// ```
///
/// ```gleam
/// dynamic.from("hello")
/// |> datetime.from_dynamic_unix_utc
/// // -> Error([
/// // dynamic.DecodeError(
/// // expected: "Int",
/// // found: "String",
/// // path: [],
/// // ),
/// // ])
/// ```
pub fn from_dynamic_unix_micro_utc(
dynamic_ts: dynamic.Dynamic,
) -> Result(tempo.DateTime, List(dynamic.DecodeError)) {
use dt: Int <- result.map(dynamic.int(dynamic_ts))
from_unix_micro_utc(dt)
}
/// Gets the date of a datetime.
///
/// ## Examples
///
/// ```gleam
/// datetime.literal("2024-06-21T13:42:11.195Z")
/// |> datetime.get_date
/// // -> date.literal("2024-06-21")
/// ```
pub fn get_date(datetime: tempo.DateTime) -> tempo.Date {
datetime |> tempo.datetime_get_naive |> tempo.naive_datetime_get_date
}
/// Gets the time of a datetime.
///
/// ## Examples
///
/// ```gleam
/// datetime.literal("2024-06-21T13:42:11.195Z")
/// |> datetime.get_time
/// // -> time.literal("13:42:11.195")
/// ```
pub fn get_time(datetime: tempo.DateTime) -> tempo.Time {
datetime |> tempo.datetime_get_naive |> tempo.naive_datetime_get_time
}
/// Gets the offset of a datetime.
///
/// ## Examples
///
/// ```gleam
/// datetime.literal("2024-06-12T13:42:11.195-04:00")
/// |> datetime.get_offset
/// // -> offset.literal("+04:00")
/// ```
pub fn get_offset(datetime: tempo.DateTime) -> tempo.Offset {
datetime |> tempo.datetime_get_offset
}
/// Drops the time of a datetime, leaving the date and time values unchanged.
///
/// ## Examples
///
/// ```gleam
/// datetime.literal("2024-06-13T13:42:11.195Z")
/// |> datetime.drop_offset
/// // -> naive_datetime.literal("2024-06-13T13:42:11")
/// ```
pub fn drop_offset(datetime: tempo.DateTime) -> tempo.NaiveDateTime {
datetime |> tempo.datetime_get_naive
}
/// Drops the time of a datetime, leaving the date value unchanged.
///
/// ## Examples
///
/// ```gleam
/// datetime.literal("2024-06-18T13:42:11.195Z")
/// |> datetime.drop_time
/// // -> naive_datetime.literal("2024-06-18T00:00:00Z")
/// ```
pub fn drop_time(datetime: tempo.DateTime) -> tempo.DateTime {
tempo.datetime(
naive_datetime.drop_time(datetime |> tempo.datetime_get_naive),
offset: datetime |> tempo.datetime_get_offset,
)
}
/// Applies the offset of a datetime to the date and time values, resulting
/// in a new naive datetime value that represents the original datetime in
/// UTC time.
///
/// P## Examples
///
/// ```gleam
/// datetime.literal("2024-06-21T05:36:11.195-04:00")
/// |> datetime.apply_offset
/// // -> naive_datetime.literal("2024-06-21T09:36:11.195")
/// ```
pub fn apply_offset(datetime: tempo.DateTime) -> tempo.NaiveDateTime {
let original_time =
tempo.datetime_get_naive(datetime) |> tempo.naive_datetime_get_time
let applied =
datetime
|> add(offset.to_duration(datetime |> tempo.datetime_get_offset))
|> drop_offset
// Applying an offset does not change the abosolute time value, so we need
// to preserve the monotonic and unique values.
tempo.naive_datetime(
date: naive_datetime.get_date(applied),
time: naive_datetime.get_time(applied)
|> tempo.time_set_mono(
tempo.time_get_mono(original_time),
tempo.time_get_unique(original_time),
),
)
}
/// Converts a datetime to the equivalent UTC time.
///
/// ## Examples
///
/// ```gleam
/// datetime.literal("2024-06-21T05:36:11.195-04:00")
/// |> datetime.to_utc
/// // -> datetime.literal("2024-06-21T09:36:11.195Z")
/// ```
pub fn to_utc(datetime: tempo.DateTime) -> tempo.DateTime {
datetime
|> apply_offset
|> naive_datetime.set_offset(tempo.utc)
}
/// Converts a datetime to the equivalent time in an offset.
///
/// ## Examples
///
/// ```gleam
/// datetime.literal("2024-06-21T05:36:11.195-04:00")
/// |> datetime.to_offset(offset.literal("+10:00"))
/// // -> datetime.literal("2024-06-21T19:36:11.195+10:00")
/// ```
pub fn to_offset(
datetime: tempo.DateTime,
offset: tempo.Offset,
) -> tempo.DateTime {
datetime
|> to_utc
|> subtract(offset.to_duration(offset))
|> drop_offset
|> naive_datetime.set_offset(offset)
}
/// Converts a datetime to the equivalent local datetime. The return value
/// indicates if the conversion was precise or imprecise. Use mattern
/// matching to handle the two cases.
///
/// Conversion is based on the host's current offset. If the date of the
/// supplied datetime matches the date of the host, then we can apply the
/// current host's offset to get the local time safely, resulting in a precise
/// conversion. If the date does not match the host's, then we can not be
/// sure the current offset is still applicable, and will perform an
/// imprecise conversion. The imprecise conversion can be inaccurate to the
/// degree the local offset changes throughout the year. For example, in
/// North America where Daylight Savings Time is observed with a one-hour
/// time shift, the imprecise conversion can be off by up to an hour depending
/// on the time of year.
///
/// ## Examples
///
/// ```gleam
/// datetime.literal("2024-06-21T09:57:11.195Z")
/// |> datetime.to_local
/// // -> tempo.Precise(datetime.literal("2024-06-21T05:57:11.195-04:00"))
/// ```
///
/// ```gleam
/// datetime.literal("1998-08-23T09:57:11.195Z")
/// |> datetime.to_local
/// // -> tempo.Imprecise(datetime.literal("1998-08-23T05:57:11.195-04:00"))
/// ```
pub fn to_local(
datetime: tempo.DateTime,
) -> tempo.UncertainConversion(tempo.DateTime) {
use <- bool.lazy_guard(
when: datetime |> tempo.datetime_get_offset == offset.local(),
return: fn() { tempo.Precise(datetime) },
)
let local_dt = datetime |> to_offset(offset.local())
case
local_dt |> tempo.datetime_get_naive |> tempo.naive_datetime_get_date
== date.current_local()
{
True -> tempo.Precise(local_dt)
False -> tempo.Imprecise(local_dt)
}
}
/// Converts a datetime to the equivalent local time. The return value
/// indicates if the conversion was precise or imprecise. Use mattern
/// matching to handle the two cases.
///
/// Conversion is based on the host's current offset. If the date of the
/// supplied datetime matches the date of the host, then we can apply the
/// current host's offset to get the local time safely, resulting in a precise
/// conversion. If the date does not match the host's, then we can not be
/// sure the current offset is still applicable, and will perform an
/// imprecise conversion. The imprecise conversion can be inaccurate to the
/// degree the local offset changes throughout the year. For example, in
/// North America where Daylight Savings Time is observed with a one-hour
/// time shift, the imprecise conversion can be off by up to an hour depending
/// on the time of year.
///
/// ## Examples
///
/// ```gleam
/// datetime.literal("2024-06-21T09:57:11.195Z")
/// |> datetime.to_local_time
/// // -> tempo.Precise(time.literal("05:57:11.195"))
/// ```
///
/// ```gleam
/// datetime.literal("1998-08-23T09:57:11.195Z")
/// |> datetime.to_local_time
/// // -> tempo.Imprecise(time.literal("05:57:11.195"))
/// ```
pub fn to_local_time(
datetime: tempo.DateTime,
) -> tempo.UncertainConversion(tempo.Time) {
case to_local(datetime) {
tempo.Precise(datetime) ->
datetime
|> tempo.datetime_get_naive
|> tempo.naive_datetime_get_time
|> tempo.Precise
tempo.Imprecise(datetime) ->
datetime
|> tempo.datetime_get_naive
|> tempo.naive_datetime_get_time
|> tempo.Imprecise
}
}
/// Converts a datetime to the equivalent local time. The return value
/// indicates if the conversion was precise or imprecise. Use mattern
/// matching to handle the two cases.
///
/// Conversion is based on the host's current offset. If the date of the
/// supplied datetime matches the date of the host, then we can apply the
/// current host's offset to get the local time safely, resulting in a precise
/// conversion. If the date does not match the host's, then we can not be
/// sure the current offset is still applicable, and will perform an
/// imprecise conversion. The imprecise conversion can be inaccurate to the
/// degree the local offset changes throughout the year. For example, in
/// North America where Daylight Savings Time is observed with a one-hour
/// time shift, the imprecise conversion can be off by up to an hour depending
/// on the time of year.
///
/// ## Examples
///
/// ```gleam
/// datetime.literal("2024-06-19T01:35:11.195Z")
/// |> datetime.to_local_date
/// // -> tempo.Precise(date.literal("2024-06-18"))
/// ```
///
/// ```gleam
/// datetime.literal("1998-08-23T01:57:11.195Z")
/// |> datetime.to_local_date
/// // -> tempo.Imprecise(date.literal("1998-08-22"))
/// ```
pub fn to_local_date(
datetime: tempo.DateTime,
) -> tempo.UncertainConversion(tempo.Date) {
case to_local(datetime) {
tempo.Precise(datetime) ->
datetime
|> tempo.datetime_get_naive
|> tempo.naive_datetime_get_date
|> tempo.Precise
tempo.Imprecise(datetime) ->
datetime
|> tempo.datetime_get_naive
|> tempo.naive_datetime_get_date
|> tempo.Imprecise
}
}
/// Sets a datetime's time value to a second precision. Drops any milliseconds
/// from the underlying time value.
///
/// ## Examples
///
/// ```gleam
/// datetime.literal("2024-06-13T13:42:11.195423Z")
/// |> datetime.to_second_precision
/// |> datetime.to_string
/// // -> "2024-06-13T13:42:11Z"
/// ```
pub fn to_second_precision(datetime: tempo.DateTime) -> tempo.DateTime {
new(
datetime |> tempo.datetime_get_naive |> tempo.naive_datetime_get_date,
datetime
|> tempo.datetime_get_naive
|> tempo.naive_datetime_get_time
|> time.to_second_precision,
datetime |> tempo.datetime_get_offset,
)
}
/// Sets a datetime's time value to a millisecond precision. Drops any
/// microseconds from the underlying time value.
///
/// ## Examples
///
/// ```gleam
/// datetime.literal("2024-06-13T13:42:11.195423Z")
/// |> datetime.to_milli_precision
/// |> datetime.to_string
/// // -> "2024-06-13T13:42:11.195Z"
/// ```
pub fn to_milli_precision(datetime: tempo.DateTime) -> tempo.DateTime {
new(
datetime |> tempo.datetime_get_naive |> tempo.naive_datetime_get_date,
datetime
|> tempo.datetime_get_naive
|> tempo.naive_datetime_get_time
|> time.to_milli_precision,
datetime |> tempo.datetime_get_offset,
)
}
/// Sets a datetime's time value to a microsecond precision. Drops any
/// nanoseconds from the underlying time value.
///
/// ## Examples
///
/// ```gleam
/// datetime.literal("2024-06-13T13:42:11.195423534Z")
/// |> datetime.to_micro_precision
/// |> datetime.to_string
/// // -> "2024-06-13T13:42:11.195423Z"
/// ```
pub fn to_micro_precision(datetime: tempo.DateTime) -> tempo.DateTime {
new(
datetime |> tempo.datetime_get_naive |> tempo.naive_datetime_get_date,
datetime
|> tempo.datetime_get_naive
|> tempo.naive_datetime_get_time
|> time.to_micro_precision,
datetime |> tempo.datetime_get_offset,
)
}
/// Sets a datetime's time value to a nanosecond precision. Leaves the
/// underlying time value unchanged.
///
/// ## Examples
///
/// ```gleam
/// datetime.literal("2024-06-13T13:42:11.195Z")
/// |> datetime.to_nano_precision
/// |> datetime.to_string
/// // -> "2024-06-13T13:42:11.195000000Z"
/// ```
pub fn to_nano_precision(datetime: tempo.DateTime) -> tempo.DateTime {
new(
datetime |> tempo.datetime_get_naive |> tempo.naive_datetime_get_date,
datetime
|> tempo.datetime_get_naive
|> tempo.naive_datetime_get_time
|> time.to_nano_precision,
datetime |> tempo.datetime_get_offset,
)
}
/// Compares two datetimes.
///
/// ## Examples
///
/// ```gleam
/// datetime.literal("2024-06-21T23:47:00+09:05")
/// |> datetime.compare(to: datetime.literal("2024-06-21T23:47:00+09:05"))
/// // -> order.Eq
/// ```
///
/// ```gleam
/// datetime.literal("2023-05-11T13:30:00-04:00")
/// |> datetime.compare(to: datetime.literal("2023-05-11T13:15:00Z"))
/// // -> order.Lt
/// ```
///
/// ```gleam
/// datetime.literal("2024-06-12T23:47:00+09:05")
/// |> datetime.compare(to: datetime.literal("2022-04-12T00:00:00"))
/// // -> order.Gt
/// ```
pub fn compare(a: tempo.DateTime, to b: tempo.DateTime) {
apply_offset(a) |> naive_datetime.compare(to: apply_offset(b))
}
/// Checks if the first datetime is earlier than the second datetime.
///
/// ## Examples
///
/// ```gleam
/// datetime.literal("2024-06-21T23:47:00+09:05")
/// |> datetime.is_earlier(
/// than: datetime.literal("2024-06-21T23:47:00+09:05"),
/// )
/// // -> False
/// ```
///
/// ```gleam
/// datetime.literal("2023-05-11T13:30:00-04:00")
/// |> datetime.is_earlier(
/// than: datetime.literal("2023-05-11T13:15:00Z"),
/// )
/// // -> True
/// ```
pub fn is_earlier(a: tempo.DateTime, than b: tempo.DateTime) -> Bool {
compare(a, b) == order.Lt
}
/// Checks if the first datetime is earlier or equal to the second datetime.
///
/// ## Examples
/// ```gleam
/// datetime.literal("2024-06-21T23:47:00+09:05")
/// |> datetime.is_earlier_or_equal(
/// to: datetime.literal("2024-06-21T23:47:00+09:05"),
/// )
/// // -> True
/// ```
///
/// ```gleam
/// datetime.literal("2024-07-15T23:40:00-04:00")
/// |> datetime.is_earlier_or_equal(
/// to: datetime.literal("2023-05-11T13:15:00Z"),
/// )
/// // -> False
/// ```
pub fn is_earlier_or_equal(a: tempo.DateTime, to b: tempo.DateTime) -> Bool {
compare(a, b) == order.Lt || compare(a, b) == order.Eq
}
/// Checks if the first datetime is equal to the second datetime.
///
/// ## Examples
/// ```gleam
/// datetime.literal("2024-06-21T09:44:00Z")
/// |> datetime.is_equal(
/// to: datetime.literal("2024-06-21T05:44:00-04:00"),
/// )
/// // -> True
/// ```
///
/// ```gleam
/// datetime.literal("2024-06-21T09:44:00Z")
/// |> datetime.is_equal(
/// to: datetime.literal("2024-06-21T09:44:00.045Z"),
/// )
/// // -> False
/// ```
pub fn is_equal(a: tempo.DateTime, to b: tempo.DateTime) -> Bool {
compare(a, b) == order.Eq
}
/// Checks if the first datetime is later than the second datetime.
///
/// ## Examples
///
/// ```gleam
/// datetime.literal("2024-06-21T23:47:00+09:05")
/// |> datetime.is_later(
/// than: datetime.literal("2024-06-21T23:47:00+09:05"),
/// )
/// // -> False
/// ```
///
/// ```gleam
/// datetime.literal("2023-05-11T13:00:00+04:00")
/// |> datetime.is_later(
/// than: datetime.literal("2023-05-11T13:15:00.534Z"),
/// )
/// // -> True
/// ```
pub fn is_later(a: tempo.DateTime, than b: tempo.DateTime) -> Bool {
compare(a, b) == order.Gt
}
/// Checks if the first datetime is later or equal to the second datetime.
///
/// ## Examples
/// ```gleam
/// datetime.literal("2016-01-11T03:47:00+09:05")
/// |> datetime.is_later_or_equal(
/// to: datetime.literal("2024-06-21T23:47:00+09:05"),
/// )
/// // -> False
/// ```
///
/// ```gleam
/// datetime.literal("2024-07-15T23:40:00-04:00")
/// |> datetime.is_later_or_equal(
/// to: datetime.literal("2023-05-11T13:15:00Z"),
/// )
/// // -> True
/// ```
pub fn is_later_or_equal(a: tempo.DateTime, to b: tempo.DateTime) -> Bool {
compare(a, b) == order.Gt || compare(a, b) == order.Eq
}
@internal
pub fn difference_from(a: tempo.DateTime, from b: tempo.DateTime) {
// Sadly the `difference` function is messed up because it is the same name
// as the `time.difference` and `date.difference` function with one of the
// same labels, but with opposite logic.
as_period(b, a)
}
/// Returns the difference between two datetimes as a period between their
/// equivalent UTC times.
///
/// ## Examples
///
/// ```gleam
/// datetime.literal("2024-06-12T23:17:00Z")
/// |> datetime.difference(
/// from: datetime.literal("2024-06-16T01:16:12Z"),
/// )
/// |> period.as_days
/// // -> 3
/// ```
///
/// ```gleam
/// datetime.literal("2024-06-12T23:17:00Z")
/// |> datetime.difference(
/// from: datetime.literal("2024-06-16T01:18:12Z"),
/// )
/// |> period.format
/// // -> "3 days, 2 hours, and 1 minute"
/// ```
@deprecated("Use `as_period` instead, this function is an alias for it. This function has the same name and one label as the `time.difference` and `date.difference` functions, but with different logic, making it too confusing.")
pub fn difference(from a: tempo.DateTime, to b: tempo.DateTime) -> tempo.Period {
as_period(a, b)
}
/// Creates a period between two datetimes, where the start and end times are
/// the equivalent UTC times of the provided datetimes. The specified start
/// and end datetimes will be swapped if the start datetime is later than the
/// end datetime.
///
/// ## Examples
///
/// ```gleam
/// datetime.to_period(
/// start: datetime.literal("2024-06-12T23:17:00Z")
/// end: datetime.literal("2024-06-16T01:16:12Z"),
/// )
/// |> period.as_days
/// // -> 3
/// ```
///
/// ```gleam
/// datetime.to_period(
/// start: datetime.literal("2024-06-12T23:17:00Z")
/// end: datetime.literal("2024-06-16T01:18:12Z"),
/// )
/// |> period.format
/// // -> "3 days, 2 hours, and 1 minute"
/// ```
pub fn as_period(
start start: tempo.DateTime,
end end: tempo.DateTime,
) -> tempo.Period {
let #(start, end) = case start |> is_earlier_or_equal(to: end) {
True -> #(start, end)
False -> #(end, start)
}
tempo.Period(start: start, end: end)
}
/// Adds a duration to a datetime.
///
/// ## Examples
///
/// ```gleam
/// datetime.literal("2024-06-12T23:17:00Z")
/// |> datetime.add(duration |> tempo.offset_get_minutes(3))
/// // -> datetime.literal("2024-06-12T23:20:00Z")
/// ```
pub fn add(
datetime: tempo.DateTime,
duration duration_to_add: tempo.Duration,
) -> tempo.DateTime {
datetime
|> drop_offset
|> naive_datetime.add(duration: duration_to_add)
|> naive_datetime.set_offset(datetime |> tempo.datetime_get_offset)
}
/// Subtracts a duration from a datetime.
///
/// ## Examples
///
/// ```gleam
/// datetime.literal("2024-06-21T23:17:00Z")
/// |> datetime.subtract(duration.days(3))
/// // -> datetime.literal("2024-06-18T23:17:00Z")
/// ```
pub fn subtract(
datetime: tempo.DateTime,
duration duration_to_subtract: tempo.Duration,
) -> tempo.DateTime {
datetime
|> drop_offset
|> naive_datetime.subtract(duration: duration_to_subtract)
|> naive_datetime.set_offset(datetime |> tempo.datetime_get_offset)
}
/// Gets the time left in the day.
///
/// Does **not** account for leap seconds like the rest of the package.
///
/// ## Examples
///
/// ```gleam
/// naive_datetime.literal("2015-06-30T23:59:03Z")
/// |> naive_datetime.time_left_in_day
/// // -> time.literal("00:00:57")
/// ```
///
/// ```gleam
/// naive_datetime.literal("2024-06-18T08:05:20-04:00")
/// |> naive_datetime.time_left_in_day
/// // -> time.literal("15:54:40")
/// ```
pub fn time_left_in_day(datetime: tempo.DateTime) -> tempo.Time {
datetime
|> tempo.datetime_get_naive
|> tempo.naive_datetime_get_time
|> time.left_in_day
}