use std::fmt;
use crate::{Error, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Grain {
Month,
Quarter,
Semester,
Year,
}
impl Grain {
pub const fn months(self) -> u32 {
match self {
Self::Month => 1,
Self::Quarter => 3,
Self::Semester => 6,
Self::Year => 12,
}
}
pub const fn is_multiple_of(self, finer: Grain) -> bool {
self.months() % finer.months() == 0
}
}
impl fmt::Display for Grain {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::Month => "M",
Self::Quarter => "Q",
Self::Semester => "S",
Self::Year => "Y",
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Month {
year: i32,
month: u8,
}
impl Month {
pub fn new(year: i32, month: u8) -> Result<Self> {
if !(1..=12).contains(&month) {
return Err(Error::InvalidMonth(month));
}
Ok(Self { year, month })
}
pub const fn january(year: i32) -> Self {
Self { year, month: 1 }
}
pub const fn year(self) -> i32 {
self.year
}
pub const fn month(self) -> u8 {
self.month
}
const fn ordinal(self) -> i64 {
self.year as i64 * 12 + (self.month as i64 - 1)
}
fn from_ordinal(ordinal: i64) -> Self {
Self {
year: ordinal.div_euclid(12) as i32,
month: (ordinal.rem_euclid(12) + 1) as u8,
}
}
pub fn add_months(self, n: i64) -> Self {
Self::from_ordinal(self.ordinal() + n)
}
pub const fn months_since(self, earlier: Month) -> i64 {
self.ordinal() - earlier.ordinal()
}
pub fn floor(self, grain: Grain) -> Self {
let size = grain.months() as u8;
Self {
year: self.year,
month: (self.month - 1) / size * size + 1,
}
}
}
impl fmt::Display for Month {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}-{:02}", self.year, self.month)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Period {
start: Month,
grain: Grain,
}
impl Period {
pub fn containing(month: Month, grain: Grain) -> Self {
Self {
start: month.floor(grain),
grain,
}
}
pub const fn year(year: i32) -> Self {
Self {
start: Month::january(year),
grain: Grain::Year,
}
}
pub const fn start(self) -> Month {
self.start
}
pub fn end(self) -> Month {
self.start.add_months(self.grain.months() as i64 - 1)
}
pub const fn grain(self) -> Grain {
self.grain
}
}
impl fmt::Display for Period {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let Month { year, month } = self.start;
match self.grain {
Grain::Year => write!(f, "{year}"),
Grain::Semester => write!(f, "{year}H{}", (month - 1) / 6 + 1),
Grain::Quarter => write!(f, "{year}Q{}", (month - 1) / 3 + 1),
Grain::Month => write!(f, "{}", self.start),
}
}
}
pub type Lag = u32;
#[cfg(test)]
mod tests {
use super::*;
fn m(year: i32, month: u8) -> Month {
Month::new(year, month).unwrap()
}
#[test]
fn month_arithmetic_crosses_years() {
assert_eq!(m(2020, 1).add_months(-1), m(2019, 12));
assert_eq!(m(2020, 12).add_months(13), m(2022, 1));
assert_eq!(m(2022, 1).months_since(m(2020, 12)), 13);
assert_eq!(m(-1, 12).add_months(1), m(0, 1));
}
#[test]
fn rejects_bad_month() {
assert_eq!(Month::new(2020, 0), Err(Error::InvalidMonth(0)));
assert_eq!(Month::new(2020, 13), Err(Error::InvalidMonth(13)));
}
#[test]
fn floor_aligns_to_calendar() {
assert_eq!(m(2021, 12).floor(Grain::Quarter), m(2021, 10));
assert_eq!(m(2021, 6).floor(Grain::Semester), m(2021, 1));
assert_eq!(m(2021, 7).floor(Grain::Semester), m(2021, 7));
assert_eq!(m(2021, 7).floor(Grain::Year), m(2021, 1));
assert_eq!(m(2021, 7).floor(Grain::Month), m(2021, 7));
}
#[test]
fn period_labels() {
assert_eq!(Period::year(1981).to_string(), "1981");
assert_eq!(
Period::containing(m(2021, 7), Grain::Semester).to_string(),
"2021H2"
);
assert_eq!(
Period::containing(m(2021, 3), Grain::Month).to_string(),
"2021-03"
);
assert_eq!(Period::year(1981).end(), m(1981, 12));
}
#[test]
fn grain_multiples() {
assert!(Grain::Year.is_multiple_of(Grain::Quarter));
assert!(Grain::Year.is_multiple_of(Grain::Semester));
assert!(!Grain::Quarter.is_multiple_of(Grain::Semester));
}
}