use crate::calendar::{MAX_SERIAL, MIN_YEAR, MONTHS, month_length};
use crate::{Error, Month, NepaliDate};
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum MonthOverflow {
#[default]
Clamp,
Overflow,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct DateDiff {
pub negative: bool,
pub years: u16,
pub months: u8,
pub days: u8,
pub total_days: i32,
}
impl NepaliDate {
#[must_use = "the result may be out of range"]
pub fn checked_add_days(self, days: i32) -> Result<Self, Error> {
let s = i64::from(self.to_serial()) + i64::from(days);
if s < 0 || s > i64::from(MAX_SERIAL) {
return Err(Error::OutOfRange);
}
let s = u32::try_from(s).map_err(|_| Error::OutOfRange)?;
Self::from_serial(s)
}
#[must_use = "the result may be out of range"]
pub fn checked_add_months(self, months: i32, overflow: MonthOverflow) -> Result<Self, Error> {
self.add_months_i64(i64::from(months), overflow)
}
#[must_use = "the result may be out of range"]
pub fn checked_add_years(self, years: i32, overflow: MonthOverflow) -> Result<Self, Error> {
self.add_months_i64(12 * i64::from(years), overflow)
}
#[allow(clippy::cast_possible_wrap)]
fn add_months_i64(self, months: i64, overflow: MonthOverflow) -> Result<Self, Error> {
let total = self.month_index() as i64 + months;
let Some(i) = usize::try_from(total).ok().filter(|&i| i < MONTHS) else {
return Err(Error::OutOfRange);
};
let target = self.in_month_clamped(i);
if target.day() == self.day() || overflow == MonthOverflow::Clamp {
return Ok(target);
}
let spill = self.day() - target.day();
if i + 1 >= MONTHS {
return Err(Error::OutOfRange);
}
let next = Self::first_of_month_index(i + 1);
Ok(Self::new_unchecked(next.year(), next.month(), spill))
}
#[allow(clippy::indexing_slicing, clippy::cast_possible_truncation)]
fn first_of_month_index(i: usize) -> Self {
Self::new_unchecked(MIN_YEAR + (i / 12) as u16, Month::ALL[i % 12], 1)
}
fn in_month_clamped(self, i: usize) -> Self {
let first = Self::first_of_month_index(i);
let len = month_length(first.year(), first.month_number());
Self::new_unchecked(first.year(), first.month(), self.day().min(len))
}
#[must_use]
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
pub fn diff(self, other: Self) -> DateDiff {
let total_days = self.days_until(other);
let (a, b) = if self > other {
(other, self)
} else {
(self, other)
};
let mut n = b.month_index() - a.month_index();
let mut landed = a.in_month_clamped(b.month_index());
if landed > b {
n -= 1;
landed = a.in_month_clamped(b.month_index() - 1);
}
DateDiff {
negative: self > other,
years: (n / 12) as u16,
months: (n % 12) as u8,
days: landed.days_until(b) as u8,
total_days,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn bs(y: u16, m: u8, d: u8) -> NepaliDate {
NepaliDate::new(y, m, d).unwrap_or(NepaliDate::MIN)
}
fn months(d: NepaliDate, n: i32, o: MonthOverflow) -> Result<NepaliDate, Error> {
d.checked_add_months(n, o)
}
#[test]
fn algorithm_8_examples() {
use MonthOverflow::{Clamp, Overflow};
let d = bs(2081, 4, 32);
assert_eq!(months(d, 2, Clamp), Ok(bs(2081, 6, 30)));
assert_eq!(months(d, 2, Overflow), Ok(bs(2081, 7, 2)));
assert_eq!(months(d, -2, Clamp), Ok(bs(2081, 2, 32)));
assert_eq!(months(d, -2, Overflow), Ok(bs(2081, 2, 32)));
assert_eq!(months(d, 5, Clamp), Ok(bs(2081, 9, 29)));
assert_eq!(months(d, 5, Overflow), Ok(bs(2081, 10, 3)));
assert_eq!(bs(2080, 12, 30).checked_add_days(5), Ok(bs(2081, 1, 5)));
assert_eq!(bs(2081, 1, 5).checked_add_days(-5), Ok(bs(2080, 12, 30)));
}
#[test]
fn months_across_years_and_zero() {
use MonthOverflow::Clamp;
assert_eq!(months(bs(2080, 11, 15), 5, Clamp), Ok(bs(2081, 4, 15)));
assert_eq!(months(bs(2080, 3, 15), -5, Clamp), Ok(bs(2079, 10, 15)));
assert_eq!(months(bs(2081, 4, 32), 0, Clamp), Ok(bs(2081, 4, 32)));
assert_eq!(
bs(2081, 4, 32).checked_add_years(-1, Clamp),
Ok(bs(2080, 4, 32))
);
}
#[test]
fn extreme_inputs_are_out_of_range() {
use MonthOverflow::{Clamp, Overflow};
let max = NepaliDate::MAX;
let min = NepaliDate::MIN;
assert_eq!(max.checked_add_days(1), Err(Error::OutOfRange));
assert_eq!(min.checked_add_days(-1), Err(Error::OutOfRange));
assert_eq!(min.checked_add_days(i32::MAX), Err(Error::OutOfRange));
assert_eq!(max.checked_add_days(i32::MIN), Err(Error::OutOfRange));
assert_eq!(min.checked_add_days(109_211), Ok(max));
assert_eq!(max.checked_add_days(-109_211), Ok(min));
for o in [Clamp, Overflow] {
assert_eq!(min.checked_add_months(i32::MIN, o), Err(Error::OutOfRange));
assert_eq!(max.checked_add_months(i32::MAX, o), Err(Error::OutOfRange));
assert_eq!(min.checked_add_years(i32::MIN, o), Err(Error::OutOfRange));
assert_eq!(max.checked_add_years(i32::MAX, o), Err(Error::OutOfRange));
assert_eq!(min.checked_add_months(-1, o), Err(Error::OutOfRange));
assert_eq!(max.checked_add_months(1, o), Err(Error::OutOfRange));
assert_eq!(
min.checked_add_months(3587, o).map(NepaliDate::year),
Ok(2199)
);
}
}
#[test]
fn overflow_past_the_last_month_is_out_of_range() {
assert_eq!(bs(2199, 12, 1).month_length(), 30);
let d = bs(2199, 5, 31);
assert_eq!(
d.checked_add_months(7, MonthOverflow::Overflow),
Err(Error::OutOfRange)
);
assert_eq!(
d.checked_add_months(7, MonthOverflow::Clamp),
Ok(NepaliDate::MAX)
);
}
#[test]
fn diff_examples_from_algorithm_8a() {
let cases = [
(bs(2080, 1, 1), bs(2081, 4, 15), (false, 1, 3, 14, 473)),
(bs(2081, 4, 15), bs(2080, 1, 1), (true, 1, 3, 14, -473)),
(bs(2081, 4, 32), bs(2081, 5, 31), (false, 0, 1, 0, 31)),
(bs(2081, 4, 32), bs(2081, 6, 1), (false, 0, 1, 1, 32)),
(bs(2081, 1, 31), bs(2081, 2, 30), (false, 0, 0, 30, 30)),
(bs(2081, 3, 15), bs(2081, 3, 15), (false, 0, 0, 0, 0)),
(bs(2080, 1, 1), bs(2081, 1, 1), (false, 1, 0, 0, 365)),
(bs(2081, 4, 32), bs(2081, 5, 1), (false, 0, 0, 1, 1)),
];
for (a, b, (negative, years, months, days, total_days)) in cases {
let expected = DateDiff {
negative,
years,
months,
days,
total_days,
};
assert_eq!(a.diff(b), expected, "{a:?} -> {b:?}");
}
}
#[test]
fn diff_reversed_has_same_magnitudes() {
let (a, b) = (bs(2081, 4, 32), bs(2083, 2, 7));
let (f, r) = (a.diff(b), b.diff(a));
assert!(!f.negative && r.negative);
assert_eq!((f.years, f.months, f.days), (r.years, r.months, r.days));
assert_eq!(f.total_days, -r.total_days);
let whole = NepaliDate::MIN.diff(NepaliDate::MAX);
assert_eq!((whole.years, whole.months), (298, 11));
assert_eq!(whole.total_days, 109_211);
}
}