use serde::{
Deserialize, Deserializer, Serialize, Serializer,
de::{Error as DeError, Visitor},
};
use std::borrow::Cow;
use std::fmt;
use crate::error::DomainError;
use chrono::{Datelike, NaiveDate};
use paft_utils::Canonical;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct PeriodYear(u16);
impl PeriodYear {
pub const MIN: u16 = 0;
pub const MAX: u16 = 9999;
pub fn new(year: i32) -> Result<Self, DomainError> {
let Ok(year_u16) = u16::try_from(year) else {
return Err(DomainError::InvalidPeriodYear { year });
};
if year_u16 <= Self::MAX {
Ok(Self(year_u16))
} else {
Err(DomainError::InvalidPeriodYear { year })
}
}
#[must_use]
pub const fn get(self) -> i32 {
self.0 as i32
}
#[must_use]
pub const fn as_u16(self) -> u16 {
self.0
}
}
impl TryFrom<i32> for PeriodYear {
type Error = DomainError;
fn try_from(year: i32) -> Result<Self, Self::Error> {
Self::new(year)
}
}
impl TryFrom<u16> for PeriodYear {
type Error = DomainError;
fn try_from(year: u16) -> Result<Self, Self::Error> {
Self::new(i32::from(year))
}
}
impl From<PeriodYear> for i32 {
fn from(year: PeriodYear) -> Self {
year.get()
}
}
impl From<PeriodYear> for u16 {
fn from(year: PeriodYear) -> Self {
year.as_u16()
}
}
impl fmt::Display for PeriodYear {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:04}", self.0)
}
}
impl Serialize for PeriodYear {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
impl<'de> Deserialize<'de> for PeriodYear {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(PeriodYearVisitor)
}
}
struct PeriodYearVisitor;
impl Visitor<'_> for PeriodYearVisitor {
type Value = PeriodYear;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("a canonical four-digit period year string or integer in 0..=9999")
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: DeError,
{
parse_period_year_code(value).map_err(DeError::custom)
}
fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
where
E: DeError,
{
period_year_from_i64(value).map_err(DeError::custom)
}
fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
where
E: DeError,
{
period_year_from_u64(value).map_err(DeError::custom)
}
}
fn parse_period_year_code(value: &str) -> Result<PeriodYear, DomainError> {
let bytes = value.as_bytes();
if bytes.len() != 4 || !bytes.iter().all(u8::is_ascii_digit) {
return Err(DomainError::InvalidPeriodFormat {
format: value.to_string(),
});
}
let year = i32::from(bytes[0] - b'0') * 1_000
+ i32::from(bytes[1] - b'0') * 100
+ i32::from(bytes[2] - b'0') * 10
+ i32::from(bytes[3] - b'0');
PeriodYear::new(year)
}
fn period_year_from_i64(value: i64) -> Result<PeriodYear, DomainError> {
let Ok(year) = i32::try_from(value) else {
return Err(DomainError::InvalidPeriodFormat {
format: value.to_string(),
});
};
PeriodYear::new(year)
}
fn period_year_from_u64(value: u64) -> Result<PeriodYear, DomainError> {
let Ok(year) = i32::try_from(value) else {
return Err(DomainError::InvalidPeriodFormat {
format: value.to_string(),
});
};
PeriodYear::new(year)
}
fn parse_period_date_code(value: &str) -> Result<PeriodDate, DomainError> {
let invalid = || DomainError::InvalidPeriodFormat {
format: value.to_string(),
};
let bytes = value.as_bytes();
if bytes.len() != 10 || bytes[4] != b'-' || bytes[7] != b'-' {
return Err(invalid());
}
let Some(year) = read_4_digits(bytes, 0) else {
return Err(invalid());
};
if !bytes[5..7].iter().all(u8::is_ascii_digit) || !bytes[8..10].iter().all(u8::is_ascii_digit) {
return Err(invalid());
}
let month = u32::from(bytes[5] - b'0') * 10 + u32::from(bytes[6] - b'0');
let day = u32::from(bytes[8] - b'0') * 10 + u32::from(bytes[9] - b'0');
let date = NaiveDate::from_ymd_opt(year, month, day).ok_or_else(invalid)?;
PeriodDate::new(date)
}
fn parse_quarter_of_year_code(value: &str) -> Result<QuarterOfYear, DomainError> {
let bytes = value.as_bytes();
if bytes.len() != 1 || !bytes[0].is_ascii_digit() {
return Err(DomainError::InvalidPeriodFormat {
format: value.to_string(),
});
}
QuarterOfYear::new(bytes[0] - b'0')
}
fn quarter_of_year_from_i64(value: i64) -> Result<QuarterOfYear, DomainError> {
let Ok(quarter) = u8::try_from(value) else {
return Err(DomainError::InvalidPeriodFormat {
format: value.to_string(),
});
};
QuarterOfYear::new(quarter)
}
fn quarter_of_year_from_u64(value: u64) -> Result<QuarterOfYear, DomainError> {
let Ok(quarter) = u8::try_from(value) else {
return Err(DomainError::InvalidPeriodFormat {
format: value.to_string(),
});
};
QuarterOfYear::new(quarter)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct PeriodDate(NaiveDate);
impl PeriodDate {
pub fn new(date: NaiveDate) -> Result<Self, DomainError> {
PeriodYear::new(date.year())?;
Ok(Self(date))
}
#[must_use]
pub const fn get(self) -> NaiveDate {
self.0
}
}
impl TryFrom<NaiveDate> for PeriodDate {
type Error = DomainError;
fn try_from(date: NaiveDate) -> Result<Self, Self::Error> {
Self::new(date)
}
}
impl From<PeriodDate> for NaiveDate {
fn from(date: PeriodDate) -> Self {
date.get()
}
}
impl fmt::Display for PeriodDate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0.format("%Y-%m-%d"))
}
}
impl Serialize for PeriodDate {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
impl<'de> Deserialize<'de> for PeriodDate {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let raw = String::deserialize(deserializer)?;
parse_period_date_code(&raw).map_err(DeError::custom)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct QuarterOfYear(u8);
impl QuarterOfYear {
pub const Q1: Self = Self(1);
pub const Q2: Self = Self(2);
pub const Q3: Self = Self(3);
pub const Q4: Self = Self(4);
pub const MIN: u8 = 1;
pub const MAX: u8 = 4;
pub const fn new(quarter: u8) -> Result<Self, DomainError> {
if quarter >= Self::MIN && quarter <= Self::MAX {
Ok(Self(quarter))
} else {
Err(DomainError::InvalidPeriodQuarter { quarter })
}
}
#[must_use]
pub const fn get(self) -> u8 {
self.0
}
}
impl TryFrom<u8> for QuarterOfYear {
type Error = DomainError;
fn try_from(quarter: u8) -> Result<Self, Self::Error> {
Self::new(quarter)
}
}
impl From<QuarterOfYear> for u8 {
fn from(quarter: QuarterOfYear) -> Self {
quarter.get()
}
}
impl fmt::Display for QuarterOfYear {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
impl Serialize for QuarterOfYear {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
impl<'de> Deserialize<'de> for QuarterOfYear {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(QuarterOfYearVisitor)
}
}
struct QuarterOfYearVisitor;
impl Visitor<'_> for QuarterOfYearVisitor {
type Value = QuarterOfYear;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("a canonical quarter string or integer in 1..=4")
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: DeError,
{
parse_quarter_of_year_code(value).map_err(DeError::custom)
}
fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
where
E: DeError,
{
quarter_of_year_from_i64(value).map_err(DeError::custom)
}
fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
where
E: DeError,
{
quarter_of_year_from_u64(value).map_err(DeError::custom)
}
}
paft_core::other_string_code_type!(
pub struct OtherPeriod for ReportingPeriod;
type Error = DomainError;
parse(input) => input.parse::<ReportingPeriod>();
invalid(input) => DomainError::InvalidPeriodFormat {
format: input.to_string(),
};
);
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ReportingPeriod {
Quarter {
year: PeriodYear,
quarter: QuarterOfYear,
},
Year {
year: PeriodYear,
},
Date(
PeriodDate,
),
Other(OtherPeriod),
}
impl ReportingPeriod {
pub fn quarterly(year: i32, quarter: u8) -> Result<Self, DomainError> {
Ok(Self::Quarter {
year: PeriodYear::new(year)?,
quarter: QuarterOfYear::new(quarter)?,
})
}
pub fn annual(year: i32) -> Result<Self, DomainError> {
Ok(Self::Year {
year: PeriodYear::new(year)?,
})
}
pub fn date(date: NaiveDate) -> Result<Self, DomainError> {
Ok(Self::Date(PeriodDate::new(date)?))
}
pub fn other(input: &str) -> Result<Self, DomainError> {
OtherPeriod::new(input).map(Self::Other)
}
#[must_use]
pub fn code(&self) -> Cow<'_, str> {
match self {
Self::Quarter { year, quarter } => Cow::Owned(format!("{year}Q{quarter}")),
Self::Year { year } => Cow::Owned(year.to_string()),
Self::Date(date) => Cow::Owned(date.to_string()),
Self::Other(s) => Cow::Borrowed(s.as_ref()),
}
}
#[must_use]
pub const fn year(&self) -> Option<i32> {
match self {
Self::Quarter { year, .. } | Self::Year { year } => Some(year.get()),
_ => None,
}
}
#[must_use]
pub const fn period_year(&self) -> Option<PeriodYear> {
match self {
Self::Quarter { year, .. } | Self::Year { year } => Some(*year),
_ => None,
}
}
#[must_use]
pub const fn quarter(&self) -> Option<u8> {
match self {
Self::Quarter { quarter, .. } => Some(quarter.get()),
_ => None,
}
}
#[must_use]
pub const fn quarter_of_year(&self) -> Option<QuarterOfYear> {
match self {
Self::Quarter { quarter, .. } => Some(*quarter),
_ => None,
}
}
#[must_use]
pub const fn is_quarterly(&self) -> bool {
matches!(self, Self::Quarter { .. })
}
#[must_use]
pub const fn is_annual(&self) -> bool {
matches!(self, Self::Year { .. })
}
#[must_use]
pub const fn is_date(&self) -> bool {
matches!(self, Self::Date(_))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum CalendarPeriod {
Quarter {
year: PeriodYear,
quarter: QuarterOfYear,
},
Year {
year: PeriodYear,
},
Date(
PeriodDate,
),
}
impl CalendarPeriod {
pub fn quarterly(year: i32, quarter: u8) -> Result<Self, DomainError> {
Ok(Self::Quarter {
year: PeriodYear::new(year)?,
quarter: QuarterOfYear::new(quarter)?,
})
}
pub fn annual(year: i32) -> Result<Self, DomainError> {
Ok(Self::Year {
year: PeriodYear::new(year)?,
})
}
pub fn date(date: NaiveDate) -> Result<Self, DomainError> {
Ok(Self::Date(PeriodDate::new(date)?))
}
#[must_use]
pub fn code(&self) -> Cow<'_, str> {
match self {
Self::Quarter { year, quarter } => Cow::Owned(format!("{year}Q{quarter}")),
Self::Year { year } => Cow::Owned(year.to_string()),
Self::Date(date) => Cow::Owned(date.to_string()),
}
}
#[must_use]
pub const fn year(&self) -> Option<i32> {
match self {
Self::Quarter { year, .. } | Self::Year { year } => Some(year.get()),
Self::Date(_) => None,
}
}
#[must_use]
pub const fn period_year(&self) -> Option<PeriodYear> {
match self {
Self::Quarter { year, .. } | Self::Year { year } => Some(*year),
Self::Date(_) => None,
}
}
#[must_use]
pub const fn quarter(&self) -> Option<u8> {
match self {
Self::Quarter { quarter, .. } => Some(quarter.get()),
Self::Year { .. } | Self::Date(_) => None,
}
}
#[must_use]
pub const fn quarter_of_year(&self) -> Option<QuarterOfYear> {
match self {
Self::Quarter { quarter, .. } => Some(*quarter),
Self::Year { .. } | Self::Date(_) => None,
}
}
#[must_use]
pub const fn is_quarterly(&self) -> bool {
matches!(self, Self::Quarter { .. })
}
#[must_use]
pub const fn is_annual(&self) -> bool {
matches!(self, Self::Year { .. })
}
#[must_use]
pub const fn is_date(&self) -> bool {
matches!(self, Self::Date(_))
}
#[must_use]
pub fn next_quarter(&self) -> Option<Self> {
match self {
Self::Date(d) => {
let (year, quarter) = quarter_for_date(d.get())?;
let (year, quarter) = increment_quarter(year, quarter)?;
Some(Self::Quarter { year, quarter })
}
Self::Quarter { year, quarter } => {
let (year, quarter) = increment_quarter(*year, *quarter)?;
Some(Self::Quarter { year, quarter })
}
Self::Year { year } => {
let next_year = PeriodYear::new(year.get() + 1).ok()?;
Some(Self::Quarter {
year: next_year,
quarter: QuarterOfYear::Q1,
})
}
}
}
#[must_use]
pub fn year_end(&self) -> NaiveDate {
let y = match self {
Self::Date(d) => d.get().year(),
Self::Quarter { year, .. } | Self::Year { year } => year.get(),
};
expect_valid_date(y, 12, 31)
}
#[must_use]
pub const fn start_date(&self) -> NaiveDate {
match self {
Self::Date(d) => d.get(),
Self::Quarter { year, quarter } => {
let month = match quarter.get() {
1 => 1,
2 => 4,
3 => 7,
4 => 10,
_ => unreachable!(),
};
expect_valid_date(year.get(), month, 1)
}
Self::Year { year } => expect_valid_date(year.get(), 1, 1),
}
}
#[must_use]
pub const fn end_date(&self) -> NaiveDate {
match self {
Self::Date(d) => d.get(),
Self::Quarter { year, quarter } => {
let (month, day) = match quarter.get() {
1 => (3, 31),
2 => (6, 30),
3 => (9, 30),
4 => (12, 31),
_ => unreachable!(),
};
expect_valid_date(year.get(), month, day)
}
Self::Year { year } => expect_valid_date(year.get(), 12, 31),
}
}
#[must_use]
pub fn overlaps(&self, other: &Self) -> bool {
self.start_date() <= other.end_date() && other.start_date() <= self.end_date()
}
#[must_use]
pub fn contains(&self, other: &Self) -> bool {
self.start_date() <= other.start_date() && self.end_date() >= other.end_date()
}
#[must_use]
pub fn is_same_exact_bucket_as(&self, other: &Self) -> bool {
self == other
}
}
type ReportingPeriodAttempt = Option<Result<ReportingPeriod, ()>>;
#[inline]
const fn expect_valid_date(year: i32, month: u32, day: u32) -> NaiveDate {
let Some(date) = NaiveDate::from_ymd_opt(year, month, day) else {
unreachable!();
};
date
}
#[inline]
fn read_4_digits(b: &[u8], start: usize) -> Option<i32> {
if start + 4 > b.len() {
return None;
}
let mut v: i32 = 0;
for &c in &b[start..start + 4] {
if !c.is_ascii_digit() {
return None;
}
v = v * 10 + i32::from(c - b'0');
}
Some(v)
}
#[inline]
fn read_1_or_2_digits(b: &[u8], start: usize) -> Option<(u32, usize)> {
let &first = b.get(start)?;
if !first.is_ascii_digit() {
return None;
}
let d1 = u32::from(first - b'0');
if let Some(&second) = b.get(start + 1)
&& second.is_ascii_digit()
{
Some((d1 * 10 + u32::from(second - b'0'), 2))
} else {
Some((d1, 1))
}
}
#[inline]
fn date_or_err(year: i32, month: u32, day: u32) -> Result<ReportingPeriod, ()> {
NaiveDate::from_ymd_opt(year, month, day)
.ok_or(())
.and_then(|date| ReportingPeriod::date(date).map_err(|_| ()))
}
fn calendar_year(s: &str) -> Option<PeriodYear> {
let b = s.as_bytes();
if b.len() != 4 {
return None;
}
PeriodYear::new(read_4_digits(b, 0)?).ok()
}
fn quarter_for_date(d: NaiveDate) -> Option<(PeriodYear, QuarterOfYear)> {
let year = PeriodYear::new(d.year()).ok()?;
let m = d.month();
let quarter = match m {
1..=3 => QuarterOfYear::Q1,
4..=6 => QuarterOfYear::Q2,
7..=9 => QuarterOfYear::Q3,
_ => QuarterOfYear::Q4,
};
Some((year, quarter))
}
fn increment_quarter(
year: PeriodYear,
quarter: QuarterOfYear,
) -> Option<(PeriodYear, QuarterOfYear)> {
if quarter.get() < QuarterOfYear::MAX {
let next_quarter = QuarterOfYear::new(quarter.get() + 1).ok()?;
Some((year, next_quarter))
} else {
let next_year = PeriodYear::new(year.get() + 1).ok()?;
Some((next_year, QuarterOfYear::Q1))
}
}
impl ReportingPeriod {
fn parse_quarterly(s: &str) -> ReportingPeriodAttempt {
let b = s.as_bytes();
if b.len() < 6 {
return None;
}
let year = PeriodYear::new(read_4_digits(b, 0)?).ok()?;
let mut idx = 4;
if b[idx] == b'-' {
idx += 1;
} else {
while idx < b.len() && b[idx].is_ascii_whitespace() {
idx += 1;
}
}
if idx >= b.len() {
return None;
}
if b[idx] != b'Q' && b[idx] != b'q' {
return None;
}
idx += 1;
let q_bytes = b.get(idx..)?;
if q_bytes.is_empty() {
return None;
}
if q_bytes.len() > 1 {
return q_bytes.iter().all(u8::is_ascii_digit).then_some(Err(()));
}
let c = q_bytes[0];
if !c.is_ascii_digit() {
return None;
}
let quarter = c - b'0';
let Ok(quarter) = QuarterOfYear::new(quarter) else {
return Some(Err(()));
};
Some(Ok(Self::Quarter { year, quarter }))
}
fn parse_year(s: &str) -> Option<Self> {
let b = s.as_bytes();
let digits_start = match b.len() {
4 => 0,
6 if b[..2].eq_ignore_ascii_case(b"FY") => 2,
n if n >= 11 && b[..6].eq_ignore_ascii_case(b"FISCAL") => {
let mut i = 6;
while i < n && b[i].is_ascii_whitespace() {
i += 1;
}
if i == 6 {
return None;
}
i
}
_ => return None,
};
if b.len() - digits_start != 4 {
return None;
}
let year = PeriodYear::new(read_4_digits(b, digits_start)?).ok()?;
Some(Self::Year { year })
}
fn parse_date(s: &str) -> ReportingPeriodAttempt {
let b = s.as_bytes();
if !(8..=10).contains(&b.len()) {
return None;
}
if let Some(year) = read_4_digits(b, 0)
&& (b[4] == b'-' || b[4] == b'/')
{
let sep = b[4];
let (month, m_len) = read_1_or_2_digits(b, 5)?;
let after_m = 5 + m_len;
if b.get(after_m).copied() == Some(sep) {
let (day, d_len) = read_1_or_2_digits(b, after_m + 1)?;
if after_m + 1 + d_len == b.len() {
return Some(date_or_err(year, month, day));
}
}
return None;
}
let (first, first_len) = read_1_or_2_digits(b, 0)?;
let sep = *b.get(first_len)?;
if sep != b'/' && sep != b'-' {
return None;
}
let (second, second_len) = read_1_or_2_digits(b, first_len + 1)?;
let after_second = first_len + 1 + second_len;
if b.get(after_second).copied() != Some(sep) {
return None;
}
let year_start = after_second + 1;
if b.len() - year_start != 4 {
return None;
}
let year = read_4_digits(b, year_start)?;
let (month, day) = if sep == b'/' {
(first, second)
} else {
(second, first)
};
Some(date_or_err(year, month, day))
}
}
impl From<ReportingPeriod> for String {
fn from(val: ReportingPeriod) -> Self {
val.code().into_owned()
}
}
impl fmt::Display for ReportingPeriod {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.code())
}
}
impl Serialize for ReportingPeriod {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.code())
}
}
impl<'de> Deserialize<'de> for ReportingPeriod {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let raw = String::deserialize(deserializer)?;
raw.parse::<Self>().map_err(DeError::custom)
}
}
impl std::str::FromStr for ReportingPeriod {
type Err = DomainError;
#[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", err))]
fn from_str(s: &str) -> Result<Self, Self::Err> {
let trimmed = s.trim();
if trimmed.is_empty() {
return Err(DomainError::InvalidPeriodFormat {
format: s.to_string(),
});
}
let invalid = || DomainError::InvalidPeriodFormat {
format: s.to_string(),
};
match Self::parse_quarterly(trimmed) {
Some(Ok(period)) => return Ok(period),
Some(Err(())) => return Err(invalid()),
None => {}
}
if let Some(period) = Self::parse_year(trimmed) {
return Ok(period);
}
match Self::parse_date(trimmed) {
Some(Ok(period)) => return Ok(period),
Some(Err(())) => return Err(invalid()),
None => {}
}
let canonical = Canonical::try_new(trimmed).map_err(|_| invalid())?;
let canonical_str = canonical.as_ref();
if Self::parse_quarterly(canonical_str).is_some() {
return Err(invalid());
}
if Self::parse_year(canonical_str).is_some() {
return Err(invalid());
}
if Self::parse_date(canonical_str).is_some() {
return Err(invalid());
}
Ok(Self::Other(OtherPeriod::from_canonical_unchecked(
canonical,
)))
}
}
impl TryFrom<String> for ReportingPeriod {
type Error = DomainError;
fn try_from(s: String) -> Result<Self, Self::Error> {
s.as_str().parse()
}
}
impl TryFrom<ReportingPeriod> for CalendarPeriod {
type Error = DomainError;
fn try_from(period: ReportingPeriod) -> Result<Self, Self::Error> {
match period {
ReportingPeriod::Quarter { year, quarter } => Ok(Self::Quarter { year, quarter }),
ReportingPeriod::Year { year } => Ok(Self::Year { year }),
ReportingPeriod::Date(date) => Ok(Self::Date(date)),
ReportingPeriod::Other(other) => Err(DomainError::InvalidPeriodFormat {
format: other.to_string(),
}),
}
}
}
impl From<CalendarPeriod> for ReportingPeriod {
fn from(period: CalendarPeriod) -> Self {
match period {
CalendarPeriod::Quarter { year, quarter } => Self::Quarter { year, quarter },
CalendarPeriod::Year { year } => Self::Year { year },
CalendarPeriod::Date(date) => Self::Date(date),
}
}
}
impl From<CalendarPeriod> for String {
fn from(val: CalendarPeriod) -> Self {
val.code().into_owned()
}
}
impl fmt::Display for CalendarPeriod {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.code())
}
}
impl Serialize for CalendarPeriod {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.code())
}
}
impl<'de> Deserialize<'de> for CalendarPeriod {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let raw = String::deserialize(deserializer)?;
raw.parse::<Self>().map_err(DeError::custom)
}
}
impl std::str::FromStr for CalendarPeriod {
type Err = DomainError;
#[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", err))]
fn from_str(s: &str) -> Result<Self, Self::Err> {
let trimmed = s.trim();
if trimmed.is_empty() {
return Err(DomainError::InvalidPeriodFormat {
format: s.to_string(),
});
}
let invalid = || DomainError::InvalidPeriodFormat {
format: s.to_string(),
};
match ReportingPeriod::parse_quarterly(trimmed) {
Some(Ok(ReportingPeriod::Quarter { year, quarter })) => {
return Ok(Self::Quarter { year, quarter });
}
Some(Ok(_)) => unreachable!("quarter parser only emits quarter periods"),
Some(Err(())) => return Err(invalid()),
None => {}
}
if let Some(year) = calendar_year(trimmed) {
return Ok(Self::Year { year });
}
match ReportingPeriod::parse_date(trimmed) {
Some(Ok(ReportingPeriod::Date(date))) => return Ok(Self::Date(date)),
Some(Ok(_)) => unreachable!("date parser only emits date periods"),
Some(Err(())) => return Err(invalid()),
None => {}
}
Err(invalid())
}
}
impl TryFrom<String> for CalendarPeriod {
type Error = DomainError;
fn try_from(s: String) -> Result<Self, Self::Error> {
s.as_str().parse()
}
}