quarterly/
lib.rs

1use std::str::FromStr;
2
3/// Represents one of the four quarters of a year
4#[derive(Debug, PartialEq)]
5pub enum QuarterNumber {
6    Q1,
7    Q2,
8    Q3,
9    Q4,
10}
11
12#[derive(Debug, PartialEq, Eq)]
13pub struct ParseQuarterError;
14
15impl FromStr for QuarterNumber {
16    type Err = ParseQuarterError;
17
18    /// ```
19    /// # use quarterly::*;
20    /// assert_eq!("Q1".parse::<QuarterNumber>(), Ok(QuarterNumber::Q1));
21    /// assert_eq!("q1".parse::<QuarterNumber>(), Ok(QuarterNumber::Q1));
22    /// assert_eq!("banana".parse::<QuarterNumber>(), Err(ParseQuarterError));
23    /// ```
24    fn from_str(s: &str) -> Result<Self, Self::Err> {
25        match s.to_uppercase().as_str() {
26            "Q1" => Ok(QuarterNumber::Q1),
27            "Q2" => Ok(QuarterNumber::Q2),
28            "Q3" => Ok(QuarterNumber::Q3),
29            "Q4" => Ok(QuarterNumber::Q4),
30            _ => Err(ParseQuarterError),
31        }
32    }
33}
34
35impl TryFrom<u8> for QuarterNumber {
36    type Error = ParseQuarterError;
37
38    /// ```
39    /// # use quarterly::*;
40    /// assert_eq!(QuarterNumber::try_from(1), Ok(QuarterNumber::Q1));
41    /// assert_eq!(QuarterNumber::try_from(2), Ok(QuarterNumber::Q2));
42    /// assert_eq!(QuarterNumber::try_from(5), Err(ParseQuarterError));
43    /// ```
44    fn try_from(value: u8) -> Result<Self, ParseQuarterError> {
45        match value {
46            1 => Ok(QuarterNumber::Q1),
47            2 => Ok(QuarterNumber::Q2),
48            3 => Ok(QuarterNumber::Q3),
49            4 => Ok(QuarterNumber::Q4),
50            _ => Err(ParseQuarterError),
51        }
52    }
53}
54
55#[derive(Debug, PartialEq)]
56pub struct Quarter {
57    pub quarter_number: QuarterNumber,
58    pub year: u16,
59}
60
61impl Quarter {
62    /// ```
63    /// # use quarterly::*;
64    /// assert_eq!(Quarter::new(QuarterNumber::Q1, 2023), Quarter { quarter_number: QuarterNumber::Q1, year: 2023 })
65    /// ```
66    pub fn new(quarter_number: QuarterNumber, year: u16) -> Self {
67        Quarter {
68            quarter_number,
69            year,
70        }
71    }
72
73    /// Determines next calendar quarter, handling year wrapping
74    ///
75    /// ```
76    /// # use quarterly::*;
77    /// assert_eq!(Quarter::new(QuarterNumber::Q1, 2023).next_quarter(), Quarter::new(QuarterNumber::Q2, 2023));
78    /// assert_eq!(Quarter::new(QuarterNumber::Q4, 2023).next_quarter(), Quarter::new(QuarterNumber::Q1, 2024));
79    /// ```
80    pub fn next_quarter(&self) -> Self {
81        match self.quarter_number {
82            QuarterNumber::Q1 => Quarter {
83                year: self.year,
84                quarter_number: QuarterNumber::Q2,
85            },
86            QuarterNumber::Q2 => Quarter {
87                year: self.year,
88                quarter_number: QuarterNumber::Q3,
89            },
90            QuarterNumber::Q3 => Quarter {
91                year: self.year,
92                quarter_number: QuarterNumber::Q4,
93            },
94            QuarterNumber::Q4 => Quarter {
95                year: self.year + 1,
96                quarter_number: QuarterNumber::Q1,
97            },
98        }
99    }
100}
101
102impl FromStr for Quarter {
103    type Err = ParseQuarterError;
104
105    /// ```
106    /// # use quarterly::*;
107    /// assert_eq!("Q2 2023".parse::<Quarter>(), Ok(Quarter { year: 2023, quarter_number: QuarterNumber::Q2 } ));
108    /// assert_eq!("Q2".parse::<Quarter>(), Err(ParseQuarterError));
109    /// ```
110    fn from_str(s: &str) -> Result<Self, Self::Err> {
111        let (quarter_number, year) = s.split_once(' ').ok_or(ParseQuarterError)?;
112
113        let quarter_number_fromstr = quarter_number
114            .parse::<QuarterNumber>()
115            .map_err(|_| ParseQuarterError)?;
116        let year_fromstr: u16 = year.parse().map_err(|_| ParseQuarterError)?;
117
118        Ok(Quarter {
119            year: year_fromstr,
120            quarter_number: quarter_number_fromstr,
121        })
122    }
123}