aimcal_ical/value/
rrule.rs

1// SPDX-FileCopyrightText: 2025-2026 Zexin Yuan <aim@yzx9.xyz>
2//
3// SPDX-License-Identifier: Apache-2.0
4
5//! Recurrence rule type definitions for iCalendar.
6
7use 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/// Recurrence rule
30#[derive(Debug, Clone)]
31pub struct ValueRecurrenceRule {
32    /// Frequency of recurrence
33    pub freq: RecurrenceFrequency,
34    /// Until date for recurrence
35    pub until: Option<ValueDateTime>,
36    /// Number of occurrences
37    pub count: Option<u32>,
38    /// Interval between recurrences
39    pub interval: Option<u32>,
40    /// Second specifier
41    pub by_second: Vec<u8>,
42    /// Minute specifier
43    pub by_minute: Vec<u8>,
44    /// Hour specifier
45    pub by_hour: Vec<u8>,
46    /// Day of month specifier
47    pub by_month_day: Vec<i8>,
48    /// Day of year specifier
49    pub by_year_day: Vec<i16>,
50    /// Week number specifier
51    pub by_week_no: Vec<i8>,
52    /// Month specifier
53    pub by_month: Vec<u8>,
54    /// Day of week specifier
55    pub by_day: Vec<WeekDayNum>,
56    /// Position in month
57    pub by_set_pos: Vec<i16>,
58    /// Start day of week
59    pub wkst: Option<WeekDay>,
60}
61
62/// Recurrence frequency
63#[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/// Day of week with optional occurrence
90#[derive(Debug, Clone, Copy)]
91pub struct WeekDayNum {
92    /// Day of the week
93    pub day: WeekDay,
94    /// Occurrence in month (optional)
95    pub occurrence: Option<i8>,
96}
97
98/// Day of the week
99#[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
125/// Format Definition:  This value type is defined by the following notation:
126///
127/// ```txt
128/// recur           = recur-rule-part *( ";" recur-rule-part )
129///                 ;
130///                 ; The rule parts are not ordered in any
131///                 ; particular sequence.
132///                 ;
133///                 ; The FREQ rule part is REQUIRED,
134///                 ; but MUST NOT occur more than once.
135///                 ;
136///                 ; The UNTIL or COUNT rule parts are OPTIONAL,
137///                 ; but they MUST NOT occur in the same 'recur'.
138///                 ;
139///                 ; The other rule parts are OPTIONAL,
140///                 ; but MUST NOT occur more than once.
141/// ```
142pub 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    // Validate required FREQ
325    let freq =
326        freq.ok_or_else(|| Err::expected_found([ValueExpected::RRuleRequiredFreq], None, span))?;
327
328    // Validate UNTIL and COUNT are mutually exclusive
329    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
373/// ```txt
374/// recur-rule-part = ( "FREQ" "=" freq )
375///                 / ( "UNTIL" "=" enddate )
376///                 / ( "COUNT" "=" 1*DIGIT )
377///                 / ( "INTERVAL" "=" 1*DIGIT )
378///                 / ( "BYSECOND" "=" byseclist )
379///                 / ( "BYMINUTE" "=" byminlist )
380///                 / ( "BYHOUR" "=" byhrlist )
381///                 / ( "BYDAY" "=" bywdaylist )
382///                 / ( "BYMONTHDAY" "=" bymodaylist )
383///                 / ( "BYYEARDAY" "=" byyrdaylist )
384///                 / ( "BYWEEKNO" "=" bywknolist )
385///                 / ( "BYMONTH" "=" bymolist )
386///                 / ( "BYSETPOS" "=" bysplist )
387///                 / ( "WKST" "=" weekday )
388/// ```
389fn 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    // Frequency parser
398    let freq = kw(KW_RRULE_FREQ).ignore_then(freq()).map(Part::Freq);
399
400    // UNTIL can be a date or date-time
401    let until = kw(KW_RRULE_UNTIL).ignore_then(enddate()).map(Part::Until);
402
403    // COUNT - positive integer
404    let count = kw(KW_RRULE_COUNT)
405        .ignore_then(u32_non_zero())
406        .map(Part::Count);
407
408    // INTERVAL - positive integer
409    let interval = kw(KW_RRULE_INTERVAL)
410        .ignore_then(u32_non_zero())
411        .map(Part::Interval);
412
413    // BYSECOND - 0 to 60
414    let by_second = kw(KW_RRULE_BYSECOND)
415        .ignore_then(byseclist())
416        .map(Part::BySecond);
417
418    // BYMINUTE - 0 to 59
419    let by_minute = kw(KW_RRULE_BYMINUTE)
420        .ignore_then(byminlist())
421        .map(Part::ByMinute);
422
423    // BYHOUR - 0 to 23
424    let by_hour = kw(KW_RRULE_BYHOUR)
425        .ignore_then(byhrlist())
426        .map(Part::ByHour);
427
428    // BYDAY - weekday with optional occurrence
429    let by_day = kw(KW_RRULE_BYDAY)
430        .ignore_then(bywdaylist())
431        .map(Part::ByDay);
432
433    // BYMONTHDAY - -31 to -1 and 1 to 31
434    let by_month_day = kw(KW_RRULE_BYMONTHDAY)
435        .ignore_then(bymodaylist())
436        .map(Part::ByMonthDay);
437
438    // BYYEARDAY - -366 to -1 and 1 to 366
439    let by_year_day = kw(KW_RRULE_BYYEARDAY)
440        .ignore_then(byyrdaylist())
441        .map(Part::ByYearDay);
442
443    // BYWEEKNO - -53 to -1 and 1 to 53
444    let by_week_no = kw(KW_RRULE_BYWEEKNO)
445        .ignore_then(bywknolist())
446        .map(Part::ByWeekNo);
447
448    // BYMONTH - 1 to 12
449    let by_month = kw(KW_RRULE_BYMONTH)
450        .ignore_then(bymolist())
451        .map(Part::ByMonth);
452
453    // BYSETPOS - -366 to -1 and 1 to 366
454    let by_set_pos = kw(KW_RRULE_BYSETPOS)
455        .ignore_then(bysplist())
456        .map(Part::BySetPos);
457
458    // WKST - single weekday
459    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
479/// ```txt
480/// freq        = "SECONDLY" / "MINUTELY" / "HOURLY" / "DAILY"
481///             / "WEEKLY" / "MONTHLY" / "YEARLY"
482/// ```
483fn 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
499/// ```txt
500/// enddate     = date / date-time
501/// ```
502// TODO: According to RFC 5545, the UNTIL value MUST be a date or date-time
503// that matches the type of the DTSTART property.
504fn 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    // Try date-time first, then fall back to date
511    // PERF: Could be optimized to avoid backtracking
512    choice((
513        value_date_time(),
514        value_date().map(|date| ValueDateTime::new(date, ValueTime::new(0, 0, 0, false))),
515    ))
516}
517
518/// ```txt
519/// byseclist   = ( seconds *("," seconds) )
520/// ```
521fn 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
529/// ```txt
530/// seconds     = 1*2DIGIT       ;0 to 60
531/// ```
532fn 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), // 00-59
539        just("60").to(60),                                // 60
540        u8_0_9(),                                         // 0-9
541    ))
542}
543
544/// ```txt
545/// byminlist   = ( minutes *("," minutes) )
546/// ```
547fn 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
555/// ```txt
556/// minutes     = 1*2DIGIT       ;0 to 59
557/// ```
558fn 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), // 00-59
565        u8_0_9(),                                         // 0-9
566    ))
567}
568
569/// ```txt
570/// byhrlist    = ( hour *("," hour) )
571/// ```
572fn 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
580/// ```txt
581/// hour        = 1*2DIGIT       ;0 to 23
582/// ```
583fn 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), // 00-19
590        just('2').ignore_then(u8_0_3()).map(|b| 20 + b),  // 20-23
591        u8_0_9(),                                         // 0-9
592    ))
593}
594
595/// ```txt
596/// bywdaylist  = ( weekdaynum *("," weekdaynum) )
597/// ```
598fn 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
606/// ```txt
607/// weekdaynum  = [[plus / minus] ordwk] weekday
608/// plus        = "+"
609/// minus       = "-"
610/// ```
611fn 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
624/// ```txt
625/// ordwk       = 1*2DIGIT       ;1 to 53
626/// ```
627fn 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), // 10-49
634        just('5').ignore_then(i8_0_3()).map(|a| 50 + a),  // 50-53
635        just('0').ignore_then(i8_1_9()),                  // 01-09
636        i8_1_9(),                                         // 1-9
637    ))
638}
639
640/// ```txt
641/// weekday     = "SU" / "MO" / "TU" / "WE" / "TH" / "FR" / "SA"
642/// ```
643fn 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
659/// ```txt
660/// bymodaylist = ( monthdaynum *("," monthdaynum) )
661/// ```
662fn 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
670/// ```txt
671/// monthdaynum = [plus / minus] ordmoday
672/// ```
673fn 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
683/// ```txt
684/// ordmoday    = 1*2DIGIT       ;1 to 31
685/// ```
686fn 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), // 10-29
693        just('3').ignore_then(i8_0_1()).map(|a| 30 + a),  // 30-31
694        just('0').or_not().ignore_then(i8_1_9()),         // 1-9 / 01-09
695    ))
696}
697
698/// ```txt
699/// byyrdaylist = ( yeardaynum *("," yeardaynum) )
700/// ```
701fn 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
709/// ```txt
710/// yeardaynum  = [plus / minus] ordyrday
711/// ```
712fn 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
722/// ```txt
723/// ordyrday    = 1*3DIGIT      ;1 to 366
724/// ```
725fn 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, // 10-99
732        None => a,             // 1-9
733    });
734
735    choice((
736        just('3').ignore_then(choice((
737            just('6').ignore_then(i16_0_6()).map(|a| 360 + a), // 360- 366
738            i16_0_5().then(i16_0_9()).map(|(a, b)| 300 + a * 10 + b), // 300-359
739        ))),
740        i16_1_2()
741            .then(i16_0_9())
742            .then(i16_0_9())
743            .map(|((a, b), c)| a * 100 + b * 10 + c), // 100-299
744        just('0').or_not().ignore_then(choice((
745            just('0').ignore_then(i16_0_9()), // 01-09 / 001-009
746            i16_1_99,                         // 1-9 / 10-99 / 01-09 / 010-099
747        ))),
748    ))
749}
750
751/// ```txt
752/// bywknolist  = ( weeknum *("," weeknum) )
753/// ```
754fn 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
762/// ```txt
763/// weeknum     = [plus / minus] ordwk
764/// ```
765fn 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
775/// ```txt
776/// bymolist    = ( monthnum *("," monthnum) )
777/// ```
778fn 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
786/// ```txt
787/// monthnum    = 1*2DIGIT       ;1 to 12
788/// ```
789fn 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()),                 // 01-09
796        just('1').ignore_then(u8_0_9()).map(|a| 10 + a), // 10-12
797        u8_1_9(),                                        // 1-9
798    ))
799}
800
801/// ```txt
802/// bysplist    = ( setposday *("," setposday) )
803/// ```
804fn 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
812/// ```txt
813/// setposday   = yeardaynum
814/// ```
815fn 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
823// Helper parsers
824
825fn 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
835/// Parse u32 (1 or more digits)
836fn 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) // u32 max is 10 digits
846        .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        // Test all frequency values
878        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        // Simple days
946        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        // With occurrence
957        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        // Example from RFC 5545
1036        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        // Missing FREQ should fail
1050        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        // UNTIL and COUNT together should fail
1057        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        // Parts in different order
1064        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}