1215 lines
35 KiB
JavaScript
1215 lines
35 KiB
JavaScript
import * as $bit_array from "../../../gleam_stdlib/gleam/bit_array.mjs";
|
|
import * as $float from "../../../gleam_stdlib/gleam/float.mjs";
|
|
import * as $int from "../../../gleam_stdlib/gleam/int.mjs";
|
|
import * as $list from "../../../gleam_stdlib/gleam/list.mjs";
|
|
import * as $order from "../../../gleam_stdlib/gleam/order.mjs";
|
|
import * as $result from "../../../gleam_stdlib/gleam/result.mjs";
|
|
import * as $string from "../../../gleam_stdlib/gleam/string.mjs";
|
|
import {
|
|
Ok,
|
|
Error,
|
|
toList,
|
|
Empty as $Empty,
|
|
prepend as listPrepend,
|
|
CustomType as $CustomType,
|
|
remainderInt,
|
|
divideFloat,
|
|
divideInt,
|
|
toBitArray,
|
|
bitArraySlice,
|
|
} from "../../gleam.mjs";
|
|
import * as $calendar from "../../gleam/time/calendar.mjs";
|
|
import * as $duration from "../../gleam/time/duration.mjs";
|
|
import { system_time as get_system_time } from "../../gleam_time_ffi.mjs";
|
|
|
|
class Timestamp extends $CustomType {
|
|
constructor(seconds, nanoseconds) {
|
|
super();
|
|
this.seconds = seconds;
|
|
this.nanoseconds = nanoseconds;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*
|
|
* @ignore
|
|
*/
|
|
function normalise(timestamp) {
|
|
let multiplier = 1_000_000_000;
|
|
let nanoseconds = remainderInt(timestamp.nanoseconds, multiplier);
|
|
let overflow = timestamp.nanoseconds - nanoseconds;
|
|
let seconds = timestamp.seconds + (divideInt(overflow, multiplier));
|
|
let $ = nanoseconds >= 0;
|
|
if ($) {
|
|
return new Timestamp(seconds, nanoseconds);
|
|
} else {
|
|
return new 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
|
|
* ```
|
|
*/
|
|
export function compare(left, right) {
|
|
return $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
|
|
*/
|
|
export function system_time() {
|
|
let $ = get_system_time();
|
|
let seconds;
|
|
let nanoseconds;
|
|
seconds = $[0];
|
|
nanoseconds = $[1];
|
|
return normalise(new Timestamp(seconds, nanoseconds));
|
|
}
|
|
|
|
/**
|
|
* 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)
|
|
* ```
|
|
*/
|
|
export function difference(left, right) {
|
|
let seconds = $duration.seconds(right.seconds - left.seconds);
|
|
let nanoseconds = $duration.nanoseconds(right.nanoseconds - left.nanoseconds);
|
|
return $duration.add(seconds, nanoseconds);
|
|
}
|
|
|
|
/**
|
|
* Add a duration to a timestamp.
|
|
*
|
|
* # Examples
|
|
*
|
|
* ```gleam
|
|
* add(from_unix_seconds(1000), duration.seconds(5))
|
|
* // -> from_unix_seconds(1005)
|
|
* ```
|
|
*/
|
|
export function add(timestamp, duration) {
|
|
let $ = $duration.to_seconds_and_nanoseconds(duration);
|
|
let seconds;
|
|
let nanoseconds;
|
|
seconds = $[0];
|
|
nanoseconds = $[1];
|
|
let _pipe = new Timestamp(
|
|
timestamp.seconds + seconds,
|
|
timestamp.nanoseconds + nanoseconds,
|
|
);
|
|
return normalise(_pipe);
|
|
}
|
|
|
|
function pad_digit(digit, desired_length) {
|
|
let _pipe = $int.to_string(digit);
|
|
return $string.pad_start(_pipe, desired_length, "0");
|
|
}
|
|
|
|
function duration_to_minutes(duration) {
|
|
return $float.round($duration.to_seconds(duration) / 60.0);
|
|
}
|
|
|
|
function modulo(n, m) {
|
|
let $ = $int.modulo(n, m);
|
|
if ($ instanceof Ok) {
|
|
let n$1 = $[0];
|
|
return n$1;
|
|
} else {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
function floored_div(numerator, denominator) {
|
|
let n = divideFloat($int.to_float(numerator), denominator);
|
|
return $float.round($float.floor(n));
|
|
}
|
|
|
|
function to_civil(minutes) {
|
|
let raw_day = floored_div(minutes, (60.0 * 24.0)) + 719_468;
|
|
let _block;
|
|
let $ = raw_day >= 0;
|
|
if ($) {
|
|
_block = globalThis.Math.trunc(raw_day / 146_097);
|
|
} else {
|
|
_block = globalThis.Math.trunc((raw_day - 146_096) / 146_097);
|
|
}
|
|
let era = _block;
|
|
let day_of_era = raw_day - era * 146_097;
|
|
let year_of_era = globalThis.Math.trunc(
|
|
(((day_of_era - (globalThis.Math.trunc(day_of_era / 1460))) + (globalThis.Math.trunc(
|
|
day_of_era / 36_524
|
|
))) - (globalThis.Math.trunc(day_of_era / 146_096))) / 365
|
|
);
|
|
let year = year_of_era + era * 400;
|
|
let day_of_year = day_of_era - ((365 * year_of_era + (globalThis.Math.trunc(
|
|
year_of_era / 4
|
|
))) - (globalThis.Math.trunc(year_of_era / 100)));
|
|
let mp = globalThis.Math.trunc((5 * day_of_year + 2) / 153);
|
|
let _block$1;
|
|
let $1 = mp < 10;
|
|
if ($1) {
|
|
_block$1 = mp + 3;
|
|
} else {
|
|
_block$1 = mp - 9;
|
|
}
|
|
let month = _block$1;
|
|
let day = (day_of_year - (globalThis.Math.trunc((153 * mp + 2) / 5))) + 1;
|
|
let _block$2;
|
|
let $2 = month <= 2;
|
|
if ($2) {
|
|
_block$2 = year + 1;
|
|
} else {
|
|
_block$2 = year;
|
|
}
|
|
let year$1 = _block$2;
|
|
return [year$1, month, day];
|
|
}
|
|
|
|
function to_calendar_from_offset(timestamp, offset) {
|
|
let total = timestamp.seconds + offset * 60;
|
|
let seconds = modulo(total, 60);
|
|
let total_minutes = floored_div(total, 60.0);
|
|
let minutes = globalThis.Math.trunc(modulo(total, 60 * 60) / 60);
|
|
let hours = divideInt(modulo(total, 24 * 60 * 60), 60 * 60);
|
|
let $ = to_civil(total_minutes);
|
|
let year;
|
|
let month;
|
|
let day;
|
|
year = $[0];
|
|
month = $[1];
|
|
day = $[2];
|
|
return [year, month, day, hours, minutes, seconds];
|
|
}
|
|
|
|
/**
|
|
* 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))
|
|
* ```
|
|
*/
|
|
export function to_calendar(timestamp, offset) {
|
|
let offset$1 = duration_to_minutes(offset);
|
|
let $ = to_calendar_from_offset(timestamp, offset$1);
|
|
let year;
|
|
let month;
|
|
let day;
|
|
let hours;
|
|
let minutes;
|
|
let seconds;
|
|
year = $[0];
|
|
month = $[1];
|
|
day = $[2];
|
|
hours = $[3];
|
|
minutes = $[4];
|
|
seconds = $[5];
|
|
let _block;
|
|
if (month === 1) {
|
|
_block = new $calendar.January();
|
|
} else if (month === 2) {
|
|
_block = new $calendar.February();
|
|
} else if (month === 3) {
|
|
_block = new $calendar.March();
|
|
} else if (month === 4) {
|
|
_block = new $calendar.April();
|
|
} else if (month === 5) {
|
|
_block = new $calendar.May();
|
|
} else if (month === 6) {
|
|
_block = new $calendar.June();
|
|
} else if (month === 7) {
|
|
_block = new $calendar.July();
|
|
} else if (month === 8) {
|
|
_block = new $calendar.August();
|
|
} else if (month === 9) {
|
|
_block = new $calendar.September();
|
|
} else if (month === 10) {
|
|
_block = new $calendar.October();
|
|
} else if (month === 11) {
|
|
_block = new $calendar.November();
|
|
} else {
|
|
_block = new $calendar.December();
|
|
}
|
|
let month$1 = _block;
|
|
let nanoseconds = timestamp.nanoseconds;
|
|
let date = new $calendar.Date(year, month$1, day);
|
|
let time = new $calendar.TimeOfDay(hours, minutes, seconds, nanoseconds);
|
|
return [date, time];
|
|
}
|
|
|
|
function do_remove_trailing_zeros(loop$reversed_digits) {
|
|
while (true) {
|
|
let reversed_digits = loop$reversed_digits;
|
|
if (reversed_digits instanceof $Empty) {
|
|
return reversed_digits;
|
|
} else {
|
|
let digit = reversed_digits.head;
|
|
if (digit === 0) {
|
|
let digits = reversed_digits.tail;
|
|
loop$reversed_digits = digits;
|
|
} else {
|
|
let reversed_digits$1 = reversed_digits;
|
|
return $list.reverse(reversed_digits$1);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Given a list of digits, return new list with any trailing zeros removed.
|
|
*
|
|
* @ignore
|
|
*/
|
|
function remove_trailing_zeros(digits) {
|
|
let reversed_digits = $list.reverse(digits);
|
|
return do_remove_trailing_zeros(reversed_digits);
|
|
}
|
|
|
|
function do_get_zero_padded_digits(loop$number, loop$digits, loop$count) {
|
|
while (true) {
|
|
let number = loop$number;
|
|
let digits = loop$digits;
|
|
let count = loop$count;
|
|
let number$1 = number;
|
|
if ((number$1 <= 0) && (count >= 9)) {
|
|
return digits;
|
|
} else {
|
|
let number$2 = number;
|
|
if (number$2 <= 0) {
|
|
loop$number = number$2;
|
|
loop$digits = listPrepend(0, digits);
|
|
loop$count = count + 1;
|
|
} else {
|
|
let number$3 = number;
|
|
let digit = number$3 % 10;
|
|
let number$4 = floored_div(number$3, 10.0);
|
|
loop$number = number$4;
|
|
loop$digits = listPrepend(digit, digits);
|
|
loop$count = count + 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the list of digits of `number`. If the number of digits is less
|
|
* than 9, the result is zero-padded at the front.
|
|
*
|
|
* @ignore
|
|
*/
|
|
function get_zero_padded_digits(number) {
|
|
return do_get_zero_padded_digits(number, toList([]), 0);
|
|
}
|
|
|
|
/**
|
|
* Converts nanoseconds into a `String` representation of fractional seconds.
|
|
*
|
|
* Assumes that `nanoseconds < 1_000_000_000`, which will be true for any
|
|
* normalised timestamp.
|
|
*
|
|
* @ignore
|
|
*/
|
|
function show_second_fraction(nanoseconds) {
|
|
let $ = $int.compare(nanoseconds, 0);
|
|
if ($ instanceof $order.Lt) {
|
|
return "";
|
|
} else if ($ instanceof $order.Eq) {
|
|
return "";
|
|
} else {
|
|
let _block;
|
|
let _pipe = nanoseconds;
|
|
let _pipe$1 = get_zero_padded_digits(_pipe);
|
|
let _pipe$2 = remove_trailing_zeros(_pipe$1);
|
|
let _pipe$3 = $list.map(_pipe$2, $int.to_string);
|
|
_block = $string.join(_pipe$3, "");
|
|
let second_fraction_part = _block;
|
|
return "." + second_fraction_part;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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"
|
|
* ```
|
|
*/
|
|
export function to_rfc3339(timestamp, offset) {
|
|
let offset$1 = duration_to_minutes(offset);
|
|
let $ = to_calendar_from_offset(timestamp, offset$1);
|
|
let years;
|
|
let months;
|
|
let days;
|
|
let hours;
|
|
let minutes;
|
|
let seconds;
|
|
years = $[0];
|
|
months = $[1];
|
|
days = $[2];
|
|
hours = $[3];
|
|
minutes = $[4];
|
|
seconds = $[5];
|
|
let offset_minutes = modulo(offset$1, 60);
|
|
let offset_hours = $int.absolute_value(floored_div(offset$1, 60.0));
|
|
let n2 = (_capture) => { return pad_digit(_capture, 2); };
|
|
let n4 = (_capture) => { return pad_digit(_capture, 4); };
|
|
let out = "";
|
|
let out$1 = ((((out + n4(years)) + "-") + n2(months)) + "-") + n2(days);
|
|
let out$2 = out$1 + "T";
|
|
let out$3 = ((((out$2 + n2(hours)) + ":") + n2(minutes)) + ":") + n2(seconds);
|
|
let out$4 = out$3 + show_second_fraction(timestamp.nanoseconds);
|
|
let $1 = $int.compare(offset$1, 0);
|
|
if ($1 instanceof $order.Lt) {
|
|
return (((out$4 + "-") + n2(offset_hours)) + ":") + n2(offset_minutes);
|
|
} else if ($1 instanceof $order.Eq) {
|
|
return out$4 + "Z";
|
|
} else {
|
|
return (((out$4 + "+") + n2(offset_hours)) + ":") + n2(offset_minutes);
|
|
}
|
|
}
|
|
|
|
function is_leap_year(year) {
|
|
return ((year % 4) === 0) && (((year % 100) !== 0) || ((year % 400) === 0));
|
|
}
|
|
|
|
function parse_sign(bytes) {
|
|
if (bytes.bitSize >= 8) {
|
|
if (bytes.byteAt(0) === 43) {
|
|
if ((bytes.bitSize - 8) % 8 === 0) {
|
|
let remaining_bytes = bitArraySlice(bytes, 8);
|
|
return new Ok(["+", remaining_bytes]);
|
|
} else {
|
|
return new Error(undefined);
|
|
}
|
|
} else if (bytes.byteAt(0) === 45 && (bytes.bitSize - 8) % 8 === 0) {
|
|
let remaining_bytes = bitArraySlice(bytes, 8);
|
|
return new Ok(["-", remaining_bytes]);
|
|
} else {
|
|
return new Error(undefined);
|
|
}
|
|
} else {
|
|
return new Error(undefined);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Accept the given value from `bytes` and move past it if found.
|
|
*
|
|
* @ignore
|
|
*/
|
|
function accept_byte(bytes, value) {
|
|
if (bytes.bitSize >= 8 && (bytes.bitSize - 8) % 8 === 0) {
|
|
let byte = bytes.byteAt(0);
|
|
if (byte === value) {
|
|
let remaining_bytes = bitArraySlice(bytes, 8);
|
|
return new Ok(remaining_bytes);
|
|
} else {
|
|
return new Error(undefined);
|
|
}
|
|
} else {
|
|
return new Error(undefined);
|
|
}
|
|
}
|
|
|
|
function accept_empty(bytes) {
|
|
if (bytes.bitSize === 0) {
|
|
return new Ok(undefined);
|
|
} else {
|
|
return new Error(undefined);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Note: It is the callers responsibility to ensure the inputs are valid.
|
|
*
|
|
* See https://www.tondering.dk/claus/cal/julperiod.php#formula
|
|
*
|
|
* @ignore
|
|
*/
|
|
function julian_day_from_ymd(year, month, day) {
|
|
let adjustment = globalThis.Math.trunc((14 - month) / 12);
|
|
let adjusted_year = (year + 4800) - adjustment;
|
|
let adjusted_month = (month + 12 * adjustment) - 3;
|
|
return (((((day + (globalThis.Math.trunc((153 * adjusted_month + 2) / 5))) + 365 * adjusted_year) + (globalThis.Math.trunc(
|
|
adjusted_year / 4
|
|
))) - (globalThis.Math.trunc(adjusted_year / 100))) + (globalThis.Math.trunc(
|
|
adjusted_year / 400
|
|
))) - 32_045;
|
|
}
|
|
|
|
/**
|
|
* Create a timestamp from a number of seconds since 00:00:00 UTC on 1 January
|
|
* 1970.
|
|
*/
|
|
export function from_unix_seconds(seconds) {
|
|
return new 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.
|
|
*/
|
|
export function from_unix_seconds_and_nanoseconds(seconds, nanoseconds) {
|
|
let _pipe = new Timestamp(seconds, nanoseconds);
|
|
return normalise(_pipe);
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
export function to_unix_seconds(timestamp) {
|
|
let seconds = $int.to_float(timestamp.seconds);
|
|
let nanoseconds = $int.to_float(timestamp.nanoseconds);
|
|
return 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.
|
|
*/
|
|
export function to_unix_seconds_and_nanoseconds(timestamp) {
|
|
return [timestamp.seconds, timestamp.nanoseconds];
|
|
}
|
|
|
|
const seconds_per_day = 86_400;
|
|
|
|
const seconds_per_hour = 3600;
|
|
|
|
const seconds_per_minute = 60;
|
|
|
|
function offset_to_seconds(sign, hours, minutes) {
|
|
let abs_seconds = hours * seconds_per_hour + minutes * seconds_per_minute;
|
|
if (sign === "-") {
|
|
return - abs_seconds;
|
|
} else {
|
|
return abs_seconds;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* `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
|
|
*
|
|
* @ignore
|
|
*/
|
|
function julian_seconds_from_parts(year, month, day, hours, minutes, seconds) {
|
|
let julian_day_seconds = julian_day_from_ymd(year, month, day) * seconds_per_day;
|
|
return ((julian_day_seconds + hours * seconds_per_hour) + minutes * seconds_per_minute) + seconds;
|
|
}
|
|
|
|
const nanoseconds_per_second = 1_000_000_000;
|
|
|
|
/**
|
|
* The `:` character as a byte
|
|
*
|
|
* @ignore
|
|
*/
|
|
const byte_colon = 0x3A;
|
|
|
|
/**
|
|
* The `-` character as a byte
|
|
*
|
|
* @ignore
|
|
*/
|
|
const byte_minus = 0x2D;
|
|
|
|
/**
|
|
* The `0` character as a byte
|
|
*
|
|
* @ignore
|
|
*/
|
|
const byte_zero = 0x30;
|
|
|
|
/**
|
|
* The `9` character as a byte
|
|
*
|
|
* @ignore
|
|
*/
|
|
const byte_nine = 0x39;
|
|
|
|
function do_parse_second_fraction_as_nanoseconds(
|
|
loop$bytes,
|
|
loop$acc,
|
|
loop$power
|
|
) {
|
|
while (true) {
|
|
let bytes = loop$bytes;
|
|
let acc = loop$acc;
|
|
let power = loop$power;
|
|
let power$1 = globalThis.Math.trunc(power / 10);
|
|
if (bytes.bitSize >= 8 && (bytes.bitSize - 8) % 8 === 0) {
|
|
let byte = bytes.byteAt(0);
|
|
if (((0x30 <= byte) && (byte <= 0x39)) && (power$1 < 1)) {
|
|
let remaining_bytes = bitArraySlice(bytes, 8);
|
|
loop$bytes = remaining_bytes;
|
|
loop$acc = acc;
|
|
loop$power = power$1;
|
|
} else {
|
|
let byte$1 = bytes.byteAt(0);
|
|
if ((0x30 <= byte$1) && (byte$1 <= 0x39)) {
|
|
let remaining_bytes = bitArraySlice(bytes, 8);
|
|
let digit = byte$1 - 0x30;
|
|
loop$bytes = remaining_bytes;
|
|
loop$acc = acc + digit * power$1;
|
|
loop$power = power$1;
|
|
} else {
|
|
return new Ok([acc, bytes]);
|
|
}
|
|
}
|
|
} else {
|
|
return new Ok([acc, bytes]);
|
|
}
|
|
}
|
|
}
|
|
|
|
function parse_second_fraction_as_nanoseconds(bytes) {
|
|
if (bytes.bitSize >= 8 && bytes.byteAt(0) === 46) {
|
|
if (bytes.bitSize >= 16 && (bytes.bitSize - 16) % 8 === 0) {
|
|
let byte = bytes.byteAt(1);
|
|
if ((0x30 <= byte) && (byte <= 0x39)) {
|
|
let remaining_bytes = bitArraySlice(bytes, 16);
|
|
return do_parse_second_fraction_as_nanoseconds(
|
|
toBitArray([byte, remaining_bytes]),
|
|
0,
|
|
nanoseconds_per_second,
|
|
);
|
|
} else if ((bytes.bitSize - 8) % 8 === 0) {
|
|
return new Error(undefined);
|
|
} else {
|
|
return new Ok([0, bytes]);
|
|
}
|
|
} else if ((bytes.bitSize - 8) % 8 === 0) {
|
|
return new Error(undefined);
|
|
} else {
|
|
return new Ok([0, bytes]);
|
|
}
|
|
} else {
|
|
return new Ok([0, bytes]);
|
|
}
|
|
}
|
|
|
|
function do_parse_digits(loop$bytes, loop$count, loop$acc, loop$k) {
|
|
while (true) {
|
|
let bytes = loop$bytes;
|
|
let count = loop$count;
|
|
let acc = loop$acc;
|
|
let k = loop$k;
|
|
if (k >= count) {
|
|
return new Ok([acc, bytes]);
|
|
} else if (bytes.bitSize >= 8 && (bytes.bitSize - 8) % 8 === 0) {
|
|
let byte = bytes.byteAt(0);
|
|
if ((0x30 <= byte) && (byte <= 0x39)) {
|
|
let remaining_bytes = bitArraySlice(bytes, 8);
|
|
loop$bytes = remaining_bytes;
|
|
loop$count = count;
|
|
loop$acc = acc * 10 + (byte - 0x30);
|
|
loop$k = k + 1;
|
|
} else {
|
|
return new Error(undefined);
|
|
}
|
|
} else {
|
|
return new Error(undefined);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parse and return the given number of digits from the given bytes.
|
|
*
|
|
* @ignore
|
|
*/
|
|
function parse_digits(bytes, count) {
|
|
return do_parse_digits(bytes, count, 0, 0);
|
|
}
|
|
|
|
function parse_year(bytes) {
|
|
return parse_digits(bytes, 4);
|
|
}
|
|
|
|
function parse_month(bytes) {
|
|
return $result.try$(
|
|
parse_digits(bytes, 2),
|
|
(_use0) => {
|
|
let month;
|
|
let bytes$1;
|
|
month = _use0[0];
|
|
bytes$1 = _use0[1];
|
|
let $ = (1 <= month) && (month <= 12);
|
|
if ($) {
|
|
return new Ok([month, bytes$1]);
|
|
} else {
|
|
return new Error(undefined);
|
|
}
|
|
},
|
|
);
|
|
}
|
|
|
|
function parse_day(bytes, year, month) {
|
|
return $result.try$(
|
|
parse_digits(bytes, 2),
|
|
(_use0) => {
|
|
let day;
|
|
let bytes$1;
|
|
day = _use0[0];
|
|
bytes$1 = _use0[1];
|
|
return $result.try$(
|
|
(() => {
|
|
if (month === 1) {
|
|
return new Ok(31);
|
|
} else if (month === 3) {
|
|
return new Ok(31);
|
|
} else if (month === 5) {
|
|
return new Ok(31);
|
|
} else if (month === 7) {
|
|
return new Ok(31);
|
|
} else if (month === 8) {
|
|
return new Ok(31);
|
|
} else if (month === 10) {
|
|
return new Ok(31);
|
|
} else if (month === 12) {
|
|
return new Ok(31);
|
|
} else if (month === 4) {
|
|
return new Ok(30);
|
|
} else if (month === 6) {
|
|
return new Ok(30);
|
|
} else if (month === 9) {
|
|
return new Ok(30);
|
|
} else if (month === 11) {
|
|
return new Ok(30);
|
|
} else if (month === 2) {
|
|
let $ = is_leap_year(year);
|
|
if ($) {
|
|
return new Ok(29);
|
|
} else {
|
|
return new Ok(28);
|
|
}
|
|
} else {
|
|
return new Error(undefined);
|
|
}
|
|
})(),
|
|
(max_day) => {
|
|
let $ = (1 <= day) && (day <= max_day);
|
|
if ($) {
|
|
return new Ok([day, bytes$1]);
|
|
} else {
|
|
return new Error(undefined);
|
|
}
|
|
},
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
function parse_hours(bytes) {
|
|
return $result.try$(
|
|
parse_digits(bytes, 2),
|
|
(_use0) => {
|
|
let hours;
|
|
let bytes$1;
|
|
hours = _use0[0];
|
|
bytes$1 = _use0[1];
|
|
let $ = (0 <= hours) && (hours <= 23);
|
|
if ($) {
|
|
return new Ok([hours, bytes$1]);
|
|
} else {
|
|
return new Error(undefined);
|
|
}
|
|
},
|
|
);
|
|
}
|
|
|
|
function parse_minutes(bytes) {
|
|
return $result.try$(
|
|
parse_digits(bytes, 2),
|
|
(_use0) => {
|
|
let minutes;
|
|
let bytes$1;
|
|
minutes = _use0[0];
|
|
bytes$1 = _use0[1];
|
|
let $ = (0 <= minutes) && (minutes <= 59);
|
|
if ($) {
|
|
return new Ok([minutes, bytes$1]);
|
|
} else {
|
|
return new Error(undefined);
|
|
}
|
|
},
|
|
);
|
|
}
|
|
|
|
function parse_seconds(bytes) {
|
|
return $result.try$(
|
|
parse_digits(bytes, 2),
|
|
(_use0) => {
|
|
let seconds;
|
|
let bytes$1;
|
|
seconds = _use0[0];
|
|
bytes$1 = _use0[1];
|
|
let $ = (0 <= seconds) && (seconds <= 60);
|
|
if ($) {
|
|
return new Ok([seconds, bytes$1]);
|
|
} else {
|
|
return new Error(undefined);
|
|
}
|
|
},
|
|
);
|
|
}
|
|
|
|
function parse_numeric_offset(bytes) {
|
|
return $result.try$(
|
|
parse_sign(bytes),
|
|
(_use0) => {
|
|
let sign;
|
|
let bytes$1;
|
|
sign = _use0[0];
|
|
bytes$1 = _use0[1];
|
|
return $result.try$(
|
|
parse_hours(bytes$1),
|
|
(_use0) => {
|
|
let hours;
|
|
let bytes$2;
|
|
hours = _use0[0];
|
|
bytes$2 = _use0[1];
|
|
return $result.try$(
|
|
accept_byte(bytes$2, byte_colon),
|
|
(bytes) => {
|
|
return $result.try$(
|
|
parse_minutes(bytes),
|
|
(_use0) => {
|
|
let minutes;
|
|
let bytes$1;
|
|
minutes = _use0[0];
|
|
bytes$1 = _use0[1];
|
|
let offset_seconds = offset_to_seconds(sign, hours, minutes);
|
|
return new Ok([offset_seconds, bytes$1]);
|
|
},
|
|
);
|
|
},
|
|
);
|
|
},
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
function parse_offset(bytes) {
|
|
if (bytes.bitSize >= 8) {
|
|
if (bytes.byteAt(0) === 90) {
|
|
if ((bytes.bitSize - 8) % 8 === 0) {
|
|
let remaining_bytes = bitArraySlice(bytes, 8);
|
|
return new Ok([0, remaining_bytes]);
|
|
} else {
|
|
return parse_numeric_offset(bytes);
|
|
}
|
|
} else if (bytes.byteAt(0) === 122 && (bytes.bitSize - 8) % 8 === 0) {
|
|
let remaining_bytes = bitArraySlice(bytes, 8);
|
|
return new Ok([0, remaining_bytes]);
|
|
} else {
|
|
return parse_numeric_offset(bytes);
|
|
}
|
|
} else {
|
|
return parse_numeric_offset(bytes);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The `t` character as a byte
|
|
*
|
|
* @ignore
|
|
*/
|
|
const byte_t_lowercase = 0x74;
|
|
|
|
/**
|
|
* The `T` character as a byte
|
|
*
|
|
* @ignore
|
|
*/
|
|
const byte_t_uppercase = 0x54;
|
|
|
|
/**
|
|
* The `T` character as a byte
|
|
*
|
|
* @ignore
|
|
*/
|
|
const byte_space = 0x20;
|
|
|
|
function accept_date_time_separator(bytes) {
|
|
if (bytes.bitSize >= 8 && (bytes.bitSize - 8) % 8 === 0) {
|
|
let byte = bytes.byteAt(0);
|
|
if (((byte === 0x54) || (byte === 0x74)) || (byte === 0x20)) {
|
|
let remaining_bytes = bitArraySlice(bytes, 8);
|
|
return new Ok(remaining_bytes);
|
|
} else {
|
|
return new Error(undefined);
|
|
}
|
|
} else {
|
|
return new Error(undefined);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The Julian seconds of the UNIX epoch (Julian day is 2_440_588)
|
|
*
|
|
* @ignore
|
|
*/
|
|
const julian_seconds_unix_epoch = 210_866_803_200;
|
|
|
|
/**
|
|
* Note: The caller of this function must ensure that all inputs are valid.
|
|
*
|
|
* @ignore
|
|
*/
|
|
function from_date_time(
|
|
year,
|
|
month,
|
|
day,
|
|
hours,
|
|
minutes,
|
|
seconds,
|
|
second_fraction_as_nanoseconds,
|
|
offset_seconds
|
|
) {
|
|
let julian_seconds = julian_seconds_from_parts(
|
|
year,
|
|
month,
|
|
day,
|
|
hours,
|
|
minutes,
|
|
seconds,
|
|
);
|
|
let julian_seconds_since_epoch = julian_seconds - julian_seconds_unix_epoch;
|
|
let _pipe = new Timestamp(
|
|
julian_seconds_since_epoch - offset_seconds,
|
|
second_fraction_as_nanoseconds,
|
|
);
|
|
return normalise(_pipe);
|
|
}
|
|
|
|
/**
|
|
* 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"
|
|
* ```
|
|
*/
|
|
export function from_calendar(date, time, offset) {
|
|
let _block;
|
|
let $ = date.month;
|
|
if ($ instanceof $calendar.January) {
|
|
_block = 1;
|
|
} else if ($ instanceof $calendar.February) {
|
|
_block = 2;
|
|
} else if ($ instanceof $calendar.March) {
|
|
_block = 3;
|
|
} else if ($ instanceof $calendar.April) {
|
|
_block = 4;
|
|
} else if ($ instanceof $calendar.May) {
|
|
_block = 5;
|
|
} else if ($ instanceof $calendar.June) {
|
|
_block = 6;
|
|
} else if ($ instanceof $calendar.July) {
|
|
_block = 7;
|
|
} else if ($ instanceof $calendar.August) {
|
|
_block = 8;
|
|
} else if ($ instanceof $calendar.September) {
|
|
_block = 9;
|
|
} else if ($ instanceof $calendar.October) {
|
|
_block = 10;
|
|
} else if ($ instanceof $calendar.November) {
|
|
_block = 11;
|
|
} else {
|
|
_block = 12;
|
|
}
|
|
let month = _block;
|
|
return from_date_time(
|
|
date.year,
|
|
month,
|
|
date.day,
|
|
time.hours,
|
|
time.minutes,
|
|
time.seconds,
|
|
time.nanoseconds,
|
|
$float.round($duration.to_seconds(offset)),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 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`.
|
|
*/
|
|
export function parse_rfc3339(input) {
|
|
let bytes = $bit_array.from_string(input);
|
|
return $result.try$(
|
|
parse_year(bytes),
|
|
(_use0) => {
|
|
let year;
|
|
let bytes$1;
|
|
year = _use0[0];
|
|
bytes$1 = _use0[1];
|
|
return $result.try$(
|
|
accept_byte(bytes$1, byte_minus),
|
|
(bytes) => {
|
|
return $result.try$(
|
|
parse_month(bytes),
|
|
(_use0) => {
|
|
let month;
|
|
let bytes$1;
|
|
month = _use0[0];
|
|
bytes$1 = _use0[1];
|
|
return $result.try$(
|
|
accept_byte(bytes$1, byte_minus),
|
|
(bytes) => {
|
|
return $result.try$(
|
|
parse_day(bytes, year, month),
|
|
(_use0) => {
|
|
let day;
|
|
let bytes$1;
|
|
day = _use0[0];
|
|
bytes$1 = _use0[1];
|
|
return $result.try$(
|
|
accept_date_time_separator(bytes$1),
|
|
(bytes) => {
|
|
return $result.try$(
|
|
parse_hours(bytes),
|
|
(_use0) => {
|
|
let hours;
|
|
let bytes$1;
|
|
hours = _use0[0];
|
|
bytes$1 = _use0[1];
|
|
return $result.try$(
|
|
accept_byte(bytes$1, byte_colon),
|
|
(bytes) => {
|
|
return $result.try$(
|
|
parse_minutes(bytes),
|
|
(_use0) => {
|
|
let minutes;
|
|
let bytes$1;
|
|
minutes = _use0[0];
|
|
bytes$1 = _use0[1];
|
|
return $result.try$(
|
|
accept_byte(bytes$1, byte_colon),
|
|
(bytes) => {
|
|
return $result.try$(
|
|
parse_seconds(bytes),
|
|
(_use0) => {
|
|
let seconds;
|
|
let bytes$1;
|
|
seconds = _use0[0];
|
|
bytes$1 = _use0[1];
|
|
return $result.try$(
|
|
parse_second_fraction_as_nanoseconds(
|
|
bytes$1,
|
|
),
|
|
(_use0) => {
|
|
let second_fraction_as_nanoseconds;
|
|
let bytes$2;
|
|
second_fraction_as_nanoseconds = _use0[0];
|
|
bytes$2 = _use0[1];
|
|
return $result.try$(
|
|
parse_offset(bytes$2),
|
|
(_use0) => {
|
|
let offset_seconds;
|
|
let bytes$3;
|
|
offset_seconds = _use0[0];
|
|
bytes$3 = _use0[1];
|
|
return $result.try$(
|
|
accept_empty(bytes$3),
|
|
(_use0) => {
|
|
|
|
return new Ok(
|
|
from_date_time(
|
|
year,
|
|
month,
|
|
day,
|
|
hours,
|
|
minutes,
|
|
seconds,
|
|
second_fraction_as_nanoseconds,
|
|
offset_seconds,
|
|
),
|
|
);
|
|
},
|
|
);
|
|
},
|
|
);
|
|
},
|
|
);
|
|
},
|
|
);
|
|
},
|
|
);
|
|
},
|
|
);
|
|
},
|
|
);
|
|
},
|
|
);
|
|
},
|
|
);
|
|
},
|
|
);
|
|
},
|
|
);
|
|
},
|
|
);
|
|
},
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
/**
|
|
* The epoch of Unix time, which is 00:00:00 UTC on 1 January 1970.
|
|
*/
|
|
export const unix_epoch = /* @__PURE__ */ new Timestamp(0, 0);
|