Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 50 additions & 20 deletions src/builtins/core/duration/normalized.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@ use num_traits::AsPrimitive;
use timezone_provider::epoch_nanoseconds::EpochNanoseconds;

use crate::{
builtins::core::{time_zone::TimeZone, PlainDate, PlainDateTime},
builtins::{
core::{time_zone::TimeZone, PlainDate, PlainDateTime},
duration::TWO_POWER_FIFTY_THREE,
},
iso::{IsoDate, IsoDateTime},
options::{
Disambiguation, Overflow, ResolvedRoundingOptions, RoundingIncrement, RoundingMode, Unit,
UNIT_VALUE_TABLE,
},
primitive::FiniteF64,
primitive::{DoubleDouble, FiniteF64},
provider::TimeZoneProvider,
rounding::IncrementRounder,
Calendar, TemporalError, TemporalResult, TemporalUnwrap, NS_PER_DAY, NS_PER_DAY_NONZERO,
Expand All @@ -21,6 +24,7 @@ use crate::{
use super::{DateDuration, Duration, Sign};

const MAX_TIME_DURATION: i128 = 9_007_199_254_740_991_999_999_999;
const MAX_SAFE_INTEGER: i128 = TWO_POWER_FIFTY_THREE - 1;

// Nanoseconds constants

Expand Down Expand Up @@ -171,7 +175,7 @@ impl TimeDuration {
// 2. NOTE: The following step cannot be implemented directly using floating-point arithmetic when 𝔽(timeDuration) is not a safe integer.
// The division can be implemented in C++ with the __float128 type if the compiler supports it, or with software emulation such as in the SoftFP library.
// 3. Return timeDuration / divisor.
DurationTotal::new(time_duration, unit_nanoseconds.get() as u64).to_fractional_total()
Ok(Fraction::new(time_duration, unit_nanoseconds.get() as f64).to_finite_f64())
}

pub(crate) fn round_to_fractional_days(
Expand Down Expand Up @@ -229,29 +233,55 @@ impl Add<Self> for TimeDuration {
}
}

// Struct to handle division steps in `TotalTimeDuration`
struct DurationTotal {
quotient: i128,
remainder: i128,
unit_nanoseconds: u64,
// Struct to fractional division steps in `TotalTimeDuration`
struct Fraction {
numerator: i128,
denominator: f64,
}

impl DurationTotal {
pub fn new(time_duration: i128, unit_nanoseconds: u64) -> Self {
let quotient = time_duration.div_euclid(unit_nanoseconds as i128);
let remainder = time_duration.rem_euclid(unit_nanoseconds as i128);

impl Fraction {
pub fn new(numerator: i128, denominator: f64) -> Self {
Self {
quotient,
remainder,
unit_nanoseconds,
numerator,
denominator,
}
}

// NOTE: Functionally similar to SM and JSC's `fractionToDouble`
//
// For more information on this implementation, see the
// JavaScriptCore's [fractionToDouble.cpp](https://github.com/WebKit/WebKit/blob/main/Source/JavaScriptCore/runtime/FractionToDouble.cpp)
// or SpiderMonkey's [FractionToDouble](https://github.com/mozilla-firefox/firefox/blob/main/js/src/builtin/temporal/Temporal.cpp#L683)
//
// The JavaScriptCore implementation is recommended as it has fairly robust documentation
// on the underlying papers that have informed this approach.
/// Calculate an `f64` from a numerator and denominator that may
/// be beyond the max safe integer range.
pub(crate) fn to_finite_f64(&self) -> FiniteF64 {
if self.denominator == 1. {
return FiniteF64(self.numerator as f64); // This operation is lossy.
}
if self.numerator.abs() < MAX_SAFE_INTEGER {
return FiniteF64(self.numerator as f64 / self.denominator);
}
self.to_finite_f64_slow()
}

pub(crate) fn to_fractional_total(&self) -> TemporalResult<FiniteF64> {
let fractional = FiniteF64::try_from(self.remainder)?
.checked_div(&FiniteF64::try_from(self.unit_nanoseconds)?)?;
FiniteF64::try_from(self.quotient)?.checked_add(&fractional)
/// The slow path for calculating a `f64` beyond the max safe integer range.
#[cold]
pub(crate) fn to_finite_f64_slow(&self) -> FiniteF64 {
let dd_numerator = DoubleDouble::from(self.numerator);
// First approx quotient
let q0 = dd_numerator.hi / self.denominator;
// Find remainder
let product = DoubleDouble::mul(q0, self.denominator);
let remainder = DoubleDouble::sum(dd_numerator.hi, -product.hi);

// Find second approx. quotient
let error = remainder.lo + dd_numerator.lo - product.lo;
let q1 = (remainder.hi + error) / self.denominator;

FiniteF64(q0 + q1)
}
}

Expand Down
29 changes: 20 additions & 9 deletions src/builtins/core/duration/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,10 +159,9 @@ fn negative_fields_to_string() {

#[test]
fn preserve_precision_loss() {
const MAX_SAFE_INT: i64 = 9_007_199_254_740_991;
let duration = Duration::from_partial_duration(PartialDuration {
milliseconds: Some(MAX_SAFE_INT),
microseconds: Some(MAX_SAFE_INT as i128),
milliseconds: Some(MAX_SAFE_INTEGER),
microseconds: Some(MAX_SAFE_INTEGER as i128),
..Default::default()
})
.unwrap();
Expand Down Expand Up @@ -198,8 +197,6 @@ fn duration_from_str() {

#[test]
fn duration_max_safe() {
const MAX_SAFE_INTEGER: i64 = 9007199254740991;

// From test262 built-ins/Temporal/Duration/prototype/subtract/result-out-of-range-3.js
assert!(Duration::new(0, 0, 0, 0, 0, 0, 0, 0, 9_007_199_254_740_991_926_258, 0).is_err());

Expand Down Expand Up @@ -405,11 +402,11 @@ fn test_duration_compare() {
}
}

const MAX_SAFE_INT: i64 = 9_007_199_254_740_991;
const MAX_SAFE_INTEGER: i64 = 9_007_199_254_740_991;

#[test]
fn duration_round_out_of_range_norm_conversion() {
let duration = Duration::new(0, 0, 0, 0, 0, 0, MAX_SAFE_INT, 0, 0, 999_999_999).unwrap();
let duration = Duration::new(0, 0, 0, 0, 0, 0, MAX_SAFE_INTEGER, 0, 0, 999_999_999).unwrap();
let err = duration.round_with_provider(
RoundingOptions {
largest_unit: Some(Unit::Nanosecond),
Expand All @@ -426,8 +423,8 @@ fn duration_round_out_of_range_norm_conversion() {
#[cfg_attr(not(feature = "float64_representable_durations"), should_panic)]
fn duration_float64_representable() {
// built-ins/Temporal/Duration/prototype/add/float64-representable-integer
let duration = Duration::new(0, 0, 0, 0, 0, 0, 0, 0, MAX_SAFE_INT as i128, 0).unwrap();
let duration2 = Duration::new(0, 0, 0, 0, 0, 0, 0, 0, MAX_SAFE_INT as i128 - 1, 0).unwrap();
let duration = Duration::new(0, 0, 0, 0, 0, 0, 0, 0, MAX_SAFE_INTEGER as i128, 0).unwrap();
let duration2 = Duration::new(0, 0, 0, 0, 0, 0, 0, 0, MAX_SAFE_INTEGER as i128 - 1, 0).unwrap();
let added = duration.add(&duration2).unwrap();
assert_eq!(added.microseconds, 18014398509481980);
assert_eq!(
Expand All @@ -441,3 +438,17 @@ fn duration_float64_representable() {
"Should not internally use a more accurate representation when adding"
);
}

#[test]
#[cfg(feature = "compiled_data")]
fn total_full_numeric_precision() {
// Tests that Duration::total operates without any loss of precision

// built-ins/Temporal/Duration/prototype/total/precision-exact-mathematical-values-6
let d = Duration::new(0, 0, 0, 0, 816, 0, 0, 0, 0, 2_049_187_497_660).unwrap();
assert_eq!(d.total(Unit::Hour, None).unwrap(), 816.56921874935);

// built-ins/Temporal/Duration/prototype/total/precision-exact-mathematical-values-7
let d = Duration::new(0, 0, 0, 0, 0, 0, 0, MAX_SAFE_INTEGER + 1, 1999, 0).unwrap();
assert_eq!(d.total(Unit::Millisecond, None).unwrap(), 9007199254740994.);
}
44 changes: 44 additions & 0 deletions src/primitive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,50 @@ impl Ord for FiniteF64 {
}
}

/// An intermediate primitive type for calculating
/// double64 results.
#[derive(Debug, Clone, Copy)]
pub(crate) struct DoubleDouble {
pub(crate) hi: f64,
pub(crate) lo: f64,
}

impl DoubleDouble {
/// Creates a `DoubleDouble` from the product of two `f64` values.
pub(crate) fn mul(a: f64, b: f64) -> Self {
// Mul
let product = a * b;
let error = core_maths::CoreFloat::mul_add(a, b, -product);
Self {
hi: product,
lo: error,
}
}

/// Creates a `DoubleDouble` from the sum of two `f64` values.
pub(crate) fn sum(one: f64, two: f64) -> Self {
// Sum
let sum = one + two;

// Calculate error
let calc_one = sum - one;
let calc_two = sum - two;
let two_roundoff = two - calc_one;
let one_roundoff = one - calc_two;
let error = one_roundoff + two_roundoff;

Self { hi: sum, lo: error }
}
}

impl From<i128> for DoubleDouble {
fn from(value: i128) -> Self {
let hi = value as f64;
let lo = (value - hi as i128) as f64;
Self { hi, lo }
}
}

#[cfg(test)]
mod tests {
use super::FiniteF64;
Expand Down