diff --git a/core/temporal/src/components/duration.rs b/core/temporal/src/components/duration.rs index c6aff0fbc8..74193c5b2c 100644 --- a/core/temporal/src/components/duration.rs +++ b/core/temporal/src/components/duration.rs @@ -61,11 +61,6 @@ impl Duration { pub(crate) fn one_week(week_value: f64) -> Self { Self::from_date_duration(DateDuration::new_unchecked(0f64, 0f64, week_value, 0f64)) } - - /// Utility that checks whether `Duration`'s `DateDuration` is empty. - pub(crate) fn is_time_duration(&self) -> bool { - self.date().iter().any(|x| x != 0f64) - } } // ==== Public Duration API ==== @@ -137,6 +132,20 @@ impl Duration { pub fn is_time_within_range(&self) -> bool { self.time.is_within_range() } + + /// Returns whether `Duration`'s `DateDuration` isn't empty and is therefore a `DateDuration` or `Duration`. + #[inline] + #[must_use] + pub fn is_date_duration(&self) -> bool { + self.date().iter().any(|x| x != 0.0) && self.time().iter().all(|x| x == 0.0) + } + + /// Returns whether `Duration`'s `DateDuration` is empty and is therefore a `TimeDuration`. + #[inline] + #[must_use] + pub fn is_time_duration(&self) -> bool { + self.time().iter().any(|x| x != 0.0) && self.date().iter().all(|x| x == 0.0) + } } // ==== Public `Duration` Getters/Setters ==== diff --git a/core/temporal/src/components/time.rs b/core/temporal/src/components/time.rs index c6450e61b8..a1e4d70298 100644 --- a/core/temporal/src/components/time.rs +++ b/core/temporal/src/components/time.rs @@ -1,10 +1,14 @@ //! This module implements `Time` and any directly related algorithms. -use crate::{iso::IsoTime, options::ArithmeticOverflow, TemporalResult}; +use crate::{ + components::{duration::TimeDuration, Duration}, + iso::IsoTime, + options::{ArithmeticOverflow, TemporalRoundingMode, TemporalUnit}, + utils, TemporalError, TemporalResult, +}; /// The native Rust implementation of `Temporal.PlainTime`. -#[derive(Debug, Default, Clone, Copy)] -#[allow(dead_code)] +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub struct Time { iso: IsoTime, } @@ -23,8 +27,28 @@ impl Time { pub(crate) fn is_valid(&self) -> bool { self.iso.is_valid() } + + /// Adds a `TimeDuration` to the current `Time`. + /// + /// Spec Equivalent: `AddDurationToOrSubtractDurationFromPlainTime` AND `AddTime`. + pub(crate) fn add_to_time(&self, duration: &TimeDuration) -> Self { + let (_, result) = IsoTime::balance( + f64::from(self.hour()) + duration.hours(), + f64::from(self.minute()) + duration.minutes(), + f64::from(self.second()) + duration.seconds(), + f64::from(self.millisecond()) + duration.milliseconds(), + f64::from(self.microsecond()) + duration.microseconds(), + f64::from(self.nanosecond()) + duration.nanoseconds(), + ); + + // NOTE (nekevss): IsoTime::balance should never return an invalid `IsoTime` + + Self::new_unchecked(result) + } } +// ==== Public API ==== + impl Time { /// Creates a new `IsoTime` value. pub fn new( @@ -47,4 +71,204 @@ impl Time { )?; Ok(Self::new_unchecked(time)) } + + /// Returns the internal `hour` field. + #[inline] + #[must_use] + pub const fn hour(&self) -> u8 { + self.iso.hour + } + + /// Returns the internal `minute` field. + #[inline] + #[must_use] + pub const fn minute(&self) -> u8 { + self.iso.minute + } + + /// Returns the internal `second` field. + #[inline] + #[must_use] + pub const fn second(&self) -> u8 { + self.iso.second + } + + /// Returns the internal `millisecond` field. + #[inline] + #[must_use] + pub const fn millisecond(&self) -> u16 { + self.iso.millisecond + } + + /// Returns the internal `microsecond` field. + #[inline] + #[must_use] + pub const fn microsecond(&self) -> u16 { + self.iso.microsecond + } + + /// Returns the internal `nanosecond` field. + #[inline] + #[must_use] + pub const fn nanosecond(&self) -> u16 { + self.iso.nanosecond + } + + /// Add a `Duration` to the current `Time`. + pub fn add(&self, duration: &Duration) -> TemporalResult { + if !duration.is_time_duration() { + return Err(TemporalError::range() + .with_message("DateDuration values cannot be added to `Time`.")); + } + Ok(self.add_time_duration(duration.time())) + } + + /// Adds a `TimeDuration` to the current `Time`. + #[inline] + #[must_use] + pub fn add_time_duration(&self, duration: &TimeDuration) -> Self { + self.add_to_time(duration) + } + + /// Subtract a `Duration` to the current `Time`. + pub fn subtract(&self, duration: &Duration) -> TemporalResult { + if !duration.is_time_duration() { + return Err(TemporalError::range() + .with_message("DateDuration values cannot be added to `Time` component.")); + } + Ok(self.add_time_duration(duration.time())) + } + + /// Adds a `TimeDuration` to the current `Time`. + #[inline] + #[must_use] + pub fn subtract_time_duration(&self, duration: &TimeDuration) -> Self { + self.add_to_time(&duration.neg()) + } + + // TODO (nekevss): optimize and test rounding_increment type (f64 vs. u64). + /// Rounds the current `Time` according to provided options. + pub fn round( + &self, + smallest_unit: TemporalUnit, + rounding_increment: Option, + rounding_mode: Option, + ) -> TemporalResult { + let increment = utils::to_rounding_increment(rounding_increment)?; + let mode = rounding_mode.unwrap_or(TemporalRoundingMode::HalfExpand); + + let max = smallest_unit + .to_maximum_rounding_increment() + .ok_or_else(|| { + TemporalError::range().with_message("smallestUnit must be a time value.") + })?; + + // Safety (nekevss): to_rounding_increment returns a value in the range of a u32. + utils::validate_temporal_rounding_increment(increment as u32, u64::from(max), false)?; + + let (_, result) = self.iso.round(increment, smallest_unit, mode, None)?; + + Ok(Self::new_unchecked(result)) + } +} + +// ==== Test land ==== + +#[cfg(test)] +mod tests { + use crate::{components::Duration, iso::IsoTime, options::TemporalUnit}; + + use super::Time; + + fn assert_time(result: Time, values: (u8, u8, u8, u16, u16, u16)) { + assert!(result.hour() == values.0); + assert!(result.minute() == values.1); + assert!(result.second() == values.2); + assert!(result.millisecond() == values.3); + assert!(result.microsecond() == values.4); + assert!(result.nanosecond() == values.5); + } + + #[test] + fn time_round_millisecond() { + let base = Time::new_unchecked(IsoTime::new_unchecked(3, 34, 56, 987, 654, 321)); + + let result_1 = base + .round(TemporalUnit::Millisecond, Some(1.0), None) + .unwrap(); + assert_time(result_1, (3, 34, 56, 988, 0, 0)); + + let result_2 = base + .round(TemporalUnit::Millisecond, Some(2.0), None) + .unwrap(); + assert_time(result_2, (3, 34, 56, 988, 0, 0)); + + let result_3 = base + .round(TemporalUnit::Millisecond, Some(4.0), None) + .unwrap(); + assert_time(result_3, (3, 34, 56, 988, 0, 0)); + + let result_4 = base + .round(TemporalUnit::Millisecond, Some(5.0), None) + .unwrap(); + assert_time(result_4, (3, 34, 56, 990, 0, 0)); + } + + #[test] + fn time_round_microsecond() { + let base = Time::new_unchecked(IsoTime::new_unchecked(3, 34, 56, 987, 654, 321)); + + let result_1 = base + .round(TemporalUnit::Microsecond, Some(1.0), None) + .unwrap(); + assert_time(result_1, (3, 34, 56, 987, 654, 0)); + + let result_2 = base + .round(TemporalUnit::Microsecond, Some(2.0), None) + .unwrap(); + assert_time(result_2, (3, 34, 56, 987, 654, 0)); + + let result_3 = base + .round(TemporalUnit::Microsecond, Some(4.0), None) + .unwrap(); + assert_time(result_3, (3, 34, 56, 987, 656, 0)); + + let result_4 = base + .round(TemporalUnit::Microsecond, Some(5.0), None) + .unwrap(); + assert_time(result_4, (3, 34, 56, 987, 655, 0)); + } + + #[test] + fn time_round_nanoseconds() { + let base = Time::new_unchecked(IsoTime::new_unchecked(3, 34, 56, 987, 654, 321)); + + let result_1 = base + .round(TemporalUnit::Nanosecond, Some(1.0), None) + .unwrap(); + assert_time(result_1, (3, 34, 56, 987, 654, 321)); + + let result_2 = base + .round(TemporalUnit::Nanosecond, Some(2.0), None) + .unwrap(); + assert_time(result_2, (3, 34, 56, 987, 654, 322)); + + let result_3 = base + .round(TemporalUnit::Nanosecond, Some(4.0), None) + .unwrap(); + assert_time(result_3, (3, 34, 56, 987, 654, 320)); + + let result_4 = base + .round(TemporalUnit::Nanosecond, Some(5.0), None) + .unwrap(); + assert_time(result_4, (3, 34, 56, 987, 654, 320)); + } + + #[test] + fn add_duration_basic() { + let base = Time::new_unchecked(IsoTime::new_unchecked(15, 23, 30, 123, 456, 789)); + let result = base.add(&"PT16H".parse::().unwrap()).unwrap(); + + assert_time(result, (7, 23, 30, 123, 456, 789)); + } } diff --git a/core/temporal/src/iso.rs b/core/temporal/src/iso.rs index a11d897843..9cdcd87439 100644 --- a/core/temporal/src/iso.rs +++ b/core/temporal/src/iso.rs @@ -13,8 +13,10 @@ //! An `IsoDateTime` has the internal slots of both an `IsoDate` and `IsoTime`. use crate::{ - components::duration::DateDuration, error::TemporalError, options::ArithmeticOverflow, utils, - TemporalResult, + components::duration::DateDuration, + error::TemporalError, + options::{ArithmeticOverflow, TemporalRoundingMode, TemporalUnit}, + utils, TemporalResult, NS_PER_DAY, }; use icu_calendar::{Date as IcuDate, Iso}; use num_bigint::BigInt; @@ -106,8 +108,8 @@ impl IsoDateTime { return false; }; - let max = BigInt::from(crate::NS_MAX_INSTANT + i128::from(crate::NS_PER_DAY)); - let min = BigInt::from(crate::NS_MIN_INSTANT - i128::from(crate::NS_PER_DAY)); + let max = BigInt::from(crate::NS_MAX_INSTANT + i128::from(NS_PER_DAY)); + let min = BigInt::from(crate::NS_MIN_INSTANT - i128::from(NS_PER_DAY)); min < ns && max > ns } @@ -276,7 +278,7 @@ impl IsoDate { /// An `IsoTime` record that contains `Temporal`'s /// time slots. -#[derive(Debug, Default, Clone, Copy)] +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub struct IsoTime { pub(crate) hour: u8, // 0..=23 pub(crate) minute: u8, // 0..=59 @@ -387,42 +389,182 @@ impl IsoTime { nanosecond: f64, ) -> (i32, Self) { // 1. Set microsecond to microsecond + floor(nanosecond / 1000). - let mut mis = microsecond + (nanosecond / 1000f64).floor(); // 2. Set nanosecond to nanosecond modulo 1000. - let ns = nanosecond % 1000f64; + let (quotient, nanosecond) = div_mod(nanosecond, 1000f64); + let microsecond = microsecond + quotient; + // 3. Set millisecond to millisecond + floor(microsecond / 1000). - let mut ms = millisecond + (mis / 1000f64).floor(); // 4. Set microsecond to microsecond modulo 1000. - mis = mis.rem_euclid(1000f64); + let (quotient, microsecond) = div_mod(microsecond, 1000f64); + let millisecond = millisecond + quotient; + // 5. Set second to second + floor(millisecond / 1000). - let mut secs = second + (ms / 1000f64).floor(); // 6. Set millisecond to millisecond modulo 1000. - ms = ms.rem_euclid(1000f64); + let (quotient, millisecond) = div_mod(millisecond, 1000f64); + let second = second + quotient; + // 7. Set minute to minute + floor(second / 60). - let mut minutes = minute + (secs / 60f64).floor(); // 8. Set second to second modulo 60. - secs = secs.rem_euclid(60f64); + let (quotient, second) = div_mod(second, 60f64); + let minute = minute + quotient; + // 9. Set hour to hour + floor(minute / 60). - let mut hours = hour + (minutes / 60f64).floor(); // 10. Set minute to minute modulo 60. - minutes = minutes.rem_euclid(60f64); + let (quotient, minute) = div_mod(minute, 60f64); + let hour = hour + quotient; + // 11. Let days be floor(hour / 24). - let days = (hours / 24f64).floor(); // 12. Set hour to hour modulo 24. - hours = hours.rem_euclid(24f64); + let (days, hour) = div_mod(hour, 24f64); let time = Self::new_unchecked( - hours as u8, - minutes as u8, - secs as u8, - ms as u16, - mis as u16, - ns as u16, + hour as u8, + minute as u8, + second as u8, + millisecond as u16, + microsecond as u16, + nanosecond as u16, ); (days as i32, time) } + // NOTE (nekevss): Specification seemed to be off / not entirely working, so the below was adapted from the + // temporal-polyfill + // TODO: DayLengthNS can probably be a u64, but keep as is for now and optimize. + /// Rounds the current `IsoTime` according to the provided settings. + pub(crate) fn round( + &self, + increment: f64, + unit: TemporalUnit, + mode: TemporalRoundingMode, + day_length_ns: Option, + ) -> 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 + } + // 4. Else if unit is "minute", then + // a. Let quantity be fractionalSecond / 60 + minute. + 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 + } + // 5. Else if unit is "second", then + // a. Let quantity be fractionalSecond. + 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 + } + // 6. Else if unit is "millisecond", then + // a. Let quantity be nanosecond × 10-6 + microsecond × 10-3 + millisecond. + TemporalUnit::Millisecond => { + u64::from(self.nanosecond) + + u64::from(self.microsecond) * 1_000 + + u64::from(self.millisecond) * 1_000_000 + } + // 7. Else if unit is "microsecond", then + // a. Let quantity be nanosecond × 10-3 + microsecond. + TemporalUnit::Microsecond => { + u64::from(self.nanosecond) + 1_000 * u64::from(self.microsecond) + } + // 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.")) + } + }; + + let ns_per_unit = if unit == TemporalUnit::Day { + day_length_ns.unwrap_or(NS_PER_DAY) as f64 + } else { + unit.as_nanoseconds().expect("Only valid time values are ") + }; + + // TODO: Verify validity of cast or handle better. + // 9. Let result be RoundNumberToIncrement(quantity, increment, roundingMode). + let result = + utils::round_number_to_increment(quantity as f64, ns_per_unit * increment, mode) + / ns_per_unit; + + 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 + // a. Return BalanceTime(result, 0, 0, 0, 0, 0). + TemporalUnit::Hour => IsoTime::balance(result, 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, 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, + 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, + 0.0, + 0.0, + ), + // 15. 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, + 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, + ), + _ => unreachable!("Error is thrown in previous match."), + }; + + Ok(result) + } + /// Checks if the time is a valid `IsoTime` pub(crate) fn is_valid(&self) -> bool { if !(0..=23).contains(&self.hour) { @@ -482,6 +624,8 @@ fn is_valid_date(year: i32, month: i32, day: i32) -> bool { (1..=days_in_month).contains(&day) } +// ==== `IsoTime` specific utilities ==== + #[inline] fn is_valid_time(hour: i32, minute: i32, second: i32, ms: i32, mis: i32, ns: i32) -> bool { if !(0..=23).contains(&hour) { @@ -496,3 +640,9 @@ fn is_valid_time(hour: i32, minute: i32, second: i32, ms: i32, mis: i32, ns: i32 let sub_second = 0..=999; sub_second.contains(&ms) && sub_second.contains(&mis) && sub_second.contains(&ns) } + +// NOTE(nekevss): Considering the below: Balance can probably be altered from f64. +#[inline] +fn div_mod(dividend: f64, divisor: f64) -> (f64, f64) { + (dividend.div_euclid(divisor), dividend.rem_euclid(divisor)) +} diff --git a/core/temporal/src/options.rs b/core/temporal/src/options.rs index 93fe94978d..75192802a1 100644 --- a/core/temporal/src/options.rs +++ b/core/temporal/src/options.rs @@ -62,6 +62,25 @@ impl TemporalUnit { Auto => unreachable!(), } } + + // TODO: potentiall use a u64 + /// Returns the `Nanosecond amount for any given value.` + #[must_use] + pub fn as_nanoseconds(&self) -> Option { + use TemporalUnit::{ + Auto, Day, Hour, Microsecond, Millisecond, Minute, Month, Nanosecond, Second, Week, + Year, + }; + match self { + Year | Month | Week | Day | Auto => None, + Hour => Some(3600e9), + Minute => Some(60e9), + Second => Some(1e9), + Millisecond => Some(1e6), + Microsecond => Some(1e3), + Nanosecond => Some(1f64), + } + } } /// A parsing error for `TemporalUnit`