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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            
628
172
    Ok(())
629
176
}