Skip to main content

use_quarter/
lib.rs

1#![forbid(unsafe_code)]
2//! Primitive calendar quarter helpers.
3//!
4//! These helpers provide simple calendar quarter boundaries and checks.
5//!
6//! # Examples
7//!
8//! ```rust
9//! use use_quarter::{Quarter, month_in_quarter, quarter_end_date, quarter_for_month};
10//!
11//! assert_eq!(quarter_for_month(5).unwrap(), Quarter::Q2);
12//! assert_eq!(quarter_end_date(2024, Quarter::Q1).day(), 31);
13//! assert!(month_in_quarter(5, Quarter::Q2).unwrap());
14//! ```
15
16use use_date::CalendarDate;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum Quarter {
20    Q1,
21    Q2,
22    Q3,
23    Q4,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum QuarterError {
28    InvalidMonth,
29}
30
31impl Quarter {
32    #[must_use]
33    pub fn number(&self) -> u8 {
34        match self {
35            Self::Q1 => 1,
36            Self::Q2 => 2,
37            Self::Q3 => 3,
38            Self::Q4 => 4,
39        }
40    }
41
42    #[must_use]
43    pub fn start_month(&self) -> u8 {
44        match self {
45            Self::Q1 => 1,
46            Self::Q2 => 4,
47            Self::Q3 => 7,
48            Self::Q4 => 10,
49        }
50    }
51
52    #[must_use]
53    pub fn end_month(&self) -> u8 {
54        match self {
55            Self::Q1 => 3,
56            Self::Q2 => 6,
57            Self::Q3 => 9,
58            Self::Q4 => 12,
59        }
60    }
61}
62
63pub fn quarter_for_month(month: u8) -> Result<Quarter, QuarterError> {
64    Ok(match month {
65        1..=3 => Quarter::Q1,
66        4..=6 => Quarter::Q2,
67        7..=9 => Quarter::Q3,
68        10..=12 => Quarter::Q4,
69        _ => return Err(QuarterError::InvalidMonth),
70    })
71}
72
73#[must_use]
74pub fn quarter_start_date(year: i32, quarter: Quarter) -> CalendarDate {
75    CalendarDate::new(year, quarter.start_month(), 1).unwrap()
76}
77
78#[must_use]
79pub fn quarter_end_date(year: i32, quarter: Quarter) -> CalendarDate {
80    let (month, day) = match quarter {
81        Quarter::Q1 => (3, 31),
82        Quarter::Q2 => (6, 30),
83        Quarter::Q3 => (9, 30),
84        Quarter::Q4 => (12, 31),
85    };
86
87    CalendarDate::new(year, month, day).unwrap()
88}
89
90pub fn month_in_quarter(month: u8, quarter: Quarter) -> Result<bool, QuarterError> {
91    Ok(quarter_for_month(month)? == quarter)
92}
93
94#[cfg(test)]
95mod tests {
96    use super::{
97        month_in_quarter, quarter_end_date, quarter_for_month, quarter_start_date, Quarter,
98        QuarterError,
99    };
100    use use_date::CalendarDate;
101
102    #[test]
103    fn maps_months_to_quarters() {
104        assert_eq!(quarter_for_month(1).unwrap(), Quarter::Q1);
105        assert_eq!(quarter_for_month(5).unwrap(), Quarter::Q2);
106        assert_eq!(quarter_for_month(8).unwrap(), Quarter::Q3);
107        assert_eq!(quarter_for_month(12).unwrap(), Quarter::Q4);
108        assert_eq!(Quarter::Q3.start_month(), 7);
109        assert_eq!(Quarter::Q4.end_month(), 12);
110    }
111
112    #[test]
113    fn builds_quarter_boundaries() {
114        assert_eq!(
115            quarter_start_date(2024, Quarter::Q2),
116            CalendarDate::new(2024, 4, 1).unwrap()
117        );
118        assert_eq!(
119            quarter_end_date(2024, Quarter::Q2),
120            CalendarDate::new(2024, 6, 30).unwrap()
121        );
122        assert!(month_in_quarter(5, Quarter::Q2).unwrap());
123        assert!(!month_in_quarter(7, Quarter::Q2).unwrap());
124    }
125
126    #[test]
127    fn rejects_invalid_quarter_months() {
128        assert_eq!(quarter_for_month(0), Err(QuarterError::InvalidMonth));
129        assert_eq!(
130            month_in_quarter(13, Quarter::Q1),
131            Err(QuarterError::InvalidMonth)
132        );
133    }
134}