use core::fmt;
use crate::calendar::{
AD_MONTH_START, AD_MONTHS, BS_MONTH_AD, MAX_SERIAL, MAX_YEAR, MIN_YEAR, MONTH_INFO,
month_length,
};
use crate::civil::greg_month_length;
use crate::{DateOrder, Error, GregorianDate, Lang, Month, Separator, Weekday};
const EST_MUL: u64 = 137_797;
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct NepaliDate {
year: u16,
month: Month,
day: u8,
}
impl NepaliDate {
pub const MIN: NepaliDate = NepaliDate::new_unchecked(1901, Month::Baishakh, 1);
pub const MAX: NepaliDate = NepaliDate::new_unchecked(2199, Month::Chaitra, 30);
pub const fn new(year: u16, month: u8, day: u8) -> Result<Self, Error> {
if year < MIN_YEAR || year > MAX_YEAR {
return Err(Error::OutOfRange);
}
let Some(month_value) = Month::from_number(month) else {
return Err(Error::InvalidMonth(month));
};
let len = month_length(year, month);
if day < 1 || day > len {
return Err(Error::InvalidDay {
month_length: len,
day,
});
}
Ok(Self {
year,
month: month_value,
day,
})
}
pub(crate) const fn new_unchecked(year: u16, month: Month, day: u8) -> Self {
Self { year, month, day }
}
#[must_use]
pub const fn year(self) -> u16 {
self.year
}
#[must_use]
pub const fn month(self) -> Month {
self.month
}
#[must_use]
pub const fn month_number(self) -> u8 {
self.month.number()
}
#[must_use]
pub const fn day(self) -> u8 {
self.day
}
#[must_use]
pub fn month_length(self) -> u8 {
month_length(self.year, self.month.number())
}
#[must_use]
pub fn first_day_of_month(self) -> Self {
Self::new_unchecked(self.year, self.month, 1)
}
#[must_use]
pub fn last_day_of_month(self) -> Self {
Self::new_unchecked(self.year, self.month, self.month_length())
}
}
impl NepaliDate {
pub(crate) const fn month_index(self) -> usize {
(self.year - MIN_YEAR) as usize * 12 + self.month.number() as usize - 1
}
#[allow(clippy::indexing_slicing)]
pub(crate) fn to_serial(self) -> u32 {
(MONTH_INFO[self.month_index()] >> 2) + u32::from(self.day) - 1
}
#[allow(clippy::indexing_slicing, clippy::cast_possible_truncation)]
pub(crate) fn from_serial(s: u32) -> Result<Self, Error> {
if s > MAX_SERIAL {
return Err(Error::OutOfRange);
}
let mut i = ((u64::from(s) * EST_MUL) >> 22) as usize;
if MONTH_INFO[i] >> 2 > s {
i -= 1;
} else if MONTH_INFO[i + 1] >> 2 <= s {
i += 1;
}
let year = MIN_YEAR + (i / 12) as u16;
let month = Month::ALL[i % 12];
let day = (s - (MONTH_INFO[i] >> 2) + 1) as u8;
Ok(Self::new_unchecked(year, month, day))
}
#[must_use = "the conversion may fail"]
#[allow(
clippy::indexing_slicing,
clippy::cast_possible_truncation,
clippy::cast_sign_loss
)]
pub fn from_gregorian(year: i32, month: u8, day: u8) -> Result<Self, Error> {
let ad = GregorianDate::new(year, month, day)?;
if !(1844..=2143).contains(&ad.year()) {
return Err(Error::OutOfRange);
}
let j = (ad.year() - 1844) * 12 + i32::from(ad.month()) - 4;
if j < 0 || j as usize >= AD_MONTHS {
return Err(Error::OutOfRange);
}
let s = AD_MONTH_START[j as usize] + i32::from(ad.day()) - 1;
if s < 0 {
return Err(Error::OutOfRange);
}
Self::from_serial(s as u32)
}
#[must_use]
#[allow(
clippy::indexing_slicing,
clippy::cast_possible_truncation,
clippy::cast_possible_wrap
)]
pub fn to_gregorian(self) -> GregorianDate {
let v = BS_MONTH_AD[self.month_index()];
let mut year = (v >> 9) as i32;
let mut month = ((v >> 5) & 0xF) as u8;
let mut day = (v & 0x1F) as u8 + self.day - 1;
let len = greg_month_length(i64::from(year), month);
if day > len {
day -= len;
if month == 12 {
month = 1;
year += 1;
} else {
month += 1;
}
}
GregorianDate::new_unchecked(year, month, day)
}
#[must_use]
#[allow(clippy::indexing_slicing)] pub fn weekday(self) -> Weekday {
Weekday::ALL[((self.to_serial() + 4) % 7) as usize]
}
#[must_use]
#[allow(clippy::indexing_slicing, clippy::cast_possible_truncation)]
pub fn day_of_year(self) -> u16 {
let first = MONTH_INFO[(self.year - MIN_YEAR) as usize * 12] >> 2;
(self.to_serial() - first + 1) as u16
}
#[must_use]
pub fn succ(self) -> Option<Self> {
Self::from_serial(self.to_serial() + 1).ok()
}
#[must_use]
pub fn pred(self) -> Option<Self> {
let s = self.to_serial().checked_sub(1)?;
Self::from_serial(s).ok()
}
#[must_use]
#[allow(clippy::cast_possible_wrap)] pub fn days_until(self, other: Self) -> i32 {
other.to_serial() as i32 - self.to_serial() as i32
}
}
impl TryFrom<GregorianDate> for NepaliDate {
type Error = Error;
fn try_from(ad: GregorianDate) -> Result<Self, Error> {
Self::from_gregorian(ad.year(), ad.month(), ad.day())
}
}
impl From<NepaliDate> for GregorianDate {
fn from(date: NepaliDate) -> Self {
date.to_gregorian()
}
}
impl fmt::Display for NepaliDate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let short = self.short(DateOrder::Ymd, Separator::Slash, true, Lang::En);
crate::format::write_once(f, |out| short.write_to(out))
}
}
impl fmt::Debug for NepaliDate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"NepaliDate({:04}-{:02}-{:02})",
self.year,
self.month.number(),
self.day
)
}
}
#[cfg(test)]
mod tests {
extern crate std;
use super::*;
use core::mem::size_of;
use std::format;
#[test]
fn error_precedence() {
assert_eq!(NepaliDate::new(1900, 13, 40), Err(Error::OutOfRange));
assert_eq!(NepaliDate::new(2080, 13, 40), Err(Error::InvalidMonth(13)));
assert_eq!(
NepaliDate::new(2080, 1, 32),
Err(Error::InvalidDay {
month_length: 31,
day: 32
})
);
assert!(NepaliDate::new(2081, 4, 32).is_ok());
assert_eq!(
NepaliDate::new(2081, 4, 33),
Err(Error::InvalidDay {
month_length: 32,
day: 33
})
);
assert_eq!(NepaliDate::new(2081, 0, 1), Err(Error::InvalidMonth(0)));
assert_eq!(NepaliDate::new(2200, 1, 1), Err(Error::OutOfRange));
assert_eq!(
NepaliDate::new(2081, 1, 0),
Err(Error::InvalidDay {
month_length: 31,
day: 0
})
);
}
#[test]
fn sizes() {
assert_eq!(size_of::<NepaliDate>(), 4);
assert_eq!(size_of::<Option<NepaliDate>>(), 4);
assert!(size_of::<Result<NepaliDate, Error>>() <= 4);
}
#[test]
fn ordering() {
let a = NepaliDate::new(2080, 12, 30);
let b = NepaliDate::new(2081, 1, 1);
assert!(matches!((a, b), (Ok(a), Ok(b)) if a < b));
assert!(NepaliDate::MIN < NepaliDate::MAX);
}
#[test]
fn const_construction() {
const D: Result<NepaliDate, Error> = NepaliDate::new(2081, 4, 15);
assert!(matches!(D, Ok(d) if d.day() == 15));
}
#[test]
fn min_max_are_valid() {
assert_eq!(NepaliDate::new(1901, 1, 1), Ok(NepaliDate::MIN));
assert_eq!(NepaliDate::new(2199, 12, 30), Ok(NepaliDate::MAX));
assert_eq!(NepaliDate::MAX.month_length(), 30);
}
#[test]
fn serial_ends() {
assert_eq!(NepaliDate::MIN.to_serial(), 0);
assert_eq!(NepaliDate::MAX.to_serial(), 109_211);
assert_eq!(NepaliDate::from_serial(109_212), Err(Error::OutOfRange));
assert_eq!(NepaliDate::from_serial(0), Ok(NepaliDate::MIN));
}
#[test]
fn month_estimate_is_off_by_at_most_one() {
let starts: std::vec::Vec<u32> = MONTH_INFO.iter().map(|v| v >> 2).collect();
for s in 0..=MAX_SERIAL {
let truth = starts.partition_point(|&start| start <= s) - 1;
#[allow(clippy::cast_possible_truncation)]
let estimate = ((u64::from(s) * EST_MUL) >> 22) as usize;
assert!(
estimate.abs_diff(truth) <= 1,
"serial {s}: {estimate} vs {truth}"
);
assert!(estimate < crate::calendar::MONTHS, "serial {s}");
}
}
#[test]
fn gregorian_edges() {
assert_eq!(NepaliDate::from_gregorian(1844, 4, 11), Ok(NepaliDate::MIN));
assert_eq!(NepaliDate::from_gregorian(2143, 4, 15), Ok(NepaliDate::MAX));
for (y, m, d) in [
(1844, 4, 10),
(1844, 4, 1),
(2143, 4, 16),
(1844, 3, 31),
(2143, 5, 1),
] {
assert_eq!(
NepaliDate::from_gregorian(y, m, d),
Err(Error::OutOfRange),
"{y}-{m}-{d}"
);
}
assert_eq!(
NepaliDate::from_gregorian(2023, 2, 29),
Err(Error::InvalidGregorian)
);
assert_eq!(
NepaliDate::from_gregorian(i32::MAX, 1, 1),
Err(Error::OutOfRange)
);
assert_eq!(
NepaliDate::from_gregorian(i32::MIN, 1, 1),
Err(Error::OutOfRange)
);
assert_eq!(NepaliDate::MAX.succ(), None);
assert_eq!(NepaliDate::MIN.pred(), None);
assert_eq!(NepaliDate::MIN.days_until(NepaliDate::MAX), 109_211);
assert_eq!(NepaliDate::MAX.days_until(NepaliDate::MIN), -109_211);
}
#[test]
fn display_and_debug() {
let d = NepaliDate::new_unchecked(2081, Month::Shrawan, 5);
assert_eq!(format!("{d}"), "2081/04/05");
assert_eq!(format!("{d:?}"), "NepaliDate(2081-04-05)");
}
#[test]
fn every_valid_day_builds_and_every_invalid_day_fails() {
for year in MIN_YEAR..=MAX_YEAR {
for month in 1..=12u8 {
let len = month_length(year, month);
for day in 1..=len {
assert!(NepaliDate::new(year, month, day).is_ok());
}
assert_eq!(
NepaliDate::new(year, month, len + 1),
Err(Error::InvalidDay {
month_length: len,
day: len + 1
})
);
}
}
}
}