From 555f6b8d7c7cc6e745580b24cfaa4869ff742c96 Mon Sep 17 00:00:00 2001 From: Tpt Date: Fri, 8 Sep 2023 21:33:37 +0200 Subject: [PATCH] xsd:duration: properly fails when building not-serializable durations P1M1D - P3D is giving 1M and -3D. This is not serializable with xsd:duration formatting --- lib/oxsdatatypes/src/date_time.rs | 4 +- lib/oxsdatatypes/src/duration.rs | 92 +++++++++++++++++++++++++------ lib/oxsdatatypes/src/lib.rs | 3 +- 3 files changed, 80 insertions(+), 19 deletions(-) diff --git a/lib/oxsdatatypes/src/date_time.rs b/lib/oxsdatatypes/src/date_time.rs index 671d00a8..00920b43 100644 --- a/lib/oxsdatatypes/src/date_time.rs +++ b/lib/oxsdatatypes/src/date_time.rs @@ -1877,11 +1877,11 @@ pub fn since_unix_epoch() -> Duration { target_os = "unknown" ))] fn since_unix_epoch() -> Duration { - Duration::new( - 0, + DayTimeDuration::new( Decimal::try_from(crate::Double::from(js_sys::Date::now() / 1000.)) .expect("The current time seems way in the future, it's strange"), ) + .into() } #[cfg(not(any( diff --git a/lib/oxsdatatypes/src/duration.rs b/lib/oxsdatatypes/src/duration.rs index 87be5a22..f8162272 100644 --- a/lib/oxsdatatypes/src/duration.rs +++ b/lib/oxsdatatypes/src/duration.rs @@ -16,12 +16,30 @@ pub struct Duration { impl Duration { #[inline] - #[must_use] - pub fn new(months: impl Into, seconds: impl Into) -> Self { - Self { - year_month: YearMonthDuration::new(months), - day_time: DayTimeDuration::new(seconds), + pub fn new( + months: impl Into, + seconds: impl Into, + ) -> Result { + Self::construct( + YearMonthDuration::new(months), + DayTimeDuration::new(seconds), + ) + } + + #[inline] + fn construct( + year_month: YearMonthDuration, + day_time: DayTimeDuration, + ) -> Result { + if (year_month > YearMonthDuration::default() && day_time < DayTimeDuration::default()) + || (year_month < YearMonthDuration::default() && day_time > DayTimeDuration::default()) + { + return Err(OppositeSignInDurationComponentsError); } + Ok(Self { + year_month, + day_time, + }) } #[inline] @@ -103,10 +121,11 @@ impl Duration { #[must_use] pub fn checked_add(self, rhs: impl Into) -> Option { let rhs = rhs.into(); - Some(Self { - year_month: self.year_month.checked_add(rhs.year_month)?, - day_time: self.day_time.checked_add(rhs.day_time)?, - }) + Self::construct( + self.year_month.checked_add(rhs.year_month)?, + self.day_time.checked_add(rhs.day_time)?, + ) + .ok() } /// [op:subtract-yearMonthDurations](https://www.w3.org/TR/xpath-functions-31/#func-subtract-yearMonthDurations) and [op:subtract-dayTimeDurations](https://www.w3.org/TR/xpath-functions-31/#func-subtract-dayTimeDurations) @@ -116,10 +135,11 @@ impl Duration { #[must_use] pub fn checked_sub(self, rhs: impl Into) -> Option { let rhs = rhs.into(); - Some(Self { - year_month: self.year_month.checked_sub(rhs.year_month)?, - day_time: self.day_time.checked_sub(rhs.day_time)?, - }) + Self::construct( + self.year_month.checked_sub(rhs.year_month)?, + self.day_time.checked_sub(rhs.day_time)?, + ) + .ok() } /// Unary negation. @@ -172,7 +192,7 @@ impl FromStr for Duration { Ok(Self::new( parts.year_month.unwrap_or(0), parts.day_time.unwrap_or_default(), - )) + )?) } } @@ -183,6 +203,9 @@ impl fmt::Display for Duration { let ym = self.year_month.months; let ss = self.day_time.seconds; + if (ym < 0 && ss > 0.into()) || (ym > 0 && ss < 0.into()) { + return Err(fmt::Error); // Not able to format with only a part of the duration that is negative + } if ym < 0 || ss < 0.into() { write!(f, "-")?; } @@ -950,14 +973,35 @@ impl fmt::Display for DurationOverflowError { impl Error for DurationOverflowError {} +/// The year-month and the day-time components of a [`Duration\] have an opposite sign. +#[derive(Debug, Clone, Copy)] +pub struct OppositeSignInDurationComponentsError; + +impl fmt::Display for OppositeSignInDurationComponentsError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "The xsd:yearMonthDuration and xsd:dayTimeDuration components of a xsd:duration can't have opposite sign") + } +} + +impl Error for OppositeSignInDurationComponentsError {} + +impl From for ParseDurationError { + #[inline] + fn from(_: OppositeSignInDurationComponentsError) -> Self { + Self { + msg: "The xsd:yearMonthDuration and xsd:dayTimeDuration components of a xsd:duration can't have opposite sign" + } + } +} + #[cfg(test)] mod tests { use super::*; #[test] fn from_str() -> Result<(), ParseDurationError> { - let min = Duration::new(i64::MIN, Decimal::MIN); - let max = Duration::new(i64::MAX, Decimal::MAX); + let min = Duration::new(i64::MIN, Decimal::MIN)?; + let max = Duration::new(i64::MAX, Decimal::MAX)?; assert_eq!(YearMonthDuration::from_str("P1Y")?.to_string(), "P1Y"); assert_eq!(Duration::from_str("P1Y")?.to_string(), "P1Y"); @@ -1164,6 +1208,14 @@ mod tests { Duration::from_str("P2DT12H5M")?.checked_add(Duration::from_str("P5DT12H")?), Some(Duration::from_str("P8DT5M")?) ); + assert_eq!( + Duration::from_str("P1M2D")?.checked_add(Duration::from_str("-P3D")?), + None + ); + assert_eq!( + Duration::from_str("P1M2D")?.checked_add(Duration::from_str("-P2M")?), + None + ); Ok(()) } @@ -1177,6 +1229,14 @@ mod tests { Duration::from_str("P2DT12H")?.checked_sub(Duration::from_str("P1DT10H30M")?), Some(Duration::from_str("P1DT1H30M")?) ); + assert_eq!( + Duration::from_str("P1M2D")?.checked_sub(Duration::from_str("P3D")?), + None + ); + assert_eq!( + Duration::from_str("P1M2D")?.checked_sub(Duration::from_str("P2M")?), + None + ); Ok(()) } diff --git a/lib/oxsdatatypes/src/lib.rs b/lib/oxsdatatypes/src/lib.rs index a31caf61..336bdd20 100644 --- a/lib/oxsdatatypes/src/lib.rs +++ b/lib/oxsdatatypes/src/lib.rs @@ -20,7 +20,8 @@ pub use self::date_time::{ pub use self::decimal::{Decimal, ParseDecimalError, TooLargeForDecimalError}; pub use self::double::Double; pub use self::duration::{ - DayTimeDuration, Duration, DurationOverflowError, ParseDurationError, YearMonthDuration, + DayTimeDuration, Duration, DurationOverflowError, OppositeSignInDurationComponentsError, + ParseDurationError, YearMonthDuration, }; pub use self::float::Float; pub use self::integer::{Integer, TooLargeForIntegerError};