From 077c1fc1a85bd27b783d78daa06c1b6b215b40b3 Mon Sep 17 00:00:00 2001 From: Tpt Date: Thu, 20 Jul 2023 18:47:45 +0200 Subject: [PATCH] Improves XSD errors and code organization --- lib/oxsdatatypes/README.md | 4 +- lib/oxsdatatypes/src/date_time.rs | 989 +++++++++++++++++++++++------- lib/oxsdatatypes/src/decimal.rs | 299 ++++++--- lib/oxsdatatypes/src/duration.rs | 484 +++++++++++++-- lib/oxsdatatypes/src/integer.rs | 64 +- lib/oxsdatatypes/src/lib.rs | 13 +- lib/oxsdatatypes/src/parser.rs | 626 ------------------- lib/src/sparql/eval.rs | 24 +- 8 files changed, 1492 insertions(+), 1011 deletions(-) delete mode 100644 lib/oxsdatatypes/src/parser.rs diff --git a/lib/oxsdatatypes/README.md b/lib/oxsdatatypes/README.md index a164f282..f198d5ae 100644 --- a/lib/oxsdatatypes/README.md +++ b/lib/oxsdatatypes/README.md @@ -38,10 +38,10 @@ The `DateTime::now()` function needs special OS support. Currently: - If the `custom-now` feature is enabled, a function computing `now` must be set: ```rust - use oxsdatatypes::{DateTimeError, Duration}; + use oxsdatatypes::Duration; #[no_mangle] - fn custom_ox_now() -> Result { + fn custom_ox_now() -> Duration { unimplemented!("now implementation") } ``` diff --git a/lib/oxsdatatypes/src/date_time.rs b/lib/oxsdatatypes/src/date_time.rs index 0bfe29d1..671d00a8 100644 --- a/lib/oxsdatatypes/src/date_time.rs +++ b/lib/oxsdatatypes/src/date_time.rs @@ -1,14 +1,11 @@ -use super::{DayTimeDuration, Decimal, Duration, XsdParseError, YearMonthDuration}; -use crate::parser::{ - parse_date, parse_date_time, parse_g_day, parse_g_month, parse_g_month_day, parse_g_year, - parse_g_year_month, parse_time, -}; +#![allow(clippy::expect_used)] + +use crate::{DayTimeDuration, Decimal, Duration, YearMonthDuration}; use std::cmp::{min, Ordering}; use std::error::Error; use std::fmt; use std::hash::{Hash, Hasher}; use std::str::FromStr; -use std::time::SystemTimeError; /// [XML Schema `dateTime` datatype](https://www.w3.org/TR/xmlschema11-2/#dateTime) /// @@ -29,7 +26,7 @@ impl DateTime { minute: u8, second: Decimal, timezone_offset: Option, - ) -> Result { + ) -> Result { Ok(Self { timestamp: Timestamp::new(&DateTimeSevenPropertyModel { year: Some(year), @@ -45,10 +42,10 @@ impl DateTime { /// [fn:current-dateTime](https://www.w3.org/TR/xpath-functions-31/#func-current-dateTime) #[inline] - pub fn now() -> Result { - Ok(Self { - timestamp: Timestamp::now()?, - }) + pub fn now() -> Self { + Self { + timestamp: Timestamp::now(), + } } #[inline] @@ -134,6 +131,8 @@ impl DateTime { } /// [op:subtract-dateTimes](https://www.w3.org/TR/xpath-functions-31/#func-subtract-dateTimes) + /// + /// Returns `None` in case of overflow ([`FODT0001`](https://www.w3.org/TR/xpath-functions-31/#ERRFODT0001)). #[inline] #[must_use] pub fn checked_sub(self, rhs: impl Into) -> Option { @@ -141,6 +140,8 @@ impl DateTime { } /// [op:add-yearMonthDuration-to-dateTime](https://www.w3.org/TR/xpath-functions-31/#func-add-yearMonthDuration-to-dateTime) + /// + /// Returns `None` in case of overflow ([`FODT0001`](https://www.w3.org/TR/xpath-functions-31/#ERRFODT0001)). #[inline] #[must_use] pub fn checked_add_year_month_duration( @@ -151,6 +152,8 @@ impl DateTime { } /// [op:add-dayTimeDuration-to-dateTime](https://www.w3.org/TR/xpath-functions-31/#func-add-dayTimeDuration-to-dateTime) + /// + /// Returns `None` in case of overflow ([`FODT0001`](https://www.w3.org/TR/xpath-functions-31/#ERRFODT0001)). #[inline] #[must_use] pub fn checked_add_day_time_duration(self, rhs: impl Into) -> Option { @@ -161,6 +164,8 @@ impl DateTime { } /// [op:add-yearMonthDuration-to-dateTime](https://www.w3.org/TR/xpath-functions-31/#func-add-yearMonthDuration-to-dateTime) and [op:add-dayTimeDuration-to-dateTime](https://www.w3.org/TR/xpath-functions-31/#func-add-dayTimeDuration-to-dateTime) + /// + /// Returns `None` in case of overflow ([`FODT0001`](https://www.w3.org/TR/xpath-functions-31/#ERRFODT0001)). #[inline] #[must_use] pub fn checked_add_duration(self, rhs: impl Into) -> Option { @@ -176,6 +181,8 @@ impl DateTime { } /// [op:subtract-yearMonthDuration-from-dateTime](https://www.w3.org/TR/xpath-functions-31/#func-subtract-yearMonthDuration-from-dateTime) + /// + /// Returns `None` in case of overflow ([`FODT0001`](https://www.w3.org/TR/xpath-functions-31/#ERRFODT0001)). #[inline] #[must_use] pub fn checked_sub_year_month_duration( @@ -186,6 +193,8 @@ impl DateTime { } /// [op:subtract-dayTimeDuration-from-dateTime](https://www.w3.org/TR/xpath-functions-31/#func-subtract-dayTimeDuration-from-dateTime) + /// + /// Returns `None` in case of overflow ([`FODT0001`](https://www.w3.org/TR/xpath-functions-31/#ERRFODT0001)). #[inline] #[must_use] pub fn checked_sub_day_time_duration(self, rhs: impl Into) -> Option { @@ -196,6 +205,8 @@ impl DateTime { } /// [op:subtract-yearMonthDuration-from-dateTime](https://www.w3.org/TR/xpath-functions-31/#func-subtract-yearMonthDuration-from-dateTime) and [op:subtract-dayTimeDuration-from-dateTime](https://www.w3.org/TR/xpath-functions-31/#func-subtract-dayTimeDuration-from-dateTime) + /// + /// Returns `None` in case of overflow ([`FODT0001`](https://www.w3.org/TR/xpath-functions-31/#ERRFODT0001)). #[inline] #[must_use] pub fn checked_sub_duration(self, rhs: impl Into) -> Option { @@ -214,6 +225,8 @@ impl DateTime { } /// [fn:adjust-dateTime-to-timezone](https://www.w3.org/TR/xpath-functions-31/#func-adjust-dateTime-to-timezone) + /// + /// Returns `None` in case of overflow ([`FODT0001`](https://www.w3.org/TR/xpath-functions-31/#ERRFODT0001)). #[inline] #[must_use] pub fn adjust(self, timezone_offset: Option) -> Option { @@ -228,14 +241,22 @@ impl DateTime { pub fn is_identical_with(self, other: Self) -> bool { self.timestamp.is_identical_with(other.timestamp) } + + pub const MIN: Self = Self { + timestamp: Timestamp::MIN, + }; + + pub const MAX: Self = Self { + timestamp: Timestamp::MAX, + }; } /// Conversion according to [XPath cast rules](https://www.w3.org/TR/xpath-functions-31/#casting-to-datetimes). impl TryFrom for DateTime { - type Error = DateTimeError; + type Error = DateTimeOverflowError; #[inline] - fn try_from(date: Date) -> Result { + fn try_from(date: Date) -> Result { Self::new( date.year(), date.month(), @@ -249,10 +270,10 @@ impl TryFrom for DateTime { } impl FromStr for DateTime { - type Err = XsdParseError; + type Err = ParseDateTimeError; - fn from_str(input: &str) -> Result { - parse_date_time(input) + fn from_str(input: &str) -> Result { + ensure_complete(input, date_time_lexical_rep) } } @@ -263,6 +284,7 @@ impl fmt::Display for DateTime { if year < 0 { write!(f, "-")?; } + let second = self.second(); write!( f, "{:04}-{:02}-{:02}T{:02}:{:02}:{}{}", @@ -271,12 +293,12 @@ impl fmt::Display for DateTime { self.day(), self.hour(), self.minute(), - if self.second().abs() >= 10.into() { - "" - } else { + if Decimal::from(-10) < second && second < Decimal::from(10) { "0" + } else { + "" }, - self.second() + second )?; if let Some(timezone_offset) = self.timezone_offset() { write!(f, "{timezone_offset}")?; @@ -296,12 +318,12 @@ pub struct Time { impl Time { #[inline] - pub(super) fn new( + fn new( mut hour: u8, minute: u8, second: Decimal, timezone_offset: Option, - ) -> Result { + ) -> Result { if hour == 24 && minute == 0 && second == Decimal::default() { hour = 0; } @@ -328,8 +350,10 @@ impl Time { /// [fn:current-time](https://www.w3.org/TR/xpath-functions-31/#func-current-time) #[inline] - pub fn now() -> Result { - DateTime::now()?.try_into() + pub fn now() -> Self { + Self { + timestamp: Timestamp::now(), + } } /// [fn:hour-from-time](https://www.w3.org/TR/xpath-functions-31/#func-hours-from-time) @@ -373,6 +397,8 @@ impl Time { } /// [op:subtract-times](https://www.w3.org/TR/xpath-functions-31/#func-subtract-times) + /// + /// Returns `None` in case of overflow ([`FODT0001`](https://www.w3.org/TR/xpath-functions-31/#ERRFODT0001)). #[inline] #[must_use] pub fn checked_sub(self, rhs: impl Into) -> Option { @@ -380,6 +406,8 @@ impl Time { } /// [op:add-dayTimeDuration-to-time](https://www.w3.org/TR/xpath-functions-31/#func-add-dayTimeDuration-to-time) + /// + /// Returns `None` in case of overflow ([`FODT0001`](https://www.w3.org/TR/xpath-functions-31/#ERRFODT0001)). #[inline] #[must_use] pub fn checked_add_day_time_duration(self, rhs: impl Into) -> Option { @@ -387,6 +415,8 @@ impl Time { } /// [op:add-dayTimeDuration-to-time](https://www.w3.org/TR/xpath-functions-31/#func-add-dayTimeDuration-to-time) + /// + /// Returns `None` in case of overflow ([`FODT0001`](https://www.w3.org/TR/xpath-functions-31/#ERRFODT0001)). #[inline] #[must_use] pub fn checked_add_duration(self, rhs: impl Into) -> Option { @@ -406,6 +436,8 @@ impl Time { } /// [op:subtract-dayTimeDuration-from-time](https://www.w3.org/TR/xpath-functions-31/#func-subtract-dayTimeDuration-from-time) + /// + /// Returns `None` in case of overflow ([`FODT0001`](https://www.w3.org/TR/xpath-functions-31/#ERRFODT0001)). #[inline] #[must_use] pub fn checked_sub_day_time_duration(self, rhs: impl Into) -> Option { @@ -413,6 +445,8 @@ impl Time { } /// [op:subtract-dayTimeDuration-from-time](https://www.w3.org/TR/xpath-functions-31/#func-subtract-dayTimeDuration-from-time) + /// + /// Returns `None` in case of overflow ([`FODT0001`](https://www.w3.org/TR/xpath-functions-31/#ERRFODT0001)). #[inline] #[must_use] pub fn checked_sub_duration(self, rhs: impl Into) -> Option { @@ -456,45 +490,61 @@ impl Time { pub fn is_identical_with(self, other: Self) -> bool { self.timestamp.is_identical_with(other.timestamp) } + + #[cfg(test)] + const MIN: Self = Self { + timestamp: Timestamp { + value: Decimal::new_from_i128_unchecked(62_230_154_400), + timezone_offset: Some(TimezoneOffset::MAX), + }, + }; + + #[cfg(test)] + const MAX: Self = Self { + timestamp: Timestamp { + value: Decimal::new_from_i128_unchecked(62_230_255_200), + timezone_offset: Some(TimezoneOffset::MIN), + }, + }; } /// Conversion according to [XPath cast rules](https://www.w3.org/TR/xpath-functions-31/#casting-to-datetimes). -impl TryFrom for Time { - type Error = DateTimeError; - +impl From for Time { #[inline] - fn try_from(date_time: DateTime) -> Result { + fn from(date_time: DateTime) -> Self { Self::new( date_time.hour(), date_time.minute(), date_time.second(), date_time.timezone_offset(), ) + .expect("Casting from xsd:dateTime to xsd:date can't fail") } } impl FromStr for Time { - type Err = XsdParseError; + type Err = ParseDateTimeError; - fn from_str(input: &str) -> Result { - parse_time(input) + fn from_str(input: &str) -> Result { + ensure_complete(input, time_lexical_rep) } } impl fmt::Display for Time { #[inline] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let second = self.second(); write!( f, "{:02}:{:02}:{}{}", self.hour(), self.minute(), - if self.second().abs() >= 10.into() { - "" - } else { + if Decimal::from(-10) < second && second < Decimal::from(10) { "0" + } else { + "" }, - self.second() + second )?; if let Some(timezone_offset) = self.timezone_offset() { write!(f, "{timezone_offset}")?; @@ -514,12 +564,12 @@ pub struct Date { impl Date { #[inline] - pub(super) fn new( + fn new( year: i64, month: u8, day: u8, timezone_offset: Option, - ) -> Result { + ) -> Result { Ok(Self { timestamp: Timestamp::new(&DateTimeSevenPropertyModel { year: Some(year), @@ -543,8 +593,10 @@ impl Date { /// [fn:current-date](https://www.w3.org/TR/xpath-functions-31/#func-current-date) #[inline] - pub fn now() -> Result { - DateTime::now()?.try_into() + pub fn now() -> Self { + DateTime::now() + .try_into() + .expect("The current time seems way in the future, it's strange") } /// [fn:year-from-date](https://www.w3.org/TR/xpath-functions-31/#func-year-from-date) @@ -588,6 +640,8 @@ impl Date { } /// [op:subtract-dates](https://www.w3.org/TR/xpath-functions-31/#func-subtract-dates) + /// + /// Returns `None` in case of overflow ([`FODT0001`](https://www.w3.org/TR/xpath-functions-31/#ERRFODT0001)). #[inline] #[must_use] pub fn checked_sub(self, rhs: impl Into) -> Option { @@ -595,6 +649,8 @@ impl Date { } /// [op:add-yearMonthDuration-to-date](https://www.w3.org/TR/xpath-functions-31/#func-add-yearMonthDuration-to-date) + /// + /// Returns `None` in case of overflow ([`FODT0001`](https://www.w3.org/TR/xpath-functions-31/#ERRFODT0001)). #[inline] #[must_use] pub fn checked_add_year_month_duration( @@ -605,6 +661,8 @@ impl Date { } /// [op:add-dayTimeDuration-to-dateTime](https://www.w3.org/TR/xpath-functions-31/#func-add-dayTimeDuration-to-date) + /// + /// Returns `None` in case of overflow ([`FODT0001`](https://www.w3.org/TR/xpath-functions-31/#ERRFODT0001)). #[inline] #[must_use] pub fn checked_add_day_time_duration(self, rhs: impl Into) -> Option { @@ -612,6 +670,8 @@ impl Date { } /// [op:add-yearMonthDuration-to-date](https://www.w3.org/TR/xpath-functions-31/#func-add-yearMonthDuration-to-date) and [op:add-dayTimeDuration-to-dateTime](https://www.w3.org/TR/xpath-functions-31/#func-add-dayTimeDuration-to-date) + /// + /// Returns `None` in case of overflow ([`FODT0001`](https://www.w3.org/TR/xpath-functions-31/#ERRFODT0001)). #[inline] #[must_use] pub fn checked_add_duration(self, rhs: impl Into) -> Option { @@ -623,6 +683,8 @@ impl Date { } /// [op:subtract-yearMonthDuration-from-date](https://www.w3.org/TR/xpath-functions-31/#func-subtract-yearMonthDuration-from-date) + /// + /// Returns `None` in case of overflow ([`FODT0001`](https://www.w3.org/TR/xpath-functions-31/#ERRFODT0001)). #[inline] #[must_use] pub fn checked_sub_year_month_duration( @@ -633,6 +695,8 @@ impl Date { } /// [op:subtract-dayTimeDuration-from-date](https://www.w3.org/TR/xpath-functions-31/#func-subtract-dayTimeDuration-from-date) + /// + /// Returns `None` in case of overflow ([`FODT0001`](https://www.w3.org/TR/xpath-functions-31/#ERRFODT0001)). #[inline] #[must_use] pub fn checked_sub_day_time_duration(self, rhs: impl Into) -> Option { @@ -675,14 +739,27 @@ impl Date { pub fn is_identical_with(self, other: Self) -> bool { self.timestamp.is_identical_with(other.timestamp) } + + pub const MIN: Self = Self { + timestamp: Timestamp { + value: Decimal::new_from_i128_unchecked(-170_141_183_460_469_216_800), + timezone_offset: Some(TimezoneOffset::MIN), + }, + }; + pub const MAX: Self = Self { + timestamp: Timestamp { + value: Decimal::new_from_i128_unchecked(170_141_183_460_469_216_800), + timezone_offset: Some(TimezoneOffset::MAX), + }, + }; } /// Conversion according to [XPath cast rules](https://www.w3.org/TR/xpath-functions-31/#casting-to-datetimes). impl TryFrom for Date { - type Error = DateTimeError; + type Error = DateTimeOverflowError; #[inline] - fn try_from(date_time: DateTime) -> Result { + fn try_from(date_time: DateTime) -> Result { Self::new( date_time.year(), date_time.month(), @@ -693,10 +770,10 @@ impl TryFrom for Date { } impl FromStr for Date { - type Err = XsdParseError; + type Err = ParseDateTimeError; - fn from_str(input: &str) -> Result { - parse_date(input) + fn from_str(input: &str) -> Result { + ensure_complete(input, date_lexical_rep) } } @@ -726,11 +803,11 @@ pub struct GYearMonth { impl GYearMonth { #[inline] - pub(super) fn new( + fn new( year: i64, month: u8, timezone_offset: Option, - ) -> Result { + ) -> Result { Ok(Self { timestamp: Timestamp::new(&DateTimeSevenPropertyModel { year: Some(year), @@ -796,14 +873,27 @@ impl GYearMonth { pub fn is_identical_with(self, other: Self) -> bool { self.timestamp.is_identical_with(other.timestamp) } + + pub const MIN: Self = Self { + timestamp: Timestamp { + value: Decimal::new_from_i128_unchecked(-170_141_183_460_466_970_400), + timezone_offset: Some(TimezoneOffset::MIN), + }, + }; + pub const MAX: Self = Self { + timestamp: Timestamp { + value: Decimal::new_from_i128_unchecked(170_141_183_460_469_216_800), + timezone_offset: Some(TimezoneOffset::MAX), + }, + }; } /// Conversion according to [XPath cast rules](https://www.w3.org/TR/xpath-functions-31/#casting-to-datetimes). impl TryFrom for GYearMonth { - type Error = DateTimeError; + type Error = DateTimeOverflowError; #[inline] - fn try_from(date_time: DateTime) -> Result { + fn try_from(date_time: DateTime) -> Result { Self::new( date_time.year(), date_time.month(), @@ -813,20 +903,19 @@ impl TryFrom for GYearMonth { } /// Conversion according to [XPath cast rules](https://www.w3.org/TR/xpath-functions-31/#casting-to-datetimes). -impl TryFrom for GYearMonth { - type Error = DateTimeError; - +impl From for GYearMonth { #[inline] - fn try_from(date: Date) -> Result { + fn from(date: Date) -> Self { Self::new(date.year(), date.month(), date.timezone_offset()) + .expect("Casting from xsd:date to xsd:gYearMonth can't fail") } } impl FromStr for GYearMonth { - type Err = XsdParseError; + type Err = ParseDateTimeError; - fn from_str(input: &str) -> Result { - parse_g_year_month(input) + fn from_str(input: &str) -> Result { + ensure_complete(input, g_year_month_lexical_rep) } } @@ -856,10 +945,10 @@ pub struct GYear { impl GYear { #[inline] - pub(super) fn new( + fn new( year: i64, timezone_offset: Option, - ) -> Result { + ) -> Result { Ok(Self { timestamp: Timestamp::new(&DateTimeSevenPropertyModel { year: Some(year), @@ -919,42 +1008,55 @@ impl GYear { pub fn is_identical_with(self, other: Self) -> bool { self.timestamp.is_identical_with(other.timestamp) } + + pub const MIN: Self = Self { + timestamp: Timestamp { + value: Decimal::new_from_i128_unchecked(-170_141_183_460_461_700_000), + timezone_offset: Some(TimezoneOffset::MIN), + }, + }; + pub const MAX: Self = Self { + timestamp: Timestamp { + value: Decimal::new_from_i128_unchecked(170_141_183_460_461_440_800), + timezone_offset: Some(TimezoneOffset::MAX), + }, + }; } /// Conversion according to [XPath cast rules](https://www.w3.org/TR/xpath-functions-31/#casting-to-datetimes). impl TryFrom for GYear { - type Error = DateTimeError; + type Error = DateTimeOverflowError; #[inline] - fn try_from(date_time: DateTime) -> Result { + fn try_from(date_time: DateTime) -> Result { Self::new(date_time.year(), date_time.timezone_offset()) } } /// Conversion according to [XPath cast rules](https://www.w3.org/TR/xpath-functions-31/#casting-to-datetimes). impl TryFrom for GYear { - type Error = DateTimeError; + type Error = DateTimeOverflowError; #[inline] - fn try_from(date: Date) -> Result { + fn try_from(date: Date) -> Result { Self::new(date.year(), date.timezone_offset()) } } impl TryFrom for GYear { - type Error = DateTimeError; + type Error = DateTimeOverflowError; #[inline] - fn try_from(year_month: GYearMonth) -> Result { + fn try_from(year_month: GYearMonth) -> Result { Self::new(year_month.year(), year_month.timezone_offset()) } } impl FromStr for GYear { - type Err = XsdParseError; + type Err = ParseDateTimeError; - fn from_str(input: &str) -> Result { - parse_g_year(input) + fn from_str(input: &str) -> Result { + ensure_complete(input, g_year_lexical_rep) } } @@ -984,11 +1086,11 @@ pub struct GMonthDay { impl GMonthDay { #[inline] - pub(super) fn new( + fn new( month: u8, day: u8, timezone_offset: Option, - ) -> Result { + ) -> Result { Ok(Self { timestamp: Timestamp::new(&DateTimeSevenPropertyModel { year: None, @@ -1057,34 +1159,32 @@ impl GMonthDay { } /// Conversion according to [XPath cast rules](https://www.w3.org/TR/xpath-functions-31/#casting-to-datetimes). -impl TryFrom for GMonthDay { - type Error = DateTimeError; - +impl From for GMonthDay { #[inline] - fn try_from(date_time: DateTime) -> Result { + fn from(date_time: DateTime) -> Self { Self::new( date_time.month(), date_time.day(), date_time.timezone_offset(), ) + .expect("Casting from xsd:dateTime to xsd:gMonthDay can't fail") } } /// Conversion according to [XPath cast rules](https://www.w3.org/TR/xpath-functions-31/#casting-to-datetimes). -impl TryFrom for GMonthDay { - type Error = DateTimeError; - +impl From for GMonthDay { #[inline] - fn try_from(date: Date) -> Result { + fn from(date: Date) -> Self { Self::new(date.month(), date.day(), date.timezone_offset()) + .expect("Casting from xsd:date to xsd:gMonthDay can't fail") } } impl FromStr for GMonthDay { - type Err = XsdParseError; + type Err = ParseDateTimeError; - fn from_str(input: &str) -> Result { - parse_g_month_day(input) + fn from_str(input: &str) -> Result { + ensure_complete(input, g_month_day_lexical_rep) } } @@ -1110,10 +1210,10 @@ pub struct GMonth { impl GMonth { #[inline] - pub(super) fn new( + fn new( month: u8, timezone_offset: Option, - ) -> Result { + ) -> Result { Ok(Self { timestamp: Timestamp::new(&DateTimeSevenPropertyModel { year: None, @@ -1176,48 +1276,44 @@ impl GMonth { } /// Conversion according to [XPath cast rules](https://www.w3.org/TR/xpath-functions-31/#casting-to-datetimes). -impl TryFrom for GMonth { - type Error = DateTimeError; - +impl From for GMonth { #[inline] - fn try_from(date_time: DateTime) -> Result { + fn from(date_time: DateTime) -> Self { Self::new(date_time.month(), date_time.timezone_offset()) + .expect("Casting from xsd:dateTime to xsd:gMonth can't fail") } } /// Conversion according to [XPath cast rules](https://www.w3.org/TR/xpath-functions-31/#casting-to-datetimes). -impl TryFrom for GMonth { - type Error = DateTimeError; - +impl From for GMonth { #[inline] - fn try_from(date: Date) -> Result { + fn from(date: Date) -> Self { Self::new(date.month(), date.timezone_offset()) + .expect("Casting from xsd:date to xsd:gMonth can't fail") } } -impl TryFrom for GMonth { - type Error = DateTimeError; - +impl From for GMonth { #[inline] - fn try_from(year_month: GYearMonth) -> Result { + fn from(year_month: GYearMonth) -> Self { Self::new(year_month.month(), year_month.timezone_offset()) + .expect("Casting from xsd:gYearMonth to xsd:gMonth can't fail") } } -impl TryFrom for GMonth { - type Error = DateTimeError; - +impl From for GMonth { #[inline] - fn try_from(month_day: GMonthDay) -> Result { + fn from(month_day: GMonthDay) -> Self { Self::new(month_day.month(), month_day.timezone_offset()) + .expect("Casting from xsd:gMonthDay to xsd:gMonth can't fail") } } impl FromStr for GMonth { - type Err = XsdParseError; + type Err = ParseDateTimeError; - fn from_str(input: &str) -> Result { - parse_g_month(input) + fn from_str(input: &str) -> Result { + ensure_complete(input, g_month_lexical_rep) } } @@ -1243,10 +1339,10 @@ pub struct GDay { impl GDay { #[inline] - pub(super) fn new( + fn new( day: u8, timezone_offset: Option, - ) -> Result { + ) -> Result { Ok(Self { timestamp: Timestamp::new(&DateTimeSevenPropertyModel { year: None, @@ -1309,39 +1405,36 @@ impl GDay { } /// Conversion according to [XPath cast rules](https://www.w3.org/TR/xpath-functions-31/#casting-to-datetimes). -impl TryFrom for GDay { - type Error = DateTimeError; - +impl From for GDay { #[inline] - fn try_from(date_time: DateTime) -> Result { + fn from(date_time: DateTime) -> Self { Self::new(date_time.day(), date_time.timezone_offset()) + .expect("Casting from xsd:dateTime to xsd:gDay can't fail") } } /// Conversion according to [XPath cast rules](https://www.w3.org/TR/xpath-functions-31/#casting-to-datetimes). -impl TryFrom for GDay { - type Error = DateTimeError; - +impl From for GDay { #[inline] - fn try_from(date: Date) -> Result { + fn from(date: Date) -> Self { Self::new(date.day(), date.timezone_offset()) + .expect("Casting from xsd:date to xsd:gDay can't fail") } } -impl TryFrom for GDay { - type Error = DateTimeError; - +impl From for GDay { #[inline] - fn try_from(month_day: GMonthDay) -> Result { + fn from(month_day: GMonthDay) -> Self { Self::new(month_day.day(), month_day.timezone_offset()) + .expect("Casting from xsd:gMonthDay to xsd:gDay can't fail") } } impl FromStr for GDay { - type Err = XsdParseError; + type Err = ParseDateTimeError; - fn from_str(input: &str) -> Result { - parse_g_day(input) + fn from_str(input: &str) -> Result { + ensure_complete(input, g_day_lexical_rep) } } @@ -1367,14 +1460,16 @@ pub struct TimezoneOffset { impl TimezoneOffset { /// From offset in minute with respect to UTC #[inline] - pub fn new(offset_in_minutes: i16) -> Result { + 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) + Err(InvalidTimezoneError { + offset_in_minutes: offset_in_minutes.into(), + }) } } @@ -1398,31 +1493,34 @@ impl TimezoneOffset { } impl TryFrom for TimezoneOffset { - type Error = DateTimeError; + type Error = InvalidTimezoneError; #[inline] - fn try_from(value: DayTimeDuration) -> Result { + fn try_from(value: DayTimeDuration) -> Result { + let offset_in_minutes = value.minutes() + value.hours() * 60; let result = Self::new( - (value.minutes() + value.hours() * 60) + offset_in_minutes .try_into() - .map_err(|_| DATE_TIME_OVERFLOW)?, + .map_err(|_| InvalidTimezoneError { offset_in_minutes })?, )?; if DayTimeDuration::from(result) == value { Ok(result) } else { // The value is not an integral number of minutes or overflow problems - Err(DATE_TIME_OVERFLOW) + Err(InvalidTimezoneError { offset_in_minutes }) } } } impl TryFrom for TimezoneOffset { - type Error = DateTimeError; + type Error = InvalidTimezoneError; #[inline] - fn try_from(value: Duration) -> Result { + fn try_from(value: Duration) -> Result { DayTimeDuration::try_from(value) - .map_err(|_| DATE_TIME_OVERFLOW)? + .map_err(|_| InvalidTimezoneError { + offset_in_minutes: 0, + })? .try_into() } } @@ -1522,28 +1620,18 @@ impl Hash for Timestamp { impl Timestamp { #[inline] - fn new(props: &DateTimeSevenPropertyModel) -> Result { - // Validation - if let (Some(day), Some(month)) = (props.day, props.month) { - // Constraint: Day-of-month Values - if day > days_in_month(props.year, month) { - return Err(DateTimeError { - kind: DateTimeErrorKind::InvalidDayOfMonth { day, month }, - }); - } - } - + fn new(props: &DateTimeSevenPropertyModel) -> Result { Ok(Self { timezone_offset: props.timezone_offset, - value: time_on_timeline(props).ok_or(DATE_TIME_OVERFLOW)?, + value: time_on_timeline(props).ok_or(DateTimeOverflowError)?, }) } #[inline] - fn now() -> Result { + fn now() -> Self { Self::new( &date_time_plus_duration( - since_unix_epoch()?, + since_unix_epoch(), &DateTimeSevenPropertyModel { year: Some(1970), month: Some(1), @@ -1554,8 +1642,9 @@ impl Timestamp { timezone_offset: Some(TimezoneOffset::UTC), }, ) - .ok_or(DATE_TIME_OVERFLOW)?, + .expect("The current time seems way in the future, it's strange"), ) + .expect("The current time seems way in the future, it's strange") } #[inline] @@ -1669,7 +1758,11 @@ impl Timestamp { #[inline] #[must_use] fn second(&self) -> Decimal { - self.value.checked_rem_euclid(60).unwrap().abs() + self.value + .checked_rem_euclid(60) + .unwrap() + .checked_abs() + .unwrap() } #[inline] @@ -1755,13 +1848,23 @@ impl Timestamp { pub fn is_identical_with(self, other: Self) -> bool { self.value == other.value && self.timezone_offset == other.timezone_offset } + + pub const MIN: Self = Self { + value: Decimal::MIN, + timezone_offset: Some(TimezoneOffset::MIN), + }; + + pub const MAX: Self = Self { + value: Decimal::MAX, + timezone_offset: Some(TimezoneOffset::MAX), + }; } #[cfg(feature = "custom-now")] #[allow(unsafe_code)] -pub fn since_unix_epoch() -> Result { +pub fn since_unix_epoch() -> Duration { extern "Rust" { - fn custom_ox_now() -> Result; + fn custom_ox_now() -> Duration; } unsafe { custom_ox_now() } @@ -1773,25 +1876,26 @@ pub fn since_unix_epoch() -> Result { target_family = "wasm", target_os = "unknown" ))] -fn since_unix_epoch() -> Result { - Ok(Duration::new( +fn since_unix_epoch() -> Duration { + Duration::new( 0, Decimal::try_from(crate::Double::from(js_sys::Date::now() / 1000.)) - .map_err(|_| DATE_TIME_OVERFLOW)?, - )) + .expect("The current time seems way in the future, it's strange"), + ) } #[cfg(not(any( feature = "custom-now", all(feature = "js", target_family = "wasm", target_os = "unknown") )))] -fn since_unix_epoch() -> Result { +fn since_unix_epoch() -> Duration { use std::time::SystemTime; SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH)? + .duration_since(SystemTime::UNIX_EPOCH) + .expect("System time before UNIX epoch") .try_into() - .map_err(|_| DATE_TIME_OVERFLOW) + .expect("The current time seems way in the future, it's strange") } /// The [normalizeMonth](https://www.w3.org/TR/xmlschema11-2/#f-dt-normMo) function @@ -1933,61 +2037,424 @@ fn time_on_timeline(props: &DateTimeSevenPropertyModel) -> Option { .checked_add(se) } -/// An error when doing [`DateTime`] operations. +/// A parsing error #[derive(Debug, Clone)] -pub struct DateTimeError { - kind: DateTimeErrorKind, +pub struct ParseDateTimeError { + kind: ParseDateTimeErrorKind, } #[derive(Debug, Clone)] -enum DateTimeErrorKind { +enum ParseDateTimeErrorKind { InvalidDayOfMonth { day: u8, month: u8 }, - Overflow, - SystemTime(SystemTimeError), + Overflow(DateTimeOverflowError), + InvalidTimezone(InvalidTimezoneError), + Message(&'static str), } -impl fmt::Display for DateTimeError { - #[inline] +impl fmt::Display for ParseDateTimeError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match &self.kind { - DateTimeErrorKind::InvalidDayOfMonth { day, month } => { + ParseDateTimeErrorKind::InvalidDayOfMonth { day, month } => { write!(f, "{day} is not a valid day of {month}") } - DateTimeErrorKind::Overflow => write!(f, "Overflow during date time normalization"), - DateTimeErrorKind::SystemTime(error) => error.fmt(f), + ParseDateTimeErrorKind::Overflow(error) => error.fmt(f), + ParseDateTimeErrorKind::InvalidTimezone(error) => error.fmt(f), + ParseDateTimeErrorKind::Message(msg) => write!(f, "{msg}"), } } } -impl Error for DateTimeError { - #[inline] - fn source(&self) -> Option<&(dyn Error + 'static)> { - match &self.kind { - DateTimeErrorKind::SystemTime(error) => Some(error), - _ => None, +impl ParseDateTimeError { + const fn msg(message: &'static str) -> Self { + Self { + kind: ParseDateTimeErrorKind::Message(message), } } } -impl From for DateTimeError { - #[inline] - fn from(error: SystemTimeError) -> Self { - Self { - kind: DateTimeErrorKind::SystemTime(error), +impl Error for ParseDateTimeError {} + +// [16] dateTimeLexicalRep ::= yearFrag '-' monthFrag '-' dayFrag 'T' ((hourFrag ':' minuteFrag ':' secondFrag) | endOfDayFrag) timezoneFrag? +fn date_time_lexical_rep(input: &str) -> Result<(DateTime, &str), ParseDateTimeError> { + let (year, input) = year_frag(input)?; + let input = expect_char(input, '-', "The year and month must be separated by '-'")?; + let (month, input) = month_frag(input)?; + let input = expect_char(input, '-', "The month and day must be separated by '-'")?; + let (day, input) = day_frag(input)?; + let input = expect_char(input, 'T', "The date and time must be separated by 'T'")?; + let (hour, input) = hour_frag(input)?; + let input = expect_char(input, ':', "The hours and minutes must be separated by ':'")?; + let (minute, input) = minute_frag(input)?; + let input = expect_char( + input, + ':', + "The minutes and seconds must be separated by ':'", + )?; + let (second, input) = second_frag(input)?; + // We validate 24:00:00 + if hour == 24 && minute != 0 && second != Decimal::from(0) { + return Err(ParseDateTimeError::msg( + "Times are not allowed to be after 24:00:00", + )); + } + let (timezone_offset, input) = optional_end(input, timezone_frag)?; + validate_day_of_month(Some(year), month, day)?; + Ok(( + DateTime::new(year, month, day, hour, minute, second, timezone_offset)?, + input, + )) +} + +// [17] timeLexicalRep ::= ((hourFrag ':' minuteFrag ':' secondFrag) | endOfDayFrag) timezoneFrag? +fn time_lexical_rep(input: &str) -> Result<(Time, &str), ParseDateTimeError> { + let (hour, input) = hour_frag(input)?; + let input = expect_char(input, ':', "The hours and minutes must be separated by ':'")?; + let (minute, input) = minute_frag(input)?; + let input = expect_char( + input, + ':', + "The minutes and seconds must be separated by ':'", + )?; + let (second, input) = second_frag(input)?; + // We validate 24:00:00 + if hour == 24 && minute != 0 && second != Decimal::from(0) { + return Err(ParseDateTimeError::msg( + "Times are not allowed to be after 24:00:00", + )); + } + let (timezone_offset, input) = optional_end(input, timezone_frag)?; + Ok((Time::new(hour, minute, second, timezone_offset)?, input)) +} + +// [18] dateLexicalRep ::= yearFrag '-' monthFrag '-' dayFrag timezoneFrag? Constraint: Day-of-month Representations +fn date_lexical_rep(input: &str) -> Result<(Date, &str), ParseDateTimeError> { + let (year, input) = year_frag(input)?; + let input = expect_char(input, '-', "The year and month must be separated by '-'")?; + let (month, input) = month_frag(input)?; + let input = expect_char(input, '-', "The month and day must be separated by '-'")?; + let (day, input) = day_frag(input)?; + let (timezone_offset, input) = optional_end(input, timezone_frag)?; + validate_day_of_month(Some(year), month, day)?; + Ok((Date::new(year, month, day, timezone_offset)?, input)) +} + +// [19] gYearMonthLexicalRep ::= yearFrag '-' monthFrag timezoneFrag? +fn g_year_month_lexical_rep(input: &str) -> Result<(GYearMonth, &str), ParseDateTimeError> { + let (year, input) = year_frag(input)?; + let input = expect_char(input, '-', "The year and month must be separated by '-'")?; + let (month, input) = month_frag(input)?; + let (timezone_offset, input) = optional_end(input, timezone_frag)?; + Ok((GYearMonth::new(year, month, timezone_offset)?, input)) +} + +// [20] gYearLexicalRep ::= yearFrag timezoneFrag? +fn g_year_lexical_rep(input: &str) -> Result<(GYear, &str), ParseDateTimeError> { + let (year, input) = year_frag(input)?; + let (timezone_offset, input) = optional_end(input, timezone_frag)?; + Ok((GYear::new(year, timezone_offset)?, input)) +} + +// [21] gMonthDayLexicalRep ::= '--' monthFrag '-' dayFrag timezoneFrag? Constraint: Day-of-month Representations +fn g_month_day_lexical_rep(input: &str) -> Result<(GMonthDay, &str), ParseDateTimeError> { + let input = expect_char(input, '-', "gMonthDay values must start with '--'")?; + let input = expect_char(input, '-', "gMonthDay values must start with '--'")?; + let (month, input) = month_frag(input)?; + let input = expect_char(input, '-', "The month and day must be separated by '-'")?; + let (day, input) = day_frag(input)?; + let (timezone_offset, input) = optional_end(input, timezone_frag)?; + validate_day_of_month(None, month, day)?; + Ok((GMonthDay::new(month, day, timezone_offset)?, input)) +} + +// [22] gDayLexicalRep ::= '---' dayFrag timezoneFrag? +fn g_day_lexical_rep(input: &str) -> Result<(GDay, &str), ParseDateTimeError> { + let input = expect_char(input, '-', "gDay values must start with '---'")?; + let input = expect_char(input, '-', "gDay values must start with '---'")?; + let input = expect_char(input, '-', "gDay values must start with '---'")?; + let (day, input) = day_frag(input)?; + let (timezone_offset, input) = optional_end(input, timezone_frag)?; + Ok((GDay::new(day, timezone_offset)?, input)) +} + +// [23] gMonthLexicalRep ::= '--' monthFrag timezoneFrag? +fn g_month_lexical_rep(input: &str) -> Result<(GMonth, &str), ParseDateTimeError> { + let input = expect_char(input, '-', "gMonth values must start with '--'")?; + let input = expect_char(input, '-', "gMonth values must start with '--'")?; + let (month, input) = month_frag(input)?; + let (timezone_offset, input) = optional_end(input, timezone_frag)?; + Ok((GMonth::new(month, timezone_offset)?, input)) +} + +// [56] yearFrag ::= '-'? (([1-9] digit digit digit+)) | ('0' digit digit digit)) +fn year_frag(input: &str) -> Result<(i64, &str), ParseDateTimeError> { + let (sign, input) = if let Some(left) = input.strip_prefix('-') { + (-1, left) + } else { + (1, input) + }; + let (number_str, input) = integer_prefix(input); + if number_str.len() < 4 { + return Err(ParseDateTimeError::msg( + "The year should be encoded on 4 digits", + )); + } + if number_str.len() > 4 && number_str.starts_with('0') { + return Err(ParseDateTimeError::msg( + "The years value must not start with 0 if it can be encoded in at least 4 digits", + )); + } + let number = i64::from_str(number_str).expect("valid integer"); + Ok((sign * number, input)) +} + +// [57] monthFrag ::= ('0' [1-9]) | ('1' [0-2]) +fn month_frag(input: &str) -> Result<(u8, &str), ParseDateTimeError> { + let (number_str, input) = integer_prefix(input); + if number_str.len() != 2 { + return Err(ParseDateTimeError::msg( + "Month must be encoded with two digits", + )); + } + let number = u8::from_str(number_str).expect("valid integer"); + if !(1..=12).contains(&number) { + return Err(ParseDateTimeError::msg("Month must be between 01 and 12")); + } + Ok((number, input)) +} + +// [58] dayFrag ::= ('0' [1-9]) | ([12] digit) | ('3' [01]) +fn day_frag(input: &str) -> Result<(u8, &str), ParseDateTimeError> { + let (number_str, input) = integer_prefix(input); + if number_str.len() != 2 { + return Err(ParseDateTimeError::msg( + "Day must be encoded with two digits", + )); + } + let number = u8::from_str(number_str).expect("valid integer"); + if !(1..=31).contains(&number) { + return Err(ParseDateTimeError::msg("Day must be between 01 and 31")); + } + Ok((number, input)) +} + +// [59] hourFrag ::= ([01] digit) | ('2' [0-3]) +// We also allow 24 for ease of parsing +fn hour_frag(input: &str) -> Result<(u8, &str), ParseDateTimeError> { + let (number_str, input) = integer_prefix(input); + if number_str.len() != 2 { + return Err(ParseDateTimeError::msg( + "Hours must be encoded with two digits", + )); + } + let number = u8::from_str(number_str).expect("valid integer"); + if !(0..=24).contains(&number) { + return Err(ParseDateTimeError::msg("Hours must be between 00 and 24")); + } + Ok((number, input)) +} + +// [60] minuteFrag ::= [0-5] digit +fn minute_frag(input: &str) -> Result<(u8, &str), ParseDateTimeError> { + let (number_str, input) = integer_prefix(input); + if number_str.len() != 2 { + return Err(ParseDateTimeError::msg( + "Minutes must be encoded with two digits", + )); + } + let number = u8::from_str(number_str).expect("valid integer"); + if !(0..=59).contains(&number) { + return Err(ParseDateTimeError::msg("Minutes must be between 00 and 59")); + } + Ok((number, input)) +} + +// [61] secondFrag ::= ([0-5] digit) ('.' digit+)? +fn second_frag(input: &str) -> Result<(Decimal, &str), ParseDateTimeError> { + let (number_str, input) = decimal_prefix(input); + let (before_dot_str, _) = number_str.split_once('.').unwrap_or((number_str, "")); + if before_dot_str.len() != 2 { + return Err(ParseDateTimeError::msg( + "Seconds must be encoded with two digits", + )); + } + let number = Decimal::from_str(number_str) + .map_err(|_| ParseDateTimeError::msg("The second precision is too large"))?; + if number < Decimal::from(0) || number >= Decimal::from(60) { + return Err(ParseDateTimeError::msg("Seconds must be between 00 and 60")); + } + if number_str.ends_with('.') { + return Err(ParseDateTimeError::msg( + "Seconds are not allowed to end with a dot", + )); + } + Ok((number, input)) +} + +// [63] timezoneFrag ::= 'Z' | ('+' | '-') (('0' digit | '1' [0-3]) ':' minuteFrag | '14:00') +fn timezone_frag(input: &str) -> Result<(TimezoneOffset, &str), ParseDateTimeError> { + if let Some(left) = input.strip_prefix('Z') { + return Ok((TimezoneOffset::UTC, left)); + } + let (sign, input) = if let Some(left) = input.strip_prefix('-') { + (-1, left) + } else if let Some(left) = input.strip_prefix('+') { + (1, left) + } else { + (1, input) + }; + + let (hour_str, input) = integer_prefix(input); + if hour_str.len() != 2 { + return Err(ParseDateTimeError::msg( + "The timezone hours must be encoded with two digits", + )); + } + let hours = i16::from_str(hour_str).expect("valid integer"); + + let input = expect_char( + input, + ':', + "The timezone hours and minutes must be separated by ':'", + )?; + let (minutes, input) = minute_frag(input)?; + + if hours > 13 && !(hours == 14 && minutes == 0) { + return Err(ParseDateTimeError::msg( + "The timezone hours must be between 00 and 13", + )); + } + + Ok(( + TimezoneOffset::new(sign * (hours * 60 + i16::from(minutes))).map_err(|e| { + ParseDateTimeError { + kind: ParseDateTimeErrorKind::InvalidTimezone(e), + } + })?, + input, + )) +} + +fn ensure_complete( + input: &str, + parse: impl FnOnce(&str) -> Result<(T, &str), ParseDateTimeError>, +) -> Result { + let (result, left) = parse(input)?; + if !left.is_empty() { + return Err(ParseDateTimeError::msg("Unrecognized value suffix")); + } + Ok(result) +} + +fn expect_char<'a>( + input: &'a str, + constant: char, + error_message: &'static str, +) -> Result<&'a str, ParseDateTimeError> { + if let Some(left) = input.strip_prefix(constant) { + Ok(left) + } else { + Err(ParseDateTimeError::msg(error_message)) + } +} + +fn integer_prefix(input: &str) -> (&str, &str) { + let mut end = input.len(); + for (i, c) in input.char_indices() { + if !c.is_ascii_digit() { + end = i; + break; + } + } + input.split_at(end) +} + +fn decimal_prefix(input: &str) -> (&str, &str) { + let mut end = input.len(); + let mut dot_seen = false; + for (i, c) in input.char_indices() { + if c.is_ascii_digit() { + // Ok + } else if c == '.' && !dot_seen { + dot_seen = true; + } else { + end = i; + break; + } + } + input.split_at(end) +} + +fn optional_end( + input: &str, + parse: impl FnOnce(&str) -> Result<(T, &str), ParseDateTimeError>, +) -> Result<(Option, &str), ParseDateTimeError> { + Ok(if input.is_empty() { + (None, input) + } else { + let (result, input) = parse(input)?; + (Some(result), input) + }) +} + +fn validate_day_of_month(year: Option, month: u8, day: u8) -> Result<(), ParseDateTimeError> { + // Constraint: Day-of-month Values + if day > days_in_month(year, month) { + return Err(ParseDateTimeError { + kind: ParseDateTimeErrorKind::InvalidDayOfMonth { day, month }, + }); + } + Ok(()) +} + +/// An overflow during [`DateTime`]-related operations. +/// +/// Matches XPath [`FODT0001` error](https://www.w3.org/TR/xpath-functions-31/#ERRFODT0001). +#[derive(Debug, Clone, Copy)] +pub struct DateTimeOverflowError; + +impl fmt::Display for DateTimeOverflowError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "overflow during xsd:dateTime computation") + } +} + +impl Error for DateTimeOverflowError {} + +impl From for ParseDateTimeError { + fn from(error: DateTimeOverflowError) -> Self { + ParseDateTimeError { + kind: ParseDateTimeErrorKind::Overflow(error), } } } -const DATE_TIME_OVERFLOW: DateTimeError = DateTimeError { - kind: DateTimeErrorKind::Overflow, -}; +/// The value provided as timezone is not valid. +/// +/// Matches XPath [`FODT0003` error](https://www.w3.org/TR/xpath-functions-31/#ERRFODT0003). +#[derive(Debug, Clone, Copy)] +pub struct InvalidTimezoneError { + offset_in_minutes: i64, +} + +impl fmt::Display for InvalidTimezoneError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "invalid timezone offset {}:{}", + self.offset_in_minutes / 60, + self.offset_in_minutes.abs() % 60 + ) + } +} + +impl Error for InvalidTimezoneError {} #[cfg(test)] mod tests { use super::*; #[test] - fn from_str() -> Result<(), XsdParseError> { + fn from_str() -> Result<(), ParseDateTimeError> { assert_eq!(Time::from_str("00:00:00Z")?.to_string(), "00:00:00Z"); assert_eq!(Time::from_str("00:00:00+00:00")?.to_string(), "00:00:00Z"); assert_eq!(Time::from_str("00:00:00-00:00")?.to_string(), "00:00:00Z"); @@ -2148,11 +2615,57 @@ mod tests { assert!(GYear::from_str("02020").is_err()); assert!(GYear::from_str("+2020").is_err()); assert!(GYear::from_str("33").is_err()); + + assert_eq!(Time::from_str("00:00:00+14:00")?, Time::MIN); + assert_eq!(Time::from_str("24:00:00-14:00")?, Time::MAX); + Ok(()) + } + + #[test] + fn to_be_bytes() -> Result<(), ParseDateTimeError> { + assert_eq!( + DateTime::from_be_bytes(DateTime::MIN.to_be_bytes()), + DateTime::MIN + ); + assert_eq!( + DateTime::from_be_bytes(DateTime::MAX.to_be_bytes()), + DateTime::MAX + ); + assert_eq!( + DateTime::from_be_bytes(DateTime::from_str("2022-01-03T01:02:03")?.to_be_bytes()), + DateTime::from_str("2022-01-03T01:02:03")? + ); + assert_eq!(Date::from_be_bytes(Date::MIN.to_be_bytes()), Date::MIN); + assert_eq!(Date::from_be_bytes(Date::MAX.to_be_bytes()), Date::MAX); + assert_eq!( + Date::from_be_bytes(Date::from_str("2022-01-03")?.to_be_bytes()), + Date::from_str("2022-01-03")? + ); + assert_eq!(Time::from_be_bytes(Time::MIN.to_be_bytes()), Time::MIN); + assert_eq!(Time::from_be_bytes(Time::MAX.to_be_bytes()), Time::MAX); + assert_eq!( + Time::from_be_bytes(Time::from_str("01:02:03")?.to_be_bytes()), + Time::from_str("01:02:03")? + ); + assert_eq!( + Time::from_be_bytes(Time::from_str("01:02:03")?.to_be_bytes()), + Time::from_str("01:02:03")? + ); + assert_eq!( + GYearMonth::from_be_bytes(GYearMonth::MIN.to_be_bytes()), + GYearMonth::MIN + ); + assert_eq!( + GYearMonth::from_be_bytes(GYearMonth::MAX.to_be_bytes()), + GYearMonth::MAX + ); + assert_eq!(GYear::from_be_bytes(GYear::MIN.to_be_bytes()), GYear::MIN); + assert_eq!(GYear::from_be_bytes(GYear::MAX.to_be_bytes()), GYear::MAX); Ok(()) } #[test] - fn equals() -> Result<(), XsdParseError> { + fn equals() -> Result<(), ParseDateTimeError> { assert_eq!( DateTime::from_str("2002-04-02T12:00:00-01:00")?, DateTime::from_str("2002-04-02T17:00:00+04:00")? @@ -2253,7 +2766,7 @@ mod tests { #[test] #[allow(clippy::neg_cmp_op_on_partial_ord)] - fn cmp() -> Result<(), XsdParseError> { + fn cmp() -> Result<(), ParseDateTimeError> { assert!(Date::from_str("2004-12-25Z")? < Date::from_str("2004-12-25-05:00")?); assert!(!(Date::from_str("2004-12-25-12:00")? < Date::from_str("2004-12-26+12:00")?)); @@ -2280,7 +2793,7 @@ mod tests { } #[test] - fn year() -> Result<(), XsdParseError> { + fn year() -> Result<(), ParseDateTimeError> { assert_eq!( DateTime::from_str("1999-05-31T13:20:00-05:00")?.year(), 1999 @@ -2303,7 +2816,7 @@ mod tests { } #[test] - fn month() -> Result<(), XsdParseError> { + fn month() -> Result<(), ParseDateTimeError> { assert_eq!(DateTime::from_str("1999-05-31T13:20:00-05:00")?.month(), 5); assert_eq!(DateTime::from_str("1999-12-31T19:20:00-05:00")?.month(), 12); @@ -2317,7 +2830,7 @@ mod tests { } #[test] - fn day() -> Result<(), XsdParseError> { + fn day() -> Result<(), ParseDateTimeError> { assert_eq!(DateTime::from_str("1999-05-31T13:20:00-05:00")?.day(), 31); assert_eq!(DateTime::from_str("1999-12-31T20:00:00-05:00")?.day(), 31); @@ -2330,7 +2843,7 @@ mod tests { } #[test] - fn hour() -> Result<(), XsdParseError> { + fn hour() -> Result<(), ParseDateTimeError> { assert_eq!(DateTime::from_str("1999-05-31T08:20:00-05:00")?.hour(), 8); assert_eq!(DateTime::from_str("1999-12-31T21:20:00-05:00")?.hour(), 21); assert_eq!(DateTime::from_str("1999-12-31T12:00:00")?.hour(), 12); @@ -2344,7 +2857,7 @@ mod tests { } #[test] - fn minute() -> Result<(), XsdParseError> { + fn minute() -> Result<(), ParseDateTimeError> { assert_eq!( DateTime::from_str("1999-05-31T13:20:00-05:00")?.minute(), 20 @@ -2359,7 +2872,7 @@ mod tests { } #[test] - fn second() -> Result<(), XsdParseError> { + fn second() -> Result<(), Box> { assert_eq!( DateTime::from_str("1999-05-31T13:20:00-05:00")?.second(), Decimal::from(0) @@ -2373,7 +2886,7 @@ mod tests { } #[test] - fn timezone() -> Result<(), XsdParseError> { + fn timezone() -> Result<(), Box> { assert_eq!( DateTime::from_str("1999-05-31T13:20:00-05:00")?.timezone(), Some(DayTimeDuration::from_str("-PT5H")?) @@ -2402,7 +2915,7 @@ mod tests { } #[test] - fn sub() -> Result<(), XsdParseError> { + fn sub() -> Result<(), Box> { assert_eq!( DateTime::from_str("2000-10-30T06:12:00-05:00")? .checked_sub(DateTime::from_str("1999-11-28T09:00:00Z")?), @@ -2442,7 +2955,7 @@ mod tests { } #[test] - fn add_duration() -> Result<(), XsdParseError> { + fn add_duration() -> Result<(), Box> { assert_eq!( DateTime::from_str("2000-01-12T12:13:14Z")? .checked_add_duration(Duration::from_str("P1Y3M5DT7H10M3.3S")?), @@ -2506,7 +3019,7 @@ mod tests { } #[test] - fn sub_duration() -> Result<(), XsdParseError> { + fn sub_duration() -> Result<(), Box> { assert_eq!( DateTime::from_str("2000-10-30T11:12:00")? .checked_sub_duration(Duration::from_str("P1Y2M")?), @@ -2548,17 +3061,15 @@ mod tests { } #[test] - fn adjust() -> Result<(), XsdParseError> { + fn adjust() -> Result<(), Box> { assert_eq!( - DateTime::from_str("2002-03-07T10:00:00-07:00")?.adjust(Some( - DayTimeDuration::from_str("PT10H")?.try_into().unwrap() - )), + DateTime::from_str("2002-03-07T10:00:00-07:00")? + .adjust(Some(DayTimeDuration::from_str("PT10H")?.try_into()?)), Some(DateTime::from_str("2002-03-08T03:00:00+10:00")?) ); assert_eq!( - DateTime::from_str("2002-03-07T00:00:00+01:00")?.adjust(Some( - DayTimeDuration::from_str("-PT8H")?.try_into().unwrap() - )), + DateTime::from_str("2002-03-07T00:00:00+01:00")? + .adjust(Some(DayTimeDuration::from_str("-PT8H")?.try_into()?)), Some(DateTime::from_str("2002-03-06T15:00:00-08:00")?) ); assert_eq!( @@ -2571,18 +3082,13 @@ mod tests { ); assert_eq!( - Date::from_str("2002-03-07")?.adjust(Some( - DayTimeDuration::from_str("-PT10H")?.try_into().unwrap() - )), + Date::from_str("2002-03-07")? + .adjust(Some(DayTimeDuration::from_str("-PT10H")?.try_into()?)), Some(Date::from_str("2002-03-07-10:00")?) ); assert_eq!( - Date::from_str("2002-03-07-07:00")?.adjust(Some( - DayTimeDuration::from_str("-PT10H") - .unwrap() - .try_into() - .unwrap() - )), + Date::from_str("2002-03-07-07:00")? + .adjust(Some(DayTimeDuration::from_str("-PT10H")?.try_into()?)), Some(Date::from_str("2002-03-06-10:00")?) ); assert_eq!( @@ -2595,15 +3101,13 @@ mod tests { ); assert_eq!( - Time::from_str("10:00:00")?.adjust(Some( - DayTimeDuration::from_str("-PT10H")?.try_into().unwrap() - )), + Time::from_str("10:00:00")? + .adjust(Some(DayTimeDuration::from_str("-PT10H")?.try_into()?)), Some(Time::from_str("10:00:00-10:00")?) ); assert_eq!( - Time::from_str("10:00:00-07:00")?.adjust(Some( - DayTimeDuration::from_str("-PT10H")?.try_into().unwrap() - )), + Time::from_str("10:00:00-07:00")? + .adjust(Some(DayTimeDuration::from_str("-PT10H")?.try_into()?)), Some(Time::from_str("07:00:00-10:00")?) ); assert_eq!( @@ -2615,35 +3119,78 @@ mod tests { Some(Time::from_str("10:00:00")?) ); assert_eq!( - Time::from_str("10:00:00-07:00")?.adjust(Some( - DayTimeDuration::from_str("PT10H")?.try_into().unwrap() - )), + Time::from_str("10:00:00-07:00")? + .adjust(Some(DayTimeDuration::from_str("PT10H")?.try_into()?)), Some(Time::from_str("03:00:00+10:00")?) ); Ok(()) } + #[test] + fn time_from_datetime() -> Result<(), ParseDateTimeError> { + assert_eq!( + Time::from(DateTime::MIN), + Time::from_str("19:51:08.312696284115894272-14:00")? + ); + assert_eq!( + Time::from(DateTime::MAX), + Time::from_str("04:08:51.687303715884105727+14:00")? + ); + Ok(()) + } + + #[test] + fn date_from_datetime() -> Result<(), Box> { + assert_eq!( + Date::try_from( + DateTime::MIN + .checked_add_day_time_duration(DayTimeDuration::from_str("P1D")?) + .unwrap() + )?, + Date::MIN + ); + assert_eq!(Date::try_from(DateTime::MAX)?, Date::MAX); + Ok(()) + } + + #[test] + fn g_year_month_from_date() -> Result<(), ParseDateTimeError> { + assert_eq!(GYearMonth::from(Date::MIN), GYearMonth::MIN); + assert_eq!(GYearMonth::from(Date::MAX), GYearMonth::MAX); + Ok(()) + } + + #[test] + fn g_year_from_g_year_month() -> Result<(), ParseDateTimeError> { + assert_eq!(GYear::try_from(GYearMonth::MIN)?, GYear::MIN); + assert_eq!( + GYear::try_from(GYearMonth::from_str("5391559471918-12+14:00")?)?, + GYear::MAX + ); + Ok(()) + } + #[cfg(feature = "custom-now")] #[test] fn custom_now() { #[no_mangle] - fn custom_ox_now() -> Result { - Ok(Duration::default()) + fn custom_ox_now() -> Duration { + Duration::default() } - assert!(DateTime::now().is_ok()); + DateTime::now(); } #[cfg(not(feature = "custom-now"))] #[test] - fn now() -> Result<(), XsdParseError> { - let now = DateTime::now().unwrap(); + fn now() -> Result<(), ParseDateTimeError> { + let now = DateTime::now(); assert!(DateTime::from_str("2022-01-01T00:00:00Z")? < now); assert!(now < DateTime::from_str("2100-01-01T00:00:00Z")?); Ok(()) } #[test] - fn minimally_conformant() -> Result<(), XsdParseError> { + fn minimally_conformant() -> Result<(), ParseDateTimeError> { // All minimally conforming processors must support nonnegative year values less than 10000 // (i.e., those expressible with four digits) in all datatypes which // use the seven-property model defined in The Seven-property Model (§D.2.1) diff --git a/lib/oxsdatatypes/src/decimal.rs b/lib/oxsdatatypes/src/decimal.rs index f3880dcb..6a309108 100644 --- a/lib/oxsdatatypes/src/decimal.rs +++ b/lib/oxsdatatypes/src/decimal.rs @@ -1,4 +1,4 @@ -use crate::{Boolean, Double, Float, Integer}; +use crate::{Boolean, Double, Float, Integer, TooLargeForIntegerError}; use std::error::Error; use std::fmt; use std::fmt::Write; @@ -21,15 +21,20 @@ pub struct Decimal { impl Decimal { /// Constructs the decimal i / 10^n #[inline] - pub fn new(i: i128, n: u32) -> Result { - let shift = DECIMAL_PART_DIGITS - .checked_sub(n) - .ok_or(DecimalOverflowError)?; - Ok(Self { - value: i - .checked_mul(10_i128.pow(shift)) - .ok_or(DecimalOverflowError)?, - }) + pub const fn new(i: i128, n: u32) -> Result { + let Some(shift) = DECIMAL_PART_DIGITS.checked_sub(n) else { + return Err(TooLargeForDecimalError); + }; + let Some(value) = i.checked_mul(10_i128.pow(shift)) else { + return Err(TooLargeForDecimalError); + }; + Ok(Self { value }) + } + + pub(crate) const fn new_from_i128_unchecked(value: i128) -> Self { + Self { + value: value * DECIMAL_PART_POW, + } } #[inline] @@ -47,6 +52,8 @@ impl Decimal { } /// [op:numeric-add](https://www.w3.org/TR/xpath-functions-31/#func-numeric-add) + /// + /// Returns `None` in case of overflow ([FOAR0002](https://www.w3.org/TR/xpath-functions-31/#ERRFOAR0002)). #[inline] #[must_use] pub fn checked_add(self, rhs: impl Into) -> Option { @@ -56,6 +63,8 @@ impl Decimal { } /// [op:numeric-subtract](https://www.w3.org/TR/xpath-functions-31/#func-numeric-subtract) + /// + /// Returns `None` in case of overflow ([FOAR0002](https://www.w3.org/TR/xpath-functions-31/#ERRFOAR0002)). #[inline] #[must_use] pub fn checked_sub(self, rhs: impl Into) -> Option { @@ -65,6 +74,8 @@ impl Decimal { } /// [op:numeric-multiply](https://www.w3.org/TR/xpath-functions-31/#func-numeric-multiply) + /// + /// Returns `None` in case of overflow ([FOAR0002](https://www.w3.org/TR/xpath-functions-31/#ERRFOAR0002)). #[inline] #[must_use] pub fn checked_mul(self, rhs: impl Into) -> Option { @@ -98,6 +109,8 @@ impl Decimal { } /// [op:numeric-divide](https://www.w3.org/TR/xpath-functions-31/#func-numeric-divide) + /// + /// Returns `None` in case of division by 0 ([FOAR0001](https://www.w3.org/TR/xpath-functions-31/#ERRFOAR0001)) or overflow ([FOAR0002](https://www.w3.org/TR/xpath-functions-31/#ERRFOAR0002)). #[inline] #[must_use] pub fn checked_div(self, rhs: impl Into) -> Option { @@ -132,6 +145,8 @@ impl Decimal { } /// [op:numeric-mod](https://www.w3.org/TR/xpath-functions-31/#func-numeric-mod) + /// + /// Returns `None` in case of division by 0 ([FOAR0001](https://www.w3.org/TR/xpath-functions-31/#ERRFOAR0001)) or overflow ([FOAR0002](https://www.w3.org/TR/xpath-functions-31/#ERRFOAR0002)). #[inline] #[must_use] pub fn checked_rem(self, rhs: impl Into) -> Option { @@ -140,6 +155,9 @@ impl Decimal { }) } + /// Euclidean remainder + /// + /// Returns `None` in case of division by 0 ([FOAR0001](https://www.w3.org/TR/xpath-functions-31/#ERRFOAR0001)) or overflow ([FOAR0002](https://www.w3.org/TR/xpath-functions-31/#ERRFOAR0002)). #[inline] #[must_use] pub fn checked_rem_euclid(self, rhs: impl Into) -> Option { @@ -149,6 +167,8 @@ impl Decimal { } /// [op:numeric-unary-minus](https://www.w3.org/TR/xpath-functions-31/#func-numeric-unary-minus) + /// + /// Returns `None` in case of overflow ([FOAR0002](https://www.w3.org/TR/xpath-functions-31/#ERRFOAR0002)). #[inline] #[must_use] pub fn checked_neg(self) -> Option { @@ -158,52 +178,63 @@ impl Decimal { } /// [fn:abs](https://www.w3.org/TR/xpath-functions-31/#func-abs) + /// + /// Returns `None` in case of overflow ([FOAR0002](https://www.w3.org/TR/xpath-functions-31/#ERRFOAR0002)). #[inline] #[must_use] - pub const fn abs(self) -> Self { - Self { - value: self.value.abs(), - } + pub fn checked_abs(self) -> Option { + Some(Self { + value: self.value.checked_abs()?, + }) } /// [fn:round](https://www.w3.org/TR/xpath-functions-31/#func-round) + /// + /// Returns `None` in case of overflow ([FOAR0002](https://www.w3.org/TR/xpath-functions-31/#ERRFOAR0002)). #[inline] #[must_use] - pub fn round(self) -> Self { + pub fn checked_round(self) -> Option { let value = self.value / DECIMAL_PART_POW_MINUS_ONE; - Self { + Some(Self { value: if value >= 0 { - (value / 10 + i128::from(value % 10 >= 5)) * DECIMAL_PART_POW + value / 10 + i128::from(value % 10 >= 5) } else { - (value / 10 - i128::from(-value % 10 > 5)) * DECIMAL_PART_POW - }, - } + value / 10 - i128::from(-value % 10 > 5) + } + .checked_mul(DECIMAL_PART_POW)?, + }) } /// [fn:ceiling](https://www.w3.org/TR/xpath-functions-31/#func-ceiling) + /// + /// Returns `None` in case of overflow ([FOAR0002](https://www.w3.org/TR/xpath-functions-31/#ERRFOAR0002)). #[inline] #[must_use] - pub fn ceil(self) -> Self { - Self { - value: if self.value >= 0 && self.value % DECIMAL_PART_POW != 0 { - (self.value / DECIMAL_PART_POW + 1) * DECIMAL_PART_POW + pub fn checked_ceil(self) -> Option { + Some(Self { + value: if self.value > 0 && self.value % DECIMAL_PART_POW != 0 { + self.value / DECIMAL_PART_POW + 1 } else { - (self.value / DECIMAL_PART_POW) * DECIMAL_PART_POW - }, - } + self.value / DECIMAL_PART_POW + } + .checked_mul(DECIMAL_PART_POW)?, + }) } /// [fn:floor](https://www.w3.org/TR/xpath-functions-31/#func-floor) + /// + /// Returns `None` in case of overflow ([FOAR0002](https://www.w3.org/TR/xpath-functions-31/#ERRFOAR0002)). #[inline] #[must_use] - pub fn floor(self) -> Self { - Self { + pub fn checked_floor(self) -> Option { + Some(Self { value: if self.value >= 0 || self.value % DECIMAL_PART_POW == 0 { - (self.value / DECIMAL_PART_POW) * DECIMAL_PART_POW + self.value / DECIMAL_PART_POW } else { - (self.value / DECIMAL_PART_POW - 1) * DECIMAL_PART_POW - }, - } + self.value / DECIMAL_PART_POW - 1 + } + .checked_mul(DECIMAL_PART_POW)?, + }) } #[inline] @@ -328,28 +359,28 @@ impl From for Decimal { } impl TryFrom for Decimal { - type Error = DecimalOverflowError; + type Error = TooLargeForDecimalError; #[inline] - fn try_from(value: i128) -> Result { + fn try_from(value: i128) -> Result { Ok(Self { value: value .checked_mul(DECIMAL_PART_POW) - .ok_or(DecimalOverflowError)?, + .ok_or(TooLargeForDecimalError)?, }) } } impl TryFrom for Decimal { - type Error = DecimalOverflowError; + type Error = TooLargeForDecimalError; #[inline] - fn try_from(value: u128) -> Result { + fn try_from(value: u128) -> Result { Ok(Self { value: i128::try_from(value) - .map_err(|_| DecimalOverflowError)? + .map_err(|_| TooLargeForDecimalError)? .checked_mul(DECIMAL_PART_POW) - .ok_or(DecimalOverflowError)?, + .ok_or(TooLargeForDecimalError)?, }) } } @@ -362,27 +393,27 @@ impl From for Decimal { } impl TryFrom for Decimal { - type Error = DecimalOverflowError; + type Error = TooLargeForDecimalError; #[inline] - fn try_from(value: Float) -> Result { + fn try_from(value: Float) -> Result { Double::from(value).try_into() } } impl TryFrom for Decimal { - type Error = DecimalOverflowError; + type Error = TooLargeForDecimalError; #[inline] #[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)] - fn try_from(value: Double) -> Result { + fn try_from(value: Double) -> Result { let shifted = f64::from(value) * (DECIMAL_PART_POW as f64); - if shifted.is_finite() && (i128::MIN as f64) <= shifted && shifted <= (i128::MAX as f64) { + if (i128::MIN as f64) <= shifted && shifted <= (i128::MAX as f64) { Ok(Self { value: shifted as i128, }) } else { - Err(DecimalOverflowError) + Err(TooLargeForDecimalError) } } } @@ -415,17 +446,17 @@ impl From for Double { } impl TryFrom for Integer { - type Error = DecimalOverflowError; + type Error = TooLargeForIntegerError; #[inline] - fn try_from(value: Decimal) -> Result { + fn try_from(value: Decimal) -> Result { Ok(i64::try_from( value .value .checked_div(DECIMAL_PART_POW) - .ok_or(DecimalOverflowError)?, + .ok_or(TooLargeForIntegerError)?, ) - .map_err(|_| DecimalOverflowError)? + .map_err(|_| TooLargeForIntegerError)? .into()) } } @@ -620,25 +651,27 @@ impl fmt::Display for ParseDecimalError { impl Error for ParseDecimalError {} -impl From for ParseDecimalError { - fn from(_: DecimalOverflowError) -> Self { +impl From for ParseDecimalError { + fn from(_: TooLargeForDecimalError) -> Self { Self { kind: DecimalParseErrorKind::Overflow, } } } -/// An overflow in [`Decimal`] computations. +/// The input is too large to fit into a [`Decimal`]. +/// +/// Matches XPath [`FOCA0001` error](https://www.w3.org/TR/xpath-functions-31/#ERRFOCA0001). #[derive(Debug, Clone, Copy)] -pub struct DecimalOverflowError; +pub struct TooLargeForDecimalError; -impl fmt::Display for DecimalOverflowError { +impl fmt::Display for TooLargeForDecimalError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Value overflow") + write!(f, "Value too large for xsd:decimal internal representation") } } -impl Error for DecimalOverflowError {} +impl Error for TooLargeForDecimalError {} #[cfg(test)] mod tests { @@ -797,45 +830,153 @@ mod tests { Some(Decimal::from_str("0.9")?) ); assert_eq!(Decimal::from(1).checked_rem(0), None); + assert_eq!( + Decimal::MAX.checked_rem(1), + Some(Decimal::from_str("0.687303715884105727")?) + ); + assert_eq!( + Decimal::MIN.checked_rem(1), + Some(Decimal::from_str("-0.687303715884105728")?) + ); + assert_eq!( + Decimal::MAX.checked_rem(Decimal::STEP), + Some(Decimal::default()) + ); + assert_eq!( + Decimal::MIN.checked_rem(Decimal::STEP), + Some(Decimal::default()) + ); + assert_eq!( + Decimal::MAX.checked_rem(Decimal::MAX), + Some(Decimal::default()) + ); + assert_eq!( + Decimal::MIN.checked_rem(Decimal::MIN), + Some(Decimal::default()) + ); Ok(()) } #[test] fn round() -> Result<(), ParseDecimalError> { - assert_eq!(Decimal::from(10).round(), Decimal::from(10)); - assert_eq!(Decimal::from(-10).round(), Decimal::from(-10)); - assert_eq!(Decimal::from_str("2.5")?.round(), Decimal::from(3)); - assert_eq!(Decimal::from_str("2.4999")?.round(), Decimal::from(2)); - assert_eq!(Decimal::from_str("-2.5")?.round(), Decimal::from(-2)); - assert_eq!(Decimal::from(i64::MIN).round(), Decimal::from(i64::MIN)); - assert_eq!(Decimal::from(i64::MAX).round(), Decimal::from(i64::MAX)); + assert_eq!(Decimal::from(10).checked_round(), Some(Decimal::from(10))); + assert_eq!(Decimal::from(-10).checked_round(), Some(Decimal::from(-10))); + assert_eq!( + Decimal::from(i64::MIN).checked_round(), + Some(Decimal::from(i64::MIN)) + ); + assert_eq!( + Decimal::from(i64::MAX).checked_round(), + Some(Decimal::from(i64::MAX)) + ); + assert_eq!( + Decimal::from_str("2.5")?.checked_round(), + Some(Decimal::from(3)) + ); + assert_eq!( + Decimal::from_str("2.4999")?.checked_round(), + Some(Decimal::from(2)) + ); + assert_eq!( + Decimal::from_str("-2.5")?.checked_round(), + Some(Decimal::from(-2)) + ); + assert_eq!(Decimal::MAX.checked_round(), None); + assert_eq!( + (Decimal::MAX.checked_sub(Decimal::from_str("0.5")?)) + .unwrap() + .checked_round(), + Some(Decimal::from_str("170141183460469231731")?) + ); + assert_eq!(Decimal::MIN.checked_round(), None); + assert_eq!( + (Decimal::MIN.checked_add(Decimal::from_str("0.5")?)) + .unwrap() + .checked_round(), + Some(Decimal::from_str("-170141183460469231731")?) + ); Ok(()) } #[test] fn ceil() -> Result<(), ParseDecimalError> { - assert_eq!(Decimal::from(10).ceil(), Decimal::from(10)); - assert_eq!(Decimal::from(-10).ceil(), Decimal::from(-10)); - assert_eq!(Decimal::from_str("10.5")?.ceil(), Decimal::from(11)); - assert_eq!(Decimal::from_str("-10.5")?.ceil(), Decimal::from(-10)); - assert_eq!(Decimal::from(i64::MIN).ceil(), Decimal::from(i64::MIN)); - assert_eq!(Decimal::from(i64::MAX).ceil(), Decimal::from(i64::MAX)); + assert_eq!(Decimal::from(10).checked_ceil(), Some(Decimal::from(10))); + assert_eq!(Decimal::from(-10).checked_ceil(), Some(Decimal::from(-10))); + assert_eq!( + Decimal::from_str("10.5")?.checked_ceil(), + Some(Decimal::from(11)) + ); + assert_eq!( + Decimal::from_str("-10.5")?.checked_ceil(), + Some(Decimal::from(-10)) + ); + assert_eq!( + Decimal::from(i64::MIN).checked_ceil(), + Some(Decimal::from(i64::MIN)) + ); + assert_eq!( + Decimal::from(i64::MAX).checked_ceil(), + Some(Decimal::from(i64::MAX)) + ); + assert_eq!(Decimal::MAX.checked_ceil(), None); + assert_eq!( + Decimal::MAX + .checked_sub(Decimal::from(1)) + .unwrap() + .checked_ceil(), + Some(Decimal::from_str("170141183460469231731")?) + ); + assert_eq!( + Decimal::MIN.checked_ceil(), + Some(Decimal::from_str("-170141183460469231731")?) + ); Ok(()) } #[test] fn floor() -> Result<(), ParseDecimalError> { - assert_eq!(Decimal::from(10).ceil(), Decimal::from(10)); - assert_eq!(Decimal::from(-10).ceil(), Decimal::from(-10)); - assert_eq!(Decimal::from_str("10.5")?.floor(), Decimal::from(10)); - assert_eq!(Decimal::from_str("-10.5")?.floor(), Decimal::from(-11)); - assert_eq!(Decimal::from(i64::MIN).floor(), Decimal::from(i64::MIN)); - assert_eq!(Decimal::from(i64::MAX).floor(), Decimal::from(i64::MAX)); + assert_eq!(Decimal::from(10).checked_floor(), Some(Decimal::from(10))); + assert_eq!(Decimal::from(-10).checked_floor(), Some(Decimal::from(-10))); + assert_eq!( + Decimal::from_str("10.5")?.checked_floor(), + Some(Decimal::from(10)) + ); + assert_eq!( + Decimal::from_str("-10.5")?.checked_floor(), + Some(Decimal::from(-11)) + ); + assert_eq!( + Decimal::from(i64::MIN).checked_floor(), + Some(Decimal::from(i64::MIN)) + ); + assert_eq!( + Decimal::from(i64::MAX).checked_floor(), + Some(Decimal::from(i64::MAX)) + ); + assert_eq!( + Decimal::MAX.checked_floor(), + Some(Decimal::from_str("170141183460469231731")?) + ); + assert_eq!(Decimal::MIN.checked_floor(), None); + assert_eq!( + (Decimal::MIN.checked_add(Decimal::from_str("1")?)) + .unwrap() + .checked_floor(), + Some(Decimal::from_str("-170141183460469231731")?) + ); Ok(()) } #[test] fn to_be_bytes() -> Result<(), ParseDecimalError> { + assert_eq!( + Decimal::from_be_bytes(Decimal::MIN.to_be_bytes()), + Decimal::MIN + ); + assert_eq!( + Decimal::from_be_bytes(Decimal::MAX.to_be_bytes()), + Decimal::MAX + ); assert_eq!( Decimal::from_be_bytes(Decimal::from(i64::MIN).to_be_bytes()), Decimal::from(i64::MIN) @@ -889,7 +1030,8 @@ mod tests { .unwrap() .checked_sub(Decimal::from(1_672_507_293_696_i64)) .unwrap() - .abs() + .checked_abs() + .unwrap() < Decimal::from(1) ); Ok(()) @@ -914,7 +1056,8 @@ mod tests { .unwrap() .checked_sub(Decimal::from(1_672_507_302_466_i64)) .unwrap() - .abs() + .checked_abs() + .unwrap() < Decimal::from(1) ); assert!(Decimal::try_from(Double::from(f64::NAN)).is_err()); diff --git a/lib/oxsdatatypes/src/duration.rs b/lib/oxsdatatypes/src/duration.rs index 27ce5d97..87be5a22 100644 --- a/lib/oxsdatatypes/src/duration.rs +++ b/lib/oxsdatatypes/src/duration.rs @@ -1,7 +1,6 @@ -use super::decimal::DecimalOverflowError; -use super::parser::*; -use super::*; +use crate::{DateTime, Decimal}; use std::cmp::Ordering; +use std::error::Error; use std::fmt; use std::str::FromStr; use std::time::Duration as StdDuration; @@ -78,13 +77,13 @@ impl Duration { #[inline] #[must_use] - pub(super) const fn all_months(self) -> i64 { + pub(crate) const fn all_months(self) -> i64 { self.year_month.all_months() } #[inline] #[must_use] - pub(super) const fn all_seconds(self) -> Decimal { + pub(crate) const fn all_seconds(self) -> Decimal { self.day_time.as_seconds() } @@ -98,6 +97,8 @@ impl Duration { } /// [op:add-yearMonthDurations](https://www.w3.org/TR/xpath-functions-31/#func-add-yearMonthDurations) and [op:add-dayTimeDurations](https://www.w3.org/TR/xpath-functions-31/#func-add-dayTimeDurations) + /// + /// Returns `None` in case of overflow ([`FODT0002`](https://www.w3.org/TR/xpath-functions-31/#ERRFODT0002)). #[inline] #[must_use] pub fn checked_add(self, rhs: impl Into) -> Option { @@ -109,6 +110,8 @@ impl Duration { } /// [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) + /// + /// Returns `None` in case of overflow ([`FODT0002`](https://www.w3.org/TR/xpath-functions-31/#ERRFODT0002)). #[inline] #[must_use] pub fn checked_sub(self, rhs: impl Into) -> Option { @@ -119,6 +122,9 @@ impl Duration { }) } + /// Unary negation. + /// + /// Returns `None` in case of overflow ([`FODT0002`](https://www.w3.org/TR/xpath-functions-31/#ERRFODT0002)). #[inline] #[must_use] pub fn checked_neg(self) -> Option { @@ -134,22 +140,39 @@ impl Duration { pub fn is_identical_with(self, other: Self) -> bool { self == other } + + pub const MIN: Self = Self { + year_month: YearMonthDuration::MIN, + day_time: DayTimeDuration::MIN, + }; + + pub const MAX: Self = Self { + year_month: YearMonthDuration::MAX, + day_time: DayTimeDuration::MAX, + }; } impl TryFrom for Duration { - type Error = DecimalOverflowError; + type Error = DurationOverflowError; #[inline] - fn try_from(value: StdDuration) -> Result { + fn try_from(value: StdDuration) -> Result { Ok(DayTimeDuration::try_from(value)?.into()) } } impl FromStr for Duration { - type Err = XsdParseError; + type Err = ParseDurationError; - fn from_str(input: &str) -> Result { - parse_duration(input) + fn from_str(input: &str) -> Result { + let parts = ensure_complete(input, duration_parts)?; + if parts.year_month.is_none() && parts.day_time.is_none() { + return Err(ParseDurationError::msg("Empty duration")); + } + Ok(Self::new( + parts.year_month.unwrap_or(0), + parts.day_time.unwrap_or_default(), + )) } } @@ -208,7 +231,7 @@ impl fmt::Display for Duration { write!(f, "{}M", m.abs())?; } if s != 0.into() { - write!(f, "{}S", s.abs())?; + write!(f, "{}S", s.checked_abs().ok_or(fmt::Error)?)?; } } } @@ -282,7 +305,7 @@ impl YearMonthDuration { } #[inline] - pub(super) const fn all_months(self) -> i64 { + pub(crate) const fn all_months(self) -> i64 { self.months } @@ -292,6 +315,8 @@ impl YearMonthDuration { } /// [op:add-yearMonthDurations](https://www.w3.org/TR/xpath-functions-31/#func-add-yearMonthDurations) + /// + /// Returns `None` in case of overflow ([`FODT0002`](https://www.w3.org/TR/xpath-functions-31/#ERRFODT0002)). #[inline] pub fn checked_add(self, rhs: impl Into) -> Option { let rhs = rhs.into(); @@ -301,6 +326,8 @@ impl YearMonthDuration { } /// [op:subtract-yearMonthDurations](https://www.w3.org/TR/xpath-functions-31/#func-subtract-yearMonthDurations) + /// + /// Returns `None` in case of overflow ([`FODT0002`](https://www.w3.org/TR/xpath-functions-31/#ERRFODT0002)). #[inline] pub fn checked_sub(self, rhs: impl Into) -> Option { let rhs = rhs.into(); @@ -309,6 +336,9 @@ impl YearMonthDuration { }) } + /// Unary negation. + /// + /// Returns `None` in case of overflow ([`FODT0002`](https://www.w3.org/TR/xpath-functions-31/#ERRFODT0002)). #[inline] pub fn checked_neg(self) -> Option { Some(Self { @@ -321,6 +351,10 @@ impl YearMonthDuration { pub fn is_identical_with(self, other: Self) -> bool { self == other } + + pub const MIN: Self = Self { months: i64::MIN }; + + pub const MAX: Self = Self { months: i64::MAX }; } impl From for Duration { @@ -334,23 +368,31 @@ impl From for Duration { } impl TryFrom for YearMonthDuration { - type Error = DecimalOverflowError; + type Error = DurationOverflowError; #[inline] - fn try_from(value: Duration) -> Result { + fn try_from(value: Duration) -> Result { if value.day_time == DayTimeDuration::default() { Ok(value.year_month) } else { - Err(DecimalOverflowError {}) + Err(DurationOverflowError) } } } impl FromStr for YearMonthDuration { - type Err = XsdParseError; - - fn from_str(input: &str) -> Result { - parse_year_month_duration(input) + type Err = ParseDurationError; + + fn from_str(input: &str) -> Result { + let parts = ensure_complete(input, duration_parts)?; + if parts.day_time.is_some() { + return Err(ParseDurationError::msg( + "There must not be any day or time component in a yearMonthDuration", + )); + } + Ok(Self::new(parts.year_month.ok_or( + ParseDurationError::msg("No year and month values found"), + )?)) } } @@ -455,6 +497,8 @@ impl DayTimeDuration { } /// [op:add-dayTimeDurations](https://www.w3.org/TR/xpath-functions-31/#func-add-dayTimeDurations) + /// + /// Returns `None` in case of overflow ([`FODT0002`](https://www.w3.org/TR/xpath-functions-31/#ERRFODT0002)). #[inline] pub fn checked_add(self, rhs: impl Into) -> Option { let rhs = rhs.into(); @@ -464,6 +508,8 @@ impl DayTimeDuration { } /// [op:subtract-dayTimeDurations](https://www.w3.org/TR/xpath-functions-31/#func-subtract-dayTimeDurations) + /// + /// Returns `None` in case of overflow ([`FODT0002`](https://www.w3.org/TR/xpath-functions-31/#ERRFODT0002)). #[inline] pub fn checked_sub(self, rhs: impl Into) -> Option { let rhs = rhs.into(); @@ -472,6 +518,9 @@ impl DayTimeDuration { }) } + /// Unary negation. + /// + /// Returns `None` in case of overflow ([`FODT0002`](https://www.w3.org/TR/xpath-functions-31/#ERRFODT0002)). #[inline] pub fn checked_neg(self) -> Option { Some(Self { @@ -484,6 +533,14 @@ impl DayTimeDuration { pub fn is_identical_with(self, other: Self) -> bool { self == other } + + pub const MIN: Self = Self { + seconds: Decimal::MIN, + }; + + pub const MAX: Self = Self { + seconds: Decimal::MAX, + }; } impl From for Duration { @@ -497,65 +554,75 @@ impl From for Duration { } impl TryFrom for DayTimeDuration { - type Error = DecimalOverflowError; + type Error = DurationOverflowError; #[inline] - fn try_from(value: Duration) -> Result { + fn try_from(value: Duration) -> Result { if value.year_month == YearMonthDuration::default() { Ok(value.day_time) } else { - Err(DecimalOverflowError {}) + Err(DurationOverflowError) } } } impl TryFrom for DayTimeDuration { - type Error = DecimalOverflowError; + type Error = DurationOverflowError; #[inline] - fn try_from(value: StdDuration) -> Result { + fn try_from(value: StdDuration) -> Result { Ok(Self { seconds: Decimal::new( - i128::try_from(value.as_nanos()).map_err(|_| DecimalOverflowError)?, + i128::try_from(value.as_nanos()).map_err(|_| DurationOverflowError)?, 9, - )?, + ) + .map_err(|_| DurationOverflowError)?, }) } } impl TryFrom for StdDuration { - type Error = DecimalOverflowError; + type Error = DurationOverflowError; #[inline] - fn try_from(value: DayTimeDuration) -> Result { + fn try_from(value: DayTimeDuration) -> Result { if value.seconds.is_negative() { - return Err(DecimalOverflowError); + return Err(DurationOverflowError); } - let secs = value.seconds.floor(); + let secs = value.seconds.checked_floor().ok_or(DurationOverflowError)?; let nanos = value .seconds .checked_sub(secs) - .ok_or(DecimalOverflowError)? + .ok_or(DurationOverflowError)? .checked_mul(1_000_000_000) - .ok_or(DecimalOverflowError)? - .floor(); + .ok_or(DurationOverflowError)? + .checked_floor() + .ok_or(DurationOverflowError)?; Ok(StdDuration::new( secs.as_i128() .try_into() - .map_err(|_| DecimalOverflowError)?, + .map_err(|_| DurationOverflowError)?, nanos .as_i128() .try_into() - .map_err(|_| DecimalOverflowError)?, + .map_err(|_| DurationOverflowError)?, )) } } impl FromStr for DayTimeDuration { - type Err = XsdParseError; - - fn from_str(input: &str) -> Result { - parse_day_time_duration(input) + type Err = ParseDurationError; + + fn from_str(input: &str) -> Result { + let parts = ensure_complete(input, duration_parts)?; + if parts.year_month.is_some() { + return Err(ParseDurationError::msg( + "There must not be any year or month component in a dayTimeDuration", + )); + } + Ok(Self::new(parts.day_time.ok_or(ParseDurationError::msg( + "No day or time values found", + ))?)) } } @@ -622,12 +689,273 @@ impl PartialOrd for YearMonthDuration { } } +// [6] duYearFrag ::= unsignedNoDecimalPtNumeral 'Y' +// [7] duMonthFrag ::= unsignedNoDecimalPtNumeral 'M' +// [8] duDayFrag ::= unsignedNoDecimalPtNumeral 'D' +// [9] duHourFrag ::= unsignedNoDecimalPtNumeral 'H' +// [10] duMinuteFrag ::= unsignedNoDecimalPtNumeral 'M' +// [11] duSecondFrag ::= (unsignedNoDecimalPtNumeral | unsignedDecimalPtNumeral) 'S' +// [12] duYearMonthFrag ::= (duYearFrag duMonthFrag?) | duMonthFrag +// [13] duTimeFrag ::= 'T' ((duHourFrag duMinuteFrag? duSecondFrag?) | (duMinuteFrag duSecondFrag?) | duSecondFrag) +// [14] duDayTimeFrag ::= (duDayFrag duTimeFrag?) | duTimeFrag +// [15] durationLexicalRep ::= '-'? 'P' ((duYearMonthFrag duDayTimeFrag?) | duDayTimeFrag) +struct DurationParts { + year_month: Option, + day_time: Option, +} + +fn duration_parts(input: &str) -> Result<(DurationParts, &str), ParseDurationError> { + // States + const START: u32 = 0; + const AFTER_YEAR: u32 = 1; + const AFTER_MONTH: u32 = 2; + const AFTER_DAY: u32 = 3; + const AFTER_T: u32 = 4; + const AFTER_HOUR: u32 = 5; + const AFTER_MINUTE: u32 = 6; + const AFTER_SECOND: u32 = 7; + + let (is_negative, input) = if let Some(left) = input.strip_prefix('-') { + (true, left) + } else { + (false, input) + }; + let mut input = expect_char(input, 'P', "Durations must start with 'P'")?; + let mut state = START; + let mut year_month: Option = None; + let mut day_time: Option = None; + while !input.is_empty() { + if let Some(left) = input.strip_prefix('T') { + if state >= AFTER_T { + return Err(ParseDurationError::msg("Duplicated time separator 'T'")); + } + state = AFTER_T; + input = left; + } else { + let (number_str, left) = decimal_prefix(input); + match left.chars().next() { + Some('Y') if state < AFTER_YEAR => { + year_month = Some( + year_month + .unwrap_or_default() + .checked_add( + apply_i64_neg( + i64::from_str(number_str).map_err(|_| OVERFLOW_ERROR)?, + is_negative, + )? + .checked_mul(12) + .ok_or(OVERFLOW_ERROR)?, + ) + .ok_or(OVERFLOW_ERROR)?, + ); + state = AFTER_YEAR; + } + Some('M') if state < AFTER_MONTH => { + year_month = Some( + year_month + .unwrap_or_default() + .checked_add(apply_i64_neg( + i64::from_str(number_str).map_err(|_| OVERFLOW_ERROR)?, + is_negative, + )?) + .ok_or(OVERFLOW_ERROR)?, + ); + state = AFTER_MONTH; + } + Some('D') if state < AFTER_DAY => { + if number_str.contains('.') { + return Err(ParseDurationError::msg( + "Decimal numbers are not allowed for days", + )); + } + day_time = Some( + day_time + .unwrap_or_default() + .checked_add( + apply_decimal_neg( + Decimal::from_str(number_str).map_err(|_| OVERFLOW_ERROR)?, + is_negative, + )? + .checked_mul(86400) + .ok_or(OVERFLOW_ERROR)?, + ) + .ok_or(OVERFLOW_ERROR)?, + ); + state = AFTER_DAY; + } + Some('H') if state == AFTER_T => { + if number_str.contains('.') { + return Err(ParseDurationError::msg( + "Decimal numbers are not allowed for hours", + )); + } + day_time = Some( + day_time + .unwrap_or_default() + .checked_add( + apply_decimal_neg( + Decimal::from_str(number_str).map_err(|_| OVERFLOW_ERROR)?, + is_negative, + )? + .checked_mul(3600) + .ok_or(OVERFLOW_ERROR)?, + ) + .ok_or(OVERFLOW_ERROR)?, + ); + state = AFTER_HOUR; + } + Some('M') if (AFTER_T..AFTER_MINUTE).contains(&state) => { + if number_str.contains('.') { + return Err(ParseDurationError::msg( + "Decimal numbers are not allowed for minutes", + )); + } + day_time = Some( + day_time + .unwrap_or_default() + .checked_add( + apply_decimal_neg( + Decimal::from_str(number_str).map_err(|_| OVERFLOW_ERROR)?, + is_negative, + )? + .checked_mul(60) + .ok_or(OVERFLOW_ERROR)?, + ) + .ok_or(OVERFLOW_ERROR)?, + ); + state = AFTER_MINUTE; + } + Some('S') if (AFTER_T..AFTER_SECOND).contains(&state) => { + day_time = Some( + day_time + .unwrap_or_default() + .checked_add(apply_decimal_neg( + Decimal::from_str(number_str).map_err(|_| OVERFLOW_ERROR)?, + is_negative, + )?) + .ok_or(OVERFLOW_ERROR)?, + ); + state = AFTER_SECOND; + } + Some(_) => return Err(ParseDurationError::msg("Unexpected type character")), + None => { + return Err(ParseDurationError::msg( + "Numbers in durations must be followed by a type character", + )) + } + } + input = &left[1..]; + } + } + + Ok(( + DurationParts { + year_month, + day_time, + }, + input, + )) +} + +fn apply_i64_neg(value: i64, is_negative: bool) -> Result { + if is_negative { + value.checked_neg().ok_or(OVERFLOW_ERROR) + } else { + Ok(value) + } +} + +fn apply_decimal_neg(value: Decimal, is_negative: bool) -> Result { + if is_negative { + value.checked_neg().ok_or(OVERFLOW_ERROR) + } else { + Ok(value) + } +} + +fn ensure_complete( + input: &str, + parse: impl FnOnce(&str) -> Result<(T, &str), ParseDurationError>, +) -> Result { + let (result, left) = parse(input)?; + if !left.is_empty() { + return Err(ParseDurationError::msg("Unrecognized value suffix")); + } + Ok(result) +} + +fn expect_char<'a>( + input: &'a str, + constant: char, + error_message: &'static str, +) -> Result<&'a str, ParseDurationError> { + if let Some(left) = input.strip_prefix(constant) { + Ok(left) + } else { + Err(ParseDurationError::msg(error_message)) + } +} + +fn decimal_prefix(input: &str) -> (&str, &str) { + let mut end = input.len(); + let mut dot_seen = false; + for (i, c) in input.char_indices() { + if c.is_ascii_digit() { + // Ok + } else if c == '.' && !dot_seen { + dot_seen = true; + } else { + end = i; + break; + } + } + input.split_at(end) +} + +/// A parsing error +#[derive(Debug, Clone)] +pub struct ParseDurationError { + msg: &'static str, +} + +const OVERFLOW_ERROR: ParseDurationError = ParseDurationError { + msg: "Overflow error", +}; + +impl fmt::Display for ParseDurationError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.msg) + } +} + +impl ParseDurationError { + const fn msg(msg: &'static str) -> Self { + Self { msg } + } +} + +impl Error for ParseDurationError {} + +/// An overflow during [`Duration`]-related operations. +/// +/// Matches XPath [`FODT0002` error](https://www.w3.org/TR/xpath-functions-31/#ERRFODT0002). +#[derive(Debug, Clone, Copy)] +pub struct DurationOverflowError; + +impl fmt::Display for DurationOverflowError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "overflow during xsd:duration computation") + } +} + +impl Error for DurationOverflowError {} + #[cfg(test)] mod tests { use super::*; #[test] - fn from_str() -> Result<(), XsdParseError> { + fn from_str() -> Result<(), ParseDurationError> { let min = Duration::new(i64::MIN, Decimal::MIN); let max = Duration::new(i64::MAX, Decimal::MAX); @@ -667,25 +995,52 @@ mod tests { } #[test] - fn from_std() { + fn from_std() -> Result<(), DurationOverflowError> { assert_eq!( - Duration::try_from(StdDuration::new(10, 10)) - .unwrap() - .to_string(), + Duration::try_from(StdDuration::new(10, 10))?.to_string(), "PT10.00000001S" ); + Ok(()) } #[test] - fn to_std() -> Result<(), XsdParseError> { - let duration = StdDuration::try_from(DayTimeDuration::from_str("PT10.00000001S")?).unwrap(); + fn to_std() -> Result<(), Box> { + let duration = StdDuration::try_from(DayTimeDuration::from_str("PT10.00000001S")?)?; assert_eq!(duration.as_secs(), 10); assert_eq!(duration.subsec_nanos(), 10); Ok(()) } #[test] - fn equals() -> Result<(), XsdParseError> { + fn to_be_bytes() { + assert_eq!( + Duration::from_be_bytes(Duration::MIN.to_be_bytes()), + Duration::MIN + ); + assert_eq!( + Duration::from_be_bytes(Duration::MAX.to_be_bytes()), + Duration::MAX + ); + assert_eq!( + YearMonthDuration::from_be_bytes(YearMonthDuration::MIN.to_be_bytes()), + YearMonthDuration::MIN + ); + assert_eq!( + YearMonthDuration::from_be_bytes(YearMonthDuration::MAX.to_be_bytes()), + YearMonthDuration::MAX + ); + assert_eq!( + DayTimeDuration::from_be_bytes(DayTimeDuration::MIN.to_be_bytes()), + DayTimeDuration::MIN + ); + assert_eq!( + DayTimeDuration::from_be_bytes(DayTimeDuration::MAX.to_be_bytes()), + DayTimeDuration::MAX + ); + } + + #[test] + fn equals() -> Result<(), ParseDurationError> { assert_eq!( YearMonthDuration::from_str("P1Y")?, YearMonthDuration::from_str("P12M")? @@ -730,7 +1085,24 @@ mod tests { } #[test] - fn years() -> Result<(), XsdParseError> { + #[allow(clippy::neg_cmp_op_on_partial_ord)] + fn cmp() -> Result<(), ParseDurationError> { + assert!(Duration::from_str("P1Y1D")? < Duration::from_str("P13MT25H")?); + assert!(YearMonthDuration::from_str("P1Y")? < YearMonthDuration::from_str("P13M")?); + assert!(Duration::from_str("P1Y")? < YearMonthDuration::from_str("P13M")?); + assert!(YearMonthDuration::from_str("P1Y")? < Duration::from_str("P13M")?); + assert!(DayTimeDuration::from_str("P1D")? < DayTimeDuration::from_str("PT25H")?); + assert!(DayTimeDuration::from_str("PT1H")? < DayTimeDuration::from_str("PT61M")?); + assert!(DayTimeDuration::from_str("PT1M")? < DayTimeDuration::from_str("PT61S")?); + assert!(Duration::from_str("PT1H")? < DayTimeDuration::from_str("PT61M")?); + assert!(DayTimeDuration::from_str("PT1H")? < Duration::from_str("PT61M")?); + assert!(YearMonthDuration::from_str("P1M")? < DayTimeDuration::from_str("P40D")?); + assert!(DayTimeDuration::from_str("P25D")? < YearMonthDuration::from_str("P1M")?); + Ok(()) + } + + #[test] + fn years() -> Result<(), ParseDurationError> { assert_eq!(Duration::from_str("P20Y15M")?.years(), 21); assert_eq!(Duration::from_str("-P15M")?.years(), -1); assert_eq!(Duration::from_str("-P2DT15H")?.years(), 0); @@ -738,7 +1110,7 @@ mod tests { } #[test] - fn months() -> Result<(), XsdParseError> { + fn months() -> Result<(), ParseDurationError> { assert_eq!(Duration::from_str("P20Y15M")?.months(), 3); assert_eq!(Duration::from_str("-P20Y18M")?.months(), -6); assert_eq!(Duration::from_str("-P2DT15H0M0S")?.months(), 0); @@ -746,7 +1118,7 @@ mod tests { } #[test] - fn days() -> Result<(), XsdParseError> { + fn days() -> Result<(), ParseDurationError> { assert_eq!(Duration::from_str("P3DT10H")?.days(), 3); assert_eq!(Duration::from_str("P3DT55H")?.days(), 5); assert_eq!(Duration::from_str("P3Y5M")?.days(), 0); @@ -754,7 +1126,7 @@ mod tests { } #[test] - fn hours() -> Result<(), XsdParseError> { + fn hours() -> Result<(), ParseDurationError> { assert_eq!(Duration::from_str("P3DT10H")?.hours(), 10); assert_eq!(Duration::from_str("P3DT12H32M12S")?.hours(), 12); assert_eq!(Duration::from_str("PT123H")?.hours(), 3); @@ -763,14 +1135,14 @@ mod tests { } #[test] - fn minutes() -> Result<(), XsdParseError> { + fn minutes() -> Result<(), ParseDurationError> { assert_eq!(Duration::from_str("P3DT10H")?.minutes(), 0); assert_eq!(Duration::from_str("-P5DT12H30M")?.minutes(), -30); Ok(()) } #[test] - fn seconds() -> Result<(), XsdParseError> { + fn seconds() -> Result<(), Box> { assert_eq!( Duration::from_str("P3DT10H12.5S")?.seconds(), Decimal::from_str("12.5")? @@ -783,7 +1155,7 @@ mod tests { } #[test] - fn add() -> Result<(), XsdParseError> { + fn add() -> Result<(), ParseDurationError> { assert_eq!( Duration::from_str("P2Y11M")?.checked_add(Duration::from_str("P3Y3M")?), Some(Duration::from_str("P6Y2M")?) @@ -796,7 +1168,7 @@ mod tests { } #[test] - fn sub() -> Result<(), XsdParseError> { + fn sub() -> Result<(), ParseDurationError> { assert_eq!( Duration::from_str("P2Y11M")?.checked_sub(Duration::from_str("P3Y3M")?), Some(Duration::from_str("-P4M")?) @@ -809,7 +1181,7 @@ mod tests { } #[test] - fn minimally_conformant() -> Result<(), XsdParseError> { + fn minimally_conformant() -> Result<(), ParseDurationError> { // All minimally conforming processors must support fractional-second duration values // to milliseconds (i.e. those expressible with three fraction digits). assert_eq!(Duration::from_str("PT0.001S")?.to_string(), "PT0.001S"); diff --git a/lib/oxsdatatypes/src/integer.rs b/lib/oxsdatatypes/src/integer.rs index f376a57d..352e521a 100644 --- a/lib/oxsdatatypes/src/integer.rs +++ b/lib/oxsdatatypes/src/integer.rs @@ -1,4 +1,5 @@ -use crate::{Boolean, Decimal, DecimalOverflowError, Double, Float}; +use crate::{Boolean, Decimal, Double, Float}; +use std::error::Error; use std::fmt; use std::num::ParseIntError; use std::str::FromStr; @@ -28,6 +29,8 @@ impl Integer { } /// [op:numeric-add](https://www.w3.org/TR/xpath-functions-31/#func-numeric-add) + /// + /// Returns `None` in case of overflow ([FOAR0002](https://www.w3.org/TR/xpath-functions-31/#ERRFOAR0002)). #[inline] #[must_use] pub fn checked_add(self, rhs: impl Into) -> Option { @@ -37,6 +40,8 @@ impl Integer { } /// [op:numeric-subtract](https://www.w3.org/TR/xpath-functions-31/#func-numeric-subtract) + /// + /// Returns `None` in case of overflow ([FOAR0002](https://www.w3.org/TR/xpath-functions-31/#ERRFOAR0002)). #[inline] #[must_use] pub fn checked_sub(self, rhs: impl Into) -> Option { @@ -46,6 +51,8 @@ impl Integer { } /// [op:numeric-multiply](https://www.w3.org/TR/xpath-functions-31/#func-numeric-multiply) + /// + /// Returns `None` in case of overflow ([FOAR0002](https://www.w3.org/TR/xpath-functions-31/#ERRFOAR0002)). #[inline] #[must_use] pub fn checked_mul(self, rhs: impl Into) -> Option { @@ -55,6 +62,8 @@ impl Integer { } /// [op:numeric-integer-divide](https://www.w3.org/TR/xpath-functions-31/#func-numeric-integer-divide) + /// + /// Returns `None` in case of division by 0 ([FOAR0001](https://www.w3.org/TR/xpath-functions-31/#ERRFOAR0001)) or overflow ([FOAR0002](https://www.w3.org/TR/xpath-functions-31/#ERRFOAR0002)). #[inline] #[must_use] pub fn checked_div(self, rhs: impl Into) -> Option { @@ -64,6 +73,8 @@ impl Integer { } /// [op:numeric-mod](https://www.w3.org/TR/xpath-functions-31/#func-numeric-mod) + /// + /// Returns `None` in case of division by 0 ([FOAR0001](https://www.w3.org/TR/xpath-functions-31/#ERRFOAR0001)) or overflow ([FOAR0002](https://www.w3.org/TR/xpath-functions-31/#ERRFOAR0002)). #[inline] #[must_use] pub fn checked_rem(self, rhs: impl Into) -> Option { @@ -72,6 +83,9 @@ impl Integer { }) } + /// Euclidean remainder + /// + /// Returns `None` in case of division by 0 ([FOAR0001](https://www.w3.org/TR/xpath-functions-31/#ERRFOAR0001)) or overflow ([FOAR0002](https://www.w3.org/TR/xpath-functions-31/#ERRFOAR0002)). #[inline] #[must_use] pub fn checked_rem_euclid(self, rhs: impl Into) -> Option { @@ -81,6 +95,8 @@ impl Integer { } /// [op:numeric-unary-minus](https://www.w3.org/TR/xpath-functions-31/#func-numeric-unary-minus) + /// + /// Returns `None` in case of overflow ([FOAR0002](https://www.w3.org/TR/xpath-functions-31/#ERRFOAR0002)). #[inline] #[must_use] pub fn checked_neg(self) -> Option { @@ -90,12 +106,14 @@ impl Integer { } /// [fn:abs](https://www.w3.org/TR/xpath-functions-31/#func-abs) + /// + /// Returns `None` in case of overflow ([FOAR0002](https://www.w3.org/TR/xpath-functions-31/#ERRFOAR0002)). #[inline] #[must_use] - pub const fn abs(self) -> Self { - Self { - value: self.value.abs(), - } + pub fn checked_abs(self) -> Option { + Some(Self { + value: self.value.checked_abs()?, + }) } #[inline] @@ -223,23 +241,41 @@ impl fmt::Display for Integer { } impl TryFrom for Integer { - type Error = DecimalOverflowError; + type Error = TooLargeForIntegerError; #[inline] - fn try_from(value: Float) -> Result { - Decimal::try_from(value)?.try_into() + fn try_from(value: Float) -> Result { + Decimal::try_from(value) + .map_err(|_| TooLargeForIntegerError)? + .try_into() } } impl TryFrom for Integer { - type Error = DecimalOverflowError; + type Error = TooLargeForIntegerError; #[inline] - fn try_from(value: Double) -> Result { - Decimal::try_from(value)?.try_into() + fn try_from(value: Double) -> Result { + Decimal::try_from(value) + .map_err(|_| TooLargeForIntegerError)? + .try_into() } } +/// The input is too large to fit into an [`Integer`]. +/// +/// Matches XPath [`FOCA0003` error](https://www.w3.org/TR/xpath-functions-31/#ERRFOCA0003). +#[derive(Debug, Clone, Copy)] +pub struct TooLargeForIntegerError; + +impl fmt::Display for TooLargeForIntegerError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Value too large for xsd:integer internal representation") + } +} + +impl Error for TooLargeForIntegerError {} + #[cfg(test)] mod tests { use super::*; @@ -278,7 +314,8 @@ mod tests { .unwrap() .checked_sub(Integer::from_str("1672507300000")?) .unwrap() - .abs() + .checked_abs() + .unwrap() < Integer::from(1_000_000) ); Ok(()) @@ -303,7 +340,8 @@ mod tests { .unwrap() .checked_sub(Integer::from_str("1672507300000").unwrap()) .unwrap() - .abs() + .checked_abs() + .unwrap() < Integer::from(10) ); assert!(Integer::try_from(Double::from(f64::NAN)).is_err()); diff --git a/lib/oxsdatatypes/src/lib.rs b/lib/oxsdatatypes/src/lib.rs index 6e1cd28f..a31caf61 100644 --- a/lib/oxsdatatypes/src/lib.rs +++ b/lib/oxsdatatypes/src/lib.rs @@ -11,15 +11,16 @@ mod double; mod duration; mod float; mod integer; -mod parser; pub use self::boolean::Boolean; pub use self::date_time::{ - Date, DateTime, DateTimeError, GDay, GMonth, GMonthDay, GYear, GYearMonth, Time, TimezoneOffset, + Date, DateTime, DateTimeOverflowError, GDay, GMonth, GMonthDay, GYear, GYearMonth, + InvalidTimezoneError, ParseDateTimeError, Time, TimezoneOffset, }; -pub use self::decimal::{Decimal, DecimalOverflowError, ParseDecimalError}; +pub use self::decimal::{Decimal, ParseDecimalError, TooLargeForDecimalError}; pub use self::double::Double; -pub use self::duration::{DayTimeDuration, Duration, YearMonthDuration}; +pub use self::duration::{ + DayTimeDuration, Duration, DurationOverflowError, ParseDurationError, YearMonthDuration, +}; pub use self::float::Float; -pub use self::integer::Integer; -pub use self::parser::XsdParseError; +pub use self::integer::{Integer, TooLargeForIntegerError}; diff --git a/lib/oxsdatatypes/src/parser.rs b/lib/oxsdatatypes/src/parser.rs deleted file mode 100644 index 942c71e4..00000000 --- a/lib/oxsdatatypes/src/parser.rs +++ /dev/null @@ -1,626 +0,0 @@ -use super::date_time::{DateTimeError, GDay, GMonth, GMonthDay, GYear, GYearMonth, TimezoneOffset}; -use super::decimal::ParseDecimalError; -use super::duration::{DayTimeDuration, YearMonthDuration}; -use super::*; -use std::error::Error; -use std::fmt; -use std::num::ParseIntError; -use std::str::FromStr; - -/// A parsing error -#[derive(Debug, Clone)] -pub struct XsdParseError { - kind: XsdParseErrorKind, -} - -#[derive(Debug, Clone)] -enum XsdParseErrorKind { - ParseInt(ParseIntError), - ParseDecimal(ParseDecimalError), - DateTime(DateTimeError), - Message(&'static str), -} - -const OVERFLOW_ERROR: XsdParseError = XsdParseError { - kind: XsdParseErrorKind::Message("Overflow error"), -}; - -impl fmt::Display for XsdParseError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match &self.kind { - XsdParseErrorKind::ParseInt(error) => { - write!(f, "Error while parsing integer: {error}") - } - XsdParseErrorKind::ParseDecimal(error) => { - write!(f, "Error while parsing decimal: {error}") - } - XsdParseErrorKind::DateTime(error) => error.fmt(f), - XsdParseErrorKind::Message(msg) => write!(f, "{msg}"), - } - } -} - -impl XsdParseError { - const fn msg(message: &'static str) -> Self { - Self { - kind: XsdParseErrorKind::Message(message), - } - } -} - -impl Error for XsdParseError { - fn source(&self) -> Option<&(dyn Error + 'static)> { - match &self.kind { - XsdParseErrorKind::ParseInt(error) => Some(error), - XsdParseErrorKind::ParseDecimal(error) => Some(error), - XsdParseErrorKind::DateTime(error) => Some(error), - XsdParseErrorKind::Message(_) => None, - } - } -} - -impl From for XsdParseError { - fn from(error: ParseIntError) -> Self { - Self { - kind: XsdParseErrorKind::ParseInt(error), - } - } -} - -impl From for XsdParseError { - fn from(error: ParseDecimalError) -> Self { - Self { - kind: XsdParseErrorKind::ParseDecimal(error), - } - } -} - -impl From for XsdParseError { - fn from(error: DateTimeError) -> Self { - Self { - kind: XsdParseErrorKind::DateTime(error), - } - } -} - -// [6] duYearFrag ::= unsignedNoDecimalPtNumeral 'Y' -// [7] duMonthFrag ::= unsignedNoDecimalPtNumeral 'M' -// [8] duDayFrag ::= unsignedNoDecimalPtNumeral 'D' -// [9] duHourFrag ::= unsignedNoDecimalPtNumeral 'H' -// [10] duMinuteFrag ::= unsignedNoDecimalPtNumeral 'M' -// [11] duSecondFrag ::= (unsignedNoDecimalPtNumeral | unsignedDecimalPtNumeral) 'S' -// [12] duYearMonthFrag ::= (duYearFrag duMonthFrag?) | duMonthFrag -// [13] duTimeFrag ::= 'T' ((duHourFrag duMinuteFrag? duSecondFrag?) | (duMinuteFrag duSecondFrag?) | duSecondFrag) -// [14] duDayTimeFrag ::= (duDayFrag duTimeFrag?) | duTimeFrag -// [15] durationLexicalRep ::= '-'? 'P' ((duYearMonthFrag duDayTimeFrag?) | duDayTimeFrag) -struct DurationParts { - year_month: Option, - day_time: Option, -} - -fn duration_parts(input: &str) -> Result<(DurationParts, &str), XsdParseError> { - // States - const START: u32 = 0; - const AFTER_YEAR: u32 = 1; - const AFTER_MONTH: u32 = 2; - const AFTER_DAY: u32 = 3; - const AFTER_T: u32 = 4; - const AFTER_HOUR: u32 = 5; - const AFTER_MINUTE: u32 = 6; - const AFTER_SECOND: u32 = 7; - - let (is_negative, input) = if let Some(left) = input.strip_prefix('-') { - (true, left) - } else { - (false, input) - }; - let mut input = expect_char(input, 'P', "Durations must start with 'P'")?; - let mut state = START; - let mut year_month: Option = None; - let mut day_time: Option = None; - while !input.is_empty() { - if let Some(left) = input.strip_prefix('T') { - if state >= AFTER_T { - return Err(XsdParseError::msg("Duplicated time separator 'T'")); - } - state = AFTER_T; - input = left; - } else { - let (number_str, left) = decimal_prefix(input); - match left.chars().next() { - Some('Y') if state < AFTER_YEAR => { - year_month = Some( - year_month - .unwrap_or_default() - .checked_add( - apply_i64_neg(i64::from_str(number_str)?, is_negative)? - .checked_mul(12) - .ok_or(OVERFLOW_ERROR)?, - ) - .ok_or(OVERFLOW_ERROR)?, - ); - state = AFTER_YEAR; - } - Some('M') if state < AFTER_MONTH => { - year_month = Some( - year_month - .unwrap_or_default() - .checked_add(apply_i64_neg(i64::from_str(number_str)?, is_negative)?) - .ok_or(OVERFLOW_ERROR)?, - ); - state = AFTER_MONTH; - } - Some('D') if state < AFTER_DAY => { - if number_str.contains('.') { - return Err(XsdParseError::msg( - "Decimal numbers are not allowed for days", - )); - } - day_time = Some( - day_time - .unwrap_or_default() - .checked_add( - apply_decimal_neg(Decimal::from_str(number_str)?, is_negative)? - .checked_mul(86400) - .ok_or(OVERFLOW_ERROR)?, - ) - .ok_or(OVERFLOW_ERROR)?, - ); - state = AFTER_DAY; - } - Some('H') if state == AFTER_T => { - if number_str.contains('.') { - return Err(XsdParseError::msg( - "Decimal numbers are not allowed for hours", - )); - } - day_time = Some( - day_time - .unwrap_or_default() - .checked_add( - apply_decimal_neg(Decimal::from_str(number_str)?, is_negative)? - .checked_mul(3600) - .ok_or(OVERFLOW_ERROR)?, - ) - .ok_or(OVERFLOW_ERROR)?, - ); - state = AFTER_HOUR; - } - Some('M') if (AFTER_T..AFTER_MINUTE).contains(&state) => { - if number_str.contains('.') { - return Err(XsdParseError::msg( - "Decimal numbers are not allowed for minutes", - )); - } - day_time = Some( - day_time - .unwrap_or_default() - .checked_add( - apply_decimal_neg(Decimal::from_str(number_str)?, is_negative)? - .checked_mul(60) - .ok_or(OVERFLOW_ERROR)?, - ) - .ok_or(OVERFLOW_ERROR)?, - ); - state = AFTER_MINUTE; - } - Some('S') if (AFTER_T..AFTER_SECOND).contains(&state) => { - day_time = Some( - day_time - .unwrap_or_default() - .checked_add(apply_decimal_neg( - Decimal::from_str(number_str)?, - is_negative, - )?) - .ok_or(OVERFLOW_ERROR)?, - ); - state = AFTER_SECOND; - } - Some(_) => return Err(XsdParseError::msg("Unexpected type character")), - None => { - return Err(XsdParseError::msg( - "Numbers in durations must be followed by a type character", - )) - } - } - input = &left[1..]; - } - } - - Ok(( - DurationParts { - year_month, - day_time, - }, - input, - )) -} - -fn apply_i64_neg(value: i64, is_negative: bool) -> Result { - if is_negative { - value.checked_neg().ok_or(OVERFLOW_ERROR) - } else { - Ok(value) - } -} - -fn apply_decimal_neg(value: Decimal, is_negative: bool) -> Result { - if is_negative { - value.checked_neg().ok_or(OVERFLOW_ERROR) - } else { - Ok(value) - } -} - -pub fn parse_duration(input: &str) -> Result { - let parts = ensure_complete(input, duration_parts)?; - if parts.year_month.is_none() && parts.day_time.is_none() { - return Err(XsdParseError::msg("Empty duration")); - } - Ok(Duration::new( - parts.year_month.unwrap_or(0), - parts.day_time.unwrap_or_default(), - )) -} - -pub fn parse_year_month_duration(input: &str) -> Result { - let parts = ensure_complete(input, duration_parts)?; - if parts.day_time.is_some() { - return Err(XsdParseError::msg( - "There must not be any day or time component in a yearMonthDuration", - )); - } - Ok(YearMonthDuration::new(parts.year_month.ok_or( - XsdParseError::msg("No year and month values found"), - )?)) -} - -pub fn parse_day_time_duration(input: &str) -> Result { - let parts = ensure_complete(input, duration_parts)?; - if parts.year_month.is_some() { - return Err(XsdParseError::msg( - "There must not be any year or month component in a dayTimeDuration", - )); - } - Ok(DayTimeDuration::new(parts.day_time.ok_or( - XsdParseError::msg("No day or time values found"), - )?)) -} - -// [16] dateTimeLexicalRep ::= yearFrag '-' monthFrag '-' dayFrag 'T' ((hourFrag ':' minuteFrag ':' secondFrag) | endOfDayFrag) timezoneFrag? -fn date_time_lexical_rep(input: &str) -> Result<(DateTime, &str), XsdParseError> { - let (year, input) = year_frag(input)?; - let input = expect_char(input, '-', "The year and month must be separated by '-'")?; - let (month, input) = month_frag(input)?; - let input = expect_char(input, '-', "The month and day must be separated by '-'")?; - let (day, input) = day_frag(input)?; - let input = expect_char(input, 'T', "The date and time must be separated by 'T'")?; - let (hour, input) = hour_frag(input)?; - let input = expect_char(input, ':', "The hours and minutes must be separated by ':'")?; - let (minute, input) = minute_frag(input)?; - let input = expect_char( - input, - ':', - "The minutes and seconds must be separated by ':'", - )?; - let (second, input) = second_frag(input)?; - // We validate 24:00:00 - if hour == 24 && minute != 0 && second != Decimal::from(0) { - return Err(XsdParseError::msg( - "Times are not allowed to be after 24:00:00", - )); - } - let (timezone_offset, input) = optional_end(input, timezone_frag)?; - Ok(( - DateTime::new(year, month, day, hour, minute, second, timezone_offset)?, - input, - )) -} - -pub fn parse_date_time(input: &str) -> Result { - ensure_complete(input, date_time_lexical_rep) -} - -// [17] timeLexicalRep ::= ((hourFrag ':' minuteFrag ':' secondFrag) | endOfDayFrag) timezoneFrag? -fn time_lexical_rep(input: &str) -> Result<(Time, &str), XsdParseError> { - let (hour, input) = hour_frag(input)?; - let input = expect_char(input, ':', "The hours and minutes must be separated by ':'")?; - let (minute, input) = minute_frag(input)?; - let input = expect_char( - input, - ':', - "The minutes and seconds must be separated by ':'", - )?; - let (second, input) = second_frag(input)?; - // We validate 24:00:00 - if hour == 24 && minute != 0 && second != Decimal::from(0) { - return Err(XsdParseError::msg( - "Times are not allowed to be after 24:00:00", - )); - } - let (timezone_offset, input) = optional_end(input, timezone_frag)?; - Ok((Time::new(hour, minute, second, timezone_offset)?, input)) -} - -pub fn parse_time(input: &str) -> Result { - ensure_complete(input, time_lexical_rep) -} - -// [18] dateLexicalRep ::= yearFrag '-' monthFrag '-' dayFrag timezoneFrag? Constraint: Day-of-month Representations -fn date_lexical_rep(input: &str) -> Result<(Date, &str), XsdParseError> { - let (year, input) = year_frag(input)?; - let input = expect_char(input, '-', "The year and month must be separated by '-'")?; - let (month, input) = month_frag(input)?; - let input = expect_char(input, '-', "The month and day must be separated by '-'")?; - let (day, input) = day_frag(input)?; - let (timezone_offset, input) = optional_end(input, timezone_frag)?; - Ok((Date::new(year, month, day, timezone_offset)?, input)) -} - -pub fn parse_date(input: &str) -> Result { - ensure_complete(input, date_lexical_rep) -} - -// [19] gYearMonthLexicalRep ::= yearFrag '-' monthFrag timezoneFrag? -fn g_year_month_lexical_rep(input: &str) -> Result<(GYearMonth, &str), XsdParseError> { - let (year, input) = year_frag(input)?; - let input = expect_char(input, '-', "The year and month must be separated by '-'")?; - let (month, input) = month_frag(input)?; - let (timezone_offset, input) = optional_end(input, timezone_frag)?; - Ok((GYearMonth::new(year, month, timezone_offset)?, input)) -} - -pub fn parse_g_year_month(input: &str) -> Result { - ensure_complete(input, g_year_month_lexical_rep) -} - -// [20] gYearLexicalRep ::= yearFrag timezoneFrag? -fn g_year_lexical_rep(input: &str) -> Result<(GYear, &str), XsdParseError> { - let (year, input) = year_frag(input)?; - let (timezone_offset, input) = optional_end(input, timezone_frag)?; - Ok((GYear::new(year, timezone_offset)?, input)) -} - -pub fn parse_g_year(input: &str) -> Result { - ensure_complete(input, g_year_lexical_rep) -} - -// [21] gMonthDayLexicalRep ::= '--' monthFrag '-' dayFrag timezoneFrag? Constraint: Day-of-month Representations -fn g_month_day_lexical_rep(input: &str) -> Result<(GMonthDay, &str), XsdParseError> { - let input = expect_char(input, '-', "gMonthDay values must start with '--'")?; - let input = expect_char(input, '-', "gMonthDay values must start with '--'")?; - let (month, input) = month_frag(input)?; - let input = expect_char(input, '-', "The month and day must be separated by '-'")?; - let (day, input) = day_frag(input)?; - let (timezone_offset, input) = optional_end(input, timezone_frag)?; - Ok((GMonthDay::new(month, day, timezone_offset)?, input)) -} - -pub fn parse_g_month_day(input: &str) -> Result { - ensure_complete(input, g_month_day_lexical_rep) -} - -// [22] gDayLexicalRep ::= '---' dayFrag timezoneFrag? -fn g_day_lexical_rep(input: &str) -> Result<(GDay, &str), XsdParseError> { - let input = expect_char(input, '-', "gDay values must start with '---'")?; - let input = expect_char(input, '-', "gDay values must start with '---'")?; - let input = expect_char(input, '-', "gDay values must start with '---'")?; - let (day, input) = day_frag(input)?; - let (timezone_offset, input) = optional_end(input, timezone_frag)?; - Ok((GDay::new(day, timezone_offset)?, input)) -} - -pub fn parse_g_day(input: &str) -> Result { - ensure_complete(input, g_day_lexical_rep) -} - -// [23] gMonthLexicalRep ::= '--' monthFrag timezoneFrag? -fn g_month_lexical_rep(input: &str) -> Result<(GMonth, &str), XsdParseError> { - let input = expect_char(input, '-', "gMonth values must start with '--'")?; - let input = expect_char(input, '-', "gMonth values must start with '--'")?; - let (month, input) = month_frag(input)?; - let (timezone_offset, input) = optional_end(input, timezone_frag)?; - Ok((GMonth::new(month, timezone_offset)?, input)) -} - -pub fn parse_g_month(input: &str) -> Result { - ensure_complete(input, g_month_lexical_rep) -} - -// [56] yearFrag ::= '-'? (([1-9] digit digit digit+)) | ('0' digit digit digit)) -fn year_frag(input: &str) -> Result<(i64, &str), XsdParseError> { - let (sign, input) = if let Some(left) = input.strip_prefix('-') { - (-1, left) - } else { - (1, input) - }; - let (number_str, input) = integer_prefix(input); - if number_str.len() < 4 { - return Err(XsdParseError::msg("The year should be encoded on 4 digits")); - } - if number_str.len() > 4 && number_str.starts_with('0') { - return Err(XsdParseError::msg( - "The years value must not start with 0 if it can be encoded in at least 4 digits", - )); - } - let number = i64::from_str(number_str)?; - Ok((sign * number, input)) -} - -// [57] monthFrag ::= ('0' [1-9]) | ('1' [0-2]) -fn month_frag(input: &str) -> Result<(u8, &str), XsdParseError> { - let (number_str, input) = integer_prefix(input); - if number_str.len() != 2 { - return Err(XsdParseError::msg("Month must be encoded with two digits")); - } - let number = u8::from_str(number_str)?; - if !(1..=12).contains(&number) { - return Err(XsdParseError::msg("Month must be between 01 and 12")); - } - Ok((number, input)) -} - -// [58] dayFrag ::= ('0' [1-9]) | ([12] digit) | ('3' [01]) -fn day_frag(input: &str) -> Result<(u8, &str), XsdParseError> { - let (number_str, input) = integer_prefix(input); - if number_str.len() != 2 { - return Err(XsdParseError::msg("Day must be encoded with two digits")); - } - let number = u8::from_str(number_str)?; - if !(1..=31).contains(&number) { - return Err(XsdParseError::msg("Day must be between 01 and 31")); - } - Ok((number, input)) -} - -// [59] hourFrag ::= ([01] digit) | ('2' [0-3]) -// We also allow 24 for ease of parsing -fn hour_frag(input: &str) -> Result<(u8, &str), XsdParseError> { - let (number_str, input) = integer_prefix(input); - if number_str.len() != 2 { - return Err(XsdParseError::msg("Hours must be encoded with two digits")); - } - let number = u8::from_str(number_str)?; - if !(0..=24).contains(&number) { - return Err(XsdParseError::msg("Hours must be between 00 and 24")); - } - Ok((number, input)) -} - -// [60] minuteFrag ::= [0-5] digit -fn minute_frag(input: &str) -> Result<(u8, &str), XsdParseError> { - let (number_str, input) = integer_prefix(input); - if number_str.len() != 2 { - return Err(XsdParseError::msg( - "Minutes must be encoded with two digits", - )); - } - let number = u8::from_str(number_str)?; - if !(0..=59).contains(&number) { - return Err(XsdParseError::msg("Minutes must be between 00 and 59")); - } - Ok((number, input)) -} - -// [61] secondFrag ::= ([0-5] digit) ('.' digit+)? -fn second_frag(input: &str) -> Result<(Decimal, &str), XsdParseError> { - let (number_str, input) = decimal_prefix(input); - let (before_dot_str, _) = number_str.split_once('.').unwrap_or((number_str, "")); - if before_dot_str.len() != 2 { - return Err(XsdParseError::msg( - "Seconds must be encoded with two digits", - )); - } - let number = Decimal::from_str(number_str)?; - if number < Decimal::from(0) || number >= Decimal::from(60) { - return Err(XsdParseError::msg("Seconds must be between 00 and 60")); - } - if number_str.ends_with('.') { - return Err(XsdParseError::msg( - "Seconds are not allowed to end with a dot", - )); - } - Ok((number, input)) -} - -// [63] timezoneFrag ::= 'Z' | ('+' | '-') (('0' digit | '1' [0-3]) ':' minuteFrag | '14:00') -fn timezone_frag(input: &str) -> Result<(TimezoneOffset, &str), XsdParseError> { - if let Some(left) = input.strip_prefix('Z') { - return Ok((TimezoneOffset::UTC, left)); - } - let (sign, input) = if let Some(left) = input.strip_prefix('-') { - (-1, left) - } else if let Some(left) = input.strip_prefix('+') { - (1, left) - } else { - (1, input) - }; - - let (hour_str, input) = integer_prefix(input); - if hour_str.len() != 2 { - return Err(XsdParseError::msg( - "The timezone hours must be encoded with two digits", - )); - } - let hours = i16::from_str(hour_str)?; - - let input = expect_char( - input, - ':', - "The timezone hours and minutes must be separated by ':'", - )?; - let (minutes, input) = minute_frag(input)?; - - if hours > 13 && !(hours == 14 && minutes == 0) { - return Err(XsdParseError::msg( - "The timezone hours must be between 00 and 13", - )); - } - - Ok(( - TimezoneOffset::new(sign * (hours * 60 + i16::from(minutes)))?, - input, - )) -} - -fn ensure_complete( - input: &str, - parse: impl FnOnce(&str) -> Result<(T, &str), XsdParseError>, -) -> Result { - let (result, left) = parse(input)?; - if !left.is_empty() { - return Err(XsdParseError::msg("Unrecognized value suffix")); - } - Ok(result) -} - -fn expect_char<'a>( - input: &'a str, - constant: char, - error_message: &'static str, -) -> Result<&'a str, XsdParseError> { - if let Some(left) = input.strip_prefix(constant) { - Ok(left) - } else { - Err(XsdParseError::msg(error_message)) - } -} - -fn integer_prefix(input: &str) -> (&str, &str) { - let mut end = input.len(); - for (i, c) in input.char_indices() { - if !c.is_ascii_digit() { - end = i; - break; - } - } - input.split_at(end) -} - -fn decimal_prefix(input: &str) -> (&str, &str) { - let mut end = input.len(); - let mut dot_seen = false; - for (i, c) in input.char_indices() { - if c.is_ascii_digit() { - // Ok - } else if c == '.' && !dot_seen { - dot_seen = true; - } else { - end = i; - break; - } - } - input.split_at(end) -} - -fn optional_end( - input: &str, - parse: impl FnOnce(&str) -> Result<(T, &str), XsdParseError>, -) -> Result<(Option, &str), XsdParseError> { - Ok(if input.is_empty() { - (None, input) - } else { - let (result, input) = parse(input)?; - (Some(result), input) - }) -} diff --git a/lib/src/sparql/eval.rs b/lib/src/sparql/eval.rs index 90a52efa..ad50cd2f 100644 --- a/lib/src/sparql/eval.rs +++ b/lib/src/sparql/eval.rs @@ -142,7 +142,7 @@ impl SimpleEvaluator { Self { dataset, base_iri, - now: DateTime::now().unwrap(), + now: DateTime::now(), service_handler, custom_functions, run_stats, @@ -1605,8 +1605,8 @@ impl SimpleEvaluator { stat_children, ); Rc::new(move |tuple| match e(tuple)? { - EncodedTerm::IntegerLiteral(value) => Some(value.abs().into()), - EncodedTerm::DecimalLiteral(value) => Some(value.abs().into()), + EncodedTerm::IntegerLiteral(value) => Some(value.checked_abs()?.into()), + EncodedTerm::DecimalLiteral(value) => Some(value.checked_abs()?.into()), EncodedTerm::FloatLiteral(value) => Some(value.abs().into()), EncodedTerm::DoubleLiteral(value) => Some(value.abs().into()), _ => None, @@ -1620,7 +1620,9 @@ impl SimpleEvaluator { ); Rc::new(move |tuple| match e(tuple)? { EncodedTerm::IntegerLiteral(value) => Some(value.into()), - EncodedTerm::DecimalLiteral(value) => Some(value.ceil().into()), + EncodedTerm::DecimalLiteral(value) => { + Some(value.checked_ceil()?.into()) + } EncodedTerm::FloatLiteral(value) => Some(value.ceil().into()), EncodedTerm::DoubleLiteral(value) => Some(value.ceil().into()), _ => None, @@ -1634,7 +1636,9 @@ impl SimpleEvaluator { ); Rc::new(move |tuple| match e(tuple)? { EncodedTerm::IntegerLiteral(value) => Some(value.into()), - EncodedTerm::DecimalLiteral(value) => Some(value.floor().into()), + EncodedTerm::DecimalLiteral(value) => { + Some(value.checked_floor()?.into()) + } EncodedTerm::FloatLiteral(value) => Some(value.floor().into()), EncodedTerm::DoubleLiteral(value) => Some(value.floor().into()), _ => None, @@ -1648,7 +1652,9 @@ impl SimpleEvaluator { ); Rc::new(move |tuple| match e(tuple)? { EncodedTerm::IntegerLiteral(value) => Some(value.into()), - EncodedTerm::DecimalLiteral(value) => Some(value.round().into()), + EncodedTerm::DecimalLiteral(value) => { + Some(value.checked_round()?.into()) + } EncodedTerm::FloatLiteral(value) => Some(value.round().into()), EncodedTerm::DoubleLiteral(value) => Some(value.round().into()), _ => None, @@ -5851,18 +5857,18 @@ fn format_list(values: impl IntoIterator) -> String { } pub struct Timer { - start: Option, + start: DateTime, } impl Timer { pub fn now() -> Self { Self { - start: DateTime::now().ok(), + start: DateTime::now(), } } pub fn elapsed(&self) -> Option { - DateTime::now().ok()?.checked_sub(self.start?) + DateTime::now().checked_sub(self.start) } }