Skip to content

Commit

Permalink
Restrict Time to ISO-8601 range, remove arithmetic (#6002)
Browse files Browse the repository at this point in the history
Fixes #5987
  • Loading branch information
robertbastian authored Jan 21, 2025
1 parent 78933a6 commit beaca4d
Showing 1 changed file with 27 additions and 169 deletions.
196 changes: 27 additions & 169 deletions components/timezone/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ use icu_calendar::{AsCalendar, Date, RangeError};
/// here will return a Result on whether or not the unit is in range from the given
/// input.
macro_rules! dt_unit {
($name:ident, $storage:ident, $value:expr, $docs:expr) => {
#[doc=$docs]
($name:ident, $storage:ident, $value:expr, $(#[$docs:meta])+) => {
$(#[$docs])+
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Hash)]
pub struct $name($storage);

Expand All @@ -26,6 +26,12 @@ macro_rules! dt_unit {
pub const fn zero() -> $name {
Self(0)
}

/// Returns whether the value is zero.
#[inline]
pub fn is_zero(self) -> bool {
self.0 == 0
}
}

impl TryFrom<$storage> for $name {
Expand All @@ -34,7 +40,7 @@ macro_rules! dt_unit {
fn try_from(input: $storage) -> Result<Self, Self::Error> {
if input > $value {
Err(RangeError {
field: "$name",
field: stringify!($name),
min: 0,
max: $value,
value: input as i32,
Expand Down Expand Up @@ -73,204 +79,56 @@ macro_rules! dt_unit {
input.0 as Self
}
}

impl $name {
/// Attempts to add two values.
/// Returns `Some` if the sum is within bounds.
/// Returns `None` if the sum is out of bounds.
pub fn try_add(self, other: $storage) -> Option<Self> {
let sum = self.0.saturating_add(other);
if sum > $value {
None
} else {
Some(Self(sum))
}
}

/// Attempts to subtract two values.
/// Returns `Some` if the difference is within bounds.
/// Returns `None` if the difference is out of bounds.
pub fn try_sub(self, other: $storage) -> Option<Self> {
self.0.checked_sub(other).map(Self)
}

/// Returns whether the value is zero.
#[inline]
pub fn is_zero(self) -> bool {
self.0 == 0
}
}
};
}

dt_unit!(
IsoHour,
u8,
24,
"An ISO-8601 hour component, for use with ISO calendars.
Must be within inclusive bounds `[0, 24]`. The value could be equal to 24 to
denote the end of a day, with the writing 24:00:00. It corresponds to the same
time as the next day at 00:00:00."
23,
/// An ISO-8601 hour component, for use with ISO calendars.
///
/// Must be within inclusive bounds `[0, 23]`.
);

dt_unit!(
IsoMinute,
u8,
60,
"An ISO-8601 minute component, for use with ISO calendars.
Must be within inclusive bounds `[0, 60]`. The value could be equal to 60 to
denote the end of an hour, with the writing 12:60:00. This example corresponds
to the same time as 13:00:00. This is an extension to ISO 8601."
59,
/// An ISO-8601 minute component, for use with ISO calendars.
///
/// Must be within inclusive bounds `[0, 59]`.
);

dt_unit!(
IsoSecond,
u8,
61,
"An ISO-8601 second component, for use with ISO calendars.
Must be within inclusive bounds `[0, 61]`. `60` accommodates for leap seconds.
The value could also be equal to 60 or 61, to indicate the end of a leap second,
with the writing `23:59:61.000000000Z` or `23:59:60.000000000Z`. These examples,
if used with this goal, would correspond to the same time as the next day, at
time `00:00:00.000000000Z`. This is an extension to ISO 8601."
60,
/// An ISO-8601 second component, for use with ISO calendars.
///
/// Must be within inclusive bounds `[0, 60]`. `60` accommodates for leap seconds.
);

dt_unit!(
NanoSecond,
u32,
999_999_999,
"A fractional second component, stored as nanoseconds.
Must be within inclusive bounds `[0, 999_999_999]`."
/// A fractional second component, stored as nanoseconds.
///
/// Must be within inclusive bounds `[0, 999_999_999]`."
);

#[test]
fn test_iso_hour_arithmetic() {
const HOUR_MAX: u8 = 24;
const HOUR_VALUE: u8 = 5;
let hour = IsoHour(HOUR_VALUE);

// middle of bounds
assert_eq!(
hour.try_add(HOUR_VALUE - 1),
Some(IsoHour(HOUR_VALUE + (HOUR_VALUE - 1)))
);
assert_eq!(
hour.try_sub(HOUR_VALUE - 1),
Some(IsoHour(HOUR_VALUE - (HOUR_VALUE - 1)))
);

// edge of bounds
assert_eq!(hour.try_add(HOUR_MAX - HOUR_VALUE), Some(IsoHour(HOUR_MAX)));
assert_eq!(hour.try_sub(HOUR_VALUE), Some(IsoHour(0)));

// out of bounds
assert_eq!(hour.try_add(1 + HOUR_MAX - HOUR_VALUE), None);
assert_eq!(hour.try_sub(1 + HOUR_VALUE), None);
}

#[test]
fn test_iso_minute_arithmetic() {
const MINUTE_MAX: u8 = 60;
const MINUTE_VALUE: u8 = 5;
let minute = IsoMinute(MINUTE_VALUE);

// middle of bounds
assert_eq!(
minute.try_add(MINUTE_VALUE - 1),
Some(IsoMinute(MINUTE_VALUE + (MINUTE_VALUE - 1)))
);
assert_eq!(
minute.try_sub(MINUTE_VALUE - 1),
Some(IsoMinute(MINUTE_VALUE - (MINUTE_VALUE - 1)))
);

// edge of bounds
assert_eq!(
minute.try_add(MINUTE_MAX - MINUTE_VALUE),
Some(IsoMinute(MINUTE_MAX))
);
assert_eq!(minute.try_sub(MINUTE_VALUE), Some(IsoMinute(0)));

// out of bounds
assert_eq!(minute.try_add(1 + MINUTE_MAX - MINUTE_VALUE), None);
assert_eq!(minute.try_sub(1 + MINUTE_VALUE), None);
}

#[test]
fn test_iso_second_arithmetic() {
const SECOND_MAX: u8 = 61;
const SECOND_VALUE: u8 = 5;
let second = IsoSecond(SECOND_VALUE);

// middle of bounds
assert_eq!(
second.try_add(SECOND_VALUE - 1),
Some(IsoSecond(SECOND_VALUE + (SECOND_VALUE - 1)))
);
assert_eq!(
second.try_sub(SECOND_VALUE - 1),
Some(IsoSecond(SECOND_VALUE - (SECOND_VALUE - 1)))
);

// edge of bounds
assert_eq!(
second.try_add(SECOND_MAX - SECOND_VALUE),
Some(IsoSecond(SECOND_MAX))
);
assert_eq!(second.try_sub(SECOND_VALUE), Some(IsoSecond(0)));

// out of bounds
assert_eq!(second.try_add(1 + SECOND_MAX - SECOND_VALUE), None);
assert_eq!(second.try_sub(1 + SECOND_VALUE), None);
}

#[test]
fn test_iso_nano_second_arithmetic() {
const NANO_SECOND_MAX: u32 = 999_999_999;
const NANO_SECOND_VALUE: u32 = 5;
let nano_second = NanoSecond(NANO_SECOND_VALUE);

// middle of bounds
assert_eq!(
nano_second.try_add(NANO_SECOND_VALUE - 1),
Some(NanoSecond(NANO_SECOND_VALUE + (NANO_SECOND_VALUE - 1)))
);
assert_eq!(
nano_second.try_sub(NANO_SECOND_VALUE - 1),
Some(NanoSecond(NANO_SECOND_VALUE - (NANO_SECOND_VALUE - 1)))
);

// edge of bounds
assert_eq!(
nano_second.try_add(NANO_SECOND_MAX - NANO_SECOND_VALUE),
Some(NanoSecond(NANO_SECOND_MAX))
);
assert_eq!(nano_second.try_sub(NANO_SECOND_VALUE), Some(NanoSecond(0)));

// out of bounds
assert_eq!(
nano_second.try_add(1 + NANO_SECOND_MAX - NANO_SECOND_VALUE),
None
);
assert_eq!(nano_second.try_sub(1 + NANO_SECOND_VALUE), None);
}

/// A representation of a time in hours, minutes, seconds, and nanoseconds
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
#[allow(clippy::exhaustive_structs)] // this type is stable
pub struct Time {
/// 0-based hour.
/// Hour
pub hour: IsoHour,

/// 0-based minute.
/// Minute
pub minute: IsoMinute,

/// 0-based second.
/// Second
pub second: IsoSecond,

/// Fractional second
Expand Down

0 comments on commit beaca4d

Please sign in to comment.