Skip to content

Commit

Permalink
Merge pull request #105 from sciyoshi/rruleset-set-from-string
Browse files Browse the repository at this point in the history
Add `RRuleSet::set_from_string` to support loading rules without DTSTART
  • Loading branch information
fmeringdal authored Apr 1, 2024
2 parents 8c61634 + 1c506ab commit 307931c
Show file tree
Hide file tree
Showing 3 changed files with 49 additions and 26 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- MSRV is bumped to `v1.74.0` from `v1.64.0`
- Make `ParseError` and `ValidationError` public
- `EXRULE`s are now correctly added as exrules on the `RRuleSet` when parsed from a string, instead of being incorrectly added as an rrule.
- Add a `RRuleSet::set_from_string` method to support loading rules without DTSTART. This is useful particularly when working with the Google Calendar API.

## 0.11.0 (2023-07-18)

Expand Down
58 changes: 40 additions & 18 deletions rrule/src/core/rruleset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use crate::core::datetime::datetime_to_ical_format;
use crate::core::utils::collect_with_error;
use crate::core::DateTime;
use crate::parser::{ContentLine, Grammar};
use crate::{RRule, RRuleError};
use crate::{ParseError, RRule, RRuleError};
#[cfg(feature = "serde")]
use serde_with::{serde_as, DeserializeFromStr, SerializeDisplay};
use std::fmt::Display;
Expand Down Expand Up @@ -211,34 +211,22 @@ impl RRuleSet {
pub fn all_unchecked(self) -> Vec<DateTime> {
collect_with_error(self.into_iter(), &self.after, &self.before, true, None).dates
}
}

impl FromStr for RRuleSet {
type Err = RRuleError;

/// Creates an [`RRuleSet`] from a string if input is valid.
///
/// # Errors
///
/// Returns [`RRuleError`], if iCalendar string contains invalid parts.
fn from_str(s: &str) -> Result<Self, Self::Err> {
let Grammar {
start,
content_lines,
} = Grammar::from_str(s)?;
fn set_from_content_lines(self, content_lines: Vec<ContentLine>) -> Result<Self, RRuleError> {
let dt_start = self.dt_start;

content_lines.into_iter().try_fold(
Self::new(start.datetime),
self,
|rrule_set, content_line| match content_line {
ContentLine::RRule(rrule) => rrule
.validate(start.datetime)
.validate(dt_start)
.map(|rrule| rrule_set.rrule(rrule)),
#[allow(unused_variables)]
ContentLine::ExRule(exrule) => {
#[cfg(feature = "exrule")]
{
exrule
.validate(start.datetime)
.validate(dt_start)
.map(|exrule| rrule_set.exrule(exrule))
}
#[cfg(not(feature = "exrule"))]
Expand All @@ -256,6 +244,40 @@ impl FromStr for RRuleSet {
},
)
}

/// Set the [`RRuleSet`] properties from a string. If a DTSTART is found, it will be used as the start datetime.
pub fn set_from_string(mut self, s: &str) -> Result<Self, RRuleError> {
let Grammar {
start,
content_lines,
} = Grammar::from_str(s)?;

if let Some(dtstart) = start {
self.dt_start = dtstart.datetime;
}

self.set_from_content_lines(content_lines)
}
}

impl FromStr for RRuleSet {
type Err = RRuleError;

/// Creates an [`RRuleSet`] from a string if input is valid.
///
/// # Errors
///
/// Returns [`RRuleError`], if iCalendar string contains invalid parts.
fn from_str(s: &str) -> Result<Self, Self::Err> {
let Grammar {
start,
content_lines,
} = Grammar::from_str(s)?;

let start = start.ok_or(ParseError::MissingStartDate)?;

Self::new(start.datetime).set_from_content_lines(content_lines)
}
}

impl Display for RRuleSet {
Expand Down
16 changes: 8 additions & 8 deletions rrule/src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ use self::content_line::{PropertyName, StartDateContentLine};
/// Grammar represents a well-formatted rrule input.
#[derive(Debug, PartialEq)]
pub(crate) struct Grammar {
pub start: StartDateContentLine,
pub start: Option<StartDateContentLine>,
pub content_lines: Vec<ContentLine>,
}

Expand All @@ -36,7 +36,7 @@ impl FromStr for Grammar {
.iter()
.find(|parts| matches!(parts.property_name, PropertyName::DtStart))
.map(StartDateContentLine::try_from)
.ok_or(ParseError::MissingStartDate)??;
.transpose()?;

let mut content_lines = vec![];

Expand Down Expand Up @@ -90,7 +90,7 @@ mod test {
let tests = [
(
"DTSTART:19970902T090000Z\nRRULE:FREQ=YEARLY;COUNT=3\n", Grammar {
start: StartDateContentLine { datetime: UTC.with_ymd_and_hms(1997, 9, 2,9, 0, 0).unwrap(), timezone: Some(UTC), value: "DATE-TIME" },
start: Some(StartDateContentLine { datetime: UTC.with_ymd_and_hms(1997, 9, 2,9, 0, 0).unwrap(), timezone: Some(UTC), value: "DATE-TIME" }),
content_lines: vec![
ContentLine::RRule(RRule {
freq: Frequency::Yearly,
Expand All @@ -101,7 +101,7 @@ mod test {
}
),
("DTSTART:20120201T093000Z\nRRULE:FREQ=WEEKLY;INTERVAL=5;UNTIL=20130130T230000Z;BYDAY=MO,FR", Grammar {
start: StartDateContentLine { datetime: UTC.with_ymd_and_hms(2012, 2, 1,9, 30, 0).unwrap(), timezone: Some(UTC), value: "DATE-TIME" },
start: Some(StartDateContentLine { datetime: UTC.with_ymd_and_hms(2012, 2, 1,9, 30, 0).unwrap(), timezone: Some(UTC), value: "DATE-TIME" }),
content_lines: vec![
ContentLine::RRule(RRule {
freq: Frequency::Weekly,
Expand All @@ -113,7 +113,7 @@ mod test {
]
}),
("DTSTART:20120201T120000Z\nRRULE:FREQ=DAILY;COUNT=5\nEXDATE;TZID=Europe/Berlin:20120202T130000,20120203T130000", Grammar {
start: StartDateContentLine { datetime: UTC.with_ymd_and_hms(2012, 2, 1,12, 0, 0).unwrap(), timezone: Some(UTC), value: "DATE-TIME" },
start: Some(StartDateContentLine { datetime: UTC.with_ymd_and_hms(2012, 2, 1,12, 0, 0).unwrap(), timezone: Some(UTC), value: "DATE-TIME" }),
content_lines: vec![
ContentLine::RRule(RRule {
freq: Frequency::Daily,
Expand All @@ -127,7 +127,7 @@ mod test {
]
}),
("DTSTART:20120201T120000Z\nRRULE:FREQ=DAILY;COUNT=5\nEXDATE;TZID=Europe/Berlin:20120202T130000,20120203T130000\nEXRULE:FREQ=WEEKLY;COUNT=10", Grammar {
start: StartDateContentLine { datetime: UTC.with_ymd_and_hms(2012, 2, 1,12, 0, 0).unwrap(), timezone: Some(UTC), value: "DATE-TIME" },
start: Some(StartDateContentLine { datetime: UTC.with_ymd_and_hms(2012, 2, 1,12, 0, 0).unwrap(), timezone: Some(UTC), value: "DATE-TIME" }),
content_lines: vec![
ContentLine::RRule(RRule {
freq: Frequency::Daily,
Expand Down Expand Up @@ -167,15 +167,15 @@ mod test {
}

#[test]
fn rejects_input_without_start_date() {
fn allows_input_without_start_date() {
let tests = [
"RRULE:FREQ=WEEKLY;INTERVAL=5;UNTIL=20130130T230000Z;BYDAY=MO,FR",
"RDATE;TZID=Europe/Berlin:20120202T130000,20120203T130000",
"RRULE:FREQ=DAILY;COUNT=5\nEXDATE;TZID=Europe/Berlin:20120202T130000,20120203T130000",
];
for input in tests {
let res = Grammar::from_str(input);
assert_eq!(res, Err(ParseError::MissingStartDate));
assert!(res.is_ok());
}
}
}

0 comments on commit 307931c

Please sign in to comment.