Skip to content

Commit 2dffa94

Browse files
authored
Merge pull request #100 from fmeringdal/fix/display-rruleset
Fix display impl for RRuleSet
2 parents 307931c + 0d74f05 commit 2dffa94

File tree

3 files changed

+127
-4
lines changed

3 files changed

+127
-4
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111
- Make `ParseError` and `ValidationError` public
1212
- `EXRULE`s are now correctly added as exrules on the `RRuleSet` when parsed from a string, instead of being incorrectly added as an rrule.
1313
- Add a `RRuleSet::set_from_string` method to support loading rules without DTSTART. This is useful particularly when working with the Google Calendar API.
14+
- Fix to include `RDATE`, `EXRULE` and `EXDATE` values if used in the `Display` implementation of `RRuleSet`.
1415

1516
## 0.11.0 (2023-07-18)
1617

rrule/src/core/rruleset.rs

Lines changed: 97 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -284,14 +284,107 @@ impl Display for RRuleSet {
284284
/// Prints a valid set of iCalendar properties which can be used to create a new [`RRuleSet`] later.
285285
/// You may use the generated string to create a new iCalendar component, like VEVENT.
286286
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
287-
let properties = self
287+
let start_datetime = format!("DTSTART{}", datetime_to_ical_format(&self.dt_start));
288+
289+
let mut rrules = self
288290
.rrule
289291
.iter()
290-
.map(ToString::to_string)
292+
.map(|rrule| format!("RRULE:{rrule}"))
293+
.collect::<Vec<_>>()
294+
.join("\n");
295+
if !rrules.is_empty() {
296+
rrules = format!("\n{rrules}");
297+
}
298+
299+
let mut rdates = self
300+
.rdate
301+
.iter()
302+
.map(|dt| dt.format("%Y%m%dT%H%M%SZ").to_string())
303+
.collect::<Vec<_>>()
304+
.join(",");
305+
if !rdates.is_empty() {
306+
// TODO: check if original VALUE prop was DATE or PERIOD
307+
rdates = format!("\nRDATE;VALUE=DATE-TIME:{rdates}");
308+
}
309+
310+
let mut exrules = self
311+
.exrule
312+
.iter()
313+
.map(|exrule| format!("EXRULE:{exrule}"))
291314
.collect::<Vec<_>>()
292315
.join("\n");
293-
let datetime = datetime_to_ical_format(&self.dt_start);
316+
if !exrules.is_empty() {
317+
exrules = format!("\n{exrules}");
318+
}
319+
320+
let mut exdates = self
321+
.exdate
322+
.iter()
323+
.map(|dt| dt.format("%Y%m%dT%H%M%SZ").to_string())
324+
.collect::<Vec<_>>()
325+
.join(",");
326+
if !exdates.is_empty() {
327+
// TODO: check if original VALUE prop was DATE or PERIOD
328+
exdates = format!("\nEXDATE;VALUE=DATE-TIME:{exdates}");
329+
}
330+
331+
write!(f, "{start_datetime}{rrules}{rdates}{exrules}{exdates}")
332+
}
333+
}
294334

295-
write!(f, "DTSTART{}\n{}", datetime, properties)
335+
#[cfg(feature = "exrule")]
336+
#[cfg(test)]
337+
mod tests {
338+
use std::str::FromStr;
339+
340+
use chrono::{Month, TimeZone};
341+
342+
use crate::{Frequency, RRule, RRuleSet, Tz};
343+
344+
#[test]
345+
fn rruleset_string_roundtrip() {
346+
let rruleset_str = "DTSTART:20120201T093000Z\nRRULE:FREQ=DAILY;COUNT=3;BYHOUR=9;BYMINUTE=30;BYSECOND=0\nRDATE;VALUE=DATE-TIME:19970101T000000Z,19970120T000000Z\nEXRULE:FREQ=YEARLY;COUNT=8;BYMONTH=6,7;BYMONTHDAY=1;BYHOUR=9;BYMINUTE=30;BYSECOND=0\nEXDATE;VALUE=DATE-TIME:19970121T000000Z";
347+
let rruleset = RRuleSet::from_str(rruleset_str).unwrap();
348+
349+
// Check start date
350+
let dt_start = Tz::UTC.with_ymd_and_hms(2012, 2, 1, 9, 30, 0).unwrap();
351+
assert_eq!(rruleset.dt_start, dt_start);
352+
353+
// Check rrule
354+
assert_eq!(
355+
rruleset.rrule,
356+
vec![RRule::new(Frequency::Daily)
357+
.count(3)
358+
.validate(dt_start)
359+
.unwrap()]
360+
);
361+
362+
// Check rdate
363+
assert_eq!(
364+
rruleset.rdate,
365+
vec![
366+
Tz::UTC.with_ymd_and_hms(1997, 1, 1, 0, 0, 0).unwrap(),
367+
Tz::UTC.with_ymd_and_hms(1997, 1, 20, 0, 0, 0).unwrap()
368+
]
369+
);
370+
371+
// Check exrule
372+
assert_eq!(
373+
rruleset.exrule,
374+
vec![RRule::new(Frequency::Yearly)
375+
.count(8)
376+
.by_month(&[Month::June, Month::July])
377+
.validate(dt_start)
378+
.unwrap()]
379+
);
380+
381+
// Check exdate
382+
assert_eq!(
383+
rruleset.exdate,
384+
vec![Tz::UTC.with_ymd_and_hms(1997, 1, 21, 0, 0, 0).unwrap()]
385+
);
386+
387+
// Serialize to string again
388+
assert_eq!(rruleset.to_string(), rruleset_str);
296389
}
297390
}

rrule/src/parser/content_line/date_content_line.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
use std::{collections::HashMap, str::FromStr};
22

3+
use log::warn;
4+
35
use crate::{
46
core::DateTime,
57
parser::{
@@ -39,6 +41,33 @@ impl<'a> TryFrom<ContentLineCaptures<'a>> for Vec<DateTime> {
3941
.transpose()?
4042
.unwrap_or_default();
4143

44+
match parameters
45+
.get(&DateParameter::Value)
46+
.map(|val| val.to_ascii_lowercase())
47+
.as_deref()
48+
{
49+
Some("date") => {
50+
warn!(
51+
"Parameter `DATE` is not supported for property name: `{}`. The dates will be interpreter with the `DATE-TIME` parameter instead.",
52+
value.property_name
53+
);
54+
}
55+
Some("period") => {
56+
warn!(
57+
"Parameter `PERIOD` is not supported for property name: `{}`. The dates will be interpreter with the `DATE-TIME` parameter instead.",
58+
value.property_name
59+
);
60+
}
61+
Some("date-time") => {}
62+
Some(param) => {
63+
warn!(
64+
"Encountered unexpected parameter `{param}` for property name: `{}`",
65+
value.property_name
66+
);
67+
}
68+
None => {}
69+
}
70+
4271
let timezone = parameters
4372
.get(&DateParameter::Timezone)
4473
.map(|tz| parse_timezone(tz))

0 commit comments

Comments
 (0)