diff --git a/lib/oxsdatatypes/src/date_time.rs b/lib/oxsdatatypes/src/date_time.rs index 00098b02..25914fce 100644 --- a/lib/oxsdatatypes/src/date_time.rs +++ b/lib/oxsdatatypes/src/date_time.rs @@ -1142,21 +1142,26 @@ impl fmt::Display for GDay { } } -#[derive(Eq, PartialEq, Debug, Clone, Copy, Hash)] +/// A timezone offset with respect to UTC. +/// +/// It is encoded as a number of minutes between -PT14H and PT14H. +#[derive(Eq, PartialEq, Ord, PartialOrd, Debug, Clone, Copy, Hash)] pub struct TimezoneOffset { offset: i16, // in minute with respect to UTC } impl TimezoneOffset { - #[inline] - pub const fn utc() -> Self { - Self { offset: 0 } - } - /// From offset in minute with respect to UTC #[inline] - pub(super) const fn new(offset: i16) -> Self { - Self { offset } + pub fn new(offset_in_minutes: i16) -> Result { + let value = Self { + offset: offset_in_minutes, + }; + if Self::MIN <= value && value <= Self::MAX { + Ok(value) + } else { + Err(DATE_TIME_OVERFLOW) + } } #[inline] @@ -1170,12 +1175,35 @@ impl TimezoneOffset { pub fn to_be_bytes(self) -> [u8; 2] { self.offset.to_be_bytes() } + + pub const MIN: Self = Self { offset: -14 * 60 }; + pub const UTC: Self = Self { offset: 0 }; + pub const MAX: Self = Self { offset: 14 * 60 }; } -impl From for TimezoneOffset { +impl TryFrom for TimezoneOffset { + type Error = DateTimeError; + #[inline] - fn from(offset: i16) -> Self { - Self { offset } + fn try_from(value: DayTimeDuration) -> Result { + let result = Self::new((value.minutes() + value.hours() * 60) as i16)?; + if DayTimeDuration::from(result) == value { + Ok(result) + } else { + // The value is not an integral number of minutes or overflow problems + Err(DATE_TIME_OVERFLOW) + } + } +} + +impl TryFrom for TimezoneOffset { + type Error = DateTimeError; + + #[inline] + fn try_from(value: Duration) -> Result { + DayTimeDuration::try_from(value) + .map_err(|_| DATE_TIME_OVERFLOW)? + .try_into() } } @@ -1287,9 +1315,7 @@ impl Timestamp { Ok(Self { timezone_offset: props.timezone_offset, - value: time_on_timeline(props).ok_or(DateTimeError { - kind: DateTimeErrorKind::Overflow, - })?, + value: time_on_timeline(props).ok_or(DATE_TIME_OVERFLOW)?, }) } @@ -1305,12 +1331,10 @@ impl Timestamp { hour: Some(0), minute: Some(0), second: Some(Decimal::default()), - timezone_offset: Some(TimezoneOffset::utc()), + timezone_offset: Some(TimezoneOffset::UTC), }, ) - .ok_or(DateTimeError { - kind: DateTimeErrorKind::Overflow, - })?, + .ok_or(DATE_TIME_OVERFLOW)?, ) } @@ -1332,11 +1356,7 @@ impl Timestamp { #[inline] fn year_month_day(&self) -> (i64, u8, u8) { let mut days = (self.value.as_i128() - + i128::from( - self.timezone_offset - .unwrap_or_else(TimezoneOffset::utc) - .offset, - ) * 60) + + i128::from(self.timezone_offset.unwrap_or(TimezoneOffset::UTC).offset) * 60) .div_euclid(86400) + 366; @@ -1406,11 +1426,7 @@ impl Timestamp { #[inline] fn hour(&self) -> u8 { (((self.value.as_i128() - + i128::from( - self.timezone_offset - .unwrap_or_else(TimezoneOffset::utc) - .offset, - ) * 60) + + i128::from(self.timezone_offset.unwrap_or(TimezoneOffset::UTC).offset) * 60) .rem_euclid(86400)) / 3600) as u8 } @@ -1419,11 +1435,7 @@ impl Timestamp { #[inline] fn minute(&self) -> u8 { (((self.value.as_i128() - + i128::from( - self.timezone_offset - .unwrap_or_else(TimezoneOffset::utc) - .offset, - ) * 60) + + i128::from(self.timezone_offset.unwrap_or(TimezoneOffset::UTC).offset) * 60) .rem_euclid(3600)) / 60) as u8 } @@ -1486,11 +1498,8 @@ impl Timestamp { fn since_unix_epoch() -> Result { Ok(Duration::new( 0, - Decimal::try_from(crate::Double::from(js_sys::Date::now() / 1000.)).map_err(|_| { - DateTimeError { - kind: DateTimeErrorKind::Overflow, - } - })?, + Decimal::try_from(crate::Double::from(js_sys::Date::now() / 1000.)) + .map_err(|_| DATE_TIME_OVERFLOW)?, )) } @@ -1501,9 +1510,7 @@ fn since_unix_epoch() -> Result { SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH)? .try_into() - .map_err(|_| DateTimeError { - kind: DateTimeErrorKind::Overflow, - }) + .map_err(|_| DATE_TIME_OVERFLOW) } /// The [normalizeMonth](https://www.w3.org/TR/xmlschema11-2/#f-dt-normMo) function @@ -1627,12 +1634,7 @@ fn time_on_timeline(props: &DateTimeSevenPropertyModel) -> Option { .map_or_else(|| days_in_month(Some(yr + 1), mo) - 1, |d| d - 1); let hr = props.hour.unwrap_or(0); let mi = i128::from(props.minute.unwrap_or(0)) - - i128::from( - props - .timezone_offset - .unwrap_or_else(TimezoneOffset::utc) - .offset, - ); + - i128::from(props.timezone_offset.unwrap_or(TimezoneOffset::UTC).offset); let se = props.second.unwrap_or_default(); Decimal::try_from( @@ -1695,6 +1697,10 @@ impl From for DateTimeError { } } +const DATE_TIME_OVERFLOW: DateTimeError = DateTimeError { + kind: DateTimeErrorKind::Overflow, +}; + #[cfg(test)] mod tests { use super::*; diff --git a/lib/oxsdatatypes/src/lib.rs b/lib/oxsdatatypes/src/lib.rs index ab4ea4e5..437fc099 100644 --- a/lib/oxsdatatypes/src/lib.rs +++ b/lib/oxsdatatypes/src/lib.rs @@ -15,7 +15,7 @@ mod parser; pub use self::boolean::Boolean; pub use self::date_time::{ - Date, DateTime, DateTimeError, GDay, GMonth, GMonthDay, GYear, GYearMonth, Time, + Date, DateTime, DateTimeError, GDay, GMonth, GMonthDay, GYear, GYearMonth, Time, TimezoneOffset, }; pub use self::decimal::{Decimal, DecimalOverflowError, DecimalParseError}; pub use self::double::Double; diff --git a/lib/oxsdatatypes/src/parser.rs b/lib/oxsdatatypes/src/parser.rs index 82caa3f6..8bc169be 100644 --- a/lib/oxsdatatypes/src/parser.rs +++ b/lib/oxsdatatypes/src/parser.rs @@ -490,8 +490,8 @@ fn end_of_day_frag(input: &str) -> XsdResult<'_, (u8, u8, Decimal)> { // [63] timezoneFrag ::= 'Z' | ('+' | '-') (('0' digit | '1' [0-3]) ':' minuteFrag | '14:00') fn timezone_frag(input: &str) -> XsdResult<'_, TimezoneOffset> { alt(( - map(char('Z'), |_| TimezoneOffset::utc()), - map( + map(char('Z'), |_| TimezoneOffset::UTC), + map_res( tuple(( alt((map(char('+'), |_| 1), map(char('-'), |_| -1))), alt((