1
use crate::common::RecurFreq;
2
use crate::error::AetoliaResult;
3
use crate::model::property::{
4
    ComponentProperty, DateTimeQuery, DateTimeStartProperty, RecurRulePart, RecurrenceRule,
5
};
6
use crate::validate::{
7
    component_property_name, ComponentPropertyError, ComponentPropertyLocation,
8
    ICalendarErrorSeverity, PropertyLocation, WithinPropertyLocation,
9
};
10
use std::collections::HashMap;
11

            
12
176
pub(super) fn validate_recurrence_rule(
13
176
    errors: &mut Vec<ComponentPropertyError>,
14
176
    property: &ComponentProperty,
15
176
    rule: &RecurrenceRule,
16
176
    maybe_dt_start: Option<&DateTimeStartProperty>,
17
176
    property_location: PropertyLocation,
18
176
    property_index: usize,
19
176
) -> AetoliaResult<()> {
20
176
    let dt_start = if let Some(dt_start) = maybe_dt_start {
21
174
        dt_start
22
    } else {
23
2
        errors.push(ComponentPropertyError {
24
2
            message: "Recurrence rule must have a DTSTART property associated with it".to_string(),
25
2
            severity: ICalendarErrorSeverity::Error,
26
2
            location: Some(ComponentPropertyLocation {
27
2
                index: property_index,
28
2
                name: component_property_name(property).to_string(),
29
2
                property_location: Some(WithinPropertyLocation::Value),
30
2
            }),
31
2
        });
32
2
        return Ok(());
33
    };
34

            
35
174
    let mut freq_index = 0;
36
174
    let freq = match &rule.parts[0] {
37
170
        RecurRulePart::Freq(freq) => {
38
170
            // Frequency should be the first part, this is correct
39
170
            freq
40
        }
41
        _ => {
42
8
            let maybe_freq = rule.parts.iter().enumerate().find_map(|(index, part)| {
43
6
                if let RecurRulePart::Freq(freq) = part {
44
2
                    Some((index, freq))
45
                } else {
46
4
                    None
47
                }
48
8
            });
49
4

            
50
4
            match maybe_freq {
51
2
                Some((index, freq)) => {
52
2
                    errors.push(ComponentPropertyError {
53
2
                        message: "Recurrence rule must start with a frequency".to_string(),
54
2
                        severity: ICalendarErrorSeverity::Warning,
55
2
                        location: Some(ComponentPropertyLocation {
56
2
                            index: property_index,
57
2
                            name: component_property_name(property).to_string(),
58
2
                            property_location: Some(WithinPropertyLocation::Value),
59
2
                        }),
60
2
                    });
61
2

            
62
2
                    freq_index = index;
63
2
                    freq
64
                }
65
                None => {
66
2
                    errors.push(ComponentPropertyError {
67
2
                        message: "No frequency part found in recurrence rule, but it is required. This prevents the rest of the rule being checked".to_string(),
68
2
                        severity: ICalendarErrorSeverity::Error,
69
2
                        location: Some(ComponentPropertyLocation {
70
2
                            index: property_index,
71
2
                            name: component_property_name(property).to_string(),
72
2
                            property_location: Some(WithinPropertyLocation::Value),
73
2
                        }),
74
2
                    });
75
2
                    return Ok(());
76
                }
77
            }
78
        }
79
    };
80

            
81
172
    let mut seen_count = HashMap::<String, u32>::new();
82
670
    let add_count = |seen_count: &mut HashMap<String, u32>, key: &str| {
83
594
        *seen_count
84
594
            .entry(key.to_string())
85
594
            .and_modify(|count| *count += 1)
86
594
            .or_insert(1)
87
594
    };
88
598
    for (part_index, part) in rule.parts.iter().enumerate().skip(1) {
89
598
        match part {
90
            RecurRulePart::Freq(_) => {
91
4
                if freq_index != part_index {
92
2
                    errors.push(ComponentPropertyError {
93
2
                        message: format!("Repeated FREQ part at index {part_index}"),
94
2
                        severity: ICalendarErrorSeverity::Error,
95
2
                        location: Some(ComponentPropertyLocation {
96
2
                            index: property_index,
97
2
                            name: component_property_name(property).to_string(),
98
2
                            property_location: Some(WithinPropertyLocation::Value),
99
2
                        }),
100
2
                    });
101
2
                }
102
            }
103
50
            RecurRulePart::Until(until) => {
104
50
                let count = add_count(&mut seen_count, "UNTIL");
105
50
                if count > 1 {
106
2
                    errors.push(ComponentPropertyError {
107
2
                        message: format!("Repeated UNTIL part at index {part_index}"),
108
2
                        severity: ICalendarErrorSeverity::Error,
109
2
                        location: Some(ComponentPropertyLocation {
110
2
                            index: property_index,
111
2
                            name: component_property_name(property).to_string(),
112
2
                            property_location: Some(WithinPropertyLocation::Value),
113
2
                        }),
114
2
                    });
115
48
                }
116

            
117
50
                match property_location {
118
                    // STANDARD or DAYLIGHT have different rules
119
                    PropertyLocation::TimeZoneComponent => {
120
16
                        if !until.is_utc() {
121
2
                            errors.push(ComponentPropertyError {
122
2
                                message: format!(
123
2
                                    "UNTIL part at index {part_index} must be a UTC time here"
124
2
                                ),
125
2
                                severity: ICalendarErrorSeverity::Error,
126
2
                                location: Some(ComponentPropertyLocation {
127
2
                                    index: property_index,
128
2
                                    name: component_property_name(property).to_string(),
129
2
                                    property_location: Some(WithinPropertyLocation::Value),
130
2
                                }),
131
2
                            });
132
14
                        }
133
                    }
134
34
                    _ => match (dt_start.value.is_date_time(), until.is_date_time()) {
135
2
                        (true, false) => {
136
2
                            errors.push(ComponentPropertyError {
137
2
                                    message: format!("UNTIL part at index {part_index} is a date, but the associated DTSTART property is a date-time"),
138
2
                                severity: ICalendarErrorSeverity::Error,
139
2
                                    location: Some(ComponentPropertyLocation {
140
2
                                        index: property_index,
141
2
                                        name: component_property_name(property).to_string(),
142
2
                                        property_location: Some(WithinPropertyLocation::Value),
143
2
                                    }),
144
2
                                });
145
2
                        }
146
2
                        (false, true) => {
147
2
                            errors.push(ComponentPropertyError {
148
2
                                    message: format!("UNTIL part at index {part_index} is a date-time, but the associated DTSTART property is a date"),
149
2
                                severity: ICalendarErrorSeverity::Error,
150
2
                                    location: Some(ComponentPropertyLocation {
151
2
                                        index: property_index,
152
2
                                        name: component_property_name(property).to_string(),
153
2
                                        property_location: Some(WithinPropertyLocation::Value),
154
2
                                    }),
155
2
                                });
156
2
                        }
157
                        (true, true) => {
158
28
                            if dt_start.is_local_time() && until.is_utc() {
159
2
                                errors.push(ComponentPropertyError {
160
2
                                        message: format!("UNTIL part at index {part_index} must be a local time if the associated DTSTART property is a local time"),
161
2
                                    severity: ICalendarErrorSeverity::Error,
162
2
                                        location: Some(ComponentPropertyLocation {
163
2
                                            index: property_index,
164
2
                                            name: component_property_name(property).to_string(),
165
2
                                            property_location: Some(WithinPropertyLocation::Value),
166
2
                                        }),
167
2
                                    });
168
26
                            } else if (dt_start.is_utc() || dt_start.is_local_time_with_timezone())
169
26
                                && !until.is_utc()
170
4
                            {
171
4
                                errors.push(ComponentPropertyError {
172
4
                                        message: format!("UNTIL part at index {part_index} must be a UTC time if the associated DTSTART property is a UTC time or a local time with a timezone"),
173
4
                                    severity: ICalendarErrorSeverity::Error,
174
4
                                        location: Some(ComponentPropertyLocation {
175
4
                                            index: property_index,
176
4
                                            name: component_property_name(property).to_string(),
177
4
                                            property_location: Some(WithinPropertyLocation::Value),
178
4
                                        }),
179
4
                                    });
180
22
                            }
181
                        }
182
2
                        (false, false) => {}
183
                    },
184
                }
185
            }
186
            RecurRulePart::Count(_) => {
187
36
                let count = add_count(&mut seen_count, "COUNT");
188
36
                if count > 1 {
189
2
                    errors.push(ComponentPropertyError {
190
2
                        message: format!("Repeated COUNT part at index {part_index}"),
191
2
                        severity: ICalendarErrorSeverity::Error,
192
2
                        location: Some(ComponentPropertyLocation {
193
2
                            index: property_index,
194
2
                            name: component_property_name(property).to_string(),
195
2
                            property_location: Some(WithinPropertyLocation::Value),
196
2
                        }),
197
2
                    });
198
34
                }
199
            }
200
            RecurRulePart::Interval(_) => {
201
40
                let count = add_count(&mut seen_count, "INTERVAL");
202
40
                if count > 1 {
203
2
                    errors.push(ComponentPropertyError {
204
2
                        message: format!("Repeated INTERVAL part at index {part_index}"),
205
2
                        severity: ICalendarErrorSeverity::Error,
206
2
                        location: Some(ComponentPropertyLocation {
207
2
                            index: property_index,
208
2
                            name: component_property_name(property).to_string(),
209
2
                            property_location: Some(WithinPropertyLocation::Value),
210
2
                        }),
211
2
                    });
212
38
                }
213
            }
214
42
            RecurRulePart::BySecList(second_list) => {
215
42
                let count = add_count(&mut seen_count, "BYSECOND");
216
42
                if count > 1 {
217
2
                    errors.push(ComponentPropertyError {
218
2
                        message: format!("Repeated BYSECOND part at index {part_index}"),
219
2
                        severity: ICalendarErrorSeverity::Error,
220
2
                        location: Some(ComponentPropertyLocation {
221
2
                            index: property_index,
222
2
                            name: component_property_name(property).to_string(),
223
2
                            property_location: Some(WithinPropertyLocation::Value),
224
2
                        }),
225
2
                    });
226
40
                }
227

            
228
123
                if !second_list.iter().all(|second| *second <= 60) {
229
4
                    errors.push(ComponentPropertyError {
230
4
                        message: format!("Invalid BYSECOND part at index {part_index}, seconds must be between 0 and 60"),
231
4
                        severity: ICalendarErrorSeverity::Error,
232
4
                        location: Some(ComponentPropertyLocation {
233
4
                            index: property_index,
234
4
                            name: component_property_name(property).to_string(),
235
4
                            property_location: Some(WithinPropertyLocation::Value),
236
4
                        }),
237
4
                    });
238
38
                }
239

            
240
42
                if dt_start.value.is_date() {
241
2
                    errors.push(ComponentPropertyError {
242
2
                        message: format!("BYSECOND part at index {part_index} is not valid when the associated DTSTART property has a DATE value type"),
243
2
                        severity: ICalendarErrorSeverity::Error,
244
2
                        location: Some(ComponentPropertyLocation {
245
2
                            index: property_index,
246
2
                            name: component_property_name(property).to_string(),
247
2
                            property_location: Some(WithinPropertyLocation::Value),
248
2
                        }),
249
2
                    });
250
40
                }
251
            }
252
42
            RecurRulePart::ByMinute(minute_list) => {
253
42
                let count = add_count(&mut seen_count, "BYMINUTE");
254
42
                if count > 1 {
255
2
                    errors.push(ComponentPropertyError {
256
2
                        message: format!("Repeated BYMINUTE part at index {part_index}"),
257
2
                        severity: ICalendarErrorSeverity::Error,
258
2
                        location: Some(ComponentPropertyLocation {
259
2
                            index: property_index,
260
2
                            name: component_property_name(property).to_string(),
261
2
                            property_location: Some(WithinPropertyLocation::Value),
262
2
                        }),
263
2
                    });
264
40
                }
265

            
266
123
                if !minute_list.iter().all(|minute| *minute <= 59) {
267
4
                    errors.push(ComponentPropertyError {
268
4
                        message: format!("Invalid BYMINUTE part at index {part_index}, minutes must be between 0 and 59"),
269
4
                        severity: ICalendarErrorSeverity::Error,
270
4
                        location: Some(ComponentPropertyLocation {
271
4
                            index: property_index,
272
4
                            name: component_property_name(property).to_string(),
273
4
                            property_location: Some(WithinPropertyLocation::Value),
274
4
                        }),
275
4
                    });
276
38
                }
277

            
278
42
                if dt_start.value.is_date() {
279
2
                    errors.push(ComponentPropertyError {
280
2
                        message: format!("BYMINUTE part at index {part_index} is not valid when the associated DTSTART property has a DATE value type"),
281
2
                        severity: ICalendarErrorSeverity::Error,
282
2
                        location: Some(ComponentPropertyLocation {
283
2
                            index: property_index,
284
2
                            name: component_property_name(property).to_string(),
285
2
                            property_location: Some(WithinPropertyLocation::Value),
286
2
                        }),
287
2
                    });
288
40
                }
289
            }
290
42
            RecurRulePart::ByHour(hour_list) => {
291
42
                let count = add_count(&mut seen_count, "BYHOUR");
292
42
                if count > 1 {
293
2
                    errors.push(ComponentPropertyError {
294
2
                        message: format!("Repeated BYHOUR part at index {part_index}"),
295
2
                        severity: ICalendarErrorSeverity::Error,
296
2
                        location: Some(ComponentPropertyLocation {
297
2
                            index: property_index,
298
2
                            name: component_property_name(property).to_string(),
299
2
                            property_location: Some(WithinPropertyLocation::Value),
300
2
                        }),
301
2
                    });
302
40
                }
303

            
304
123
                if !hour_list.iter().all(|hour| *hour <= 23) {
305
4
                    errors.push(ComponentPropertyError {
306
4
                        message: format!("Invalid BYHOUR part at index {part_index}, hours must be between 0 and 23"),
307
4
                        severity: ICalendarErrorSeverity::Error,
308
4
                        location: Some(ComponentPropertyLocation {
309
4
                            index: property_index,
310
4
                            name: component_property_name(property).to_string(),
311
4
                            property_location: Some(WithinPropertyLocation::Value),
312
4
                        }),
313
4
                    });
314
38
                }
315

            
316
42
                if dt_start.value.is_date() {
317
2
                    errors.push(ComponentPropertyError {
318
2
                        message: format!("BYHOUR part at index {part_index} is not valid when the associated DTSTART property has a DATE value type"),
319
2
                        severity: ICalendarErrorSeverity::Error,
320
2
                        location: Some(ComponentPropertyLocation {
321
2
                            index: property_index,
322
2
                            name: component_property_name(property).to_string(),
323
2
                            property_location: Some(WithinPropertyLocation::Value),
324
2
                        }),
325
2
                    });
326
40
                }
327
            }
328
78
            RecurRulePart::ByDay(day_list) => {
329
78
                let count = add_count(&mut seen_count, "BYDAY");
330
78
                if count > 1 {
331
2
                    errors.push(ComponentPropertyError {
332
2
                        message: format!("Repeated BYDAY part at index {part_index}"),
333
2
                        severity: ICalendarErrorSeverity::Error,
334
2
                        location: Some(ComponentPropertyLocation {
335
2
                            index: property_index,
336
2
                            name: component_property_name(property).to_string(),
337
2
                            property_location: Some(WithinPropertyLocation::Value),
338
2
                        }),
339
2
                    });
340
76
                }
341

            
342
78
                match freq {
343
12
                    RecurFreq::Monthly => {
344
12
                        // Offsets are permitted for this frequency
345
12
                    }
346
                    RecurFreq::Yearly => {
347
42
                        let is_by_week_number_specified = rule
348
42
                            .parts
349
42
                            .iter()
350
377
                            .any(|part| matches!(part, RecurRulePart::ByWeekNumber(_)));
351
42

            
352
42
                        if is_by_week_number_specified
353
38
                            && day_list.iter().any(|day| day.offset_weeks.is_some())
354
2
                        {
355
2
                            errors.push(ComponentPropertyError {
356
2
                                message: format!("BYDAY part at index {part_index} has a day with an offset, but the frequency is YEARLY and a BYWEEKNO part is specified"),
357
2
                                severity: ICalendarErrorSeverity::Error,
358
2
                                location: Some(ComponentPropertyLocation {
359
2
                                    index: property_index,
360
2
                                    name: component_property_name(property).to_string(),
361
2
                                    property_location: Some(WithinPropertyLocation::Value),
362
2
                                }),
363
2
                            });
364
40
                        }
365
                    }
366
                    _ => {
367
36
                        if day_list.iter().any(|day| day.offset_weeks.is_some()) {
368
10
                            errors.push(ComponentPropertyError {
369
10
                                message: format!("BYDAY part at index {part_index} has a day with an offset, but the frequency is not MONTHLY or YEARLY"),
370
10
                                severity: ICalendarErrorSeverity::Error,
371
10
                                location: Some(ComponentPropertyLocation {
372
10
                                    index: property_index,
373
10
                                    name: component_property_name(property).to_string(),
374
10
                                    property_location: Some(WithinPropertyLocation::Value),
375
10
                                }),
376
10
                            });
377
14
                        }
378
                    }
379
                }
380
            }
381
42
            RecurRulePart::ByMonthDay(month_day_list) => {
382
42
                let count = add_count(&mut seen_count, "BYMONTHDAY");
383
42
                if count > 1 {
384
2
                    errors.push(ComponentPropertyError {
385
2
                        message: format!("Repeated BYMONTHDAY part at index {part_index}"),
386
2
                        severity: ICalendarErrorSeverity::Error,
387
2
                        location: Some(ComponentPropertyLocation {
388
2
                            index: property_index,
389
2
                            name: component_property_name(property).to_string(),
390
2
                            property_location: Some(WithinPropertyLocation::Value),
391
2
                        }),
392
2
                    });
393
40
                }
394

            
395
42
                if !month_day_list
396
42
                    .iter()
397
123
                    .all(|day| (-31 <= *day && *day <= -1) || (1 <= *day && *day <= 31))
398
4
                {
399
4
                    errors.push(ComponentPropertyError {
400
4
                        message: format!("Invalid BYMONTHDAY part at index {part_index}, days must be between 1 and 31, or -31 and -1"),
401
4
                        severity: ICalendarErrorSeverity::Error,
402
4
                        location: Some(ComponentPropertyLocation {
403
4
                            index: property_index,
404
4
                            name: component_property_name(property).to_string(),
405
4
                            property_location: Some(WithinPropertyLocation::Value),
406
4
                        }),
407
4
                    });
408
38
                }
409

            
410
42
                if freq == &RecurFreq::Weekly {
411
2
                    errors.push(ComponentPropertyError {
412
2
                        message: format!("BYMONTHDAY part at index {part_index} is not valid for a WEEKLY frequency"),
413
2
                        severity: ICalendarErrorSeverity::Error,
414
2
                        location: Some(ComponentPropertyLocation {
415
2
                            index: property_index,
416
2
                            name: component_property_name(property).to_string(),
417
2
                            property_location: Some(WithinPropertyLocation::Value),
418
2
                        }),
419
2
                    });
420
40
                }
421
            }
422
46
            RecurRulePart::ByYearDay(year_day_list) => {
423
46
                let count = add_count(&mut seen_count, "BYYEARDAY");
424
46
                if count > 1 {
425
2
                    errors.push(ComponentPropertyError {
426
2
                        message: format!("Repeated BYYEARDAY part at index {part_index}"),
427
2
                        severity: ICalendarErrorSeverity::Error,
428
2
                        location: Some(ComponentPropertyLocation {
429
2
                            index: property_index,
430
2
                            name: component_property_name(property).to_string(),
431
2
                            property_location: Some(WithinPropertyLocation::Value),
432
2
                        }),
433
2
                    });
434
44
                }
435

            
436
46
                if !year_day_list
437
46
                    .iter()
438
129
                    .all(|day| (-366 <= *day && *day <= -1) || (1 <= *day && *day <= 366))
439
4
                {
440
4
                    errors.push(ComponentPropertyError {
441
4
                        message: format!("Invalid BYYEARDAY part at index {part_index}, days must be between 1 and 366, or -366 and -1"),
442
4
                        severity: ICalendarErrorSeverity::Error,
443
4
                        location: Some(ComponentPropertyLocation {
444
4
                            index: property_index,
445
4
                            name: component_property_name(property).to_string(),
446
4
                            property_location: Some(WithinPropertyLocation::Value),
447
4
                        }),
448
4
                    });
449
42
                }
450

            
451
46
                match freq {
452
6
                    RecurFreq::Daily | RecurFreq::Weekly | RecurFreq::Monthly => {
453
6
                        errors.push(ComponentPropertyError {
454
6
                            message: format!("BYYEARDAY part at index {part_index} is not valid for a DAILY, WEEKLY or MONTHLY frequency"),
455
6
                            severity: ICalendarErrorSeverity::Error,
456
6
                            location: Some(ComponentPropertyLocation {
457
6
                                index: property_index,
458
6
                                name: component_property_name(property).to_string(),
459
6
                                property_location: Some(WithinPropertyLocation::Value),
460
6
                            }),
461
6
                        });
462
6
                    }
463
40
                    _ => {}
464
                }
465
            }
466
46
            RecurRulePart::ByWeekNumber(week_list) => {
467
46
                let count = add_count(&mut seen_count, "BYWEEKNO");
468
46
                if count > 1 {
469
2
                    errors.push(ComponentPropertyError {
470
2
                        message: format!("Repeated BYWEEKNO part at index {part_index}"),
471
2
                        severity: ICalendarErrorSeverity::Error,
472
2
                        location: Some(ComponentPropertyLocation {
473
2
                            index: property_index,
474
2
                            name: component_property_name(property).to_string(),
475
2
                            property_location: Some(WithinPropertyLocation::Value),
476
2
                        }),
477
2
                    });
478
44
                }
479

            
480
46
                if !week_list
481
46
                    .iter()
482
129
                    .all(|week| (-53 <= *week && *week <= -1) || (1 <= *week && *week <= 53))
483
4
                {
484
4
                    errors.push(ComponentPropertyError {
485
4
                        message: format!("Invalid BYWEEKNO part at index {part_index}, weeks must be between 1 and 53, or -53 and -1"),
486
4
                        severity: ICalendarErrorSeverity::Error,
487
4
                        location: Some(ComponentPropertyLocation {
488
4
                            index: property_index,
489
4
                            name: component_property_name(property).to_string(),
490
4
                            property_location: Some(WithinPropertyLocation::Value),
491
4
                        }),
492
4
                    });
493
42
                }
494

            
495
46
                if freq != &RecurFreq::Yearly {
496
2
                    errors.push(ComponentPropertyError {
497
2
                        message: format!("BYWEEKNO part at index {part_index} is only valid for a YEARLY frequency"),
498
2
                        severity: ICalendarErrorSeverity::Error,
499
2
                        location: Some(ComponentPropertyLocation {
500
2
                            index: property_index,
501
2
                            name: component_property_name(property).to_string(),
502
2
                            property_location: Some(WithinPropertyLocation::Value),
503
2
                        }),
504
2
                    });
505
44
                }
506
            }
507
            RecurRulePart::ByMonth(_) => {
508
40
                let count = add_count(&mut seen_count, "BYMONTH");
509
40
                if count > 1 {
510
2
                    errors.push(ComponentPropertyError {
511
2
                        message: format!("Repeated BYMONTH part at index {part_index}"),
512
2
                        severity: ICalendarErrorSeverity::Error,
513
2
                        location: Some(ComponentPropertyLocation {
514
2
                            index: property_index,
515
2
                            name: component_property_name(property).to_string(),
516
2
                            property_location: Some(WithinPropertyLocation::Value),
517
2
                        }),
518
2
                    });
519
38
                }
520
            }
521
            RecurRulePart::WeekStart(_) => {
522
46
                let count = add_count(&mut seen_count, "WKST");
523
46
                if count > 1 {
524
2
                    errors.push(ComponentPropertyError {
525
2
                        message: format!("Repeated WKST part at index {part_index}"),
526
2
                        severity: ICalendarErrorSeverity::Error,
527
2
                        location: Some(ComponentPropertyLocation {
528
2
                            index: property_index,
529
2
                            name: component_property_name(property).to_string(),
530
2
                            property_location: Some(WithinPropertyLocation::Value),
531
2
                        }),
532
2
                    });
533
44
                }
534

            
535
46
                let mut is_redundant = true;
536
46
                match freq {
537
                    RecurFreq::Weekly => {
538
14
                        let has_non_default_interval = rule.parts.iter().any(|part| matches!(part, RecurRulePart::Interval(interval) if *interval > 1));
539
10
                        let by_day_specified = rule
540
10
                            .parts
541
10
                            .iter()
542
33
                            .any(|part| matches!(part, RecurRulePart::ByDay(_)));
543
10
                        if has_non_default_interval && by_day_specified {
544
6
                            is_redundant = false;
545
6
                        }
546
                    }
547
                    RecurFreq::Yearly => {
548
34
                        let by_week_number_specified = rule
549
34
                            .parts
550
34
                            .iter()
551
345
                            .any(|part| matches!(part, RecurRulePart::ByWeekNumber(_)));
552
34
                        if by_week_number_specified {
553
32
                            is_redundant = false;
554
32
                        }
555
                    }
556
2
                    _ => {
557
2
                        // Otherwise, it's definitely redundant
558
2
                    }
559
                }
560

            
561
46
                if is_redundant {
562
8
                    errors.push(ComponentPropertyError {
563
8
                        message: format!("WKST part at index {part_index} is redundant"),
564
8
                        severity: ICalendarErrorSeverity::Warning,
565
8
                        location: Some(ComponentPropertyLocation {
566
8
                            index: property_index,
567
8
                            name: component_property_name(property).to_string(),
568
8
                            property_location: Some(WithinPropertyLocation::Value),
569
8
                        }),
570
8
                    });
571
38
                }
572
            }
573
44
            RecurRulePart::BySetPos(set_pos_list) => {
574
44
                let count = add_count(&mut seen_count, "BYSETPOS");
575
44
                if count > 1 {
576
2
                    errors.push(ComponentPropertyError {
577
2
                        message: format!("Repeated BYSETPOS part at index {part_index}"),
578
2
                        severity: ICalendarErrorSeverity::Error,
579
2
                        location: Some(ComponentPropertyLocation {
580
2
                            index: property_index,
581
2
                            name: component_property_name(property).to_string(),
582
2
                            property_location: Some(WithinPropertyLocation::Value),
583
2
                        }),
584
2
                    });
585
42
                }
586

            
587
134
                if !set_pos_list.iter().all(|set_pos| {
588
122
                    (-366 <= *set_pos && *set_pos <= -1) || (1 <= *set_pos && *set_pos <= 366)
589
134
                }) {
590
6
                    errors.push(ComponentPropertyError {
591
6
                        message: format!("Invalid BYSETPOS part at index {part_index}, set positions must be between 1 and 366, or -366 and -1"),
592
6
                        severity: ICalendarErrorSeverity::Error,
593
6
                        location: Some(ComponentPropertyLocation {
594
6
                            index: property_index,
595
6
                            name: component_property_name(property).to_string(),
596
6
                            property_location: Some(WithinPropertyLocation::Value),
597
6
                        }),
598
6
                    });
599
38
                }
600

            
601
198
                let has_other_by_rule = rule.parts.iter().any(|part| {
602
144
                    matches!(
603
186
                        part,
604
                        RecurRulePart::BySecList(_)
605
                            | RecurRulePart::ByMinute(_)
606
                            | RecurRulePart::ByHour(_)
607
                            | RecurRulePart::ByDay(_)
608
                            | RecurRulePart::ByMonthDay(_)
609
                            | RecurRulePart::ByYearDay(_)
610
                            | RecurRulePart::ByWeekNumber(_)
611
                            | RecurRulePart::ByMonth(_)
612
                    )
613
198
                });
614
44
                if !has_other_by_rule {
615
2
                    errors.push(ComponentPropertyError {
616
2
                        message: format!("BYSETPOS part at index {part_index} is not valid without another BYxxx rule part"),
617
2
                        severity: ICalendarErrorSeverity::Error,
618
2
                        location: Some(ComponentPropertyLocation {
619
2
                            index: property_index,
620
2
                            name: component_property_name(property).to_string(),
621
2
                            property_location: Some(WithinPropertyLocation::Value),
622
2
                        }),
623
2
                    });
624
42
                }
625
            }
626
        }
627
    }
628

            
629
172
    Ok(())
630
176
}