1use std::fmt::{self, Display};
8
9use chumsky::extra::ParserExtra;
10use chumsky::input::Input;
11use chumsky::label::LabelError;
12use chumsky::prelude::*;
13use chumsky::span::SimpleSpan;
14
15use crate::keyword::{
16 KW_DAY_FR, KW_DAY_MO, KW_DAY_SA, KW_DAY_SU, KW_DAY_TH, KW_DAY_TU, KW_DAY_WE, KW_RRULE_BYDAY,
17 KW_RRULE_BYHOUR, KW_RRULE_BYMINUTE, KW_RRULE_BYMONTH, KW_RRULE_BYMONTHDAY, KW_RRULE_BYSECOND,
18 KW_RRULE_BYSETPOS, KW_RRULE_BYWEEKNO, KW_RRULE_BYYEARDAY, KW_RRULE_COUNT, KW_RRULE_FREQ,
19 KW_RRULE_FREQ_DAILY, KW_RRULE_FREQ_HOURLY, KW_RRULE_FREQ_MINUTELY, KW_RRULE_FREQ_MONTHLY,
20 KW_RRULE_FREQ_SECONDLY, KW_RRULE_FREQ_WEEKLY, KW_RRULE_FREQ_YEARLY, KW_RRULE_INTERVAL,
21 KW_RRULE_UNTIL, KW_RRULE_WKST,
22};
23use crate::value::datetime::{ValueDateTime, ValueTime, value_date, value_date_time};
24use crate::value::miscellaneous::{
25 ValueExpected, i8_0_1, i8_0_3, i8_0_9, i8_1_2, i8_1_4, i8_1_9, i16_0_5, i16_0_6, i16_0_9,
26 i16_1_2, i16_1_9, u8_0_1, u8_0_3, u8_0_5, u8_0_9, u8_1_9,
27};
28
29#[derive(Debug, Clone)]
31pub struct ValueRecurrenceRule {
32 pub freq: RecurrenceFrequency,
34 pub until: Option<ValueDateTime>,
36 pub count: Option<u32>,
38 pub interval: Option<u32>,
40 pub by_second: Vec<u8>,
42 pub by_minute: Vec<u8>,
44 pub by_hour: Vec<u8>,
46 pub by_month_day: Vec<i8>,
48 pub by_year_day: Vec<i16>,
50 pub by_week_no: Vec<i8>,
52 pub by_month: Vec<u8>,
54 pub by_day: Vec<WeekDayNum>,
56 pub by_set_pos: Vec<i16>,
58 pub wkst: Option<WeekDay>,
60}
61
62#[derive(Debug, Clone, Copy, PartialEq)]
64#[expect(missing_docs)]
65pub enum RecurrenceFrequency {
66 Secondly,
67 Minutely,
68 Hourly,
69 Daily,
70 Weekly,
71 Monthly,
72 Yearly,
73}
74
75impl Display for RecurrenceFrequency {
76 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
77 match self {
78 RecurrenceFrequency::Secondly => write!(f, "{KW_RRULE_FREQ_SECONDLY}"),
79 RecurrenceFrequency::Minutely => write!(f, "{KW_RRULE_FREQ_MINUTELY}"),
80 RecurrenceFrequency::Hourly => write!(f, "{KW_RRULE_FREQ_HOURLY}"),
81 RecurrenceFrequency::Daily => write!(f, "{KW_RRULE_FREQ_DAILY}"),
82 RecurrenceFrequency::Weekly => write!(f, "{KW_RRULE_FREQ_WEEKLY}"),
83 RecurrenceFrequency::Monthly => write!(f, "{KW_RRULE_FREQ_MONTHLY}"),
84 RecurrenceFrequency::Yearly => write!(f, "{KW_RRULE_FREQ_YEARLY}"),
85 }
86 }
87}
88
89#[derive(Debug, Clone, Copy)]
91pub struct WeekDayNum {
92 pub day: WeekDay,
94 pub occurrence: Option<i8>,
96}
97
98#[derive(Debug, Clone, Copy, PartialEq)]
100#[expect(missing_docs)]
101pub enum WeekDay {
102 Sunday,
103 Monday,
104 Tuesday,
105 Wednesday,
106 Thursday,
107 Friday,
108 Saturday,
109}
110
111impl Display for WeekDay {
112 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113 match self {
114 WeekDay::Sunday => write!(f, "{KW_DAY_SU}"),
115 WeekDay::Monday => write!(f, "{KW_DAY_MO}"),
116 WeekDay::Tuesday => write!(f, "{KW_DAY_TU}"),
117 WeekDay::Wednesday => write!(f, "{KW_DAY_WE}"),
118 WeekDay::Thursday => write!(f, "{KW_DAY_TH}"),
119 WeekDay::Friday => write!(f, "{KW_DAY_FR}"),
120 WeekDay::Saturday => write!(f, "{KW_DAY_SA}"),
121 }
122 }
123}
124
125pub fn value_rrule<'src, I, E>() -> impl Parser<'src, I, ValueRecurrenceRule, E>
143where
144 I: Input<'src, Token = char, Span = SimpleSpan>,
145 E: ParserExtra<'src, I>,
146 E::Error: LabelError<'src, I, ValueExpected>,
147{
148 recur_rrule_part()
149 .separated_by(just(';'))
150 .at_least(1)
151 .collect()
152 .try_map(build_from_parts::<I, E::Error>)
153}
154
155#[expect(clippy::too_many_lines)]
156fn build_from_parts<'src, I, Err>(
157 parts: Vec<Part>,
158 span: I::Span,
159) -> Result<ValueRecurrenceRule, Err>
160where
161 I: Input<'src, Token = char, Span = SimpleSpan>,
162 Err: LabelError<'src, I, ValueExpected>,
163{
164 let mut freq = None;
165 let mut until = None;
166 let mut count = None;
167 let mut interval = None;
168 let mut by_second = Vec::new();
169 let mut by_minute = Vec::new();
170 let mut by_hour = Vec::new();
171 let mut by_month_day = Vec::new();
172 let mut by_year_day = Vec::new();
173 let mut by_week_no = Vec::new();
174 let mut by_month = Vec::new();
175 let mut by_day = Vec::new();
176 let mut by_set_pos = Vec::new();
177 let mut wkst = None;
178
179 for part in parts {
180 match part {
181 Part::Freq(f) => match freq {
182 Some(_) => {
183 return Err(Err::expected_found(
184 [ValueExpected::RRuleDuplicatePart],
185 None,
186 span,
187 ));
188 }
189 None => freq = Some(f),
190 },
191 Part::Until(u) => {
192 if until.is_some() {
193 return Err(Err::expected_found(
194 [ValueExpected::RRuleDuplicatePart],
195 None,
196 span,
197 ));
198 }
199 until = Some(u);
200 }
201 Part::Count(c) => {
202 if count.is_some() {
203 return Err(Err::expected_found(
204 [ValueExpected::RRuleDuplicatePart],
205 None,
206 span,
207 ));
208 }
209 count = Some(c);
210 }
211 Part::Interval(i) => {
212 if interval.is_some() {
213 return Err(Err::expected_found(
214 [ValueExpected::RRuleDuplicatePart],
215 None,
216 span,
217 ));
218 }
219 interval = Some(i);
220 }
221 Part::BySecond(v) => {
222 if !by_second.is_empty() {
223 return Err(Err::expected_found(
224 [ValueExpected::RRuleDuplicatePart],
225 None,
226 span,
227 ));
228 }
229 by_second = v;
230 }
231 Part::ByMinute(v) => {
232 if !by_minute.is_empty() {
233 return Err(Err::expected_found(
234 [ValueExpected::RRuleDuplicatePart],
235 None,
236 span,
237 ));
238 }
239 by_minute = v;
240 }
241 Part::ByHour(v) => {
242 if !by_hour.is_empty() {
243 return Err(Err::expected_found(
244 [ValueExpected::RRuleDuplicatePart],
245 None,
246 span,
247 ));
248 }
249 by_hour = v;
250 }
251 Part::ByMonthDay(v) => {
252 if !by_month_day.is_empty() {
253 return Err(Err::expected_found(
254 [ValueExpected::RRuleDuplicatePart],
255 None,
256 span,
257 ));
258 }
259 by_month_day = v;
260 }
261 Part::ByYearDay(v) => {
262 if !by_year_day.is_empty() {
263 return Err(Err::expected_found(
264 [ValueExpected::RRuleDuplicatePart],
265 None,
266 span,
267 ));
268 }
269 by_year_day = v;
270 }
271 Part::ByWeekNo(v) => {
272 if !by_week_no.is_empty() {
273 return Err(Err::expected_found(
274 [ValueExpected::RRuleDuplicatePart],
275 None,
276 span,
277 ));
278 }
279 by_week_no = v;
280 }
281 Part::ByMonth(v) => {
282 if !by_month.is_empty() {
283 return Err(Err::expected_found(
284 [ValueExpected::RRuleDuplicatePart],
285 None,
286 span,
287 ));
288 }
289 by_month = v;
290 }
291 Part::ByDay(v) => {
292 if !by_day.is_empty() {
293 return Err(Err::expected_found(
294 [ValueExpected::RRuleDuplicatePart],
295 None,
296 span,
297 ));
298 }
299 by_day = v;
300 }
301 Part::BySetPos(v) => {
302 if !by_set_pos.is_empty() {
303 return Err(Err::expected_found(
304 [ValueExpected::RRuleDuplicatePart],
305 None,
306 span,
307 ));
308 }
309 by_set_pos = v;
310 }
311 Part::Wkst(w) => {
312 if wkst.is_some() {
313 return Err(Err::expected_found(
314 [ValueExpected::RRuleDuplicatePart],
315 None,
316 span,
317 ));
318 }
319 wkst = Some(w);
320 }
321 }
322 }
323
324 let freq =
326 freq.ok_or_else(|| Err::expected_found([ValueExpected::RRuleRequiredFreq], None, span))?;
327
328 if until.is_some() && count.is_some() {
330 return Err(Err::expected_found(
331 [ValueExpected::RRuleCountUntilExclusion],
332 None,
333 span,
334 ));
335 }
336
337 Ok(ValueRecurrenceRule {
338 freq,
339 until,
340 count,
341 interval,
342 by_second,
343 by_minute,
344 by_hour,
345 by_month_day,
346 by_year_day,
347 by_week_no,
348 by_month,
349 by_day,
350 by_set_pos,
351 wkst,
352 })
353}
354
355#[derive(Debug, Clone)]
356enum Part {
357 Freq(RecurrenceFrequency),
358 Until(ValueDateTime),
359 Count(u32),
360 Interval(u32),
361 BySecond(Vec<u8>),
362 ByMinute(Vec<u8>),
363 ByHour(Vec<u8>),
364 ByMonthDay(Vec<i8>),
365 ByYearDay(Vec<i16>),
366 ByWeekNo(Vec<i8>),
367 ByMonth(Vec<u8>),
368 ByDay(Vec<WeekDayNum>),
369 BySetPos(Vec<i16>),
370 Wkst(WeekDay),
371}
372
373fn recur_rrule_part<'src, I, E>() -> impl Parser<'src, I, Part, E>
390where
391 I: Input<'src, Token = char, Span = SimpleSpan>,
392 E: ParserExtra<'src, I>,
393 E::Error: LabelError<'src, I, ValueExpected>,
394{
395 let kw = |kw| just(kw).ignore_then(just('='));
396
397 let freq = kw(KW_RRULE_FREQ).ignore_then(freq()).map(Part::Freq);
399
400 let until = kw(KW_RRULE_UNTIL).ignore_then(enddate()).map(Part::Until);
402
403 let count = kw(KW_RRULE_COUNT)
405 .ignore_then(u32_non_zero())
406 .map(Part::Count);
407
408 let interval = kw(KW_RRULE_INTERVAL)
410 .ignore_then(u32_non_zero())
411 .map(Part::Interval);
412
413 let by_second = kw(KW_RRULE_BYSECOND)
415 .ignore_then(byseclist())
416 .map(Part::BySecond);
417
418 let by_minute = kw(KW_RRULE_BYMINUTE)
420 .ignore_then(byminlist())
421 .map(Part::ByMinute);
422
423 let by_hour = kw(KW_RRULE_BYHOUR)
425 .ignore_then(byhrlist())
426 .map(Part::ByHour);
427
428 let by_day = kw(KW_RRULE_BYDAY)
430 .ignore_then(bywdaylist())
431 .map(Part::ByDay);
432
433 let by_month_day = kw(KW_RRULE_BYMONTHDAY)
435 .ignore_then(bymodaylist())
436 .map(Part::ByMonthDay);
437
438 let by_year_day = kw(KW_RRULE_BYYEARDAY)
440 .ignore_then(byyrdaylist())
441 .map(Part::ByYearDay);
442
443 let by_week_no = kw(KW_RRULE_BYWEEKNO)
445 .ignore_then(bywknolist())
446 .map(Part::ByWeekNo);
447
448 let by_month = kw(KW_RRULE_BYMONTH)
450 .ignore_then(bymolist())
451 .map(Part::ByMonth);
452
453 let by_set_pos = kw(KW_RRULE_BYSETPOS)
455 .ignore_then(bysplist())
456 .map(Part::BySetPos);
457
458 let wkst = kw(KW_RRULE_WKST).ignore_then(weekday()).map(Part::Wkst);
460
461 choice((
462 freq,
463 until,
464 count,
465 interval,
466 by_second,
467 by_minute,
468 by_hour,
469 by_day,
470 by_month_day,
471 by_year_day,
472 by_week_no,
473 by_month,
474 by_set_pos,
475 wkst,
476 ))
477}
478
479fn freq<'src, I, E>() -> impl Parser<'src, I, RecurrenceFrequency, E>
484where
485 I: Input<'src, Token = char, Span = SimpleSpan>,
486 E: ParserExtra<'src, I>,
487{
488 choice((
489 just(KW_RRULE_FREQ_SECONDLY).to(RecurrenceFrequency::Secondly),
490 just(KW_RRULE_FREQ_MINUTELY).to(RecurrenceFrequency::Minutely),
491 just(KW_RRULE_FREQ_HOURLY).to(RecurrenceFrequency::Hourly),
492 just(KW_RRULE_FREQ_DAILY).to(RecurrenceFrequency::Daily),
493 just(KW_RRULE_FREQ_WEEKLY).to(RecurrenceFrequency::Weekly),
494 just(KW_RRULE_FREQ_MONTHLY).to(RecurrenceFrequency::Monthly),
495 just(KW_RRULE_FREQ_YEARLY).to(RecurrenceFrequency::Yearly),
496 ))
497}
498
499fn enddate<'src, I, E>() -> impl Parser<'src, I, ValueDateTime, E>
505where
506 I: Input<'src, Token = char, Span = SimpleSpan>,
507 E: ParserExtra<'src, I>,
508 E::Error: LabelError<'src, I, ValueExpected>,
509{
510 choice((
513 value_date_time(),
514 value_date().map(|date| ValueDateTime::new(date, ValueTime::new(0, 0, 0, false))),
515 ))
516}
517
518fn byseclist<'src, I, E>() -> impl Parser<'src, I, Vec<u8>, E>
522where
523 I: Input<'src, Token = char, Span = SimpleSpan>,
524 E: ParserExtra<'src, I>,
525{
526 seconds().separated_by(just(',')).collect()
527}
528
529fn seconds<'src, I, E>() -> impl Parser<'src, I, u8, E>
533where
534 I: Input<'src, Token = char, Span = SimpleSpan>,
535 E: ParserExtra<'src, I>,
536{
537 choice((
538 u8_0_5().then(u8_0_9()).map(|(a, b)| a * 10 + b), just("60").to(60), u8_0_9(), ))
542}
543
544fn byminlist<'src, I, E>() -> impl Parser<'src, I, Vec<u8>, E>
548where
549 I: Input<'src, Token = char, Span = SimpleSpan>,
550 E: ParserExtra<'src, I>,
551{
552 minutes().separated_by(just(',')).collect()
553}
554
555fn minutes<'src, I, E>() -> impl Parser<'src, I, u8, E>
559where
560 I: Input<'src, Token = char, Span = SimpleSpan>,
561 E: ParserExtra<'src, I>,
562{
563 choice((
564 u8_0_5().then(u8_0_9()).map(|(a, b)| a * 10 + b), u8_0_9(), ))
567}
568
569fn byhrlist<'src, I, E>() -> impl Parser<'src, I, Vec<u8>, E>
573where
574 I: Input<'src, Token = char, Span = SimpleSpan>,
575 E: ParserExtra<'src, I>,
576{
577 hour().separated_by(just(',')).collect()
578}
579
580fn hour<'src, I, E>() -> impl Parser<'src, I, u8, E>
584where
585 I: Input<'src, Token = char, Span = SimpleSpan>,
586 E: ParserExtra<'src, I>,
587{
588 choice((
589 u8_0_1().then(u8_0_9()).map(|(a, b)| a * 10 + b), just('2').ignore_then(u8_0_3()).map(|b| 20 + b), u8_0_9(), ))
593}
594
595fn bywdaylist<'src, I, E>() -> impl Parser<'src, I, Vec<WeekDayNum>, E>
599where
600 I: Input<'src, Token = char, Span = SimpleSpan>,
601 E: ParserExtra<'src, I>,
602{
603 weekdaynum().separated_by(just(',')).collect()
604}
605
606fn weekdaynum<'src, I, E>() -> impl Parser<'src, I, WeekDayNum, E>
612where
613 I: Input<'src, Token = char, Span = SimpleSpan>,
614 E: ParserExtra<'src, I>,
615{
616 is_positive()
617 .then(ordwk())
618 .map(|(positive, n)| if positive { n } else { -n })
619 .or_not()
620 .then(weekday())
621 .map(|(occurrence, day)| WeekDayNum { day, occurrence })
622}
623
624fn ordwk<'src, I, E>() -> impl Parser<'src, I, i8, E>
628where
629 I: Input<'src, Token = char, Span = SimpleSpan>,
630 E: ParserExtra<'src, I>,
631{
632 choice((
633 i8_1_4().then(i8_0_9()).map(|(a, b)| a * 10 + b), just('5').ignore_then(i8_0_3()).map(|a| 50 + a), just('0').ignore_then(i8_1_9()), i8_1_9(), ))
638}
639
640fn weekday<'src, I, E>() -> impl Parser<'src, I, WeekDay, E>
644where
645 I: Input<'src, Token = char, Span = SimpleSpan>,
646 E: ParserExtra<'src, I>,
647{
648 choice((
649 just(KW_DAY_SU).to(WeekDay::Sunday),
650 just(KW_DAY_MO).to(WeekDay::Monday),
651 just(KW_DAY_TU).to(WeekDay::Tuesday),
652 just(KW_DAY_WE).to(WeekDay::Wednesday),
653 just(KW_DAY_TH).to(WeekDay::Thursday),
654 just(KW_DAY_FR).to(WeekDay::Friday),
655 just(KW_DAY_SA).to(WeekDay::Saturday),
656 ))
657}
658
659fn bymodaylist<'src, I, E>() -> impl Parser<'src, I, Vec<i8>, E>
663where
664 I: Input<'src, Token = char, Span = SimpleSpan>,
665 E: ParserExtra<'src, I>,
666{
667 monthdaynum().separated_by(just(',')).collect()
668}
669
670fn monthdaynum<'src, I, E>() -> impl Parser<'src, I, i8, E>
674where
675 I: Input<'src, Token = char, Span = SimpleSpan>,
676 E: ParserExtra<'src, I>,
677{
678 is_positive()
679 .then(ordmoday())
680 .map(|(positive, n)| if positive { n } else { -n })
681}
682
683fn ordmoday<'src, I, E>() -> impl Parser<'src, I, i8, E>
687where
688 I: Input<'src, Token = char, Span = SimpleSpan>,
689 E: ParserExtra<'src, I>,
690{
691 choice((
692 i8_1_2().then(i8_0_9()).map(|(a, b)| a * 10 + b), just('3').ignore_then(i8_0_1()).map(|a| 30 + a), just('0').or_not().ignore_then(i8_1_9()), ))
696}
697
698fn byyrdaylist<'src, I, E>() -> impl Parser<'src, I, Vec<i16>, E>
702where
703 I: Input<'src, Token = char, Span = SimpleSpan>,
704 E: ParserExtra<'src, I>,
705{
706 yeardaynum().separated_by(just(',')).collect()
707}
708
709fn yeardaynum<'src, I, E>() -> impl Parser<'src, I, i16, E>
713where
714 I: Input<'src, Token = char, Span = SimpleSpan>,
715 E: ParserExtra<'src, I>,
716{
717 is_positive()
718 .then(ordyrday())
719 .map(|(positive, n)| if positive { n } else { -n })
720}
721
722fn ordyrday<'src, I, E>() -> impl Parser<'src, I, i16, E>
726where
727 I: Input<'src, Token = char, Span = SimpleSpan>,
728 E: ParserExtra<'src, I>,
729{
730 let i16_1_99 = i16_1_9().then(i16_0_9().or_not()).map(|(a, b)| match b {
731 Some(b) => a * 10 + b, None => a, });
734
735 choice((
736 just('3').ignore_then(choice((
737 just('6').ignore_then(i16_0_6()).map(|a| 360 + a), i16_0_5().then(i16_0_9()).map(|(a, b)| 300 + a * 10 + b), ))),
740 i16_1_2()
741 .then(i16_0_9())
742 .then(i16_0_9())
743 .map(|((a, b), c)| a * 100 + b * 10 + c), just('0').or_not().ignore_then(choice((
745 just('0').ignore_then(i16_0_9()), i16_1_99, ))),
748 ))
749}
750
751fn bywknolist<'src, I, E>() -> impl Parser<'src, I, Vec<i8>, E>
755where
756 I: Input<'src, Token = char, Span = SimpleSpan>,
757 E: ParserExtra<'src, I>,
758{
759 weeknum().separated_by(just(',')).collect()
760}
761
762fn weeknum<'src, I, E>() -> impl Parser<'src, I, i8, E>
766where
767 I: Input<'src, Token = char, Span = SimpleSpan>,
768 E: ParserExtra<'src, I>,
769{
770 is_positive()
771 .then(ordwk())
772 .map(|(positive, n)| if positive { n } else { -n })
773}
774
775fn bymolist<'src, I, E>() -> impl Parser<'src, I, Vec<u8>, E>
779where
780 I: Input<'src, Token = char, Span = SimpleSpan>,
781 E: ParserExtra<'src, I>,
782{
783 monthnum().separated_by(just(',')).collect()
784}
785
786fn monthnum<'src, I, E>() -> impl Parser<'src, I, u8, E>
790where
791 I: Input<'src, Token = char, Span = SimpleSpan>,
792 E: ParserExtra<'src, I>,
793{
794 choice((
795 just('0').ignore_then(u8_1_9()), just('1').ignore_then(u8_0_9()).map(|a| 10 + a), u8_1_9(), ))
799}
800
801fn bysplist<'src, I, E>() -> impl Parser<'src, I, Vec<i16>, E>
805where
806 I: Input<'src, Token = char, Span = SimpleSpan>,
807 E: ParserExtra<'src, I>,
808{
809 setposday().separated_by(just(',')).collect()
810}
811
812fn setposday<'src, I, E>() -> impl Parser<'src, I, i16, E>
816where
817 I: Input<'src, Token = char, Span = SimpleSpan>,
818 E: ParserExtra<'src, I>,
819{
820 yeardaynum()
821}
822
823fn is_positive<'src, I, E>() -> impl Parser<'src, I, bool, E> + Copy
826where
827 I: Input<'src, Token = char, Span = SimpleSpan>,
828 E: ParserExtra<'src, I>,
829{
830 select! { c @ ('+' | '-') => c }
831 .or_not()
832 .map(|c| !matches!(c, Some('-')))
833}
834
835fn u32_non_zero<'src, I, E>() -> impl Parser<'src, I, u32, E>
837where
838 I: Input<'src, Token = char, Span = SimpleSpan>,
839 E: ParserExtra<'src, I>,
840 E::Error: LabelError<'src, I, ValueExpected>,
841{
842 select! { c @ '0'..='9' => c }
843 .repeated()
844 .at_least(1)
845 .at_most(10) .collect::<String>()
847 .try_map_with(|str, e| {
848 lexical::parse_partial::<u32, _>(&str)
849 .map_err(|_| E::Error::expected_found([ValueExpected::U32], None, e.span()))
850 .and_then(|(v, _)| match v {
851 0 => Err(E::Error::expected_found(
852 [ValueExpected::PositiveU32],
853 None,
854 e.span(),
855 )),
856 v => Ok(v),
857 })
858 })
859}
860
861#[cfg(test)]
862mod tests {
863 use chumsky::extra;
864 use chumsky::input::Stream;
865
866 use super::*;
867
868 fn parse(src: &'_ str) -> Result<ValueRecurrenceRule, Vec<Rich<'_, char>>> {
869 let stream = Stream::from_iter(src.chars());
870 value_rrule::<'_, _, extra::Err<_>>()
871 .parse(stream)
872 .into_result()
873 }
874
875 #[test]
876 fn parses_rrule_freq_only() {
877 let freqs = [
879 ("FREQ=SECONDLY", RecurrenceFrequency::Secondly),
880 ("FREQ=MINUTELY", RecurrenceFrequency::Minutely),
881 ("FREQ=HOURLY", RecurrenceFrequency::Hourly),
882 ("FREQ=DAILY", RecurrenceFrequency::Daily),
883 ("FREQ=WEEKLY", RecurrenceFrequency::Weekly),
884 ("FREQ=MONTHLY", RecurrenceFrequency::Monthly),
885 ("FREQ=YEARLY", RecurrenceFrequency::Yearly),
886 ];
887
888 for (src, expected_freq) in freqs {
889 let result = parse(src).unwrap();
890 assert_eq!(result.freq, expected_freq, "Failed for {src}");
891 assert!(result.until.is_none());
892 assert!(result.count.is_none());
893 assert!(result.interval.is_none());
894 }
895 }
896
897 #[test]
898 fn parses_rrule_with_interval() {
899 let src = "FREQ=DAILY;INTERVAL=2";
900 let result = parse(src).unwrap();
901 assert_eq!(result.freq, RecurrenceFrequency::Daily);
902 assert_eq!(result.interval, Some(2));
903 }
904
905 #[test]
906 fn parses_rrule_with_until_datetime() {
907 let src = "FREQ=DAILY;UNTIL=19971224T000000Z";
908 let result = parse(src).unwrap();
909 assert_eq!(result.freq, RecurrenceFrequency::Daily);
910 assert!(result.until.is_some());
911
912 let until = result.until.unwrap();
913 assert_eq!(until.date.year, 1997);
914 assert_eq!(until.date.month, 12);
915 assert_eq!(until.date.day, 24);
916 assert!(until.time.utc);
917 }
918
919 #[test]
920 fn parses_rrule_with_until_date() {
921 let src = "FREQ=DAILY;UNTIL=19971224";
922 let result = parse(src).unwrap();
923 assert_eq!(result.freq, RecurrenceFrequency::Daily);
924 assert!(result.until.is_some());
925 let until = result.until.unwrap();
926 assert_eq!(until.date.year, 1997);
927 assert_eq!(until.date.month, 12);
928 assert_eq!(until.date.day, 24);
929 assert!(!until.time.utc);
930 assert_eq!(until.time.hour, 0);
931 assert_eq!(until.time.minute, 0);
932 assert_eq!(until.time.second, 0);
933 }
934
935 #[test]
936 fn parses_rrule_with_count() {
937 let src = "FREQ=DAILY;COUNT=10";
938 let result = parse(src).unwrap();
939 assert_eq!(result.freq, RecurrenceFrequency::Daily);
940 assert_eq!(result.count, Some(10));
941 }
942
943 #[test]
944 fn parses_rrule_with_byday() {
945 let src = "FREQ=WEEKLY;BYDAY=MO,WE,FR";
947 let result = parse(src).unwrap();
948 assert_eq!(result.by_day.len(), 3);
949
950 let first = result.by_day.first().unwrap();
951 assert_eq!(first.day, WeekDay::Monday);
952 assert_eq!(first.occurrence, None);
953 assert_eq!(result.by_day.get(1).unwrap().day, WeekDay::Wednesday);
954 assert_eq!(result.by_day.get(2).unwrap().day, WeekDay::Friday);
955
956 let src = "FREQ=MONTHLY;BYDAY=1MO,-1MO";
958 let result = parse(src).unwrap();
959 assert_eq!(result.by_day.len(), 2);
960
961 let first = result.by_day.first().unwrap();
962 assert_eq!(first.day, WeekDay::Monday);
963 assert_eq!(first.occurrence, Some(1));
964
965 let second = result.by_day.get(1).unwrap();
966 assert_eq!(second.day, WeekDay::Monday);
967 assert_eq!(second.occurrence, Some(-1));
968 }
969
970 #[test]
971 fn parses_rrule_with_byhour() {
972 let src = "FREQ=DAILY;BYHOUR=9,10,11,12,13,14,15,16";
973 let result = parse(src).unwrap();
974 assert_eq!(result.by_hour, vec![9, 10, 11, 12, 13, 14, 15, 16]);
975 }
976
977 #[test]
978 fn parses_rrule_with_byminute() {
979 let src = "FREQ=DAILY;BYMINUTE=0,20,40";
980 let result = parse(src).unwrap();
981 assert_eq!(result.by_minute, vec![0, 20, 40]);
982 }
983
984 #[test]
985 fn parses_rrule_with_bysecond() {
986 let src = "FREQ=HOURLY;BYSECOND=0,15,30,45";
987 let result = parse(src).unwrap();
988 assert_eq!(result.by_second, vec![0, 15, 30, 45]);
989 }
990
991 #[test]
992 fn parses_rrule_with_bymonthday() {
993 let src = "FREQ=MONTHLY;BYMONTHDAY=1,15,-1";
994 let result = parse(src).unwrap();
995 assert_eq!(result.by_month_day, vec![1, 15, -1]);
996 }
997
998 #[test]
999 fn parses_rrule_with_byyearday() {
1000 let src = "FREQ=YEARLY;BYYEARDAY=1,100,200,-1";
1001 let result = parse(src).unwrap();
1002 assert_eq!(result.by_year_day, vec![1, 100, 200, -1]);
1003 }
1004
1005 #[test]
1006 fn parses_rrule_with_byweekno() {
1007 let src = "FREQ=YEARLY;BYWEEKNO=20,21,-1";
1008 let result = parse(src).unwrap();
1009 assert_eq!(result.by_week_no, vec![20, 21, -1]);
1010 }
1011
1012 #[test]
1013 fn parses_rrule_with_bymonth() {
1014 let src = "FREQ=YEARLY;BYMONTH=1,2,3";
1015 let result = parse(src).unwrap();
1016 assert_eq!(result.by_month, vec![1, 2, 3]);
1017 }
1018
1019 #[test]
1020 fn parses_rrule_with_bysetpos() {
1021 let src = "FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-1";
1022 let result = parse(src).unwrap();
1023 assert_eq!(result.by_set_pos, vec![-1]);
1024 }
1025
1026 #[test]
1027 fn parses_rrule_with_wkst() {
1028 let src = "FREQ=WEEKLY;WKST=SU";
1029 let result = parse(src).unwrap();
1030 assert_eq!(result.wkst.unwrap(), WeekDay::Sunday);
1031 }
1032
1033 #[test]
1034 fn parses_rrule_complex() {
1035 let src = "FREQ=YEARLY;INTERVAL=2;BYMONTH=1;BYDAY=SU;BYHOUR=8,9;BYMINUTE=30";
1037 let result = parse(src).unwrap();
1038 assert_eq!(result.freq, RecurrenceFrequency::Yearly);
1039 assert_eq!(result.interval, Some(2));
1040 assert_eq!(result.by_month, vec![1]);
1041 assert_eq!(result.by_day.len(), 1);
1042 assert_eq!(result.by_day.first().unwrap().day, WeekDay::Sunday);
1043 assert_eq!(result.by_hour, vec![8, 9]);
1044 assert_eq!(result.by_minute, vec![30]);
1045 }
1046
1047 #[test]
1048 fn parses_rrule_rejects_missing_freq() {
1049 let src = "INTERVAL=2;COUNT=10";
1051 assert!(parse(src).is_err(), "Missing FREQ should fail");
1052 }
1053
1054 #[test]
1055 fn parses_rrule_rejects_until_and_count_together() {
1056 let src = "FREQ=DAILY;UNTIL=19971224T000000Z;COUNT=10";
1058 assert!(parse(src).is_err(), "UNTIL and COUNT together should fail");
1059 }
1060
1061 #[test]
1062 fn parses_rrule_handles_reordered_parts() {
1063 let src = "COUNT=10;INTERVAL=2;FREQ=DAILY";
1065 let result = parse(src).unwrap();
1066 assert_eq!(result.freq, RecurrenceFrequency::Daily);
1067 assert_eq!(result.count, Some(10));
1068 assert_eq!(result.interval, Some(2));
1069 }
1070
1071 #[test]
1072 fn parses_rrule_rejects_duplicate_parts() {
1073 let test_cases = [
1074 ("FREQ=DAILY;FREQ=WEEKLY", "FREQ"),
1075 (
1076 "FREQ=DAILY;UNTIL=19971224T000000Z;UNTIL=19971225T000000Z",
1077 "UNTIL",
1078 ),
1079 ("FREQ=DAILY;COUNT=10;COUNT=20", "COUNT"),
1080 ("FREQ=DAILY;INTERVAL=1;INTERVAL=2", "INTERVAL"),
1081 ("FREQ=WEEKLY;BYDAY=MO;BYDAY=FR", "BYDAY"),
1082 ("FREQ=DAILY;BYHOUR=9;BYHOUR=10", "BYHOUR"),
1083 ];
1084
1085 for (src, part_name) in test_cases {
1086 assert!(
1087 parse(src).is_err(),
1088 "Duplicate {part_name} should fail for input: {src}"
1089 );
1090 }
1091 }
1092}