#![doc = include_str!("../README.md")]
#![no_std]
#![deny(missing_docs)]
#![forbid(unsafe_code)]
use core::iter::FusedIterator;
use nepdate::{Lang, NepaliDate, NepaliDateRange};
mod generated {
pub(crate) mod patro;
}
use crate::generated::patro::{
COVERAGE_FIRST, COVERAGE_LAST, DAY_EVENTS, DAY_TITHI, EVENT_IDS, HOLIDAY_BITS, HOLIDAY_OFFSETS,
NAMES, NO_TITHI, PATRO_DAYS, PATRO_FIRST, TITHI_NAMES,
};
const _: () = assert!(TITHI_NAMES.len() < NO_TITHI as usize);
const _: () = assert!(NAMES.len() <= u16::MAX as usize);
const _: () = assert!(EVENT_IDS.len() <= u16::MAX as usize);
const _: () = assert!(HOLIDAY_BITS.len() * 64 >= PATRO_DAYS);
const _: () = assert!(PATRO_DAYS == 2793);
const _: () = assert!(HOLIDAY_OFFSETS.len() == 294);
const _: () = assert!(EVENT_IDS.len() == 3032);
const _: () = assert!(NAMES.len() == 721);
const _: () = assert!(TITHI_NAMES.len() == 32);
const FIRST: usize = PATRO_FIRST as usize;
#[allow(clippy::panic)]
const COVERAGE: NepaliDateRange = {
let Ok(first) = NepaliDate::new(COVERAGE_FIRST.0, COVERAGE_FIRST.1, COVERAGE_FIRST.2) else {
panic!("COVERAGE_FIRST is not a valid BS date");
};
let Ok(last) = NepaliDate::new(COVERAGE_LAST.0, COVERAGE_LAST.1, COVERAGE_LAST.2) else {
panic!("COVERAGE_LAST is not a valid BS date");
};
match NepaliDateRange::new(first, last) {
Some(range) => range,
None => panic!("COVERAGE_FIRST is after COVERAGE_LAST"),
}
};
const EMPTY: CalendarInfo = CalendarInfo {
tithi: NO_TITHI,
holiday: false,
events: (0, 0),
};
#[must_use]
pub fn info(date: NepaliDate) -> CalendarInfo {
let o = usize::try_from(NepaliDate::MIN.days_until(date))
.unwrap_or(usize::MAX)
.wrapping_sub(FIRST);
let (Some(&tithi), Some(&word), Some(&start), Some(&end)) = (
DAY_TITHI.get(o),
HOLIDAY_BITS.get(o / 64),
DAY_EVENTS.get(o),
DAY_EVENTS.get(o.wrapping_add(1)),
) else {
return EMPTY;
};
CalendarInfo {
tithi,
holiday: (word >> (o % 64)) & 1 == 1,
events: (start, end),
}
}
#[must_use]
pub fn coverage() -> NepaliDateRange {
COVERAGE
}
#[must_use]
pub fn public_holidays() -> impl DoubleEndedIterator<Item = NepaliDate> + ExactSizeIterator {
PublicHolidays {
offsets: HOLIDAY_OFFSETS.iter(),
}
}
#[derive(Clone, Debug)]
struct PublicHolidays {
offsets: core::slice::Iter<'static, u16>,
}
fn holiday_date(o: u16) -> Option<NepaliDate> {
let serial = i32::try_from(FIRST + usize::from(o)).ok()?;
NepaliDate::MIN.checked_add_days(serial).ok()
}
impl Iterator for PublicHolidays {
type Item = NepaliDate;
fn next(&mut self) -> Option<NepaliDate> {
self.offsets.next().copied().and_then(holiday_date)
}
fn size_hint(&self) -> (usize, Option<usize>) {
self.offsets.size_hint()
}
}
impl DoubleEndedIterator for PublicHolidays {
fn next_back(&mut self) -> Option<NepaliDate> {
self.offsets.next_back().copied().and_then(holiday_date)
}
}
impl ExactSizeIterator for PublicHolidays {}
impl FusedIterator for PublicHolidays {}
#[derive(Clone, Copy, Debug)]
pub struct CalendarInfo {
tithi: u8,
holiday: bool,
events: (u16, u16),
}
impl CalendarInfo {
#[must_use]
pub fn tithi(self) -> Option<Tithi> {
if self.tithi == NO_TITHI {
None
} else {
Some(Tithi(self.tithi))
}
}
#[must_use]
pub fn is_public_holiday(self) -> bool {
self.holiday
}
#[must_use]
pub fn events(self) -> Events {
Events {
front: self.events.0,
back: self.events.1,
}
}
#[must_use]
pub fn has_events(self) -> bool {
self.events.0 < self.events.1
}
}
#[derive(Clone, Debug)]
pub struct Events {
front: u16,
back: u16,
}
impl Iterator for Events {
type Item = Event;
fn next(&mut self) -> Option<Event> {
if self.front >= self.back {
return None;
}
let id = *EVENT_IDS.get(usize::from(self.front))?;
self.front += 1;
Some(Event(id))
}
fn size_hint(&self) -> (usize, Option<usize>) {
let n = usize::from(self.back.saturating_sub(self.front));
(n, Some(n))
}
}
impl DoubleEndedIterator for Events {
fn next_back(&mut self) -> Option<Event> {
if self.front >= self.back {
return None;
}
let id = *EVENT_IDS.get(usize::from(self.back - 1))?;
self.back -= 1;
Some(Event(id))
}
}
impl ExactSizeIterator for Events {}
impl FusedIterator for Events {}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Tithi(u8);
impl Tithi {
#[must_use]
pub fn name(self, lang: Lang) -> &'static str {
TITHI_NAMES
.get(usize::from(self.0))
.map_or("", |&(np, en)| pick(np, en, lang))
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Event(u16);
impl Event {
#[must_use]
pub fn name(self, lang: Lang) -> &'static str {
NAMES
.get(usize::from(self.0))
.map_or("", |&(np, en)| pick(np, en, lang))
}
}
fn pick(np: &'static str, en: &'static str, lang: Lang) -> &'static str {
match lang {
Lang::Ne => np,
Lang::En => en,
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::indexing_slicing, clippy::unwrap_used)]
use super::*;
#[test]
fn table_counts() {
assert_eq!(PATRO_DAYS, 2793);
let tithi_days = DAY_TITHI.iter().filter(|&&t| t != NO_TITHI).count();
assert_eq!(tithi_days, 2190);
assert_eq!(HOLIDAY_OFFSETS.len(), 294);
let bits: u32 = HOLIDAY_BITS.iter().map(|w| w.count_ones()).sum();
assert_eq!(bits, 294);
assert_eq!(usize::from(DAY_EVENTS[PATRO_DAYS]), 3032);
assert_eq!(EVENT_IDS.len(), 3032);
let event_days = DAY_EVENTS.windows(2).filter(|w| w[0] < w[1]).count();
assert_eq!(event_days, 1671);
assert_eq!(NAMES.len(), 721);
assert_eq!(TITHI_NAMES.len(), 32);
}
#[test]
fn indices_are_in_range() {
assert!(EVENT_IDS.iter().all(|&id| usize::from(id) < NAMES.len()));
assert!(
DAY_TITHI
.iter()
.all(|&t| usize::from(t) < TITHI_NAMES.len() || t == NO_TITHI)
);
assert!(DAY_EVENTS.windows(2).all(|w| w[0] <= w[1]));
assert_eq!(DAY_EVENTS[0], 0);
}
#[test]
fn holiday_offsets_match_the_bits_and_the_coverage() {
assert!(HOLIDAY_OFFSETS.windows(2).all(|w| w[0] < w[1]));
for &o in &HOLIDAY_OFFSETS {
assert!(holiday_date(o).is_some());
let o = usize::from(o);
assert!(o < PATRO_DAYS);
assert_eq!((HOLIDAY_BITS[o / 64] >> (o % 64)) & 1, 1);
}
let unused_bits = HOLIDAY_BITS.len() * 64 - PATRO_DAYS;
let last_word = HOLIDAY_BITS[HOLIDAY_BITS.len() - 1];
assert!(last_word.leading_zeros() as usize >= unused_bits);
}
#[test]
fn patro_first_is_the_serial_of_the_coverage_start() {
let serial = NepaliDate::MIN.days_until(COVERAGE.start());
assert_eq!(usize::try_from(serial).unwrap(), FIRST);
assert_eq!(usize::try_from(COVERAGE.len()).unwrap(), PATRO_DAYS);
}
#[test]
fn names_are_never_empty() {
assert!(
NAMES
.iter()
.chain(&TITHI_NAMES)
.all(|(np, en)| !np.is_empty() && !en.is_empty())
);
}
#[test]
fn info_is_six_bytes() {
assert_eq!(core::mem::size_of::<CalendarInfo>(), 6);
}
}