///|
pub(all) enum DurationUnit {
Nanoseconds
Microseconds
Milliseconds
Seconds
Minutes
Hours
Days
} derive(Eq, Debug)
///|
pub(all) struct ConfigPeriod {
years : Int
months : Int
days : Int
} derive(Eq, Debug, ToJson)
///|
pub(all) enum TemporalAmount {
Duration(Int64)
Period(ConfigPeriod)
} derive(Eq, Debug)
///|
pub impl ToJson for TemporalAmount with fn to_json(self) {
match self {
Duration(n) =>
Json::object({ "kind": "duration", "nanoseconds": n.to_string() })
Period(p) =>
Json::object({
"kind": "period",
"years": p.years.to_json(),
"months": p.months.to_json(),
"days": p.days.to_json(),
})
}
}
///|
fn duration_unit_nanos(unit : DurationUnit) -> Int64 {
match unit {
Nanoseconds => 1L
Microseconds => 1000L
Milliseconds => 1000000L
Seconds => 1000000000L
Minutes => 60000000000L
Hours => 3600000000000L
Days => 86400000000000L
}
}
///|
fn period_text(text : String) -> ConfigPeriod raise ParseError {
if text.length() > 10000 {
raise Invalid("period length limit")
}
let text = trim_space(text)
let mut end = text.length()
// Every accepted unit is ASCII. Leaving an unsupported non-ASCII suffix in
// the number still rejects it, while preserving BMP decimal digits.
while end > 0 &&
(
(text[end - 1] >= 65 && text[end - 1] <= 90) ||
(text[end - 1] >= 97 && text[end - 1] <= 122)
) {
end -= 1
}
let spelling = trim_space(text[:end].to_owned())
let unit = text[end:].to_owned()
let number = java_integer_value(spelling).unwrap_or_else(() => {
raise Invalid("invalid period integer")
})
if number < -2147483648L || number > 2147483647L {
raise Invalid("period integer out of range")
}
let n = number.to_int()
match unit {
"" | "d" | "day" | "days" => { years: 0, months: 0, days: n, }
"w" | "week" | "weeks" => {
let days = number * 7L
if days < -2147483648L || days > 2147483647L {
raise Invalid("period weeks out of range")
}
{ years: 0, months: 0, days: days.to_int(), }
}
"m" | "mo" | "month" | "months" => { years: 0, months: n, days: 0, }
"y" | "year" | "years" => { years: n, months: 0, days: 0, }
_ => raise Invalid("unknown period unit: " + unit)
}
}
///|
/// Calendar period; unitless values are days and months remain unnormalized.
pub fn get_period(
config : Value,
key : String,
) -> ConfigPeriod raise ParseError {
period_text(get_as_string(config, key))
}
///|
/// Duration takes precedence: unitless values are milliseconds and m is minutes.
pub fn get_temporal(
config : Value,
key : String,
) -> TemporalAmount raise ParseError {
let text = get_as_string(config, key)
Duration(duration_text(text)) catch {
_ => Period(period_text(text))
}
}
///|
pub fn get_duration_in(
config : Value,
key : String,
unit : DurationUnit,
) -> Int64 raise ParseError {
get_duration(config, key) / duration_unit_nanos(unit)
}
///|
pub fn get_duration_list_in(
config : Value,
key : String,
unit : DurationUnit,
) -> Array[Int64] raise ParseError {
let scale = duration_unit_nanos(unit)
get_as_list(config, key).map(value => {
match value {
Number(text) => {
let millis = long_value(text)
// Numeric list elements convert directly from milliseconds. Converting
// through saturated nanoseconds first loses large millisecond values.
if scale >= 1000000L {
millis / (scale / 1000000L)
} else {
scale_time(millis, 1000000L / scale)
}
}
Text(text) | Bare(text) => duration_text(text) / scale
_ => raise Invalid("expected duration-compatible list element")
}
})
}
///|
pub fn get_milliseconds(config : Value, key : String) -> Int64 raise ParseError {
get_duration_in(config, key, Milliseconds)
}
///|
pub fn get_nanoseconds(config : Value, key : String) -> Int64 raise ParseError {
get_duration(config, key)
}
///|
pub fn get_milliseconds_list(
config : Value,
key : String,
) -> Array[Int64] raise ParseError {
get_duration_list_in(config, key, Milliseconds)
}
///|
pub fn get_nanoseconds_list(
config : Value,
key : String,
) -> Array[Int64] raise ParseError {
get_duration_list_in(config, key, Nanoseconds)
}