Skip to content

Commit

Permalink
Add FractionalSecond to Time (#1801)
Browse files Browse the repository at this point in the history
* Add FractionalSecond to Time

* Fix fmt and clippy

* Address review feedback
  • Loading branch information
dminor authored Apr 21, 2022
1 parent b49d84e commit 2397c6d
Show file tree
Hide file tree
Showing 12 changed files with 118 additions and 48 deletions.
2 changes: 1 addition & 1 deletion components/calendar/src/buddhist.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ impl DateTime<Buddhist> {
let iso_year = year - BUDDHIST_ERA_OFFSET;
Ok(DateTime {
date: Date::new_buddhist_date(iso_year.into(), month.try_into()?, day.try_into()?)?,
time: types::Time::try_new(hour, minute, second)?,
time: types::Time::try_new(hour, minute, second, 0)?,
})
}
}
Expand Down
2 changes: 1 addition & 1 deletion components/calendar/src/coptic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ impl DateTime<Coptic> {
) -> Result<DateTime<Coptic>, DateTimeError> {
Ok(DateTime {
date: Date::new_coptic_date_from_integers(year, month, day)?,
time: types::Time::try_new(hour, minute, second)?,
time: types::Time::try_new(hour, minute, second, 0)?,
})
}
}
3 changes: 2 additions & 1 deletion components/calendar/src/gregorian.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,10 +110,11 @@ impl DateTime<Gregorian> {
hour: u8,
minute: u8,
second: u8,
fraction: u32,
) -> Result<DateTime<Gregorian>, DateTimeError> {
Ok(DateTime {
date: Date::new_gregorian_date(year.into(), month.try_into()?, day.try_into()?)?,
time: types::Time::try_new(hour, minute, second)?,
time: types::Time::try_new(hour, minute, second, fraction)?,
})
}
}
Expand Down
2 changes: 1 addition & 1 deletion components/calendar/src/indian.rs
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ impl DateTime<Indian> {
) -> Result<DateTime<Indian>, DateTimeError> {
Ok(DateTime {
date: Date::new_indian_date_from_integers(year, month, day)?,
time: types::Time::try_new(hour, minute, second)?,
time: types::Time::try_new(hour, minute, second, 0)?,
})
}
}
2 changes: 1 addition & 1 deletion components/calendar/src/iso.rs
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,7 @@ impl DateTime<Iso> {
) -> Result<DateTime<Iso>, DateTimeError> {
Ok(DateTime {
date: Date::new_iso_date_from_integers(year, month, day)?,
time: types::Time::try_new(hour, minute, second)?,
time: types::Time::try_new(hour, minute, second, 0)?,
})
}
}
Expand Down
2 changes: 1 addition & 1 deletion components/calendar/src/julian.rs
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ impl DateTime<Julian> {
) -> Result<DateTime<Julian>, DateTimeError> {
Ok(DateTime {
date: Date::new_julian_date_from_integers(year, month, day)?,
time: types::Time::try_new(hour, minute, second)?,
time: types::Time::try_new(hour, minute, second, 0)?,
})
}
}
Expand Down
73 changes: 43 additions & 30 deletions components/calendar/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,18 +97,19 @@ fn test_day_of_week_in_month() {
assert_eq!(DayOfWeekInMonth::from(DayOfMonth(8)).0, 2);
}

/// This macro defines a struct for 0-based date fields: hours, minutes, and seconds. Each
/// unit is bounded by a range. The traits implemented here will return a Result on
/// whether or not the unit is in range from the given input.
/// This macro defines a struct for 0-based date fields: hours, minutes, seconds
/// and fractional seconds. Each unit is bounded by a range. The traits implemented
/// here will return a Result on whether or not the unit is in range from the given
/// input.
macro_rules! dt_unit {
($name:ident, $value:expr, $docs:expr) => {
($name:ident, $storage:ident, $value:expr, $docs:expr) => {
#[doc=$docs]
#[derive(Debug, Default, Clone, Copy, PartialEq, Hash)]
pub struct $name(u8);
pub struct $name($storage);

impl $name {
/// Do not validate the numeric input for this component.
pub const fn new_unchecked(input: u8) -> Self {
pub const fn new_unchecked(input: $storage) -> Self {
Self(input)
}
}
Expand All @@ -117,7 +118,7 @@ macro_rules! dt_unit {
type Err = DateTimeError;

fn from_str(input: &str) -> Result<Self, Self::Err> {
let val: u8 = input.parse()?;
let val: $storage = input.parse()?;
if val > $value {
Err(DateTimeError::Overflow {
field: "$name",
Expand All @@ -129,10 +130,10 @@ macro_rules! dt_unit {
}
}

impl TryFrom<u8> for $name {
impl TryFrom<$storage> for $name {
type Error = DateTimeError;

fn try_from(input: u8) -> Result<Self, Self::Error> {
fn try_from(input: $storage) -> Result<Self, Self::Error> {
if input > $value {
Err(DateTimeError::Overflow {
field: "$name",
Expand All @@ -154,12 +155,12 @@ macro_rules! dt_unit {
max: $value,
})
} else {
Ok(Self(input as u8))
Ok(Self(input as $storage))
}
}
}

impl From<$name> for u8 {
impl From<$name> for $storage {
fn from(input: $name) -> Self {
input.0
}
Expand All @@ -171,18 +172,18 @@ macro_rules! dt_unit {
}
}

impl Add<u8> for $name {
impl Add<$storage> for $name {
type Output = Self;

fn add(self, other: u8) -> Self {
fn add(self, other: $storage) -> Self {
Self(self.0 + other)
}
}

impl Sub<u8> for $name {
impl Sub<$storage> for $name {
type Output = Self;

fn sub(self, other: u8) -> Self {
fn sub(self, other: $storage) -> Self {
Self(self.0 - other)
}
}
Expand All @@ -191,22 +192,32 @@ macro_rules! dt_unit {

dt_unit!(
IsoHour,
u8,
24,
"An ISO-8601 hour component, for use with ISO calendars."
);

dt_unit!(
IsoMinute,
u8,
60,
"An ISO-8601 minute component, for use with ISO calendars."
);

dt_unit!(
IsoSecond,
u8,
61,
"An ISO-8601 second component, for use with ISO calendars."
);

dt_unit!(
NanoSecond,
u32,
999_999_999,
"A fractional second component, stored as nanoseconds."
);

#[derive(Debug, Copy, Clone)]
pub struct Time {
/// 0-based hour.
Expand All @@ -217,40 +228,42 @@ pub struct Time {

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

/// Fractional second
pub nanosecond: NanoSecond,
}

impl Time {
/// Do not validate the numeric input for this component.
pub const fn new(hour: IsoHour, minute: IsoMinute, second: IsoSecond) -> Self {
pub const fn new(
hour: IsoHour,
minute: IsoMinute,
second: IsoSecond,
nanosecond: NanoSecond,
) -> Self {
Self {
hour,
minute,
second,
nanosecond,
}
}

pub fn try_new(hour: u8, minute: u8, second: u8) -> Result<Self, DateTimeError> {
pub fn try_new(
hour: u8,
minute: u8,
second: u8,
nanosecond: u32,
) -> Result<Self, DateTimeError> {
Ok(Self {
hour: hour.try_into()?,
minute: minute.try_into()?,
second: second.try_into()?,
nanosecond: nanosecond.try_into()?,
})
}
}

// TODO(#485): Improve FractionalSecond.
/// A placeholder for fractional seconds support. See [Issue #485](https://github.com/unicode-org/icu4x/issues/485)
/// for tracking the support of this feature.
#[derive(Clone, Debug, PartialEq)]
pub enum FractionalSecond {
/// The millisecond component of the fractional second.
Millisecond(u16),
/// The microsecond component of the fractional second.
Microsecond(u32),
/// The nanosecond component of the fractional second.
Nanosecond(u32),
}

/// The GMT offset in seconds for a mock time zone
#[derive(Copy, Clone, Debug, Default)]
pub struct GmtOffset(i32);
Expand Down
6 changes: 3 additions & 3 deletions components/datetime/src/date.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,8 @@ pub trait IsoTimeInput {
/// Gets the second input.
fn second(&self) -> Option<IsoSecond>;

/// Gets the fractional second input.
fn fraction(&self) -> Option<FractionalSecond>;
/// Gets the nanosecond input.
fn nanosecond(&self) -> Option<NanoSecond>;
}

/// Representation of a formattable time zone.
Expand Down Expand Up @@ -389,7 +389,7 @@ impl<A: AsCalendar> IsoTimeInput for DateTime<A> {
}

/// Gets the fractional second input.
fn fraction(&self) -> Option<FractionalSecond> {
fn nanosecond(&self) -> Option<NanoSecond> {
None
}
}
8 changes: 4 additions & 4 deletions components/datetime/src/datetime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ use crate::{date::DateTimeInput, CldrCalendar, DateTimeFormatError, FormattedDat
/// .expect("Failed to create DateTimeFormat instance.");
///
///
/// let datetime = DateTime::new_gregorian_datetime_from_integers(2020, 9, 1, 12, 34, 28)
/// let datetime = DateTime::new_gregorian_datetime_from_integers(2020, 9, 1, 12, 34, 28, 0)
/// .expect("Failed to construct DateTime.");
///
/// let value = dtf.format_to_string(&datetime);
Expand Down Expand Up @@ -119,7 +119,7 @@ impl<C: CldrCalendar> DateTimeFormat<C> {
/// let dtf = DateTimeFormat::<Gregorian>::try_new(locale, &provider, &options)
/// .expect("Failed to create DateTimeFormat instance.");
///
/// let datetime = DateTime::new_gregorian_datetime_from_integers(2020, 9, 1, 12, 34, 28)
/// let datetime = DateTime::new_gregorian_datetime_from_integers(2020, 9, 1, 12, 34, 28, 0)
/// .expect("Failed to construct DateTime.");
///
/// let formatted_date = dtf.format(&datetime);
Expand Down Expand Up @@ -153,7 +153,7 @@ impl<C: CldrCalendar> DateTimeFormat<C> {
/// let dtf = DateTimeFormat::<Gregorian>::try_new(locale, &provider, &options.into())
/// .expect("Failed to create DateTimeFormat instance.");
///
/// let datetime = DateTime::new_gregorian_datetime_from_integers(2020, 9, 1, 12, 34, 28)
/// let datetime = DateTime::new_gregorian_datetime_from_integers(2020, 9, 1, 12, 34, 28, 0)
/// .expect("Failed to construct DateTime.");
///
/// let mut buffer = String::new();
Expand Down Expand Up @@ -185,7 +185,7 @@ impl<C: CldrCalendar> DateTimeFormat<C> {
/// let dtf = DateTimeFormat::<Gregorian>::try_new(locale, &provider, &options.into())
/// .expect("Failed to create DateTimeFormat instance.");
///
/// let datetime = DateTime::new_gregorian_datetime_from_integers(2020, 9, 1, 12, 34, 28)
/// let datetime = DateTime::new_gregorian_datetime_from_integers(2020, 9, 1, 12, 34, 28, 0)
/// .expect("Failed to construct DateTime.");
///
/// let _ = dtf.format_to_string(&datetime);
Expand Down
4 changes: 2 additions & 2 deletions components/datetime/src/format/datetime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ use writeable::Writeable;
/// let dtf = DateTimeFormat::<Gregorian>::try_new(locale!("en"), &provider, &options)
/// .expect("Failed to create DateTimeFormat instance.");
///
/// let datetime = DateTime::new_gregorian_datetime_from_integers(2020, 9, 1, 12, 34, 28)
/// let datetime = DateTime::new_gregorian_datetime_from_integers(2020, 9, 1, 12, 34, 28, 0)
/// .expect("Failed to construct DateTime.");
///
/// let formatted_date = dtf.format(&datetime);
Expand Down Expand Up @@ -412,7 +412,7 @@ mod tests {
.unwrap();
let pattern = "MMM".parse().unwrap();
let datetime =
DateTime::new_gregorian_datetime_from_integers(2020, 8, 1, 12, 34, 28).unwrap();
DateTime::new_gregorian_datetime_from_integers(2020, 8, 1, 12, 34, 28, 0).unwrap();
let mut sink = String::new();
let loc_datetime = DateTimeInputWithLocale::new(&datetime, None, &"und".parse().unwrap());
write_pattern(&pattern, Some(data.get()), &loc_datetime, &mut sink).unwrap();
Expand Down
58 changes: 57 additions & 1 deletion components/datetime/src/mock/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,17 @@ pub mod zoned_datetime;
/// let date: DateTime<Gregorian> = parse_gregorian_from_str("2020-10-14T13:21:00")
/// .expect("Failed to parse a datetime.");
/// ```
///
/// Optionally, fractional seconds can be specified: `YYYY-MM-DDThh:mm:ss.SSS`.
///
/// ```
/// use icu::datetime::mock::parse_gregorian_from_str;
/// use icu_calendar::{DateTime, Gregorian};
///
/// let date: DateTime<Gregorian> = parse_gregorian_from_str("2020-10-14T13:21:00.101")
/// .expect("Failed to parse a datetime.");
/// assert_eq!(u32::from(date.time.nanosecond), 101_000_000);
/// ```
pub fn parse_gregorian_from_str(input: &str) -> Result<DateTime<Gregorian>, DateTimeError> {
#[allow(clippy::indexing_slicing)] // TODO(#1668) Clippy exceptions need docs or fixing.
let year: i32 = input[0..4].parse()?;
Expand All @@ -38,5 +49,50 @@ pub fn parse_gregorian_from_str(input: &str) -> Result<DateTime<Gregorian>, Date
let minute: u8 = input[14..16].parse()?;
#[allow(clippy::indexing_slicing)] // TODO(#1668) Clippy exceptions need docs or fixing.
let second: u8 = input[17..19].parse()?;
DateTime::new_gregorian_datetime_from_integers(year, month, day, hour, minute, second)
#[allow(clippy::indexing_slicing)] // TODO(#1668) Clippy exceptions need docs or fixing.
let fraction: u32 = if input.len() > 20 {
// Extract fractional input and trim any trailing zeros
let mut fraction = input[20..].trim_end_matches('0');
// Truncate to at most 9 digits
if fraction.len() > 9 {
fraction = &fraction[0..9];
}
let as_int: u32 = fraction.parse().unwrap_or(0);
// Convert fraction to nanoseconds
as_int * (10u32.pow(9 - fraction.len() as u32))
} else {
0
};
DateTime::new_gregorian_datetime_from_integers(year, month, day, hour, minute, second, fraction)
}

#[test]
fn test_parsing_fractional_seconds() {
use icu::datetime::mock::parse_gregorian_from_str;
use icu_calendar::{DateTime, Gregorian};

// Milliseconds
let date: DateTime<Gregorian> =
parse_gregorian_from_str("2020-10-14T13:21:00.123").expect("Failed to parse a datetime.");
assert_eq!(u32::from(date.time.nanosecond), 123_000_000);

// All zeros
let date: DateTime<Gregorian> =
parse_gregorian_from_str("2020-10-14T13:21:00.000").expect("Failed to parse a datetime.");
assert_eq!(u32::from(date.time.nanosecond), 0);

// Leading zeros
let date: DateTime<Gregorian> = parse_gregorian_from_str("2020-10-14T13:21:00.000123")
.expect("Failed to parse a datetime.");
assert_eq!(u32::from(date.time.nanosecond), 123_000);

// Trailing zeros
let date: DateTime<Gregorian> = parse_gregorian_from_str("2020-10-14T13:21:00.123000000000000")
.expect("Failed to parse a datetime.");
assert_eq!(u32::from(date.time.nanosecond), 123_000_000);

// Too much precision, should truncate to nanoseconds
let date: DateTime<Gregorian> = parse_gregorian_from_str("2020-10-14T13:21:00.123456789999999")
.expect("Failed to parse a datetime.");
assert_eq!(u32::from(date.time.nanosecond), 123_456_789);
}
4 changes: 2 additions & 2 deletions components/datetime/src/mock/zoned_datetime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,8 +132,8 @@ impl IsoTimeInput for MockZonedDateTime {
self.datetime.second()
}

fn fraction(&self) -> Option<FractionalSecond> {
self.datetime.fraction()
fn nanosecond(&self) -> Option<NanoSecond> {
self.datetime.nanosecond()
}
}

Expand Down

0 comments on commit 2397c6d

Please sign in to comment.