1use std::convert::{TryFrom, TryInto};
2use std::fmt::Display;
3use std::ops::{Deref, DerefMut, RangeInclusive};
4
5use chrono::prelude::Datelike;
6use chrono::{Duration, NaiveDate};
7
8pub use chrono::Weekday;
10
11use crate::display::{write_days_offset, write_selector};
12
13fn wday_str(wday: Weekday) -> &'static str {
16 match wday {
17 Weekday::Mon => "Mo",
18 Weekday::Tue => "Tu",
19 Weekday::Wed => "We",
20 Weekday::Thu => "Th",
21 Weekday::Fri => "Fr",
22 Weekday::Sat => "Sa",
23 Weekday::Sun => "Su",
24 }
25}
26
27#[derive(Clone, Debug)]
30pub struct InvalidMonth;
31
32#[derive(Clone, Debug, Default, Hash, PartialEq, Eq)]
35pub struct DaySelector {
36 pub year: Vec<YearRange>,
37 pub monthday: Vec<MonthdayRange>,
38 pub week: Vec<WeekRange>,
39 pub weekday: Vec<WeekDayRange>,
40}
41
42impl DaySelector {
43 pub fn is_empty(&self) -> bool {
45 self.year.is_empty()
46 && self.monthday.is_empty()
47 && self.week.is_empty()
48 && self.weekday.is_empty()
49 }
50
51 pub(crate) fn display(&self, f: &mut std::fmt::Formatter<'_>, force: bool) -> std::fmt::Result {
52 if !(self.year.is_empty() && self.monthday.is_empty() && self.week.is_empty()) {
53 write_selector(f, &self.year)?;
54 write_selector(f, &self.monthday)?;
55
56 if !self.week.is_empty() {
57 if !self.year.is_empty() || !self.monthday.is_empty() {
58 write!(f, " ")?;
59 }
60
61 write!(f, "week")?;
62 write_selector(f, &self.week)?;
63 }
64
65 if !self.weekday.is_empty() {
66 write!(f, " ")?;
67 }
68 }
69
70 if force && self.weekday.is_empty() {
71 write!(f, "Mo-Su ")
72 } else {
73 write_selector(f, &self.weekday)
74 }
75 }
76}
77
78impl Display for DaySelector {
79 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80 self.display(f, false)
81 }
82}
83
84#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
87pub struct Year(pub u16);
88
89impl Deref for Year {
90 type Target = u16;
91
92 fn deref(&self) -> &Self::Target {
93 &self.0
94 }
95}
96
97impl DerefMut for Year {
98 fn deref_mut(&mut self) -> &mut Self::Target {
99 &mut self.0
100 }
101}
102
103#[derive(Clone, Debug, Hash, PartialEq, Eq)]
105pub struct YearRange {
106 pub range: RangeInclusive<Year>,
107 pub step: u16,
108}
109
110impl Display for YearRange {
111 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
112 write!(f, "{}", self.range.start().deref())?;
113
114 if self.range.start() != self.range.end() {
115 write!(f, "-{}", self.range.end().deref())?;
116 }
117
118 if self.step != 1 {
119 write!(f, "/{}", self.step)?;
120 }
121
122 Ok(())
123 }
124}
125
126#[derive(Clone, Debug, Hash, PartialEq, Eq)]
129pub enum MonthdayRange {
130 Month {
131 range: RangeInclusive<Month>,
132 year: Option<u16>,
133 },
134 Date {
135 start: (Date, DateOffset),
136 end: (Date, DateOffset),
137 },
138}
139
140impl Display for MonthdayRange {
141 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
142 match self {
143 Self::Month { range, year } => {
144 if let Some(year) = year {
145 write!(f, "{year}")?;
146 }
147
148 write!(f, "{}", range.start())?;
149
150 if range.start() != range.end() {
151 write!(f, "-{}", range.end())?;
152 }
153 }
154 Self::Date { start, end } => {
155 write!(f, "{}{}", start.0, start.1)?;
156
157 if start != end {
158 write!(f, "-{}{}", end.0, end.1)?;
159 }
160 }
161 }
162
163 Ok(())
164 }
165}
166
167#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
170pub enum Date {
171 Fixed {
172 year: Option<u16>,
173 month: Month,
174 day: u8,
175 },
176 Easter {
177 year: Option<u16>,
178 },
179}
180
181impl Date {
182 #[inline]
183 pub fn ymd(day: u8, month: Month, year: u16) -> Self {
184 Self::Fixed { day, month, year: Some(year) }
185 }
186
187 #[inline]
188 pub fn md(day: u8, month: Month) -> Self {
189 Self::Fixed { day, month, year: None }
190 }
191
192 #[inline]
193 pub fn has_year(&self) -> bool {
194 matches!(
195 self,
196 Self::Fixed { year: Some(_), .. } | Self::Easter { year: Some(_) }
197 )
198 }
199}
200
201impl Display for Date {
202 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
203 match self {
204 Date::Fixed { year, month, day } => {
205 if let Some(year) = year {
206 write!(f, "{year} ")?;
207 }
208
209 write!(f, "{month} {day}")?;
210 }
211 Date::Easter { year } => {
212 if let Some(year) = year {
213 write!(f, "{year} ")?;
214 }
215
216 write!(f, "easter")?;
217 }
218 }
219
220 Ok(())
221 }
222}
223
224#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq)]
227pub struct DateOffset {
228 pub wday_offset: WeekDayOffset,
229 pub day_offset: i64,
230}
231
232impl DateOffset {
233 #[inline]
234 pub fn apply(&self, mut date: NaiveDate) -> NaiveDate {
235 date += Duration::days(self.day_offset);
236
237 match self.wday_offset {
238 WeekDayOffset::None => {}
239 WeekDayOffset::Prev(target) => {
240 let diff = (7 + date.weekday().days_since(Weekday::Mon)
241 - target.days_since(Weekday::Mon))
242 % 7;
243
244 date -= Duration::days(diff.into());
245 debug_assert_eq!(date.weekday(), target);
246 }
247 WeekDayOffset::Next(target) => {
248 let diff = (7 + target.days_since(Weekday::Mon)
249 - date.weekday().days_since(Weekday::Mon))
250 % 7;
251
252 date += Duration::days(diff.into());
253 debug_assert_eq!(date.weekday(), target);
254 }
255 }
256
257 date
258 }
259}
260
261impl Display for DateOffset {
262 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
263 write!(f, "{}", self.wday_offset)?;
264 write_days_offset(f, self.day_offset)?;
265 Ok(())
266 }
267}
268
269#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
272pub enum WeekDayOffset {
273 None,
274 Next(Weekday),
275 Prev(Weekday),
276}
277
278impl Default for WeekDayOffset {
279 #[inline]
280 fn default() -> Self {
281 Self::None
282 }
283}
284
285impl Display for WeekDayOffset {
286 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
287 match self {
288 Self::None => {}
289 Self::Next(wday) => write!(f, "+{}", wday_str(*wday))?,
290 Self::Prev(wday) => write!(f, "-{}", wday_str(*wday))?,
291 }
292
293 Ok(())
294 }
295}
296
297#[derive(Clone, Debug, Hash, PartialEq, Eq)]
300pub enum WeekDayRange {
301 Fixed {
302 range: RangeInclusive<Weekday>,
303 offset: i64,
304 nth_from_start: [bool; 5],
305 nth_from_end: [bool; 5],
306 },
307 Holiday {
308 kind: HolidayKind,
309 offset: i64,
310 },
311}
312
313impl Display for WeekDayRange {
314 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
315 match self {
316 Self::Fixed { range, offset, nth_from_start, nth_from_end } => {
317 write!(f, "{}", wday_str(*range.start()))?;
318
319 if range.start() != range.end() {
320 write!(f, "-{}", wday_str(*range.end()))?;
321 }
322
323 if nth_from_start.contains(&false) || nth_from_end.contains(&false) {
324 let pos_weeknum_iter = nth_from_start
325 .iter()
326 .enumerate()
327 .filter(|(_, x)| **x)
328 .map(|(idx, _)| (idx + 1) as isize);
329
330 let neg_weeknum_iter = nth_from_end
331 .iter()
332 .enumerate()
333 .filter(|(_, x)| **x)
334 .map(|(idx, _)| -(idx as isize) - 1);
335
336 let mut weeknum_iter = pos_weeknum_iter.chain(neg_weeknum_iter);
337 write!(f, "[{}", weeknum_iter.next().unwrap())?;
338
339 for num in weeknum_iter {
340 write!(f, ",{num}")?;
341 }
342
343 write!(f, "]")?;
344 }
345
346 write_days_offset(f, *offset)?;
347 }
348 Self::Holiday { kind, offset } => {
349 write!(f, "{kind}")?;
350
351 if *offset != 0 {
352 write!(f, " {offset}")?;
353 }
354 }
355 }
356
357 Ok(())
358 }
359}
360
361#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
364pub enum HolidayKind {
365 Public,
366 School,
367}
368
369impl Display for HolidayKind {
370 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
371 match self {
372 Self::Public => write!(f, "PH"),
373 Self::School => write!(f, "SH"),
374 }
375 }
376}
377
378#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
381pub struct WeekNum(pub u8);
382
383impl Deref for WeekNum {
384 type Target = u8;
385
386 fn deref(&self) -> &Self::Target {
387 &self.0
388 }
389}
390
391impl DerefMut for WeekNum {
392 fn deref_mut(&mut self) -> &mut Self::Target {
393 &mut self.0
394 }
395}
396
397#[derive(Clone, Debug, Hash, PartialEq, Eq)]
400pub struct WeekRange {
401 pub range: RangeInclusive<WeekNum>,
402 pub step: u8,
403}
404
405impl Display for WeekRange {
406 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
407 if self.range.start() == self.range.end() && self.step == 1 {
408 return write!(f, "{:02}", **self.range.start());
409 }
410
411 write!(f, "{:02}-{:02}", **self.range.start(), **self.range.end())?;
412
413 if self.step != 1 {
414 write!(f, "/{}", self.step)?;
415 }
416
417 Ok(())
418 }
419}
420
421#[derive(Copy, Clone, Debug, Hash, Eq, Ord, PartialEq, PartialOrd)]
424pub enum Month {
425 January = 1,
426 February = 2,
427 March = 3,
428 April = 4,
429 May = 5,
430 June = 6,
431 July = 7,
432 August = 8,
433 September = 9,
434 October = 10,
435 November = 11,
436 December = 12,
437}
438
439impl Month {
440 #[inline]
441 pub fn next(self) -> Self {
442 let num = self as u8;
443 ((num % 12) + 1).try_into().unwrap()
444 }
445
446 #[inline]
447 pub fn prev(self) -> Self {
448 let num = self as u8;
449 (((num + 10) % 12) + 1).try_into().unwrap()
450 }
451
452 #[inline]
454 pub fn from_date(date: impl Datelike) -> Self {
455 match date.month() {
456 1 => Self::January,
457 2 => Self::February,
458 3 => Self::March,
459 4 => Self::April,
460 5 => Self::May,
461 6 => Self::June,
462 7 => Self::July,
463 8 => Self::August,
464 9 => Self::September,
465 10 => Self::October,
466 11 => Self::November,
467 12 => Self::December,
468 other => unreachable!("Unexpected month for date `{other}`"),
469 }
470 }
471
472 #[inline]
474 pub fn as_str(self) -> &'static str {
475 match self {
476 Self::January => "January",
477 Self::February => "February",
478 Self::March => "March",
479 Self::April => "April",
480 Self::May => "May",
481 Self::June => "June",
482 Self::July => "July",
483 Self::August => "August",
484 Self::September => "September",
485 Self::October => "October",
486 Self::November => "November",
487 Self::December => "December",
488 }
489 }
490}
491
492impl Display for Month {
493 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
494 write!(f, "{}", &self.as_str()[..3])
495 }
496}
497
498macro_rules! impl_convert_for_month {
499 ( $from_type: ty ) => {
500 impl TryFrom<$from_type> for Month {
501 type Error = InvalidMonth;
502
503 #[inline]
504 fn try_from(value: $from_type) -> Result<Self, Self::Error> {
505 let value: u8 = value.try_into().map_err(|_| InvalidMonth)?;
506
507 Ok(match value {
508 1 => Self::January,
509 2 => Self::February,
510 3 => Self::March,
511 4 => Self::April,
512 5 => Self::May,
513 6 => Self::June,
514 7 => Self::July,
515 8 => Self::August,
516 9 => Self::September,
517 10 => Self::October,
518 11 => Self::November,
519 12 => Self::December,
520 _ => return Err(InvalidMonth),
521 })
522 }
523 }
524
525 impl From<Month> for $from_type {
526 fn from(val: Month) -> Self {
527 val as _
528 }
529 }
530 };
531 ( $from_type: ty, $( $tail: tt )+ ) => {
532 impl_convert_for_month!($from_type);
533 impl_convert_for_month!($($tail)+);
534 };
535}
536
537impl_convert_for_month!(u8, u16, u32, u64, u128, i8, i16, i32, i64, i128, usize, isize);