oxsdatatypes/
duration.rs

1use crate::{DateTime, Decimal};
2use std::cmp::Ordering;
3use std::fmt;
4use std::str::FromStr;
5#[cfg(not(target_os = "zkvm"))]
6use std::time::Duration as StdDuration;
7
8/// [XML Schema `duration` datatype](https://www.w3.org/TR/xmlschema11-2/#duration)
9///
10/// It stores the duration using a pair of a [`YearMonthDuration`] and a [`DayTimeDuration`].
11#[derive(Eq, PartialEq, Debug, Clone, Copy, Hash, Default)]
12pub struct Duration {
13    year_month: YearMonthDuration,
14    day_time: DayTimeDuration,
15}
16
17impl Duration {
18    pub const MAX: Self = Self {
19        year_month: YearMonthDuration::MAX,
20        day_time: DayTimeDuration::MAX,
21    };
22    pub const MIN: Self = Self {
23        year_month: YearMonthDuration::MIN,
24        day_time: DayTimeDuration::MIN,
25    };
26
27    #[inline]
28    pub fn new(
29        months: impl Into<i64>,
30        seconds: impl Into<Decimal>,
31    ) -> Result<Self, OppositeSignInDurationComponentsError> {
32        Self::construct(
33            YearMonthDuration::new(months),
34            DayTimeDuration::new(seconds),
35        )
36    }
37
38    #[inline]
39    fn construct(
40        year_month: YearMonthDuration,
41        day_time: DayTimeDuration,
42    ) -> Result<Self, OppositeSignInDurationComponentsError> {
43        if (year_month > YearMonthDuration::default() && day_time < DayTimeDuration::default())
44            || (year_month < YearMonthDuration::default() && day_time > DayTimeDuration::default())
45        {
46            return Err(OppositeSignInDurationComponentsError);
47        }
48        Ok(Self {
49            year_month,
50            day_time,
51        })
52    }
53
54    #[inline]
55    #[must_use]
56    pub fn from_be_bytes(bytes: [u8; 24]) -> Self {
57        Self {
58            year_month: YearMonthDuration::from_be_bytes(bytes[0..8].try_into().unwrap()),
59            day_time: DayTimeDuration::from_be_bytes(bytes[8..24].try_into().unwrap()),
60        }
61    }
62
63    /// [fn:years-from-duration](https://www.w3.org/TR/xpath-functions-31/#func-years-from-duration)
64    #[inline]
65    #[must_use]
66    pub fn years(self) -> i64 {
67        self.year_month.years()
68    }
69
70    /// [fn:months-from-duration](https://www.w3.org/TR/xpath-functions-31/#func-months-from-duration)
71    #[inline]
72    #[must_use]
73    pub fn months(self) -> i64 {
74        self.year_month.months()
75    }
76
77    /// [fn:days-from-duration](https://www.w3.org/TR/xpath-functions-31/#func-days-from-duration)
78    #[inline]
79    #[must_use]
80    pub fn days(self) -> i64 {
81        self.day_time.days()
82    }
83
84    /// [fn:hours-from-duration](https://www.w3.org/TR/xpath-functions-31/#func-hours-from-duration)
85    #[inline]
86    #[must_use]
87    pub fn hours(self) -> i64 {
88        self.day_time.hours()
89    }
90
91    /// [fn:minutes-from-duration](https://www.w3.org/TR/xpath-functions-31/#func-minutes-from-duration)
92    #[inline]
93    #[must_use]
94    pub fn minutes(self) -> i64 {
95        self.day_time.minutes()
96    }
97
98    /// [fn:seconds-from-duration](https://www.w3.org/TR/xpath-functions-31/#func-seconds-from-duration)
99    #[inline]
100    #[must_use]
101    pub fn seconds(self) -> Decimal {
102        self.day_time.seconds()
103    }
104
105    #[inline]
106    #[must_use]
107    pub(crate) const fn all_months(self) -> i64 {
108        self.year_month.all_months()
109    }
110
111    #[inline]
112    #[must_use]
113    pub(crate) const fn all_seconds(self) -> Decimal {
114        self.day_time.as_seconds()
115    }
116
117    #[inline]
118    #[must_use]
119    pub fn to_be_bytes(self) -> [u8; 24] {
120        let mut bytes = [0; 24];
121        bytes[0..8].copy_from_slice(&self.year_month.to_be_bytes());
122        bytes[8..24].copy_from_slice(&self.day_time.to_be_bytes());
123        bytes
124    }
125
126    /// [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)
127    ///
128    /// Returns `None` in case of overflow ([`FODT0002`](https://www.w3.org/TR/xpath-functions-31/#ERRFODT0002)).
129    #[inline]
130    #[must_use]
131    pub fn checked_add(self, rhs: impl Into<Self>) -> Option<Self> {
132        let rhs = rhs.into();
133        Self::construct(
134            self.year_month.checked_add(rhs.year_month)?,
135            self.day_time.checked_add(rhs.day_time)?,
136        )
137        .ok()
138    }
139
140    /// [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)
141    ///
142    /// Returns `None` in case of overflow ([`FODT0002`](https://www.w3.org/TR/xpath-functions-31/#ERRFODT0002)).
143    #[inline]
144    #[must_use]
145    pub fn checked_sub(self, rhs: impl Into<Self>) -> Option<Self> {
146        let rhs = rhs.into();
147        Self::construct(
148            self.year_month.checked_sub(rhs.year_month)?,
149            self.day_time.checked_sub(rhs.day_time)?,
150        )
151        .ok()
152    }
153
154    /// Unary negation.
155    ///
156    /// Returns `None` in case of overflow ([`FODT0002`](https://www.w3.org/TR/xpath-functions-31/#ERRFODT0002)).
157    #[inline]
158    #[must_use]
159    pub fn checked_neg(self) -> Option<Self> {
160        Some(Self {
161            year_month: self.year_month.checked_neg()?,
162            day_time: self.day_time.checked_neg()?,
163        })
164    }
165
166    /// Checks if the two values are [identical](https://www.w3.org/TR/xmlschema11-2/#identity).
167    #[inline]
168    #[must_use]
169    pub fn is_identical_with(self, other: Self) -> bool {
170        self == other
171    }
172}
173
174#[cfg(not(target_os = "zkvm"))]
175impl TryFrom<StdDuration> for Duration {
176    type Error = DurationOverflowError;
177
178    #[inline]
179    fn try_from(value: StdDuration) -> Result<Self, Self::Error> {
180        Ok(DayTimeDuration::try_from(value)?.into())
181    }
182}
183
184impl FromStr for Duration {
185    type Err = ParseDurationError;
186
187    fn from_str(input: &str) -> Result<Self, Self::Err> {
188        let parts = ensure_complete(input, duration_parts)?;
189        if parts.year_month.is_none() && parts.day_time.is_none() {
190            return Err(Self::Err::msg("Empty duration"));
191        }
192        Ok(Self::new(
193            parts.year_month.unwrap_or(0),
194            parts.day_time.unwrap_or_default(),
195        )?)
196    }
197}
198
199impl fmt::Display for Duration {
200    #[allow(clippy::many_single_char_names)]
201    #[inline]
202    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
203        let ym = self.year_month.months;
204        let ss = self.day_time.seconds;
205
206        if (ym < 0 && ss > 0.into()) || (ym > 0 && ss < 0.into()) {
207            return Err(fmt::Error); // Not able to format with only a part of the duration that is negative
208        }
209        if ym < 0 || ss < 0.into() {
210            f.write_str("-")?;
211        }
212        f.write_str("P")?;
213
214        if ym == 0 && ss == 0.into() {
215            return f.write_str("T0S");
216        }
217
218        {
219            let y = ym / 12;
220            let m = ym % 12;
221
222            if y != 0 {
223                if m == 0 {
224                    write!(f, "{}Y", y.abs())?;
225                } else {
226                    write!(f, "{}Y{}M", y.abs(), m.abs())?;
227                }
228            } else if m != 0 || ss == 0.into() {
229                write!(f, "{}M", m.abs())?;
230            }
231        }
232
233        {
234            let s_int = ss.as_i128();
235            let d = s_int / 86400;
236            let h = (s_int % 86400) / 3600;
237            let m = (s_int % 3600) / 60;
238            let s = ss
239                .checked_sub(
240                    Decimal::try_from(d * 86400 + h * 3600 + m * 60).map_err(|_| fmt::Error)?,
241                )
242                .ok_or(fmt::Error)?;
243
244            if d != 0 {
245                write!(f, "{}D", d.abs())?;
246            }
247
248            if h != 0 || m != 0 || s != 0.into() {
249                f.write_str("T")?;
250                if h != 0 {
251                    write!(f, "{}H", h.abs())?;
252                }
253                if m != 0 {
254                    write!(f, "{}M", m.abs())?;
255                }
256                if s != 0.into() {
257                    write!(f, "{}S", s.checked_abs().ok_or(fmt::Error)?)?;
258                }
259            }
260        }
261        Ok(())
262    }
263}
264
265impl PartialOrd for Duration {
266    #[inline]
267    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
268        let first = DateTime::new(1969, 9, 1, 0, 0, 0.into(), None).ok()?;
269        let first_result = first
270            .checked_add_duration(*self)?
271            .partial_cmp(&first.checked_add_duration(*other)?);
272        let second = DateTime::new(1697, 2, 1, 0, 0, 0.into(), None).ok()?;
273        let second_result = second
274            .checked_add_duration(*self)?
275            .partial_cmp(&second.checked_add_duration(*other)?);
276        let third = DateTime::new(1903, 3, 1, 0, 0, 0.into(), None).ok()?;
277        let third_result = third
278            .checked_add_duration(*self)?
279            .partial_cmp(&third.checked_add_duration(*other)?);
280        let fourth = DateTime::new(1903, 7, 1, 0, 0, 0.into(), None).ok()?;
281        let fourth_result = fourth
282            .checked_add_duration(*self)?
283            .partial_cmp(&fourth.checked_add_duration(*other)?);
284        if first_result == second_result
285            && second_result == third_result
286            && third_result == fourth_result
287        {
288            first_result
289        } else {
290            None
291        }
292    }
293}
294
295/// [XML Schema `yearMonthDuration` datatype](https://www.w3.org/TR/xmlschema11-2/#yearMonthDuration)
296///
297/// It stores the duration as a number of months encoded using a [`i64`].
298#[derive(Eq, PartialEq, Ord, PartialOrd, Debug, Clone, Copy, Hash, Default)]
299pub struct YearMonthDuration {
300    months: i64,
301}
302
303impl YearMonthDuration {
304    pub const MAX: Self = Self { months: i64::MAX };
305    pub const MIN: Self = Self { months: i64::MIN };
306
307    #[inline]
308    pub fn new(months: impl Into<i64>) -> Self {
309        Self {
310            months: months.into(),
311        }
312    }
313
314    #[inline]
315    pub fn from_be_bytes(bytes: [u8; 8]) -> Self {
316        Self {
317            months: i64::from_be_bytes(bytes),
318        }
319    }
320
321    /// [fn:years-from-duration](https://www.w3.org/TR/xpath-functions-31/#func-years-from-duration)
322    #[inline]
323    pub fn years(self) -> i64 {
324        self.months / 12
325    }
326
327    /// [fn:months-from-duration](https://www.w3.org/TR/xpath-functions-31/#func-months-from-duration)
328    #[inline]
329    pub fn months(self) -> i64 {
330        self.months % 12
331    }
332
333    #[inline]
334    pub(crate) const fn all_months(self) -> i64 {
335        self.months
336    }
337
338    #[inline]
339    pub fn to_be_bytes(self) -> [u8; 8] {
340        self.months.to_be_bytes()
341    }
342
343    /// [op:add-yearMonthDurations](https://www.w3.org/TR/xpath-functions-31/#func-add-yearMonthDurations)
344    ///
345    /// Returns `None` in case of overflow ([`FODT0002`](https://www.w3.org/TR/xpath-functions-31/#ERRFODT0002)).
346    #[inline]
347    pub fn checked_add(self, rhs: impl Into<Self>) -> Option<Self> {
348        let rhs = rhs.into();
349        Some(Self {
350            months: self.months.checked_add(rhs.months)?,
351        })
352    }
353
354    /// [op:subtract-yearMonthDurations](https://www.w3.org/TR/xpath-functions-31/#func-subtract-yearMonthDurations)
355    ///
356    /// Returns `None` in case of overflow ([`FODT0002`](https://www.w3.org/TR/xpath-functions-31/#ERRFODT0002)).
357    #[inline]
358    pub fn checked_sub(self, rhs: impl Into<Self>) -> Option<Self> {
359        let rhs = rhs.into();
360        Some(Self {
361            months: self.months.checked_sub(rhs.months)?,
362        })
363    }
364
365    /// Unary negation.
366    ///
367    /// Returns `None` in case of overflow ([`FODT0002`](https://www.w3.org/TR/xpath-functions-31/#ERRFODT0002)).
368    #[inline]
369    pub fn checked_neg(self) -> Option<Self> {
370        Some(Self {
371            months: self.months.checked_neg()?,
372        })
373    }
374
375    /// Checks if the two values are [identical](https://www.w3.org/TR/xmlschema11-2/#identity).
376    #[inline]
377    pub fn is_identical_with(self, other: Self) -> bool {
378        self == other
379    }
380}
381
382impl From<YearMonthDuration> for Duration {
383    #[inline]
384    fn from(value: YearMonthDuration) -> Self {
385        Self {
386            year_month: value,
387            day_time: DayTimeDuration::default(),
388        }
389    }
390}
391
392impl TryFrom<Duration> for YearMonthDuration {
393    type Error = DurationOverflowError;
394
395    #[inline]
396    fn try_from(value: Duration) -> Result<Self, Self::Error> {
397        if value.day_time == DayTimeDuration::default() {
398            Ok(value.year_month)
399        } else {
400            Err(DurationOverflowError)
401        }
402    }
403}
404
405impl FromStr for YearMonthDuration {
406    type Err = ParseDurationError;
407
408    fn from_str(input: &str) -> Result<Self, Self::Err> {
409        let parts = ensure_complete(input, duration_parts)?;
410        if parts.day_time.is_some() {
411            return Err(Self::Err::msg(
412                "There must not be any day or time component in a yearMonthDuration",
413            ));
414        }
415        Ok(Self::new(
416            parts
417                .year_month
418                .ok_or(Self::Err::msg("No year and month values found"))?,
419        ))
420    }
421}
422
423impl fmt::Display for YearMonthDuration {
424    #[inline]
425    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
426        if self.months == 0 {
427            f.write_str("P0M")
428        } else {
429            Duration::from(*self).fmt(f)
430        }
431    }
432}
433
434impl PartialEq<Duration> for YearMonthDuration {
435    #[inline]
436    fn eq(&self, other: &Duration) -> bool {
437        Duration::from(*self).eq(other)
438    }
439}
440
441impl PartialEq<YearMonthDuration> for Duration {
442    #[inline]
443    fn eq(&self, other: &YearMonthDuration) -> bool {
444        self.eq(&Self::from(*other))
445    }
446}
447
448impl PartialOrd<Duration> for YearMonthDuration {
449    #[inline]
450    fn partial_cmp(&self, other: &Duration) -> Option<Ordering> {
451        Duration::from(*self).partial_cmp(other)
452    }
453}
454
455impl PartialOrd<YearMonthDuration> for Duration {
456    #[inline]
457    fn partial_cmp(&self, other: &YearMonthDuration) -> Option<Ordering> {
458        self.partial_cmp(&Self::from(*other))
459    }
460}
461
462/// [XML Schema `dayTimeDuration` datatype](https://www.w3.org/TR/xmlschema11-2/#dayTimeDuration)
463///
464/// It stores the duration as a number of seconds encoded using a [`Decimal`].
465#[derive(Eq, PartialEq, Ord, PartialOrd, Debug, Clone, Copy, Hash, Default)]
466pub struct DayTimeDuration {
467    seconds: Decimal,
468}
469
470impl DayTimeDuration {
471    pub const MAX: Self = Self {
472        seconds: Decimal::MAX,
473    };
474    pub const MIN: Self = Self {
475        seconds: Decimal::MIN,
476    };
477
478    #[inline]
479    pub fn new(seconds: impl Into<Decimal>) -> Self {
480        Self {
481            seconds: seconds.into(),
482        }
483    }
484
485    #[inline]
486    pub fn from_be_bytes(bytes: [u8; 16]) -> Self {
487        Self {
488            seconds: Decimal::from_be_bytes(bytes),
489        }
490    }
491
492    /// [fn:days-from-duration](https://www.w3.org/TR/xpath-functions-31/#func-days-from-duration)
493    #[allow(clippy::cast_possible_truncation)]
494    #[inline]
495    pub fn days(self) -> i64 {
496        (self.seconds.as_i128() / 86400) as i64
497    }
498
499    /// [fn:hours-from-duration](https://www.w3.org/TR/xpath-functions-31/#func-hours-from-duration)
500    #[allow(clippy::cast_possible_truncation)]
501    #[inline]
502    pub fn hours(self) -> i64 {
503        ((self.seconds.as_i128() % 86400) / 3600) as i64
504    }
505
506    /// [fn:minutes-from-duration](https://www.w3.org/TR/xpath-functions-31/#func-minutes-from-duration)
507    #[allow(clippy::cast_possible_truncation)]
508    #[inline]
509    pub fn minutes(self) -> i64 {
510        ((self.seconds.as_i128() % 3600) / 60) as i64
511    }
512
513    /// [fn:seconds-from-duration](https://www.w3.org/TR/xpath-functions-31/#func-seconds-from-duration)
514    #[inline]
515    pub fn seconds(self) -> Decimal {
516        self.seconds.checked_rem(60).unwrap()
517    }
518
519    /// The duration in seconds.
520    #[inline]
521    pub const fn as_seconds(self) -> Decimal {
522        self.seconds
523    }
524
525    #[inline]
526    pub fn to_be_bytes(self) -> [u8; 16] {
527        self.seconds.to_be_bytes()
528    }
529
530    /// [op:add-dayTimeDurations](https://www.w3.org/TR/xpath-functions-31/#func-add-dayTimeDurations)
531    ///
532    /// Returns `None` in case of overflow ([`FODT0002`](https://www.w3.org/TR/xpath-functions-31/#ERRFODT0002)).
533    #[inline]
534    pub fn checked_add(self, rhs: impl Into<Self>) -> Option<Self> {
535        let rhs = rhs.into();
536        Some(Self {
537            seconds: self.seconds.checked_add(rhs.seconds)?,
538        })
539    }
540
541    /// [op:subtract-dayTimeDurations](https://www.w3.org/TR/xpath-functions-31/#func-subtract-dayTimeDurations)
542    ///
543    /// Returns `None` in case of overflow ([`FODT0002`](https://www.w3.org/TR/xpath-functions-31/#ERRFODT0002)).
544    #[inline]
545    pub fn checked_sub(self, rhs: impl Into<Self>) -> Option<Self> {
546        let rhs = rhs.into();
547        Some(Self {
548            seconds: self.seconds.checked_sub(rhs.seconds)?,
549        })
550    }
551
552    /// Unary negation.
553    ///
554    /// Returns `None` in case of overflow ([`FODT0002`](https://www.w3.org/TR/xpath-functions-31/#ERRFODT0002)).
555    #[inline]
556    pub fn checked_neg(self) -> Option<Self> {
557        Some(Self {
558            seconds: self.seconds.checked_neg()?,
559        })
560    }
561
562    /// Checks if the two values are [identical](https://www.w3.org/TR/xmlschema11-2/#identity).
563    #[inline]
564    pub fn is_identical_with(self, other: Self) -> bool {
565        self == other
566    }
567}
568
569impl From<DayTimeDuration> for Duration {
570    #[inline]
571    fn from(value: DayTimeDuration) -> Self {
572        Self {
573            year_month: YearMonthDuration::default(),
574            day_time: value,
575        }
576    }
577}
578
579impl TryFrom<Duration> for DayTimeDuration {
580    type Error = DurationOverflowError;
581
582    #[inline]
583    fn try_from(value: Duration) -> Result<Self, Self::Error> {
584        if value.year_month == YearMonthDuration::default() {
585            Ok(value.day_time)
586        } else {
587            Err(DurationOverflowError)
588        }
589    }
590}
591
592#[cfg(not(target_os = "zkvm"))]
593impl TryFrom<StdDuration> for DayTimeDuration {
594    type Error = DurationOverflowError;
595
596    #[inline]
597    fn try_from(value: StdDuration) -> Result<Self, Self::Error> {
598        Ok(Self {
599            seconds: Decimal::new(
600                i128::try_from(value.as_nanos()).map_err(|_| DurationOverflowError)?,
601                9,
602            )
603            .map_err(|_| DurationOverflowError)?,
604        })
605    }
606}
607
608#[cfg(not(target_os = "zkvm"))]
609impl TryFrom<DayTimeDuration> for StdDuration {
610    type Error = DurationOverflowError;
611
612    #[inline]
613    fn try_from(value: DayTimeDuration) -> Result<Self, Self::Error> {
614        if value.seconds.is_negative() {
615            return Err(DurationOverflowError);
616        }
617        let secs = value.seconds.checked_floor().ok_or(DurationOverflowError)?;
618        let nanos = value
619            .seconds
620            .checked_sub(secs)
621            .ok_or(DurationOverflowError)?
622            .checked_mul(1_000_000_000)
623            .ok_or(DurationOverflowError)?
624            .checked_floor()
625            .ok_or(DurationOverflowError)?;
626        Ok(Self::new(
627            secs.as_i128()
628                .try_into()
629                .map_err(|_| DurationOverflowError)?,
630            nanos
631                .as_i128()
632                .try_into()
633                .map_err(|_| DurationOverflowError)?,
634        ))
635    }
636}
637
638impl FromStr for DayTimeDuration {
639    type Err = ParseDurationError;
640
641    fn from_str(input: &str) -> Result<Self, Self::Err> {
642        let parts = ensure_complete(input, duration_parts)?;
643        if parts.year_month.is_some() {
644            return Err(Self::Err::msg(
645                "There must not be any year or month component in a dayTimeDuration",
646            ));
647        }
648        Ok(Self::new(
649            parts
650                .day_time
651                .ok_or(Self::Err::msg("No day or time values found"))?,
652        ))
653    }
654}
655
656impl fmt::Display for DayTimeDuration {
657    #[inline]
658    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
659        Duration::from(*self).fmt(f)
660    }
661}
662
663impl PartialEq<Duration> for DayTimeDuration {
664    #[inline]
665    fn eq(&self, other: &Duration) -> bool {
666        Duration::from(*self).eq(other)
667    }
668}
669
670impl PartialEq<DayTimeDuration> for Duration {
671    #[inline]
672    fn eq(&self, other: &DayTimeDuration) -> bool {
673        self.eq(&Self::from(*other))
674    }
675}
676
677impl PartialEq<YearMonthDuration> for DayTimeDuration {
678    #[inline]
679    fn eq(&self, other: &YearMonthDuration) -> bool {
680        Duration::from(*self).eq(&Duration::from(*other))
681    }
682}
683
684impl PartialEq<DayTimeDuration> for YearMonthDuration {
685    #[inline]
686    fn eq(&self, other: &DayTimeDuration) -> bool {
687        Duration::from(*self).eq(&Duration::from(*other))
688    }
689}
690
691impl PartialOrd<Duration> for DayTimeDuration {
692    #[inline]
693    fn partial_cmp(&self, other: &Duration) -> Option<Ordering> {
694        Duration::from(*self).partial_cmp(other)
695    }
696}
697
698impl PartialOrd<DayTimeDuration> for Duration {
699    #[inline]
700    fn partial_cmp(&self, other: &DayTimeDuration) -> Option<Ordering> {
701        self.partial_cmp(&Self::from(*other))
702    }
703}
704
705impl PartialOrd<YearMonthDuration> for DayTimeDuration {
706    #[inline]
707    fn partial_cmp(&self, other: &YearMonthDuration) -> Option<Ordering> {
708        Duration::from(*self).partial_cmp(&Duration::from(*other))
709    }
710}
711
712impl PartialOrd<DayTimeDuration> for YearMonthDuration {
713    #[inline]
714    fn partial_cmp(&self, other: &DayTimeDuration) -> Option<Ordering> {
715        Duration::from(*self).partial_cmp(&Duration::from(*other))
716    }
717}
718
719// [6]   duYearFrag      ::= unsignedNoDecimalPtNumeral 'Y'
720// [7]   duMonthFrag     ::= unsignedNoDecimalPtNumeral 'M'
721// [8]   duDayFrag       ::= unsignedNoDecimalPtNumeral 'D'
722// [9]   duHourFrag      ::= unsignedNoDecimalPtNumeral 'H'
723// [10]  duMinuteFrag    ::= unsignedNoDecimalPtNumeral 'M'
724// [11]  duSecondFrag    ::= (unsignedNoDecimalPtNumeral | unsignedDecimalPtNumeral) 'S'
725// [12]  duYearMonthFrag ::= (duYearFrag duMonthFrag?) | duMonthFrag
726// [13]  duTimeFrag      ::= 'T' ((duHourFrag duMinuteFrag? duSecondFrag?) | (duMinuteFrag duSecondFrag?) | duSecondFrag)
727// [14]  duDayTimeFrag   ::= (duDayFrag duTimeFrag?) | duTimeFrag
728// [15]  durationLexicalRep ::= '-'? 'P' ((duYearMonthFrag duDayTimeFrag?) | duDayTimeFrag)
729struct DurationParts {
730    year_month: Option<i64>,
731    day_time: Option<Decimal>,
732}
733
734fn duration_parts(input: &str) -> Result<(DurationParts, &str), ParseDurationError> {
735    // States
736    const START: u32 = 0;
737    const AFTER_YEAR: u32 = 1;
738    const AFTER_MONTH: u32 = 2;
739    const AFTER_DAY: u32 = 3;
740    const AFTER_T: u32 = 4;
741    const AFTER_HOUR: u32 = 5;
742    const AFTER_MINUTE: u32 = 6;
743    const AFTER_SECOND: u32 = 7;
744
745    let (is_negative, input) = if let Some(left) = input.strip_prefix('-') {
746        (true, left)
747    } else {
748        (false, input)
749    };
750    let mut input = expect_char(input, 'P', "Durations must start with 'P'")?;
751    let mut state = START;
752    let mut year_month: Option<i64> = None;
753    let mut day_time: Option<Decimal> = None;
754    while !input.is_empty() {
755        if let Some(left) = input.strip_prefix('T') {
756            if state >= AFTER_T {
757                return Err(ParseDurationError::msg("Duplicated time separator 'T'"));
758            }
759            state = AFTER_T;
760            input = left;
761        } else {
762            let (number_str, left) = decimal_prefix(input);
763            match left.chars().next() {
764                Some('Y') if state < AFTER_YEAR => {
765                    year_month = Some(
766                        year_month
767                            .unwrap_or_default()
768                            .checked_add(
769                                apply_i64_neg(
770                                    i64::from_str(number_str).map_err(|_| OVERFLOW_ERROR)?,
771                                    is_negative,
772                                )?
773                                .checked_mul(12)
774                                .ok_or(OVERFLOW_ERROR)?,
775                            )
776                            .ok_or(OVERFLOW_ERROR)?,
777                    );
778                    state = AFTER_YEAR;
779                }
780                Some('M') if state < AFTER_MONTH => {
781                    year_month = Some(
782                        year_month
783                            .unwrap_or_default()
784                            .checked_add(apply_i64_neg(
785                                i64::from_str(number_str).map_err(|_| OVERFLOW_ERROR)?,
786                                is_negative,
787                            )?)
788                            .ok_or(OVERFLOW_ERROR)?,
789                    );
790                    state = AFTER_MONTH;
791                }
792                Some('D') if state < AFTER_DAY => {
793                    if number_str.contains('.') {
794                        return Err(ParseDurationError::msg(
795                            "Decimal numbers are not allowed for days",
796                        ));
797                    }
798                    day_time = Some(
799                        day_time
800                            .unwrap_or_default()
801                            .checked_add(
802                                apply_decimal_neg(
803                                    Decimal::from_str(number_str).map_err(|_| OVERFLOW_ERROR)?,
804                                    is_negative,
805                                )?
806                                .checked_mul(86400)
807                                .ok_or(OVERFLOW_ERROR)?,
808                            )
809                            .ok_or(OVERFLOW_ERROR)?,
810                    );
811                    state = AFTER_DAY;
812                }
813                Some('H') if state == AFTER_T => {
814                    if number_str.contains('.') {
815                        return Err(ParseDurationError::msg(
816                            "Decimal numbers are not allowed for hours",
817                        ));
818                    }
819                    day_time = Some(
820                        day_time
821                            .unwrap_or_default()
822                            .checked_add(
823                                apply_decimal_neg(
824                                    Decimal::from_str(number_str).map_err(|_| OVERFLOW_ERROR)?,
825                                    is_negative,
826                                )?
827                                .checked_mul(3600)
828                                .ok_or(OVERFLOW_ERROR)?,
829                            )
830                            .ok_or(OVERFLOW_ERROR)?,
831                    );
832                    state = AFTER_HOUR;
833                }
834                Some('M') if (AFTER_T..AFTER_MINUTE).contains(&state) => {
835                    if number_str.contains('.') {
836                        return Err(ParseDurationError::msg(
837                            "Decimal numbers are not allowed for minutes",
838                        ));
839                    }
840                    day_time = Some(
841                        day_time
842                            .unwrap_or_default()
843                            .checked_add(
844                                apply_decimal_neg(
845                                    Decimal::from_str(number_str).map_err(|_| OVERFLOW_ERROR)?,
846                                    is_negative,
847                                )?
848                                .checked_mul(60)
849                                .ok_or(OVERFLOW_ERROR)?,
850                            )
851                            .ok_or(OVERFLOW_ERROR)?,
852                    );
853                    state = AFTER_MINUTE;
854                }
855                Some('S') if (AFTER_T..AFTER_SECOND).contains(&state) => {
856                    day_time = Some(
857                        day_time
858                            .unwrap_or_default()
859                            .checked_add(apply_decimal_neg(
860                                Decimal::from_str(number_str).map_err(|_| OVERFLOW_ERROR)?,
861                                is_negative,
862                            )?)
863                            .ok_or(OVERFLOW_ERROR)?,
864                    );
865                    state = AFTER_SECOND;
866                }
867                Some(_) => return Err(ParseDurationError::msg("Unexpected type character")),
868                None => {
869                    return Err(ParseDurationError::msg(
870                        "Numbers in durations must be followed by a type character",
871                    ))
872                }
873            }
874            input = &left[1..];
875        }
876    }
877
878    Ok((
879        DurationParts {
880            year_month,
881            day_time,
882        },
883        input,
884    ))
885}
886
887fn apply_i64_neg(value: i64, is_negative: bool) -> Result<i64, ParseDurationError> {
888    if is_negative {
889        value.checked_neg().ok_or(OVERFLOW_ERROR)
890    } else {
891        Ok(value)
892    }
893}
894
895fn apply_decimal_neg(value: Decimal, is_negative: bool) -> Result<Decimal, ParseDurationError> {
896    if is_negative {
897        value.checked_neg().ok_or(OVERFLOW_ERROR)
898    } else {
899        Ok(value)
900    }
901}
902
903fn ensure_complete<T>(
904    input: &str,
905    parse: impl FnOnce(&str) -> Result<(T, &str), ParseDurationError>,
906) -> Result<T, ParseDurationError> {
907    let (result, left) = parse(input)?;
908    if !left.is_empty() {
909        return Err(ParseDurationError::msg("Unrecognized value suffix"));
910    }
911    Ok(result)
912}
913
914fn expect_char<'a>(
915    input: &'a str,
916    constant: char,
917    error_message: &'static str,
918) -> Result<&'a str, ParseDurationError> {
919    if let Some(left) = input.strip_prefix(constant) {
920        Ok(left)
921    } else {
922        Err(ParseDurationError::msg(error_message))
923    }
924}
925
926fn decimal_prefix(input: &str) -> (&str, &str) {
927    let mut end = input.len();
928    let mut dot_seen = false;
929    for (i, c) in input.char_indices() {
930        if c.is_ascii_digit() {
931            // Ok
932        } else if c == '.' && !dot_seen {
933            dot_seen = true;
934        } else {
935            end = i;
936            break;
937        }
938    }
939    input.split_at(end)
940}
941
942/// A parsing error
943#[derive(Debug, Clone, thiserror::Error)]
944#[error("{msg}")]
945pub struct ParseDurationError {
946    msg: &'static str,
947}
948
949const OVERFLOW_ERROR: ParseDurationError = ParseDurationError {
950    msg: "Overflow error",
951};
952
953impl ParseDurationError {
954    const fn msg(msg: &'static str) -> Self {
955        Self { msg }
956    }
957}
958
959/// An overflow during [`Duration`]-related operations.
960///
961/// Matches XPath [`FODT0002` error](https://www.w3.org/TR/xpath-functions-31/#ERRFODT0002).
962#[derive(Debug, Clone, Copy, thiserror::Error)]
963#[error("overflow during xsd:duration computation")]
964pub struct DurationOverflowError;
965
966/// The year-month and the day-time components of a [`Duration`] have an opposite sign.
967#[derive(Debug, Clone, Copy, thiserror::Error)]
968#[error("The xsd:yearMonthDuration and xsd:dayTimeDuration components of a xsd:duration can't have opposite sign")]
969pub struct OppositeSignInDurationComponentsError;
970
971impl From<OppositeSignInDurationComponentsError> for ParseDurationError {
972    #[inline]
973    fn from(_: OppositeSignInDurationComponentsError) -> Self {
974        Self {
975            msg: "The xsd:yearMonthDuration and xsd:dayTimeDuration components of a xsd:duration can't have opposite sign"
976        }
977    }
978}
979
980#[cfg(test)]
981#[allow(clippy::panic_in_result_fn)]
982mod tests {
983    use super::*;
984    use std::error::Error;
985
986    #[test]
987    fn from_str() -> Result<(), ParseDurationError> {
988        let min = Duration::new(i64::MIN, Decimal::MIN)?;
989        let max = Duration::new(i64::MAX, Decimal::MAX)?;
990
991        assert_eq!(YearMonthDuration::from_str("P1Y")?.to_string(), "P1Y");
992        assert_eq!(Duration::from_str("P1Y")?.to_string(), "P1Y");
993        assert_eq!(YearMonthDuration::from_str("P1M")?.to_string(), "P1M");
994        assert_eq!(Duration::from_str("P1M")?.to_string(), "P1M");
995        assert_eq!(DayTimeDuration::from_str("P1D")?.to_string(), "P1D");
996        assert_eq!(Duration::from_str("P1D")?.to_string(), "P1D");
997        assert_eq!(DayTimeDuration::from_str("PT1H")?.to_string(), "PT1H");
998        assert_eq!(Duration::from_str("PT1H")?.to_string(), "PT1H");
999        assert_eq!(DayTimeDuration::from_str("PT1M")?.to_string(), "PT1M");
1000        assert_eq!(Duration::from_str("PT1M")?.to_string(), "PT1M");
1001        assert_eq!(DayTimeDuration::from_str("PT1.1S")?.to_string(), "PT1.1S");
1002        assert_eq!(Duration::from_str("PT1.1S")?.to_string(), "PT1.1S");
1003        assert_eq!(YearMonthDuration::from_str("-P1Y")?.to_string(), "-P1Y");
1004        assert_eq!(Duration::from_str("-P1Y")?.to_string(), "-P1Y");
1005        assert_eq!(YearMonthDuration::from_str("-P1M")?.to_string(), "-P1M");
1006        assert_eq!(Duration::from_str("-P1M")?.to_string(), "-P1M");
1007        assert_eq!(DayTimeDuration::from_str("-P1D")?.to_string(), "-P1D");
1008        assert_eq!(Duration::from_str("-P1D")?.to_string(), "-P1D");
1009        assert_eq!(DayTimeDuration::from_str("-PT1H")?.to_string(), "-PT1H");
1010        assert_eq!(Duration::from_str("-PT1H")?.to_string(), "-PT1H");
1011        assert_eq!(DayTimeDuration::from_str("-PT1M")?.to_string(), "-PT1M");
1012        assert_eq!(Duration::from_str("-PT1M")?.to_string(), "-PT1M");
1013        assert_eq!(DayTimeDuration::from_str("-PT1S")?.to_string(), "-PT1S");
1014        assert_eq!(Duration::from_str("-PT1S")?.to_string(), "-PT1S");
1015        assert_eq!(DayTimeDuration::from_str("-PT1.1S")?.to_string(), "-PT1.1S");
1016        assert_eq!(Duration::from_str("-PT1.1S")?.to_string(), "-PT1.1S");
1017        assert_eq!(Duration::from_str(&max.to_string())?, max);
1018        assert_eq!(Duration::from_str(&min.to_string())?, min);
1019        assert_eq!(Duration::from_str("PT0H")?.to_string(), "PT0S");
1020        assert_eq!(Duration::from_str("-PT0H")?.to_string(), "PT0S");
1021        assert_eq!(YearMonthDuration::from_str("P0Y")?.to_string(), "P0M");
1022        assert_eq!(DayTimeDuration::from_str("PT0H")?.to_string(), "PT0S");
1023        Ok(())
1024    }
1025
1026    #[test]
1027    #[cfg(not(target_os = "zkvm"))]
1028    fn from_std() -> Result<(), DurationOverflowError> {
1029        assert_eq!(
1030            Duration::try_from(StdDuration::new(10, 10))?.to_string(),
1031            "PT10.00000001S"
1032        );
1033        Ok(())
1034    }
1035
1036    #[test]
1037    #[cfg(not(target_os = "zkvm"))]
1038    fn to_std() -> Result<(), Box<dyn Error>> {
1039        let duration = StdDuration::try_from(DayTimeDuration::from_str("PT10.00000001S")?)?;
1040        assert_eq!(duration.as_secs(), 10);
1041        assert_eq!(duration.subsec_nanos(), 10);
1042        Ok(())
1043    }
1044
1045    #[test]
1046    fn to_be_bytes() {
1047        assert_eq!(
1048            Duration::from_be_bytes(Duration::MIN.to_be_bytes()),
1049            Duration::MIN
1050        );
1051        assert_eq!(
1052            Duration::from_be_bytes(Duration::MAX.to_be_bytes()),
1053            Duration::MAX
1054        );
1055        assert_eq!(
1056            YearMonthDuration::from_be_bytes(YearMonthDuration::MIN.to_be_bytes()),
1057            YearMonthDuration::MIN
1058        );
1059        assert_eq!(
1060            YearMonthDuration::from_be_bytes(YearMonthDuration::MAX.to_be_bytes()),
1061            YearMonthDuration::MAX
1062        );
1063        assert_eq!(
1064            DayTimeDuration::from_be_bytes(DayTimeDuration::MIN.to_be_bytes()),
1065            DayTimeDuration::MIN
1066        );
1067        assert_eq!(
1068            DayTimeDuration::from_be_bytes(DayTimeDuration::MAX.to_be_bytes()),
1069            DayTimeDuration::MAX
1070        );
1071    }
1072
1073    #[test]
1074    fn equals() -> Result<(), ParseDurationError> {
1075        assert_eq!(
1076            YearMonthDuration::from_str("P1Y")?,
1077            YearMonthDuration::from_str("P12M")?
1078        );
1079        assert_eq!(
1080            YearMonthDuration::from_str("P1Y")?,
1081            Duration::from_str("P12M")?
1082        );
1083        assert_eq!(
1084            Duration::from_str("P1Y")?,
1085            YearMonthDuration::from_str("P12M")?
1086        );
1087        assert_eq!(Duration::from_str("P1Y")?, Duration::from_str("P12M")?);
1088        assert_eq!(
1089            DayTimeDuration::from_str("PT24H")?,
1090            DayTimeDuration::from_str("P1D")?
1091        );
1092        assert_eq!(
1093            DayTimeDuration::from_str("PT24H")?,
1094            Duration::from_str("P1D")?
1095        );
1096        assert_eq!(
1097            Duration::from_str("PT24H")?,
1098            DayTimeDuration::from_str("P1D")?
1099        );
1100        assert_eq!(Duration::from_str("PT24H")?, Duration::from_str("P1D")?);
1101        assert_ne!(Duration::from_str("P1Y")?, Duration::from_str("P365D")?);
1102        assert_eq!(Duration::from_str("P0Y")?, Duration::from_str("P0D")?);
1103        assert_ne!(Duration::from_str("P1Y")?, Duration::from_str("P365D")?);
1104        assert_eq!(Duration::from_str("P2Y")?, Duration::from_str("P24M")?);
1105        assert_eq!(Duration::from_str("P10D")?, Duration::from_str("PT240H")?);
1106        assert_eq!(
1107            Duration::from_str("P2Y0M0DT0H0M0S")?,
1108            Duration::from_str("P24M")?
1109        );
1110        assert_eq!(
1111            Duration::from_str("P0Y0M10D")?,
1112            Duration::from_str("PT240H")?
1113        );
1114        assert_ne!(Duration::from_str("P1M")?, Duration::from_str("P30D")?);
1115        Ok(())
1116    }
1117
1118    #[test]
1119    #[allow(clippy::neg_cmp_op_on_partial_ord)]
1120    fn cmp() -> Result<(), ParseDurationError> {
1121        assert!(Duration::from_str("P1Y1D")? < Duration::from_str("P13MT25H")?);
1122        assert!(YearMonthDuration::from_str("P1Y")? < YearMonthDuration::from_str("P13M")?);
1123        assert!(Duration::from_str("P1Y")? < YearMonthDuration::from_str("P13M")?);
1124        assert!(YearMonthDuration::from_str("P1Y")? < Duration::from_str("P13M")?);
1125        assert!(DayTimeDuration::from_str("P1D")? < DayTimeDuration::from_str("PT25H")?);
1126        assert!(DayTimeDuration::from_str("PT1H")? < DayTimeDuration::from_str("PT61M")?);
1127        assert!(DayTimeDuration::from_str("PT1M")? < DayTimeDuration::from_str("PT61S")?);
1128        assert!(Duration::from_str("PT1H")? < DayTimeDuration::from_str("PT61M")?);
1129        assert!(DayTimeDuration::from_str("PT1H")? < Duration::from_str("PT61M")?);
1130        assert!(YearMonthDuration::from_str("P1M")? < DayTimeDuration::from_str("P40D")?);
1131        assert!(DayTimeDuration::from_str("P25D")? < YearMonthDuration::from_str("P1M")?);
1132        Ok(())
1133    }
1134
1135    #[test]
1136    fn years() -> Result<(), ParseDurationError> {
1137        assert_eq!(Duration::from_str("P20Y15M")?.years(), 21);
1138        assert_eq!(Duration::from_str("-P15M")?.years(), -1);
1139        assert_eq!(Duration::from_str("-P2DT15H")?.years(), 0);
1140        Ok(())
1141    }
1142
1143    #[test]
1144    fn months() -> Result<(), ParseDurationError> {
1145        assert_eq!(Duration::from_str("P20Y15M")?.months(), 3);
1146        assert_eq!(Duration::from_str("-P20Y18M")?.months(), -6);
1147        assert_eq!(Duration::from_str("-P2DT15H0M0S")?.months(), 0);
1148        Ok(())
1149    }
1150
1151    #[test]
1152    fn days() -> Result<(), ParseDurationError> {
1153        assert_eq!(Duration::from_str("P3DT10H")?.days(), 3);
1154        assert_eq!(Duration::from_str("P3DT55H")?.days(), 5);
1155        assert_eq!(Duration::from_str("P3Y5M")?.days(), 0);
1156        Ok(())
1157    }
1158
1159    #[test]
1160    fn hours() -> Result<(), ParseDurationError> {
1161        assert_eq!(Duration::from_str("P3DT10H")?.hours(), 10);
1162        assert_eq!(Duration::from_str("P3DT12H32M12S")?.hours(), 12);
1163        assert_eq!(Duration::from_str("PT123H")?.hours(), 3);
1164        assert_eq!(Duration::from_str("-P3DT10H")?.hours(), -10);
1165        Ok(())
1166    }
1167
1168    #[test]
1169    fn minutes() -> Result<(), ParseDurationError> {
1170        assert_eq!(Duration::from_str("P3DT10H")?.minutes(), 0);
1171        assert_eq!(Duration::from_str("-P5DT12H30M")?.minutes(), -30);
1172        Ok(())
1173    }
1174
1175    #[test]
1176    fn seconds() -> Result<(), Box<dyn Error>> {
1177        assert_eq!(
1178            Duration::from_str("P3DT10H12.5S")?.seconds(),
1179            Decimal::from_str("12.5")?
1180        );
1181        assert_eq!(
1182            Duration::from_str("-PT256S")?.seconds(),
1183            Decimal::from_str("-16.0")?
1184        );
1185        Ok(())
1186    }
1187
1188    #[test]
1189    fn add() -> Result<(), ParseDurationError> {
1190        assert_eq!(
1191            Duration::from_str("P2Y11M")?.checked_add(Duration::from_str("P3Y3M")?),
1192            Some(Duration::from_str("P6Y2M")?)
1193        );
1194        assert_eq!(
1195            Duration::from_str("P2DT12H5M")?.checked_add(Duration::from_str("P5DT12H")?),
1196            Some(Duration::from_str("P8DT5M")?)
1197        );
1198        assert_eq!(
1199            Duration::from_str("P1M2D")?.checked_add(Duration::from_str("-P3D")?),
1200            None
1201        );
1202        assert_eq!(
1203            Duration::from_str("P1M2D")?.checked_add(Duration::from_str("-P2M")?),
1204            None
1205        );
1206        Ok(())
1207    }
1208
1209    #[test]
1210    fn sub() -> Result<(), ParseDurationError> {
1211        assert_eq!(
1212            Duration::from_str("P2Y11M")?.checked_sub(Duration::from_str("P3Y3M")?),
1213            Some(Duration::from_str("-P4M")?)
1214        );
1215        assert_eq!(
1216            Duration::from_str("P2DT12H")?.checked_sub(Duration::from_str("P1DT10H30M")?),
1217            Some(Duration::from_str("P1DT1H30M")?)
1218        );
1219        assert_eq!(
1220            Duration::from_str("P1M2D")?.checked_sub(Duration::from_str("P3D")?),
1221            None
1222        );
1223        assert_eq!(
1224            Duration::from_str("P1M2D")?.checked_sub(Duration::from_str("P2M")?),
1225            None
1226        );
1227        Ok(())
1228    }
1229
1230    #[test]
1231    fn minimally_conformant() -> Result<(), ParseDurationError> {
1232        // All minimally conforming processors must support fractional-second duration values
1233        // to milliseconds (i.e. those expressible with three fraction digits).
1234        assert_eq!(Duration::from_str("PT0.001S")?.to_string(), "PT0.001S");
1235        assert_eq!(Duration::from_str("-PT0.001S")?.to_string(), "-PT0.001S");
1236
1237        // All minimally conforming processors must support duration values with months values
1238        // in the range −119999 to 119999 months (9999 years and 11 months)
1239        // and seconds values in the range −31622400 to 31622400 seconds (one leap-year).
1240        assert_eq!(
1241            Duration::from_str("P119999MT31622400S")?.to_string(),
1242            "P9999Y11M366D"
1243        );
1244        assert_eq!(
1245            Duration::from_str("-P119999MT31622400S")?.to_string(),
1246            "-P9999Y11M366D"
1247        );
1248        Ok(())
1249    }
1250}