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