Skip to content

Commit

Permalink
Implement Datetime round method
Browse files Browse the repository at this point in the history
  • Loading branch information
nekevss committed Jul 21, 2024
1 parent d43b706 commit 908acb0
Show file tree
Hide file tree
Showing 5 changed files with 216 additions and 116 deletions.
61 changes: 59 additions & 2 deletions src/components/datetime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use crate::{
iso::{IsoDate, IsoDateSlots, IsoDateTime, IsoTime},
options::{
ArithmeticOverflow, DifferenceOperation, DifferenceSettings, ResolvedRoundingOptions,
TemporalUnit,
RoundingOptions, TemporalUnit,
},
parsers::parse_date_time,
temporal_assert, Sign, TemporalError, TemporalResult, TemporalUnwrap,
Expand Down Expand Up @@ -463,6 +463,19 @@ impl DateTime {
pub fn since(&self, other: &Self, settings: DifferenceSettings) -> TemporalResult<Duration> {
self.diff(DifferenceOperation::Since, other, settings)
}

/// Rounds the current datetime based on provided options.
pub fn round(&self, options: RoundingOptions) -> TemporalResult<Self> {
let resolved = ResolvedRoundingOptions::from_dt_options(options)?;

if resolved.is_noop() {
return Ok(self.clone());
}

let result = self.iso.round(resolved)?;

Ok(Self::new_unchecked(result, self.calendar.clone()))
}
}

// ==== Trait impls ====
Expand Down Expand Up @@ -530,7 +543,7 @@ mod tests {
use crate::{
components::{calendar::Calendar, duration::DateDuration, Duration},
iso::{IsoDate, IsoTime},
options::{DifferenceSettings, RoundingIncrement, TemporalRoundingMode, TemporalUnit},
options::{DifferenceSettings, RoundingIncrement, RoundingOptions, TemporalRoundingMode, TemporalUnit},
primitive::FiniteF64,
};

Expand Down Expand Up @@ -716,4 +729,48 @@ mod tests {
assert_eq!(result.hours(), 4.0);
assert_eq!(result.minutes(), 30.0);
}

#[test]
fn dt_round_basic() {
let assert_datetime = |dt: DateTime, expected: (i32, u8, u8, u8, u8, u8, u16, u16, u16)| {
assert_eq!(dt.iso_year(), expected.0);
assert_eq!(dt.iso_month(), expected.1);
assert_eq!(dt.iso_day(), expected.2);
assert_eq!(dt.hour(), expected.3);
assert_eq!(dt.minute(), expected.4);
assert_eq!(dt.second(), expected.5);
assert_eq!(dt.millisecond(), expected.6);
assert_eq!(dt.microsecond(), expected.7);
assert_eq!(dt.nanosecond(), expected.8);
};

let gen_rounding_options = | smallest: TemporalUnit, increment: u32 | -> RoundingOptions {
RoundingOptions {
largest_unit: None,
smallest_unit: Some(smallest),
increment: Some(RoundingIncrement::try_new(increment).unwrap()),
rounding_mode: None,
}

};
let dt = DateTime::new(1976, 11, 18, 14, 23, 30, 123, 456, 789, Calendar::default()).unwrap();

let result = dt.round(gen_rounding_options(TemporalUnit::Hour, 4)).unwrap();
assert_datetime(result, (1976, 11, 18, 16, 0, 0, 0, 0, 0));

let result = dt.round(gen_rounding_options(TemporalUnit::Minute, 15)).unwrap();
assert_datetime(result, (1976, 11, 18, 14, 30, 0, 0, 0, 0));

let result = dt.round(gen_rounding_options(TemporalUnit::Second, 30)).unwrap();
assert_datetime(result, (1976, 11, 18, 14, 23, 30, 0, 0, 0));

let result = dt.round(gen_rounding_options(TemporalUnit::Millisecond, 10)).unwrap();
assert_datetime(result, (1976, 11, 18, 14, 23, 30, 120, 0, 0));

let result = dt.round(gen_rounding_options(TemporalUnit::Microsecond, 10)).unwrap();
assert_datetime(result, (1976, 11, 18, 14, 23, 30, 123, 460, 0));

let result = dt.round(gen_rounding_options(TemporalUnit::Nanosecond, 10)).unwrap();
assert_datetime(result, (1976, 11, 18, 14, 23, 30, 123, 456, 790));
}
}
2 changes: 1 addition & 1 deletion src/components/duration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -429,7 +429,7 @@ impl Duration {
// 25. If maximum is not undefined, perform ? ValidateTemporalRoundingIncrement(roundingIncrement, maximum, false).
let existing_largest_unit = self.default_largest_unit();
let resolved_options =
ResolvedRoundingOptions::from_options(options, existing_largest_unit)?;
ResolvedRoundingOptions::from_duration_options(options, existing_largest_unit)?;

// 26. Let hoursToDaysConversionMayOccur be false.
// 27. If duration.[[Days]] ≠ 0 and zonedRelativeTo is not undefined, set hoursToDaysConversionMayOccur to true.
Expand Down
11 changes: 9 additions & 2 deletions src/components/time.rs
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ impl Time {
rounding_mode: Option<TemporalRoundingMode>,
) -> TemporalResult<Self> {
let increment = RoundingIncrement::try_from(rounding_increment.unwrap_or(1.0))?;
let mode = rounding_mode.unwrap_or(TemporalRoundingMode::HalfExpand);
let rounding_mode = rounding_mode.unwrap_or(TemporalRoundingMode::HalfExpand);

let max = smallest_unit
.to_maximum_rounding_increment()
Expand All @@ -266,7 +266,14 @@ impl Time {
// Safety (nekevss): to_rounding_increment returns a value in the range of a u32.
increment.validate(u64::from(max), false)?;

let (_, result) = self.iso.round(increment, smallest_unit, mode, None)?;
let resolved = ResolvedRoundingOptions {
largest_unit: TemporalUnit::Auto,
increment,
smallest_unit,
rounding_mode,
};

let (_, result) = self.iso.round(resolved)?;

Ok(Self::new_unchecked(result))
}
Expand Down
232 changes: 122 additions & 110 deletions src/iso.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ use crate::{
Date, Duration,
},
error::TemporalError,
options::{ArithmeticOverflow, RoundingIncrement, TemporalRoundingMode, TemporalUnit},
options::{ArithmeticOverflow, ResolvedRoundingOptions, TemporalUnit},
primitive::FiniteF64,
rounding::{IncrementRounder, Round},
temporal_assert, utils, TemporalResult, TemporalUnwrap, NS_PER_DAY,
Expand Down Expand Up @@ -173,6 +173,12 @@ impl IsoDateTime {
Ok(Self::new_unchecked(added_date.iso, t_result.1))
}

pub(crate) fn round(&self, resolved_options: ResolvedRoundingOptions) -> TemporalResult<Self> {
let (rounded_days, rounded_time) = self.time.round(resolved_options)?;
let balance_result = IsoDate::balance(self.date.year, self.date.month.into(), i32::from(self.date.day) + rounded_days);
Self::new(balance_result, rounded_time)
}

// TODO: Determine whether to provide an options object...seems duplicative.
/// 5.5.11 DifferenceISODateTime ( y1, mon1, d1, h1, min1, s1, ms1, mus1, ns1, y2, mon2, d2, h2, min2, s2, ms2, mus2, ns2, calendarRec, largestUnit, options )
pub(crate) fn diff(
Expand Down Expand Up @@ -665,140 +671,146 @@ impl IsoTime {
/// Rounds the current `IsoTime` according to the provided settings.
pub(crate) fn round(
&self,
increment: RoundingIncrement,
unit: TemporalUnit,
mode: TemporalRoundingMode,
day_length_ns: Option<u64>,
resolved_options: ResolvedRoundingOptions,
) -> TemporalResult<(i32, Self)> {
// 1. Let fractionalSecond be nanosecond × 10-9 + microsecond × 10-6 + millisecond × 10-3 + second.

let quantity = match unit {
// 2. If unit is "day", then
// a. If dayLengthNs is not present, set dayLengthNs to nsPerDay.
// b. Let quantity be (((((hour × 60 + minute) × 60 + second) × 1000 + millisecond) × 1000 + microsecond) × 1000 + nanosecond) / dayLengthNs.
// 3. Else if unit is "hour", then
// a. Let quantity be (fractionalSecond / 60 + minute) / 60 + hour.
TemporalUnit::Hour | TemporalUnit::Day => {
u64::from(self.nanosecond)
+ u64::from(self.microsecond) * 1_000
+ u64::from(self.millisecond) * 1_000_000
+ u64::from(self.second) * 1_000_000_000
+ u64::from(self.minute) * 60 * 1_000_000_000
+ u64::from(self.hour) * 60 * 60 * 1_000_000_000
// 1. If unit is "day" or "hour", then
let quantity = match resolved_options.smallest_unit {
TemporalUnit::Day | TemporalUnit::Hour => {
// a. Let quantity be ((((hour × 60 + minute) × 60 + second) × 1000 + millisecond)
// × 1000 + microsecond) × 1000 + nanosecond.
((((i128::from(self.hour) * 60 + i128::from(self.minute)) * 60
+ i128::from(self.second))
* 1000
+ i128::from(self.millisecond))
* 1000
+ i128::from(self.microsecond))
* 1000
+ i128::from(self.nanosecond)
}
// 4. Else if unit is "minute", then
// a. Let quantity be fractionalSecond / 60 + minute.
// 2. Else if unit is "minute", then
TemporalUnit::Minute => {
u64::from(self.nanosecond)
+ u64::from(self.microsecond) * 1_000
+ u64::from(self.millisecond) * 1_000_000
+ u64::from(self.second) * 1_000_000_000
+ u64::from(self.minute) * 60
// a. Let quantity be (((minute × 60 + second) × 1000 + millisecond) × 1000 + microsecond) × 1000 + nanosecond.
(((i128::from(self.minute) * 60 + i128::from(self.second)) * 1000
+ i128::from(self.millisecond))
* 1000
+ i128::from(self.microsecond))
* 1000
+ i128::from(self.nanosecond)
}
// 5. Else if unit is "second", then
// a. Let quantity be fractionalSecond.
// 3. Else if unit is "second", then
TemporalUnit::Second => {
u64::from(self.nanosecond)
+ u64::from(self.microsecond) * 1_000
+ u64::from(self.millisecond) * 1_000_000
+ u64::from(self.second) * 1_000_000_000
// a. Let quantity be ((second × 1000 + millisecond) × 1000 + microsecond) × 1000 + nanosecond.
((i128::from(self.second) * 1000 + i128::from(self.millisecond)) * 1000
+ i128::from(self.microsecond))
* 1000
+ i128::from(self.nanosecond)
}
// 6. Else if unit is "millisecond", then
// a. Let quantity be nanosecond × 10-6 + microsecond × 10-3 + millisecond.
// 4. Else if unit is "millisecond", then
TemporalUnit::Millisecond => {
u64::from(self.nanosecond)
+ u64::from(self.microsecond) * 1_000
+ u64::from(self.millisecond) * 1_000_000
// a. Let quantity be (millisecond × 1000 + microsecond) × 1000 + nanosecond.
(i128::from(self.millisecond) * 1000 + i128::from(self.microsecond)) * 1000
+ i128::from(self.nanosecond)
}
// 7. Else if unit is "microsecond", then
// a. Let quantity be nanosecond × 10-3 + microsecond.
// 5. Else if unit is "microsecond", then
TemporalUnit::Microsecond => {
u64::from(self.nanosecond) + 1_000 * u64::from(self.microsecond)
// a. Let quantity be microsecond × 1000 + nanosecond.
i128::from(self.microsecond) * 1000 + i128::from(self.nanosecond)
}
// 6. Else,
TemporalUnit::Nanosecond => {
// a. Assert: unit is "nanosecond".
// b. Let quantity be nanosecond.
i128::from(self.nanosecond)
}
// 8. Else,
// a. Assert: unit is "nanosecond".
// b. Let quantity be nanosecond.
TemporalUnit::Nanosecond => u64::from(self.nanosecond),
_ => {
return Err(TemporalError::range()
.with_message("Invalid temporal unit provided to Time.round."))
.with_message("Invalid smallestUNit value for time rounding."))
}
};
// 7. Let unitLength be the value in the "Length in Nanoseconds" column of the row of Table 22 whose "Singular" column contains unit.
let length = NonZeroU128::new(
resolved_options
.smallest_unit
.as_nanoseconds()
.temporal_unwrap()?
.into(),
)
.temporal_unwrap()?;

let ns_per_unit = if unit == TemporalUnit::Day {
unsafe { NonZeroU128::new_unchecked(day_length_ns.unwrap_or(NS_PER_DAY).into()) }
} else {
let nanos = unit.as_nanoseconds().temporal_unwrap()?;
unsafe { NonZeroU128::new_unchecked(nanos.into()) }
};

let increment = ns_per_unit
.checked_mul(increment.as_extended_increment())
.temporal_unwrap()?;
let increment = resolved_options
.increment
.as_extended_increment()
.checked_mul(length)
.ok_or(TemporalError::range().with_message("increment exceeded valid range."))?;

// TODO: Verify validity of cast or handle better for result.
// 9. Let result be RoundNumberToIncrement(quantity, increment, roundingMode).
// 8. Let result be RoundNumberToIncrement(quantity, increment × unitLength, roundingMode) / unitLength.
let result =
IncrementRounder::<i128>::from_potentially_negative_parts(quantity.into(), increment)?
.round(mode)
/ i128::from_u128(ns_per_unit.get()).temporal_unwrap()?;

let result = match unit {
// 10. If unit is "day", then
// a. Return the Record { [[Days]]: result, [[Hour]]: 0, [[Minute]]: 0, [[Second]]: 0, [[Millisecond]]: 0, [[Microsecond]]: 0, [[Nanosecond]]: 0 }.
TemporalUnit::Day => (result as i32, IsoTime::default()),
// 11. If unit is "hour", then
IncrementRounder::<i128>::from_potentially_negative_parts(quantity, increment)?
.round(resolved_options.rounding_mode)
/ length.get() as i128;

let result_f64 = f64::from_i128(result)
.ok_or(TemporalError::range().with_message("round result valid range."))?;

match resolved_options.smallest_unit {
// 9. If unit is "day", then
// a. Return Time Record { [[Days]]: result, [[Hour]]: 0, [[Minute]]: 0, [[Second]]: 0, [[Millisecond]]: 0, [[Microsecond]]: 0, [[Nanosecond]]: 0 }.
TemporalUnit::Day => Ok((result_f64 as i32, Self::default())),
// 10. If unit is "hour", then
// a. Return BalanceTime(result, 0, 0, 0, 0, 0).
TemporalUnit::Hour => IsoTime::balance(result as f64, 0.0, 0.0, 0.0, 0.0, 0.0),
// 12. If unit is "minute", then
// a. Return BalanceTime(hour, result, 0, 0, 0, 0).
TemporalUnit::Minute => {
IsoTime::balance(f64::from(self.hour), result as f64, 0.0, 0.0, 0.0, 0.0)
}
// 13. If unit is "second", then
// a. Return BalanceTime(hour, minute, result, 0, 0, 0).
TemporalUnit::Second => IsoTime::balance(
f64::from(self.hour),
f64::from(self.minute),
result as f64,
TemporalUnit::Hour => Ok(Self::balance(result_f64, 0.0, 0.0, 0.0, 0.0, 0.0)),
// 11. If unit is "minute", then
// a. Return BalanceTime(hour, result, 0.0, 0.0, 0.0, 0).
TemporalUnit::Minute => Ok(Self::balance(
self.hour.into(),
result_f64,
0.0,
0.0,
0.0,
),
// 14. If unit is "millisecond", then
// a. Return BalanceTime(hour, minute, second, result, 0, 0).
TemporalUnit::Millisecond => IsoTime::balance(
f64::from(self.hour),
f64::from(self.minute),
f64::from(self.second),
result as f64,
0.0,
)),
// 12. If unit is "second", then
// a. Return BalanceTime(hour, minute, result, 0.0, 0.0, 0).
TemporalUnit::Second => Ok(Self::balance(
self.hour.into(),
self.minute.into(),
result_f64,
0.0,
),
// 15. If unit is "microsecond", then
0.0,
0.0,
)),
// 13. If unit is "millisecond", then
// a. Return BalanceTime(hour, minute, second, result, 0.0, 0).
TemporalUnit::Millisecond => Ok(Self::balance(
self.hour.into(),
self.minute.into(),
self.second.into(),
result_f64,
0.0,
0.0,
)),
// 14. If unit is "microsecond", then
// a. Return BalanceTime(hour, minute, second, millisecond, result, 0).
TemporalUnit::Microsecond => IsoTime::balance(
f64::from(self.hour),
f64::from(self.minute),
f64::from(self.second),
f64::from(self.millisecond),
result as f64,
TemporalUnit::Microsecond => Ok(Self::balance(
self.hour.into(),
self.minute.into(),
self.second.into(),
self.millisecond.into(),
result_f64,
0.0,
),
// 16. Assert: unit is "nanosecond".
// 17. Return BalanceTime(hour, minute, second, millisecond, microsecond, result).
TemporalUnit::Nanosecond => IsoTime::balance(
f64::from(self.hour),
f64::from(self.minute),
f64::from(self.second),
f64::from(self.millisecond),
f64::from(self.microsecond),
result as f64,
),
_ => unreachable!("Error is thrown in previous match."),
};

Ok(result)
)),
// 15. Assert: unit is "nanosecond".
// 16. Return BalanceTime(hour, minute, second, millisecond, microsecond, result).
TemporalUnit::Nanosecond => Ok(Self::balance(
self.hour.into(),
self.minute.into(),
self.second.into(),
self.millisecond.into(),
self.microsecond.into(),
result_f64,
)),
_ => Err(TemporalError::assert()),
}
}

/// Checks if the time is a valid `IsoTime`
Expand Down
Loading

0 comments on commit 908acb0

Please sign in to comment.