1use serde::{
6 Deserialize, Deserializer, Serialize, Serializer,
7 de::{Error as DeError, Visitor},
8};
9use std::borrow::Cow;
10use std::fmt;
11
12use crate::error::DomainError;
13use chrono::{Datelike, NaiveDate};
14use paft_utils::Canonical;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
28pub struct PeriodYear(u16);
29
30impl PeriodYear {
31 pub const MIN: u16 = 0;
33
34 pub const MAX: u16 = 9999;
36
37 pub fn new(year: i32) -> Result<Self, DomainError> {
43 let Ok(year_u16) = u16::try_from(year) else {
44 return Err(DomainError::InvalidPeriodYear { year });
45 };
46
47 if year_u16 <= Self::MAX {
48 Ok(Self(year_u16))
49 } else {
50 Err(DomainError::InvalidPeriodYear { year })
51 }
52 }
53
54 #[must_use]
56 pub const fn get(self) -> i32 {
57 self.0 as i32
58 }
59
60 #[must_use]
62 pub const fn as_u16(self) -> u16 {
63 self.0
64 }
65}
66
67impl TryFrom<i32> for PeriodYear {
68 type Error = DomainError;
69
70 fn try_from(year: i32) -> Result<Self, Self::Error> {
71 Self::new(year)
72 }
73}
74
75impl TryFrom<u16> for PeriodYear {
76 type Error = DomainError;
77
78 fn try_from(year: u16) -> Result<Self, Self::Error> {
79 Self::new(i32::from(year))
80 }
81}
82
83impl From<PeriodYear> for i32 {
84 fn from(year: PeriodYear) -> Self {
85 year.get()
86 }
87}
88
89impl From<PeriodYear> for u16 {
90 fn from(year: PeriodYear) -> Self {
91 year.as_u16()
92 }
93}
94
95impl fmt::Display for PeriodYear {
96 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97 write!(f, "{:04}", self.0)
98 }
99}
100
101impl Serialize for PeriodYear {
102 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
103 where
104 S: Serializer,
105 {
106 serializer.serialize_str(&self.to_string())
107 }
108}
109
110impl<'de> Deserialize<'de> for PeriodYear {
111 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
112 where
113 D: Deserializer<'de>,
114 {
115 deserializer.deserialize_any(PeriodYearVisitor)
116 }
117}
118
119struct PeriodYearVisitor;
120
121impl Visitor<'_> for PeriodYearVisitor {
122 type Value = PeriodYear;
123
124 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
125 formatter.write_str("a canonical four-digit period year string or integer in 0..=9999")
126 }
127
128 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
129 where
130 E: DeError,
131 {
132 parse_period_year_code(value).map_err(DeError::custom)
133 }
134
135 fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
136 where
137 E: DeError,
138 {
139 period_year_from_i64(value).map_err(DeError::custom)
140 }
141
142 fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
143 where
144 E: DeError,
145 {
146 period_year_from_u64(value).map_err(DeError::custom)
147 }
148}
149
150fn parse_period_year_code(value: &str) -> Result<PeriodYear, DomainError> {
151 let bytes = value.as_bytes();
152 if bytes.len() != 4 || !bytes.iter().all(u8::is_ascii_digit) {
153 return Err(DomainError::InvalidPeriodFormat {
154 format: value.to_string(),
155 });
156 }
157
158 let year = i32::from(bytes[0] - b'0') * 1_000
159 + i32::from(bytes[1] - b'0') * 100
160 + i32::from(bytes[2] - b'0') * 10
161 + i32::from(bytes[3] - b'0');
162
163 PeriodYear::new(year)
164}
165
166fn period_year_from_i64(value: i64) -> Result<PeriodYear, DomainError> {
167 let Ok(year) = i32::try_from(value) else {
168 return Err(DomainError::InvalidPeriodFormat {
169 format: value.to_string(),
170 });
171 };
172
173 PeriodYear::new(year)
174}
175
176fn period_year_from_u64(value: u64) -> Result<PeriodYear, DomainError> {
177 let Ok(year) = i32::try_from(value) else {
178 return Err(DomainError::InvalidPeriodFormat {
179 format: value.to_string(),
180 });
181 };
182
183 PeriodYear::new(year)
184}
185
186fn parse_period_date_code(value: &str) -> Result<PeriodDate, DomainError> {
187 let invalid = || DomainError::InvalidPeriodFormat {
188 format: value.to_string(),
189 };
190
191 let bytes = value.as_bytes();
192 if bytes.len() != 10 || bytes[4] != b'-' || bytes[7] != b'-' {
193 return Err(invalid());
194 }
195
196 let Some(year) = read_4_digits(bytes, 0) else {
197 return Err(invalid());
198 };
199
200 if !bytes[5..7].iter().all(u8::is_ascii_digit) || !bytes[8..10].iter().all(u8::is_ascii_digit) {
201 return Err(invalid());
202 }
203
204 let month = u32::from(bytes[5] - b'0') * 10 + u32::from(bytes[6] - b'0');
205 let day = u32::from(bytes[8] - b'0') * 10 + u32::from(bytes[9] - b'0');
206 let date = NaiveDate::from_ymd_opt(year, month, day).ok_or_else(invalid)?;
207 PeriodDate::new(date)
208}
209
210fn parse_quarter_of_year_code(value: &str) -> Result<QuarterOfYear, DomainError> {
211 let bytes = value.as_bytes();
212 if bytes.len() != 1 || !bytes[0].is_ascii_digit() {
213 return Err(DomainError::InvalidPeriodFormat {
214 format: value.to_string(),
215 });
216 }
217
218 QuarterOfYear::new(bytes[0] - b'0')
219}
220
221fn quarter_of_year_from_i64(value: i64) -> Result<QuarterOfYear, DomainError> {
222 let Ok(quarter) = u8::try_from(value) else {
223 return Err(DomainError::InvalidPeriodFormat {
224 format: value.to_string(),
225 });
226 };
227
228 QuarterOfYear::new(quarter)
229}
230
231fn quarter_of_year_from_u64(value: u64) -> Result<QuarterOfYear, DomainError> {
232 let Ok(quarter) = u8::try_from(value) else {
233 return Err(DomainError::InvalidPeriodFormat {
234 format: value.to_string(),
235 });
236 };
237
238 QuarterOfYear::new(quarter)
239}
240
241#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
249pub struct PeriodDate(NaiveDate);
250
251impl PeriodDate {
252 pub fn new(date: NaiveDate) -> Result<Self, DomainError> {
258 PeriodYear::new(date.year())?;
259 Ok(Self(date))
260 }
261
262 #[must_use]
264 pub const fn get(self) -> NaiveDate {
265 self.0
266 }
267}
268
269impl TryFrom<NaiveDate> for PeriodDate {
270 type Error = DomainError;
271
272 fn try_from(date: NaiveDate) -> Result<Self, Self::Error> {
273 Self::new(date)
274 }
275}
276
277impl From<PeriodDate> for NaiveDate {
278 fn from(date: PeriodDate) -> Self {
279 date.get()
280 }
281}
282
283impl fmt::Display for PeriodDate {
284 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
285 write!(f, "{}", self.0.format("%Y-%m-%d"))
286 }
287}
288
289impl Serialize for PeriodDate {
290 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
291 where
292 S: Serializer,
293 {
294 serializer.serialize_str(&self.to_string())
295 }
296}
297
298impl<'de> Deserialize<'de> for PeriodDate {
299 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
300 where
301 D: Deserializer<'de>,
302 {
303 let raw = String::deserialize(deserializer)?;
304 parse_period_date_code(&raw).map_err(DeError::custom)
305 }
306}
307
308#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
314pub struct QuarterOfYear(u8);
315
316impl QuarterOfYear {
317 pub const Q1: Self = Self(1);
319
320 pub const Q2: Self = Self(2);
322
323 pub const Q3: Self = Self(3);
325
326 pub const Q4: Self = Self(4);
328
329 pub const MIN: u8 = 1;
331
332 pub const MAX: u8 = 4;
334
335 pub const fn new(quarter: u8) -> Result<Self, DomainError> {
341 if quarter >= Self::MIN && quarter <= Self::MAX {
342 Ok(Self(quarter))
343 } else {
344 Err(DomainError::InvalidPeriodQuarter { quarter })
345 }
346 }
347
348 #[must_use]
350 pub const fn get(self) -> u8 {
351 self.0
352 }
353}
354
355impl TryFrom<u8> for QuarterOfYear {
356 type Error = DomainError;
357
358 fn try_from(quarter: u8) -> Result<Self, Self::Error> {
359 Self::new(quarter)
360 }
361}
362
363impl From<QuarterOfYear> for u8 {
364 fn from(quarter: QuarterOfYear) -> Self {
365 quarter.get()
366 }
367}
368
369impl fmt::Display for QuarterOfYear {
370 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
371 self.0.fmt(f)
372 }
373}
374
375impl Serialize for QuarterOfYear {
376 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
377 where
378 S: Serializer,
379 {
380 serializer.serialize_str(&self.to_string())
381 }
382}
383
384impl<'de> Deserialize<'de> for QuarterOfYear {
385 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
386 where
387 D: Deserializer<'de>,
388 {
389 deserializer.deserialize_any(QuarterOfYearVisitor)
390 }
391}
392
393struct QuarterOfYearVisitor;
394
395impl Visitor<'_> for QuarterOfYearVisitor {
396 type Value = QuarterOfYear;
397
398 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
399 formatter.write_str("a canonical quarter string or integer in 1..=4")
400 }
401
402 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
403 where
404 E: DeError,
405 {
406 parse_quarter_of_year_code(value).map_err(DeError::custom)
407 }
408
409 fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
410 where
411 E: DeError,
412 {
413 quarter_of_year_from_i64(value).map_err(DeError::custom)
414 }
415
416 fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
417 where
418 E: DeError,
419 {
420 quarter_of_year_from_u64(value).map_err(DeError::custom)
421 }
422}
423
424paft_core::other_string_code_type!(
425 pub struct OtherPeriod for ReportingPeriod;
427 type Error = DomainError;
428 parse(input) => input.parse::<ReportingPeriod>();
429 invalid(input) => DomainError::InvalidPeriodFormat {
430 format: input.to_string(),
431 };
432);
433
434#[derive(Debug, Clone, PartialEq, Eq, Hash)]
462#[non_exhaustive]
463pub enum ReportingPeriod {
464 Quarter {
466 year: PeriodYear,
468 quarter: QuarterOfYear,
470 },
471 Year {
473 year: PeriodYear,
475 },
476 Date(
478 PeriodDate,
480 ),
481 Other(OtherPeriod),
483}
484
485impl ReportingPeriod {
486 pub fn quarterly(year: i32, quarter: u8) -> Result<Self, DomainError> {
493 Ok(Self::Quarter {
494 year: PeriodYear::new(year)?,
495 quarter: QuarterOfYear::new(quarter)?,
496 })
497 }
498
499 pub fn annual(year: i32) -> Result<Self, DomainError> {
505 Ok(Self::Year {
506 year: PeriodYear::new(year)?,
507 })
508 }
509
510 pub fn date(date: NaiveDate) -> Result<Self, DomainError> {
516 Ok(Self::Date(PeriodDate::new(date)?))
517 }
518
519 pub fn other(input: &str) -> Result<Self, DomainError> {
531 OtherPeriod::new(input).map(Self::Other)
532 }
533
534 #[must_use]
536 pub fn code(&self) -> Cow<'_, str> {
537 match self {
538 Self::Quarter { year, quarter } => Cow::Owned(format!("{year}Q{quarter}")),
539 Self::Year { year } => Cow::Owned(year.to_string()),
540 Self::Date(date) => Cow::Owned(date.to_string()),
541 Self::Other(s) => Cow::Borrowed(s.as_ref()),
542 }
543 }
544 #[must_use]
546 pub const fn year(&self) -> Option<i32> {
547 match self {
548 Self::Quarter { year, .. } | Self::Year { year } => Some(year.get()),
549 _ => None,
550 }
551 }
552
553 #[must_use]
555 pub const fn period_year(&self) -> Option<PeriodYear> {
556 match self {
557 Self::Quarter { year, .. } | Self::Year { year } => Some(*year),
558 _ => None,
559 }
560 }
561
562 #[must_use]
564 pub const fn quarter(&self) -> Option<u8> {
565 match self {
566 Self::Quarter { quarter, .. } => Some(quarter.get()),
567 _ => None,
568 }
569 }
570
571 #[must_use]
573 pub const fn quarter_of_year(&self) -> Option<QuarterOfYear> {
574 match self {
575 Self::Quarter { quarter, .. } => Some(*quarter),
576 _ => None,
577 }
578 }
579
580 #[must_use]
582 pub const fn is_quarterly(&self) -> bool {
583 matches!(self, Self::Quarter { .. })
584 }
585
586 #[must_use]
588 pub const fn is_annual(&self) -> bool {
589 matches!(self, Self::Year { .. })
590 }
591
592 #[must_use]
594 pub const fn is_date(&self) -> bool {
595 matches!(self, Self::Date(_))
596 }
597}
598
599#[derive(Debug, Clone, PartialEq, Eq, Hash)]
605#[non_exhaustive]
606pub enum CalendarPeriod {
607 Quarter {
609 year: PeriodYear,
611 quarter: QuarterOfYear,
613 },
614 Year {
616 year: PeriodYear,
618 },
619 Date(
621 PeriodDate,
623 ),
624}
625
626impl CalendarPeriod {
627 pub fn quarterly(year: i32, quarter: u8) -> Result<Self, DomainError> {
634 Ok(Self::Quarter {
635 year: PeriodYear::new(year)?,
636 quarter: QuarterOfYear::new(quarter)?,
637 })
638 }
639
640 pub fn annual(year: i32) -> Result<Self, DomainError> {
646 Ok(Self::Year {
647 year: PeriodYear::new(year)?,
648 })
649 }
650
651 pub fn date(date: NaiveDate) -> Result<Self, DomainError> {
657 Ok(Self::Date(PeriodDate::new(date)?))
658 }
659
660 #[must_use]
662 pub fn code(&self) -> Cow<'_, str> {
663 match self {
664 Self::Quarter { year, quarter } => Cow::Owned(format!("{year}Q{quarter}")),
665 Self::Year { year } => Cow::Owned(year.to_string()),
666 Self::Date(date) => Cow::Owned(date.to_string()),
667 }
668 }
669
670 #[must_use]
672 pub const fn year(&self) -> Option<i32> {
673 match self {
674 Self::Quarter { year, .. } | Self::Year { year } => Some(year.get()),
675 Self::Date(_) => None,
676 }
677 }
678
679 #[must_use]
681 pub const fn period_year(&self) -> Option<PeriodYear> {
682 match self {
683 Self::Quarter { year, .. } | Self::Year { year } => Some(*year),
684 Self::Date(_) => None,
685 }
686 }
687
688 #[must_use]
690 pub const fn quarter(&self) -> Option<u8> {
691 match self {
692 Self::Quarter { quarter, .. } => Some(quarter.get()),
693 Self::Year { .. } | Self::Date(_) => None,
694 }
695 }
696
697 #[must_use]
699 pub const fn quarter_of_year(&self) -> Option<QuarterOfYear> {
700 match self {
701 Self::Quarter { quarter, .. } => Some(*quarter),
702 Self::Year { .. } | Self::Date(_) => None,
703 }
704 }
705
706 #[must_use]
708 pub const fn is_quarterly(&self) -> bool {
709 matches!(self, Self::Quarter { .. })
710 }
711
712 #[must_use]
714 pub const fn is_annual(&self) -> bool {
715 matches!(self, Self::Year { .. })
716 }
717
718 #[must_use]
720 pub const fn is_date(&self) -> bool {
721 matches!(self, Self::Date(_))
722 }
723
724 #[must_use]
730 pub fn next_quarter(&self) -> Option<Self> {
731 match self {
732 Self::Date(d) => {
733 let (year, quarter) = quarter_for_date(d.get())?;
734 let (year, quarter) = increment_quarter(year, quarter)?;
735 Some(Self::Quarter { year, quarter })
736 }
737 Self::Quarter { year, quarter } => {
738 let (year, quarter) = increment_quarter(*year, *quarter)?;
739 Some(Self::Quarter { year, quarter })
740 }
741 Self::Year { year } => {
742 let next_year = PeriodYear::new(year.get() + 1).ok()?;
743 Some(Self::Quarter {
744 year: next_year,
745 quarter: QuarterOfYear::Q1,
746 })
747 }
748 }
749 }
750
751 #[must_use]
757 pub fn year_end(&self) -> NaiveDate {
758 let y = match self {
759 Self::Date(d) => d.get().year(),
760 Self::Quarter { year, .. } | Self::Year { year } => year.get(),
761 };
762 expect_valid_date(y, 12, 31)
763 }
764
765 #[must_use]
771 pub const fn start_date(&self) -> NaiveDate {
772 match self {
773 Self::Date(d) => d.get(),
774 Self::Quarter { year, quarter } => {
775 let month = match quarter.get() {
776 1 => 1,
777 2 => 4,
778 3 => 7,
779 4 => 10,
780 _ => unreachable!(),
781 };
782 expect_valid_date(year.get(), month, 1)
783 }
784 Self::Year { year } => expect_valid_date(year.get(), 1, 1),
785 }
786 }
787
788 #[must_use]
794 pub const fn end_date(&self) -> NaiveDate {
795 match self {
796 Self::Date(d) => d.get(),
797 Self::Quarter { year, quarter } => {
798 let (month, day) = match quarter.get() {
799 1 => (3, 31),
800 2 => (6, 30),
801 3 => (9, 30),
802 4 => (12, 31),
803 _ => unreachable!(),
804 };
805 expect_valid_date(year.get(), month, day)
806 }
807 Self::Year { year } => expect_valid_date(year.get(), 12, 31),
808 }
809 }
810
811 #[must_use]
817 pub fn overlaps(&self, other: &Self) -> bool {
818 self.start_date() <= other.end_date() && other.start_date() <= self.end_date()
819 }
820
821 #[must_use]
826 pub fn contains(&self, other: &Self) -> bool {
827 self.start_date() <= other.start_date() && self.end_date() >= other.end_date()
828 }
829
830 #[must_use]
835 pub fn is_same_exact_bucket_as(&self, other: &Self) -> bool {
836 self == other
837 }
838}
839
840type ReportingPeriodAttempt = Option<Result<ReportingPeriod, ()>>;
849
850#[inline]
851const fn expect_valid_date(year: i32, month: u32, day: u32) -> NaiveDate {
852 let Some(date) = NaiveDate::from_ymd_opt(year, month, day) else {
853 unreachable!();
854 };
855 date
856}
857
858#[inline]
859fn read_4_digits(b: &[u8], start: usize) -> Option<i32> {
860 if start + 4 > b.len() {
861 return None;
862 }
863 let mut v: i32 = 0;
864 for &c in &b[start..start + 4] {
865 if !c.is_ascii_digit() {
866 return None;
867 }
868 v = v * 10 + i32::from(c - b'0');
869 }
870 Some(v)
871}
872
873#[inline]
874fn read_1_or_2_digits(b: &[u8], start: usize) -> Option<(u32, usize)> {
875 let &first = b.get(start)?;
876 if !first.is_ascii_digit() {
877 return None;
878 }
879 let d1 = u32::from(first - b'0');
880 if let Some(&second) = b.get(start + 1)
881 && second.is_ascii_digit()
882 {
883 Some((d1 * 10 + u32::from(second - b'0'), 2))
884 } else {
885 Some((d1, 1))
886 }
887}
888
889#[inline]
890fn date_or_err(year: i32, month: u32, day: u32) -> Result<ReportingPeriod, ()> {
891 NaiveDate::from_ymd_opt(year, month, day)
892 .ok_or(())
893 .and_then(|date| ReportingPeriod::date(date).map_err(|_| ()))
894}
895
896fn calendar_year(s: &str) -> Option<PeriodYear> {
897 let b = s.as_bytes();
898 if b.len() != 4 {
899 return None;
900 }
901 PeriodYear::new(read_4_digits(b, 0)?).ok()
902}
903
904fn quarter_for_date(d: NaiveDate) -> Option<(PeriodYear, QuarterOfYear)> {
905 let year = PeriodYear::new(d.year()).ok()?;
906 let m = d.month();
907 let quarter = match m {
908 1..=3 => QuarterOfYear::Q1,
909 4..=6 => QuarterOfYear::Q2,
910 7..=9 => QuarterOfYear::Q3,
911 _ => QuarterOfYear::Q4,
912 };
913 Some((year, quarter))
914}
915
916fn increment_quarter(
917 year: PeriodYear,
918 quarter: QuarterOfYear,
919) -> Option<(PeriodYear, QuarterOfYear)> {
920 if quarter.get() < QuarterOfYear::MAX {
921 let next_quarter = QuarterOfYear::new(quarter.get() + 1).ok()?;
922 Some((year, next_quarter))
923 } else {
924 let next_year = PeriodYear::new(year.get() + 1).ok()?;
925 Some((next_year, QuarterOfYear::Q1))
926 }
927}
928
929impl ReportingPeriod {
930 fn parse_quarterly(s: &str) -> ReportingPeriodAttempt {
933 let b = s.as_bytes();
934 if b.len() < 6 {
936 return None;
937 }
938
939 let year = PeriodYear::new(read_4_digits(b, 0)?).ok()?;
940 let mut idx = 4;
941
942 if b[idx] == b'-' {
949 idx += 1;
950 } else {
951 while idx < b.len() && b[idx].is_ascii_whitespace() {
952 idx += 1;
953 }
954 }
955 if idx >= b.len() {
956 return None;
957 }
958
959 if b[idx] != b'Q' && b[idx] != b'q' {
961 return None;
962 }
963 idx += 1;
964
965 let q_bytes = b.get(idx..)?;
966 if q_bytes.is_empty() {
967 return None;
968 }
969
970 if q_bytes.len() > 1 {
976 return q_bytes.iter().all(u8::is_ascii_digit).then_some(Err(()));
977 }
978
979 let c = q_bytes[0];
980 if !c.is_ascii_digit() {
981 return None;
982 }
983 let quarter = c - b'0';
984 let Ok(quarter) = QuarterOfYear::new(quarter) else {
985 return Some(Err(()));
986 };
987
988 Some(Ok(Self::Quarter { year, quarter }))
989 }
990
991 fn parse_year(s: &str) -> Option<Self> {
993 let b = s.as_bytes();
994 let digits_start = match b.len() {
995 4 => 0,
996 6 if b[..2].eq_ignore_ascii_case(b"FY") => 2,
997 n if n >= 11 && b[..6].eq_ignore_ascii_case(b"FISCAL") => {
998 let mut i = 6;
999 while i < n && b[i].is_ascii_whitespace() {
1000 i += 1;
1001 }
1002 if i == 6 {
1003 return None;
1004 }
1005 i
1006 }
1007 _ => return None,
1008 };
1009
1010 if b.len() - digits_start != 4 {
1011 return None;
1012 }
1013 let year = PeriodYear::new(read_4_digits(b, digits_start)?).ok()?;
1014 Some(Self::Year { year })
1015 }
1016
1017 fn parse_date(s: &str) -> ReportingPeriodAttempt {
1020 let b = s.as_bytes();
1021 if !(8..=10).contains(&b.len()) {
1022 return None;
1023 }
1024
1025 if let Some(year) = read_4_digits(b, 0)
1027 && (b[4] == b'-' || b[4] == b'/')
1028 {
1029 let sep = b[4];
1030 let (month, m_len) = read_1_or_2_digits(b, 5)?;
1031 let after_m = 5 + m_len;
1032 if b.get(after_m).copied() == Some(sep) {
1033 let (day, d_len) = read_1_or_2_digits(b, after_m + 1)?;
1034 if after_m + 1 + d_len == b.len() {
1035 return Some(date_or_err(year, month, day));
1036 }
1037 }
1038 return None;
1042 }
1043
1044 let (first, first_len) = read_1_or_2_digits(b, 0)?;
1048 let sep = *b.get(first_len)?;
1049 if sep != b'/' && sep != b'-' {
1050 return None;
1051 }
1052
1053 let (second, second_len) = read_1_or_2_digits(b, first_len + 1)?;
1054 let after_second = first_len + 1 + second_len;
1055 if b.get(after_second).copied() != Some(sep) {
1056 return None;
1057 }
1058
1059 let year_start = after_second + 1;
1060 if b.len() - year_start != 4 {
1061 return None;
1062 }
1063 let year = read_4_digits(b, year_start)?;
1064
1065 let (month, day) = if sep == b'/' {
1066 (first, second)
1067 } else {
1068 (second, first)
1069 };
1070
1071 Some(date_or_err(year, month, day))
1072 }
1073}
1074
1075impl From<ReportingPeriod> for String {
1076 fn from(val: ReportingPeriod) -> Self {
1077 val.code().into_owned()
1078 }
1079}
1080
1081impl fmt::Display for ReportingPeriod {
1082 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1083 f.write_str(&self.code())
1084 }
1085}
1086
1087impl Serialize for ReportingPeriod {
1088 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1089 where
1090 S: Serializer,
1091 {
1092 serializer.serialize_str(&self.code())
1093 }
1094}
1095
1096impl<'de> Deserialize<'de> for ReportingPeriod {
1097 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1098 where
1099 D: Deserializer<'de>,
1100 {
1101 let raw = String::deserialize(deserializer)?;
1102 raw.parse::<Self>().map_err(DeError::custom)
1103 }
1104}
1105
1106impl std::str::FromStr for ReportingPeriod {
1107 type Err = DomainError;
1108
1109 #[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", err))]
1122 fn from_str(s: &str) -> Result<Self, Self::Err> {
1123 let trimmed = s.trim();
1124
1125 if trimmed.is_empty() {
1126 return Err(DomainError::InvalidPeriodFormat {
1127 format: s.to_string(),
1128 });
1129 }
1130
1131 let invalid = || DomainError::InvalidPeriodFormat {
1132 format: s.to_string(),
1133 };
1134
1135 match Self::parse_quarterly(trimmed) {
1136 Some(Ok(period)) => return Ok(period),
1137 Some(Err(())) => return Err(invalid()),
1138 None => {}
1139 }
1140
1141 if let Some(period) = Self::parse_year(trimmed) {
1142 return Ok(period);
1143 }
1144
1145 match Self::parse_date(trimmed) {
1146 Some(Ok(period)) => return Ok(period),
1147 Some(Err(())) => return Err(invalid()),
1148 None => {}
1149 }
1150
1151 let canonical = Canonical::try_new(trimmed).map_err(|_| invalid())?;
1152
1153 let canonical_str = canonical.as_ref();
1158 if Self::parse_quarterly(canonical_str).is_some() {
1159 return Err(invalid());
1160 }
1161 if Self::parse_year(canonical_str).is_some() {
1162 return Err(invalid());
1163 }
1164 if Self::parse_date(canonical_str).is_some() {
1165 return Err(invalid());
1166 }
1167
1168 Ok(Self::Other(OtherPeriod::from_canonical_unchecked(
1169 canonical,
1170 )))
1171 }
1172}
1173
1174impl TryFrom<String> for ReportingPeriod {
1175 type Error = DomainError;
1176
1177 fn try_from(s: String) -> Result<Self, Self::Error> {
1178 s.as_str().parse()
1179 }
1180}
1181
1182impl TryFrom<ReportingPeriod> for CalendarPeriod {
1183 type Error = DomainError;
1184
1185 fn try_from(period: ReportingPeriod) -> Result<Self, Self::Error> {
1186 match period {
1187 ReportingPeriod::Quarter { year, quarter } => Ok(Self::Quarter { year, quarter }),
1188 ReportingPeriod::Year { year } => Ok(Self::Year { year }),
1189 ReportingPeriod::Date(date) => Ok(Self::Date(date)),
1190 ReportingPeriod::Other(other) => Err(DomainError::InvalidPeriodFormat {
1191 format: other.to_string(),
1192 }),
1193 }
1194 }
1195}
1196
1197impl From<CalendarPeriod> for ReportingPeriod {
1198 fn from(period: CalendarPeriod) -> Self {
1199 match period {
1200 CalendarPeriod::Quarter { year, quarter } => Self::Quarter { year, quarter },
1201 CalendarPeriod::Year { year } => Self::Year { year },
1202 CalendarPeriod::Date(date) => Self::Date(date),
1203 }
1204 }
1205}
1206
1207impl From<CalendarPeriod> for String {
1208 fn from(val: CalendarPeriod) -> Self {
1209 val.code().into_owned()
1210 }
1211}
1212
1213impl fmt::Display for CalendarPeriod {
1214 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1215 f.write_str(&self.code())
1216 }
1217}
1218
1219impl Serialize for CalendarPeriod {
1220 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1221 where
1222 S: Serializer,
1223 {
1224 serializer.serialize_str(&self.code())
1225 }
1226}
1227
1228impl<'de> Deserialize<'de> for CalendarPeriod {
1229 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1230 where
1231 D: Deserializer<'de>,
1232 {
1233 let raw = String::deserialize(deserializer)?;
1234 raw.parse::<Self>().map_err(DeError::custom)
1235 }
1236}
1237
1238impl std::str::FromStr for CalendarPeriod {
1239 type Err = DomainError;
1240
1241 #[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", err))]
1246 fn from_str(s: &str) -> Result<Self, Self::Err> {
1247 let trimmed = s.trim();
1248
1249 if trimmed.is_empty() {
1250 return Err(DomainError::InvalidPeriodFormat {
1251 format: s.to_string(),
1252 });
1253 }
1254
1255 let invalid = || DomainError::InvalidPeriodFormat {
1256 format: s.to_string(),
1257 };
1258
1259 match ReportingPeriod::parse_quarterly(trimmed) {
1260 Some(Ok(ReportingPeriod::Quarter { year, quarter })) => {
1261 return Ok(Self::Quarter { year, quarter });
1262 }
1263 Some(Ok(_)) => unreachable!("quarter parser only emits quarter periods"),
1264 Some(Err(())) => return Err(invalid()),
1265 None => {}
1266 }
1267
1268 if let Some(year) = calendar_year(trimmed) {
1269 return Ok(Self::Year { year });
1270 }
1271
1272 match ReportingPeriod::parse_date(trimmed) {
1273 Some(Ok(ReportingPeriod::Date(date))) => return Ok(Self::Date(date)),
1274 Some(Ok(_)) => unreachable!("date parser only emits date periods"),
1275 Some(Err(())) => return Err(invalid()),
1276 None => {}
1277 }
1278
1279 Err(invalid())
1280 }
1281}
1282
1283impl TryFrom<String> for CalendarPeriod {
1284 type Error = DomainError;
1285
1286 fn try_from(s: String) -> Result<Self, Self::Error> {
1287 s.as_str().parse()
1288 }
1289}