1#![doc = include_str!("../README.md")]
2#![no_std]
3#![deny(missing_docs)]
4#![forbid(unsafe_code)]
5
6use core::iter::FusedIterator;
7
8use nepdate::{Lang, NepaliDate, NepaliDateRange};
9
10mod generated {
11 pub(crate) mod patro;
12}
13
14use crate::generated::patro::{
15 COVERAGE_FIRST, COVERAGE_LAST, DAY_EVENTS, DAY_TITHI, EVENT_IDS, HOLIDAY_BITS, HOLIDAY_OFFSETS,
16 NAMES, NO_TITHI, PATRO_DAYS, PATRO_FIRST, TITHI_NAMES,
17};
18
19const _: () = assert!(TITHI_NAMES.len() < NO_TITHI as usize);
21const _: () = assert!(NAMES.len() <= u16::MAX as usize);
22const _: () = assert!(EVENT_IDS.len() <= u16::MAX as usize);
23const _: () = assert!(HOLIDAY_BITS.len() * 64 >= PATRO_DAYS);
24const _: () = assert!(PATRO_DAYS == 2793);
26const _: () = assert!(HOLIDAY_OFFSETS.len() == 294);
27const _: () = assert!(EVENT_IDS.len() == 3032);
28const _: () = assert!(NAMES.len() == 721);
29const _: () = assert!(TITHI_NAMES.len() == 32);
30
31const FIRST: usize = PATRO_FIRST as usize;
33
34#[allow(clippy::panic)]
36const COVERAGE: NepaliDateRange = {
37 let Ok(first) = NepaliDate::new(COVERAGE_FIRST.0, COVERAGE_FIRST.1, COVERAGE_FIRST.2) else {
38 panic!("COVERAGE_FIRST is not a valid BS date");
39 };
40 let Ok(last) = NepaliDate::new(COVERAGE_LAST.0, COVERAGE_LAST.1, COVERAGE_LAST.2) else {
41 panic!("COVERAGE_LAST is not a valid BS date");
42 };
43 match NepaliDateRange::new(first, last) {
44 Some(range) => range,
45 None => panic!("COVERAGE_FIRST is after COVERAGE_LAST"),
46 }
47};
48
49const EMPTY: CalendarInfo = CalendarInfo {
51 tithi: NO_TITHI,
52 holiday: false,
53 events: (0, 0),
54};
55
56#[must_use]
75pub fn info(date: NepaliDate) -> CalendarInfo {
76 let o = usize::try_from(NepaliDate::MIN.days_until(date))
79 .unwrap_or(usize::MAX)
80 .wrapping_sub(FIRST);
81 let (Some(&tithi), Some(&word), Some(&start), Some(&end)) = (
82 DAY_TITHI.get(o),
83 HOLIDAY_BITS.get(o / 64),
84 DAY_EVENTS.get(o),
85 DAY_EVENTS.get(o.wrapping_add(1)),
86 ) else {
87 return EMPTY;
88 };
89 CalendarInfo {
90 tithi,
91 holiday: (word >> (o % 64)) & 1 == 1,
92 events: (start, end),
93 }
94}
95
96#[must_use]
105pub fn coverage() -> NepaliDateRange {
106 COVERAGE
107}
108
109#[must_use]
118pub fn public_holidays() -> impl DoubleEndedIterator<Item = NepaliDate> + ExactSizeIterator {
119 PublicHolidays {
120 offsets: HOLIDAY_OFFSETS.iter(),
121 }
122}
123
124#[derive(Clone, Debug)]
129struct PublicHolidays {
130 offsets: core::slice::Iter<'static, u16>,
131}
132
133fn holiday_date(o: u16) -> Option<NepaliDate> {
134 let serial = i32::try_from(FIRST + usize::from(o)).ok()?;
135 NepaliDate::MIN.checked_add_days(serial).ok()
136}
137
138impl Iterator for PublicHolidays {
139 type Item = NepaliDate;
140
141 fn next(&mut self) -> Option<NepaliDate> {
142 self.offsets.next().copied().and_then(holiday_date)
143 }
144
145 fn size_hint(&self) -> (usize, Option<usize>) {
146 self.offsets.size_hint()
147 }
148}
149
150impl DoubleEndedIterator for PublicHolidays {
151 fn next_back(&mut self) -> Option<NepaliDate> {
152 self.offsets.next_back().copied().and_then(holiday_date)
153 }
154}
155
156impl ExactSizeIterator for PublicHolidays {}
157
158impl FusedIterator for PublicHolidays {}
159
160#[derive(Clone, Copy, Debug)]
170pub struct CalendarInfo {
171 tithi: u8,
172 holiday: bool,
173 events: (u16, u16),
174}
175
176impl CalendarInfo {
177 #[must_use]
188 pub fn tithi(self) -> Option<Tithi> {
189 if self.tithi == NO_TITHI {
190 None
191 } else {
192 Some(Tithi(self.tithi))
193 }
194 }
195
196 #[must_use]
206 pub fn is_public_holiday(self) -> bool {
207 self.holiday
208 }
209
210 #[must_use]
221 pub fn events(self) -> Events {
222 Events {
223 front: self.events.0,
224 back: self.events.1,
225 }
226 }
227
228 #[must_use]
238 pub fn has_events(self) -> bool {
239 self.events.0 < self.events.1
240 }
241}
242
243#[derive(Clone, Debug)]
253pub struct Events {
254 front: u16,
255 back: u16,
256}
257
258impl Iterator for Events {
259 type Item = Event;
260
261 fn next(&mut self) -> Option<Event> {
262 if self.front >= self.back {
263 return None;
264 }
265 let id = *EVENT_IDS.get(usize::from(self.front))?;
266 self.front += 1;
267 Some(Event(id))
268 }
269
270 fn size_hint(&self) -> (usize, Option<usize>) {
271 let n = usize::from(self.back.saturating_sub(self.front));
272 (n, Some(n))
273 }
274}
275
276impl DoubleEndedIterator for Events {
277 fn next_back(&mut self) -> Option<Event> {
278 if self.front >= self.back {
279 return None;
280 }
281 let id = *EVENT_IDS.get(usize::from(self.back - 1))?;
282 self.back -= 1;
283 Some(Event(id))
284 }
285}
286
287impl ExactSizeIterator for Events {}
288
289impl FusedIterator for Events {}
290
291#[derive(Clone, Copy, Debug, PartialEq, Eq)]
301pub struct Tithi(u8);
302
303impl Tithi {
304 #[must_use]
315 pub fn name(self, lang: Lang) -> &'static str {
316 TITHI_NAMES
319 .get(usize::from(self.0))
320 .map_or("", |&(np, en)| pick(np, en, lang))
321 }
322}
323
324#[derive(Clone, Copy, Debug, PartialEq, Eq)]
335pub struct Event(u16);
336
337impl Event {
338 #[must_use]
348 pub fn name(self, lang: Lang) -> &'static str {
349 NAMES
351 .get(usize::from(self.0))
352 .map_or("", |&(np, en)| pick(np, en, lang))
353 }
354}
355
356fn pick(np: &'static str, en: &'static str, lang: Lang) -> &'static str {
357 match lang {
358 Lang::Ne => np,
359 Lang::En => en,
360 }
361}
362
363#[cfg(test)]
364mod tests {
365 #![allow(clippy::indexing_slicing, clippy::unwrap_used)]
366
367 use super::*;
368
369 #[test]
370 fn table_counts() {
371 assert_eq!(PATRO_DAYS, 2793);
372 let tithi_days = DAY_TITHI.iter().filter(|&&t| t != NO_TITHI).count();
373 assert_eq!(tithi_days, 2190);
374 assert_eq!(HOLIDAY_OFFSETS.len(), 294);
375 let bits: u32 = HOLIDAY_BITS.iter().map(|w| w.count_ones()).sum();
376 assert_eq!(bits, 294);
377 assert_eq!(usize::from(DAY_EVENTS[PATRO_DAYS]), 3032);
378 assert_eq!(EVENT_IDS.len(), 3032);
379 let event_days = DAY_EVENTS.windows(2).filter(|w| w[0] < w[1]).count();
380 assert_eq!(event_days, 1671);
381 assert_eq!(NAMES.len(), 721);
382 assert_eq!(TITHI_NAMES.len(), 32);
383 }
384
385 #[test]
386 fn indices_are_in_range() {
387 assert!(EVENT_IDS.iter().all(|&id| usize::from(id) < NAMES.len()));
388 assert!(
389 DAY_TITHI
390 .iter()
391 .all(|&t| usize::from(t) < TITHI_NAMES.len() || t == NO_TITHI)
392 );
393 assert!(DAY_EVENTS.windows(2).all(|w| w[0] <= w[1]));
394 assert_eq!(DAY_EVENTS[0], 0);
395 }
396
397 #[test]
398 fn holiday_offsets_match_the_bits_and_the_coverage() {
399 assert!(HOLIDAY_OFFSETS.windows(2).all(|w| w[0] < w[1]));
400 for &o in &HOLIDAY_OFFSETS {
401 assert!(holiday_date(o).is_some());
402 let o = usize::from(o);
403 assert!(o < PATRO_DAYS);
404 assert_eq!((HOLIDAY_BITS[o / 64] >> (o % 64)) & 1, 1);
405 }
406 let unused_bits = HOLIDAY_BITS.len() * 64 - PATRO_DAYS;
407 let last_word = HOLIDAY_BITS[HOLIDAY_BITS.len() - 1];
408 assert!(last_word.leading_zeros() as usize >= unused_bits);
409 }
410
411 #[test]
412 fn patro_first_is_the_serial_of_the_coverage_start() {
413 let serial = NepaliDate::MIN.days_until(COVERAGE.start());
414 assert_eq!(usize::try_from(serial).unwrap(), FIRST);
415 assert_eq!(usize::try_from(COVERAGE.len()).unwrap(), PATRO_DAYS);
416 }
417
418 #[test]
419 fn names_are_never_empty() {
420 assert!(
421 NAMES
422 .iter()
423 .chain(&TITHI_NAMES)
424 .all(|(np, en)| !np.is_empty() && !en.is_empty())
425 );
426 }
427
428 #[test]
429 fn info_is_six_bytes() {
430 assert_eq!(core::mem::size_of::<CalendarInfo>(), 6);
431 }
432}