Browse Source

Temporal: Build out `Time` and its methods (#3613)

* Build out Time and its methods

* Move public methods and remove #[allow(dead_code)]
pull/3620/head
Kevin 9 months ago committed by GitHub
parent
commit
9ffbc03d44
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 19
      core/temporal/src/components/duration.rs
  2. 230
      core/temporal/src/components/time.rs
  3. 196
      core/temporal/src/iso.rs
  4. 19
      core/temporal/src/options.rs

19
core/temporal/src/components/duration.rs

@ -61,11 +61,6 @@ impl Duration {
pub(crate) fn one_week(week_value: f64) -> Self { pub(crate) fn one_week(week_value: f64) -> Self {
Self::from_date_duration(DateDuration::new_unchecked(0f64, 0f64, week_value, 0f64)) 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 ==== // ==== Public Duration API ====
@ -137,6 +132,20 @@ impl Duration {
pub fn is_time_within_range(&self) -> bool { pub fn is_time_within_range(&self) -> bool {
self.time.is_within_range() 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 ==== // ==== Public `Duration` Getters/Setters ====

230
core/temporal/src/components/time.rs

@ -1,10 +1,14 @@
//! This module implements `Time` and any directly related algorithms. //! 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`. /// The native Rust implementation of `Temporal.PlainTime`.
#[derive(Debug, Default, Clone, Copy)] #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[allow(dead_code)]
pub struct Time { pub struct Time {
iso: IsoTime, iso: IsoTime,
} }
@ -23,8 +27,28 @@ impl Time {
pub(crate) fn is_valid(&self) -> bool { pub(crate) fn is_valid(&self) -> bool {
self.iso.is_valid() 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 { impl Time {
/// Creates a new `IsoTime` value. /// Creates a new `IsoTime` value.
pub fn new( pub fn new(
@ -47,4 +71,204 @@ impl Time {
)?; )?;
Ok(Self::new_unchecked(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<Self> {
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<Self> {
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<f64>,
rounding_mode: Option<TemporalRoundingMode>,
) -> TemporalResult<Self> {
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::<Duration>().unwrap()).unwrap();
assert_time(result, (7, 23, 30, 123, 456, 789));
}
} }

196
core/temporal/src/iso.rs

@ -13,8 +13,10 @@
//! An `IsoDateTime` has the internal slots of both an `IsoDate` and `IsoTime`. //! An `IsoDateTime` has the internal slots of both an `IsoDate` and `IsoTime`.
use crate::{ use crate::{
components::duration::DateDuration, error::TemporalError, options::ArithmeticOverflow, utils, components::duration::DateDuration,
TemporalResult, error::TemporalError,
options::{ArithmeticOverflow, TemporalRoundingMode, TemporalUnit},
utils, TemporalResult, NS_PER_DAY,
}; };
use icu_calendar::{Date as IcuDate, Iso}; use icu_calendar::{Date as IcuDate, Iso};
use num_bigint::BigInt; use num_bigint::BigInt;
@ -106,8 +108,8 @@ impl IsoDateTime {
return false; return false;
}; };
let max = BigInt::from(crate::NS_MAX_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(crate::NS_PER_DAY)); let min = BigInt::from(crate::NS_MIN_INSTANT - i128::from(NS_PER_DAY));
min < ns && max > ns min < ns && max > ns
} }
@ -276,7 +278,7 @@ impl IsoDate {
/// An `IsoTime` record that contains `Temporal`'s /// An `IsoTime` record that contains `Temporal`'s
/// time slots. /// time slots.
#[derive(Debug, Default, Clone, Copy)] #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct IsoTime { pub struct IsoTime {
pub(crate) hour: u8, // 0..=23 pub(crate) hour: u8, // 0..=23
pub(crate) minute: u8, // 0..=59 pub(crate) minute: u8, // 0..=59
@ -387,42 +389,182 @@ impl IsoTime {
nanosecond: f64, nanosecond: f64,
) -> (i32, Self) { ) -> (i32, Self) {
// 1. Set microsecond to microsecond + floor(nanosecond / 1000). // 1. Set microsecond to microsecond + floor(nanosecond / 1000).
let mut mis = microsecond + (nanosecond / 1000f64).floor();
// 2. Set nanosecond to nanosecond modulo 1000. // 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). // 3. Set millisecond to millisecond + floor(microsecond / 1000).
let mut ms = millisecond + (mis / 1000f64).floor();
// 4. Set microsecond to microsecond modulo 1000. // 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). // 5. Set second to second + floor(millisecond / 1000).
let mut secs = second + (ms / 1000f64).floor();
// 6. Set millisecond to millisecond modulo 1000. // 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). // 7. Set minute to minute + floor(second / 60).
let mut minutes = minute + (secs / 60f64).floor();
// 8. Set second to second modulo 60. // 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). // 9. Set hour to hour + floor(minute / 60).
let mut hours = hour + (minutes / 60f64).floor();
// 10. Set minute to minute modulo 60. // 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). // 11. Let days be floor(hour / 24).
let days = (hours / 24f64).floor();
// 12. Set hour to hour modulo 24. // 12. Set hour to hour modulo 24.
hours = hours.rem_euclid(24f64); let (days, hour) = div_mod(hour, 24f64);
let time = Self::new_unchecked( let time = Self::new_unchecked(
hours as u8, hour as u8,
minutes as u8, minute as u8,
secs as u8, second as u8,
ms as u16, millisecond as u16,
mis as u16, microsecond as u16,
ns as u16, nanosecond as u16,
); );
(days as i32, time) (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<i64>,
) -> 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` /// Checks if the time is a valid `IsoTime`
pub(crate) fn is_valid(&self) -> bool { pub(crate) fn is_valid(&self) -> bool {
if !(0..=23).contains(&self.hour) { 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) (1..=days_in_month).contains(&day)
} }
// ==== `IsoTime` specific utilities ====
#[inline] #[inline]
fn is_valid_time(hour: i32, minute: i32, second: i32, ms: i32, mis: i32, ns: i32) -> bool { fn is_valid_time(hour: i32, minute: i32, second: i32, ms: i32, mis: i32, ns: i32) -> bool {
if !(0..=23).contains(&hour) { 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; let sub_second = 0..=999;
sub_second.contains(&ms) && sub_second.contains(&mis) && sub_second.contains(&ns) 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))
}

19
core/temporal/src/options.rs

@ -62,6 +62,25 @@ impl TemporalUnit {
Auto => unreachable!(), Auto => unreachable!(),
} }
} }
// TODO: potentiall use a u64
/// Returns the `Nanosecond amount for any given value.`
#[must_use]
pub fn as_nanoseconds(&self) -> Option<f64> {
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` /// A parsing error for `TemporalUnit`

Loading…
Cancel
Save