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#[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 #[inline]
65 #[must_use]
66 pub fn years(self) -> i64 {
67 self.year_month.years()
68 }
69
70 #[inline]
72 #[must_use]
73 pub fn months(self) -> i64 {
74 self.year_month.months()
75 }
76
77 #[inline]
79 #[must_use]
80 pub fn days(self) -> i64 {
81 self.day_time.days()
82 }
83
84 #[inline]
86 #[must_use]
87 pub fn hours(self) -> i64 {
88 self.day_time.hours()
89 }
90
91 #[inline]
93 #[must_use]
94 pub fn minutes(self) -> i64 {
95 self.day_time.minutes()
96 }
97
98 #[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 #[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 #[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 #[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 #[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); }
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#[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 #[inline]
323 pub fn years(self) -> i64 {
324 self.months / 12
325 }
326
327 #[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 #[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 #[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 #[inline]
369 pub fn checked_neg(self) -> Option<Self> {
370 Some(Self {
371 months: self.months.checked_neg()?,
372 })
373 }
374
375 #[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#[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 #[allow(clippy::cast_possible_truncation)]
494 #[inline]
495 pub fn days(self) -> i64 {
496 (self.seconds.as_i128() / 86400) as i64
497 }
498
499 #[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 #[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 #[inline]
515 pub fn seconds(self) -> Decimal {
516 self.seconds.checked_rem(60).unwrap()
517 }
518
519 #[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 #[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 #[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 #[inline]
556 pub fn checked_neg(self) -> Option<Self> {
557 Some(Self {
558 seconds: self.seconds.checked_neg()?,
559 })
560 }
561
562 #[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
719struct DurationParts {
730 year_month: Option<i64>,
731 day_time: Option<Decimal>,
732}
733
734fn duration_parts(input: &str) -> Result<(DurationParts, &str), ParseDurationError> {
735 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 } 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#[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#[derive(Debug, Clone, Copy, thiserror::Error)]
963#[error("overflow during xsd:duration computation")]
964pub struct DurationOverflowError;
965
966#[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 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 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}