use core::fmt;
use crate::range::NepaliDateRange;
use crate::{Error, Lang, Month, NepaliDate};
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub struct FiscalYear(u16);
impl FiscalYear {
#[must_use]
pub const fn new(start_year: u16) -> Self {
Self(start_year)
}
#[must_use]
pub const fn start_year(self) -> u16 {
self.0
}
#[must_use]
pub fn containing(date: NepaliDate) -> Self {
if date.month() >= Month::Shrawan {
Self(date.year())
} else {
Self(date.year() - 1)
}
}
#[must_use = "the date may be out of range"]
pub fn start(self) -> Result<NepaliDate, Error> {
NepaliDate::new(self.0, 4, 1)
}
#[must_use = "the date may be out of range"]
pub fn end(self) -> Result<NepaliDate, Error> {
let year = self.0.checked_add(1).ok_or(Error::OutOfRange)?;
Ok(NepaliDate::new(year, 3, 1)?.last_day_of_month())
}
#[must_use = "the range may be out of range"]
pub fn range(self) -> Result<NepaliDateRange, Error> {
Ok(NepaliDateRange::new_unchecked(self.start()?, self.end()?))
}
#[must_use = "the range may be out of range"]
pub fn quarter_range(self, q: Quarter) -> Result<NepaliDateRange, Error> {
let [first, _, last] = q.months();
let year = match q {
Quarter::Q1 | Quarter::Q2 | Quarter::Q3 => Some(self.0),
Quarter::Q4 => self.0.checked_add(1),
};
let year = year.ok_or(Error::OutOfRange)?;
let start = NepaliDate::new(year, first.number(), 1)?;
let end = NepaliDate::new(year, last.number(), 1)?.last_day_of_month();
Ok(NepaliDateRange::new_unchecked(start, end))
}
#[must_use]
pub const fn label(self, lang: Lang) -> FiscalLabel {
FiscalLabel { year: self, lang }
}
}
#[derive(Clone, Copy, Debug)]
pub struct FiscalLabel {
year: FiscalYear,
lang: Lang,
}
impl FiscalLabel {
pub fn write_to<W: fmt::Write + ?Sized>(&self, out: &mut W) -> fmt::Result {
let start = u32::from(self.year.start_year());
self.lang.digits(start, 1).write_to(out)?;
out.write_str("/")?;
self.lang.digits((start + 1) % 100, 2).write_to(out)
}
}
impl fmt::Display for FiscalLabel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.write_to(f)
}
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub enum Quarter {
Q1,
Q2,
Q3,
Q4,
}
impl Quarter {
#[must_use]
pub const fn months(self) -> [Month; 3] {
match self {
Quarter::Q1 => [Month::Shrawan, Month::Bhadra, Month::Ashoj],
Quarter::Q2 => [Month::Kartik, Month::Mangsir, Month::Poush],
Quarter::Q3 => [Month::Magh, Month::Falgun, Month::Chaitra],
Quarter::Q4 => [Month::Baishakh, Month::Jestha, Month::Ashad],
}
}
}
impl NepaliDate {
#[must_use]
pub fn fiscal_year(self) -> FiscalYear {
FiscalYear::containing(self)
}
#[must_use]
pub fn quarter(self) -> Quarter {
match self.month() {
Month::Shrawan | Month::Bhadra | Month::Ashoj => Quarter::Q1,
Month::Kartik | Month::Mangsir | Month::Poush => Quarter::Q2,
Month::Magh | Month::Falgun | Month::Chaitra => Quarter::Q3,
Month::Baishakh | Month::Jestha | Month::Ashad => Quarter::Q4,
}
}
}
#[cfg(test)]
mod tests {
extern crate std;
use super::*;
use std::string::ToString;
fn bs(y: u16, m: u8, d: u8) -> NepaliDate {
NepaliDate::new(y, m, d).unwrap_or(NepaliDate::MIN)
}
#[test]
fn labels() {
assert_eq!(FiscalYear::new(2082).label(Lang::Ne).to_string(), "२०८२/८३");
assert_eq!(FiscalYear::new(2082).label(Lang::En).to_string(), "2082/83");
assert_eq!(FiscalYear::new(2099).label(Lang::En).to_string(), "2099/00");
assert_eq!(FiscalYear::new(2100).label(Lang::En).to_string(), "2100/01");
assert_eq!(FiscalYear::new(1900).label(Lang::Ne).to_string(), "१९००/०१");
assert_eq!(
FiscalYear::new(u16::MAX).label(Lang::En).to_string(),
"65535/36"
);
}
#[test]
fn containing_and_quarter() {
assert_eq!(
FiscalYear::containing(bs(2081, 3, 31)),
FiscalYear::new(2080)
);
assert_eq!(
FiscalYear::containing(bs(2081, 4, 1)),
FiscalYear::new(2081)
);
for m in 1..=12u8 {
let d = bs(2081, m, 1);
let q = d.quarter();
assert!(q.months().contains(&d.month()), "{d:?}");
let fy = d.fiscal_year().quarter_range(q);
assert!(fy.is_ok_and(|r| r.contains(d)), "{d:?}");
}
}
#[test]
fn range_ends() {
assert_eq!(FiscalYear::new(1900).start(), Err(Error::OutOfRange));
assert_eq!(FiscalYear::new(1900).end(), Ok(bs(1901, 3, 32)));
assert_eq!(FiscalYear::new(1900).range(), Err(Error::OutOfRange));
assert_eq!(FiscalYear::new(2199).end(), Err(Error::OutOfRange));
assert_eq!(FiscalYear::new(2199).start(), Ok(bs(2199, 4, 1)));
assert_eq!(FiscalYear::new(u16::MAX).end(), Err(Error::OutOfRange));
assert_eq!(
FiscalYear::new(u16::MAX).quarter_range(Quarter::Q4),
Err(Error::OutOfRange)
);
assert_eq!(
FiscalYear::new(1900)
.quarter_range(Quarter::Q4)
.map(NepaliDateRange::start),
Ok(NepaliDate::MIN)
);
assert_eq!(
FiscalYear::new(2199)
.quarter_range(Quarter::Q3)
.map(NepaliDateRange::end),
Ok(NepaliDate::MAX)
);
}
#[test]
fn quarters_split_the_year() {
let fy = FiscalYear::new(2080);
let total: u32 = [Quarter::Q1, Quarter::Q2, Quarter::Q3, Quarter::Q4]
.into_iter()
.map(|q| fy.quarter_range(q).map_or(0, NepaliDateRange::len))
.sum();
assert_eq!(fy.range().map(NepaliDateRange::len), Ok(total));
}
}