///|
/// The date portion of a duration: years, months, weeks and days.
///
/// Unlike a time duration these components cannot be normalized against each
/// other, because the length of a year or a month depends on where in the
/// calendar it starts.
///
/// Spec:
pub struct DateDuration {
/// Whole years.
years : Int64
/// Whole months.
months : Int64
/// Whole weeks.
weeks : Int64
/// Whole days.
days : Int64
} derive(Eq, Debug, Default)
///|
/// Creates a date duration without validating it.
fn DateDuration::new_unchecked(
years : Int64,
months : Int64,
weeks : Int64,
days : Int64,
) -> DateDuration {
{ years, months, weeks, days }
}
///|
/// `CreateDateDurationRecord`: creates a validated date duration.
///
/// All non-zero fields must share a sign, and each must stay inside the range
/// a `Duration` permits.
///
/// ```mbt check
/// test {
/// let d = @temporal.DateDuration::new(1, 2, 3, 4)
/// inspect(d.years(), content="1")
/// inspect(d.days(), content="4")
/// }
/// ```
pub fn DateDuration::new(
years : Int64,
months : Int64,
weeks : Int64,
days : Int64,
) -> DateDuration raise TemporalError {
if !is_valid_duration(
years, months, weeks, days, 0L, 0L, 0L, 0L, @int128.zero, @int128.zero,
) {
raise RangeError("invalid DateDuration")
}
{ years, months, weeks, days }
}
///|
/// Returns the years component.
pub fn DateDuration::years(self : DateDuration) -> Int64 {
self.years
}
///|
/// Returns the months component.
pub fn DateDuration::months(self : DateDuration) -> Int64 {
self.months
}
///|
/// Returns the weeks component.
pub fn DateDuration::weeks(self : DateDuration) -> Int64 {
self.weeks
}
///|
/// Returns the days component.
pub fn DateDuration::days(self : DateDuration) -> Int64 {
self.days
}
///|
/// Returns the duration with every component negated.
pub fn DateDuration::negated(self : DateDuration) -> DateDuration {
{
years: -self.years,
months: -self.months,
weeks: -self.weeks,
days: -self.days,
}
}
///|
/// Returns the duration with every component made non-negative.
pub fn DateDuration::abs(self : DateDuration) -> DateDuration {
{
years: self.years.abs(),
months: self.months.abs(),
weeks: self.weeks.abs(),
days: self.days.abs(),
}
}
///|
/// `DateDurationSign`: the sign shared by every non-zero component.
pub fn DateDuration::sign(self : DateDuration) -> Sign {
duration_sign([self.years, self.months, self.weeks, self.days])
}
///|
/// `AdjustDateDurationRecord`: returns a copy with `days` replaced, and
/// optionally `weeks` and `months` too.
fn DateDuration::adjust(
self : DateDuration,
days : Int64,
weeks? : Int64,
months? : Int64,
) -> DateDuration {
{
years: self.years,
months: months.unwrap_or(self.months),
weeks: weeks.unwrap_or(self.weeks),
days,
}
}
///|
/// `DurationSign`: returns the sign of the first non-zero value, or zero.
fn duration_sign(values : Array[Int64]) -> Sign {
for v in values {
if v < 0L {
return Negative
}
if v > 0L {
return Positive
}
}
Zero
}
///|
/// `IsValidDuration`: checks that the components agree in sign and that the
/// duration's total magnitude stays representable.
///
/// Spec:
fn is_valid_duration(
years : Int64,
months : Int64,
weeks : Int64,
days : Int64,
hours : Int64,
minutes : Int64,
seconds : Int64,
milliseconds : Int64,
microseconds : @int128.Int128,
nanoseconds : @int128.Int128,
) -> Bool {
let signums = [
years,
months,
weeks,
days,
hours,
minutes,
seconds,
milliseconds,
microseconds.signum().to_int64(),
nanoseconds.signum().to_int64(),
]
let sign = duration_sign(signums)
for v in signums {
if v < 0L && sign == Positive {
return false
}
if v > 0L && sign == Negative {
return false
}
}
// Years, months and weeks are stored in 32 bits by the reference
// implementation, so their magnitude is bounded independently.
let u32_max = 4_294_967_295L
if years.abs() > u32_max || months.abs() > u32_max || weeks.abs() > u32_max {
return false
}
// The spec defines the magnitude check over ECMAScript numbers, so the
// sub-minute components are first snapped to the nearest representable
// `Double`. Without this, a value one nanosecond under the limit would be
// accepted here but rejected by an engine.
// See .
let seconds = round_trip_through_double_i64(seconds)
let milliseconds = round_trip_through_double_i64(milliseconds)
let microseconds = round_trip_through_double(microseconds)
let nanoseconds = round_trip_through_double(nanoseconds)
// The whole duration, expressed in nanoseconds, must stay below 2^53
// seconds, the point past which a `Double` count of seconds loses integer
// precision.
let whole_ns = @int128.of_int64(days)
.mul(i128_ns_per_day)
.add(@int128.of_int64(hours).mul(@int128.of_int64(NS_PER_HOUR)))
.add(@int128.of_int64(minutes).mul(@int128.of_int64(NS_PER_MINUTE)))
.add(@int128.of_int64(seconds).mul(i128_billion))
let sub_ns = @int128.of_int64(milliseconds)
.mul(i128_million)
.add(microseconds.mul(i128_thousand))
.add(nanoseconds)
let total = whole_ns.add(sub_ns)
total.abs().compare(max_safe_ns_precision) < 0
}
///|
/// Rounds a value to the nearest `Double` and back, saturating if the result
/// leaves the 128-bit range.
fn round_trip_through_double(v : @int128.Int128) -> @int128.Int128 {
match @int128.of_double(v.to_double()) {
Some(rounded) => rounded
None => if v.is_negative() { @int128.min_value } else { @int128.max_value }
}
}
///|
/// Rounds a value to the nearest `Double` and back, saturating at the `Int64`
/// bounds.
fn round_trip_through_double_i64(v : Int64) -> Int64 {
round_trip_through_double(@int128.of_int64(v)).to_int64_saturating()
}
///|
/// `2^53` seconds expressed in nanoseconds: the bound on a valid duration.
let max_safe_ns_precision : @int128.Int128 = @int128.of_int64(
TWO_POWER_FIFTY_THREE,
).mul(i128_billion)