Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft: Introduce new DateTime #78

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions rrule-debugger/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@ mod iter_rrule;
mod parser_rrule;
mod simple_logger;

use chrono::DateTime;
use chrono_tz::Tz;
use clap::Parser;
use log::LevelFilter;
use rrule::DateTime;

const CRASHES_PATH: &str = "rrule-afl-fuzz/out/default/crashes/";

Expand Down Expand Up @@ -122,8 +121,8 @@ fn read_all_crash_file() -> Vec<Vec<u8>> {
list
}

pub fn print_all_datetimes(list: &[DateTime<Tz>]) {
let formatter = |dt: &DateTime<Tz>| -> String { format!(" \"{}\",\n", dt.to_rfc3339()) };
pub fn print_all_datetimes(list: &[DateTime]) {
let formatter = |dt: &DateTime| -> String { format!(" \"{}\",\n", dt.to_rfc3339()) };
println!("[\n{}]", list.iter().map(formatter).collect::<String>(),);
}

Expand Down
1 change: 0 additions & 1 deletion rrule/examples/manual_iter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
//!
//! Manually iterate over an `RRule`.

use chrono::Datelike;
use rrule::RRuleSet;

fn main() {
Expand Down
2 changes: 1 addition & 1 deletion rrule/examples/manual_rrule.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//!
//! Create an [`RRule`] object.

use chrono::{Datelike, TimeZone, Timelike};
use chrono::TimeZone;
use chrono_tz::UTC;
use rrule::{Frequency, RRule};

Expand Down
204 changes: 197 additions & 7 deletions rrule/src/core/datetime.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
use chrono::{Datelike, Duration, NaiveTime, Timelike};
use std::fmt::Display;

use chrono::{Datelike, Duration, Local, NaiveTime, TimeZone, Timelike, Weekday};
use chrono_tz::Tz;

pub(crate) type DateTime = chrono::DateTime<Tz>;
use crate::iter::add_time_to_date;

pub(crate) fn duration_from_midnight(time: NaiveTime) -> Duration {
Duration::hours(i64::from(time.hour()))
Expand Down Expand Up @@ -36,12 +38,200 @@ pub(crate) fn datetime_to_ical_format(dt: &DateTime) -> String {
let mut tz_prefix = String::new();
let mut tz_postfix = String::new();
let tz = dt.timezone();
if tz == Tz::UTC {
tz_postfix = "Z".to_string();
} else {
tz_prefix = format!(";TZID={}", tz.name());
};
match tz {
RRuleTimeZone::Local => {}
RRuleTimeZone::Tz(tz) => match tz {
Tz::UTC => {
tz_postfix = "Z".to_string();
}
_ => {
tz_prefix = format!(";TZID={}", tz.name());
}
},
}

let dt = dt.format("%Y%m%dT%H%M%S");
format!("{}:{}{}", tz_prefix, dt, tz_postfix)
}

#[derive(Debug, PartialEq)]
pub enum RRuleTimeZone {
Local,
Tz(chrono_tz::Tz),
}

impl RRuleTimeZone {
pub fn name(&self) -> String {
match self {
RRuleTimeZone::Local => "Local".into(),
RRuleTimeZone::Tz(tz) => tz.name().into(),
}
}

pub fn datetime(&self, year: i32, month: u32, day: u32, time: NaiveTime) -> Option<DateTime> {
match self {
RRuleTimeZone::Local => {
let date = Local.ymd(year, month, day);
add_time_to_date(date, time)
}
RRuleTimeZone::Tz(tz) => {
let date = tz.ymd(year, month, day);
add_time_to_date(date, time)
}
}
}
}

/// DateTime that is able to represent datetimes in Local and chrono_tz::Tz
/// timezones.
#[derive(Debug, Clone, Eq, Ord, Copy)]
pub enum DateTime {
/// Local timezone
Local(chrono::DateTime<chrono::Local>),
/// Specific non-local timezone
Tz(chrono::DateTime<chrono_tz::Tz>),
}

impl PartialEq for DateTime {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Local(l0), Self::Local(r0)) => l0 == r0,
(Self::Tz(l0), Self::Tz(r0)) => l0 == r0,
(Self::Tz(l0), Self::Local(r0)) => l0 == r0,
(Self::Local(l0), Self::Tz(r0)) => l0 == r0,
}
}
}

impl PartialOrd for DateTime {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
match (self, other) {
(Self::Local(l0), Self::Local(r0)) => l0.partial_cmp(r0),
(Self::Tz(l0), Self::Tz(r0)) => l0.partial_cmp(r0),
(Self::Tz(l0), Self::Local(r0)) => l0.partial_cmp(r0),
(Self::Local(l0), Self::Tz(r0)) => l0.partial_cmp(r0),
}
}
}

impl DateTime {
pub fn with_timezone<T: TimeZone>(&self, tz: &T) -> chrono::DateTime<T> {
match self {
Self::Local(dt) => dt.with_timezone(tz),
Self::Tz(dt) => dt.with_timezone(tz),
}
}

pub fn timezone(&self) -> RRuleTimeZone {
match self {
Self::Local(_) => RRuleTimeZone::Local,
Self::Tz(dt) => RRuleTimeZone::Tz(dt.timezone()),
}
}

pub fn year(&self) -> i32 {
match self {
Self::Local(dt) => dt.year(),
Self::Tz(dt) => dt.year(),
}
}

pub fn month(&self) -> u32 {
match self {
Self::Local(dt) => dt.month(),
Self::Tz(dt) => dt.month(),
}
}

pub fn weekday(&self) -> Weekday {
match self {
Self::Local(dt) => dt.weekday(),
Self::Tz(dt) => dt.weekday(),
}
}

pub fn day(&self) -> u32 {
match self {
Self::Local(dt) => dt.day(),
Self::Tz(dt) => dt.day(),
}
}

pub fn hour(&self) -> u32 {
match self {
Self::Local(dt) => dt.hour(),
Self::Tz(dt) => dt.hour(),
}
}

pub fn minute(&self) -> u32 {
match self {
Self::Local(dt) => dt.minute(),
Self::Tz(dt) => dt.minute(),
}
}

pub fn second(&self) -> u32 {
match self {
Self::Local(dt) => dt.second(),
Self::Tz(dt) => dt.second(),
}
}

pub fn timestamp(&self) -> i64 {
match self {
Self::Local(dt) => dt.timestamp(),
Self::Tz(dt) => dt.timestamp(),
}
}

pub fn to_rfc3339(&self) -> String {
match self {
Self::Local(dt) => dt.to_rfc3339(),
Self::Tz(dt) => dt.to_rfc3339(),
}
}

pub fn format<'a>(
&self,
fmt: &'a str,
) -> chrono::format::DelayedFormat<chrono::format::StrftimeItems<'a>> {
match self {
Self::Local(dt) => dt.format(fmt),
Self::Tz(dt) => dt.format(fmt),
}
}
}

impl From<chrono::DateTime<chrono_tz::Tz>> for DateTime {
fn from(dt: chrono::DateTime<chrono_tz::Tz>) -> Self {
Self::Tz(dt)
}
}

impl From<&chrono::DateTime<chrono_tz::Tz>> for DateTime {
fn from(dt: &chrono::DateTime<chrono_tz::Tz>) -> Self {
Self::Tz(*dt)
}
}

impl From<chrono::DateTime<chrono::Local>> for DateTime {
fn from(dt: chrono::DateTime<chrono::Local>) -> Self {
Self::Local(dt)
}
}

impl From<chrono::DateTime<chrono::Utc>> for DateTime {
fn from(dt: chrono::DateTime<chrono::Utc>) -> Self {
Self::Tz(dt.with_timezone(&chrono_tz::UTC))
}
}

impl Display for DateTime {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Local(dt) => dt.fmt(f),
Self::Tz(dt) => dt.fmt(f),
}
}
}
3 changes: 2 additions & 1 deletion rrule/src/core/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ pub(crate) mod utils;

pub use self::rrule::{Frequency, NWeekday, RRule};
pub use self::rruleset::RRuleSet;
pub use datetime::DateTime;
pub(crate) use datetime::{
duration_from_midnight, get_day, get_hour, get_minute, get_month, get_second, DateTime,
duration_from_midnight, get_day, get_hour, get_minute, get_month, get_second, RRuleTimeZone,
};

#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
Expand Down
32 changes: 27 additions & 5 deletions rrule/src/core/rrule.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@ use crate::core::get_minute;
use crate::core::get_month;
use crate::core::get_second;
use crate::parser::str_to_weekday;
use crate::parser::ContentLineCaptures;
use crate::parser::ParseError;
use crate::validator::validate_rrule;
use crate::{RRuleError, RRuleIter, RRuleSet, Unvalidated, Validated};
use chrono::{Datelike, Month, Weekday};
use chrono::{Month, Weekday};
use chrono_tz::Tz;
#[cfg(feature = "serde")]
use serde_with::{DeserializeFromStr, SerializeDisplay};
use serde_with::{serde_as, DeserializeFromStr, SerializeDisplay};
use std::cmp::Ordering;
use std::fmt::Display;
use std::fmt::Formatter;
Expand Down Expand Up @@ -212,6 +213,8 @@ fn weekday_to_str(d: Weekday) -> String {
/// - `Unvalidated`, which is the raw string representation of the RRULE
/// - `Validated`, which is when the `RRule` has been parsed and validated, based on the start date
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", serde_as)]
#[cfg_attr(feature = "serde", derive(DeserializeFromStr, SerializeDisplay))]
pub struct RRule<Stage = Validated> {
/// The frequency of the rrule.
/// For example: yearly, weekly, hourly
Expand All @@ -225,6 +228,7 @@ pub struct RRule<Stage = Validated> {
pub(crate) count: Option<u32>,
/// The end date after which new events will no longer be generated.
/// If the `DateTime` is equal to an instance of the event it will be the last event.
#[cfg_attr(feature = "serde", serde_as(as = "DisplayFromStr"))]
pub(crate) until: Option<DateTime>,
/// The start day of the week.
/// This will affect recurrences based on weekly periods.
Expand Down Expand Up @@ -269,6 +273,7 @@ pub struct RRule<Stage = Validated> {
/// Note: Only used when `by-easter` feature flag is set. Otherwise, it is ignored.
pub(crate) by_easter: Option<i16>,
/// A phantom data to have the stage (unvalidated or validated).
#[cfg_attr(feature = "serde", serde_as(as = "ignore"))]
pub(crate) stage: PhantomData<Stage>,
}

Expand Down Expand Up @@ -332,7 +337,7 @@ impl RRule<Unvalidated> {
/// upper-bound limit of the recurrence.
#[must_use]
pub fn until(mut self, until: chrono::DateTime<Tz>) -> Self {
self.until = Some(until);
self.until = Some(until.into());
self
}

Expand Down Expand Up @@ -542,7 +547,11 @@ impl RRule<Unvalidated> {
/// # Errors
///
/// If the properties are not valid it will return [`RRuleError`].
pub fn validate(self, dt_start: DateTime) -> Result<RRule<Validated>, RRuleError> {
pub fn validate<T>(self, dt_start: T) -> Result<RRule<Validated>, RRuleError>
where
DateTime: From<T>,
{
let dt_start = dt_start.into();
let rrule = self.finalize_parsed_rrule(&dt_start);

// Validate required checks (defined by RFC 5545)
Expand Down Expand Up @@ -574,7 +583,11 @@ impl RRule<Unvalidated> {
/// # Errors
///
/// Returns [`RRuleError::ValidationError`] in case the rrule is invalid.
pub fn build(self, dt_start: DateTime) -> Result<RRuleSet, RRuleError> {
pub fn build<T>(self, dt_start: T) -> Result<RRuleSet, RRuleError>
where
DateTime: From<T>,
T: Copy,
{
let rrule = self.validate(dt_start)?;
let rrule_set = RRuleSet::new(dt_start).rrule(rrule);
Ok(rrule_set)
Expand Down Expand Up @@ -817,3 +830,12 @@ impl<S> RRule<S> {
self.by_easter.as_ref()
}
}

impl FromStr for RRule<Unvalidated> {
type Err = RRuleError;

fn from_str(s: &str) -> Result<Self, Self::Err> {
let parts = ContentLineCaptures::new(s)?;
RRule::try_from(parts).map_err(From::from)
}
}
Loading