diff --git a/crates/store/src/event.rs b/crates/store/src/event.rs index a2fcac0..f77e925 100644 --- a/crates/store/src/event.rs +++ b/crates/store/src/event.rs @@ -1,13 +1,13 @@ use crate::{ - timestamps::{parse_datetime, parse_duration}, + timestamps::{parse_duration, CalDateTime}, Error, }; use anyhow::{anyhow, Result}; -use chrono::{Duration, NaiveDateTime, Timelike}; +use chrono::Duration; use ical::parser::{ical::component::IcalCalendar, Component}; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; -use std::io::BufReader; +use std::{io::BufReader, str::FromStr}; #[derive(Debug, Clone)] pub struct Event { @@ -87,7 +87,7 @@ impl Event { &self.ics } - pub fn get_first_occurence(&self) -> Result { + pub fn get_first_occurence(&self) -> Result { // This is safe since we enforce the event's existance in the constructor let event = self.cal.events.first().unwrap(); let dtstart = event @@ -96,10 +96,10 @@ impl Event { .value .to_owned() .ok_or(anyhow!("DTSTART property has no value!"))?; - parse_datetime(&dtstart) + Ok(CalDateTime::from_str(&dtstart)?) } - pub fn get_last_occurence(&self) -> Result { + pub fn get_last_occurence(&self) -> Result { // This is safe since we enforce the event's existence in the constructor let event = self.cal.events.first().unwrap(); @@ -113,7 +113,7 @@ impl Event { .value .to_owned() .ok_or(anyhow!("DTEND property has no value!"))?; - return parse_datetime(&dtend); + return Ok(CalDateTime::from_str(&dtend)?); } if let Some(dtend_prop) = event.get_property("DURATION") { @@ -126,10 +126,9 @@ impl Event { } let dtstart = self.get_first_occurence()?; - if dtstart.num_seconds_from_midnight() == 0 { - // no explicit time given => whole-day event + if let CalDateTime::Date(_) = dtstart { return Ok(dtstart + Duration::days(1)); - }; + } Err(anyhow!("help, couldn't determine any last occurence")) } diff --git a/crates/store/src/timestamps.rs b/crates/store/src/timestamps.rs index 4b9e82b..5559594 100644 --- a/crates/store/src/timestamps.rs +++ b/crates/store/src/timestamps.rs @@ -1,12 +1,66 @@ +use std::{ops::Add, str::FromStr}; + use crate::Error; -use anyhow::Result; -use chrono::{Duration, NaiveDateTime}; +use anyhow::{anyhow, Result}; +use chrono::{DateTime, Duration, NaiveDate, NaiveDateTime, NaiveTime, Utc}; use lazy_static::lazy_static; lazy_static! { static ref RE_DURATION: regex::Regex = regex::Regex::new(r"^(?[+-])?P((?P\d+)W)?((?P\d+)D)?(T((?P\d+)H)?((?P\d+)M)?((?P\d+)S)?)?$").unwrap(); } +const LOCAL_DATE_TIME: &str = "%Y%m%dT%H%M%S"; +const UTC_DATE_TIME: &str = "%Y%m%dT%H%M%SZ"; +const LOCAL_DATE: &str = "%Y%m%d"; + +pub enum CalDateTime { + // Form 1, example: 19980118T230000 + Local(NaiveDateTime), + // Form 2, example: 19980119T070000Z + Utc(DateTime), + // Form 3, example: TZID=America/New_York:19980119T020000 + // TODO: implement timezone parsing + ExplicitTZ((String, NaiveDateTime)), + Date(NaiveDate), +} + +impl Add for CalDateTime { + type Output = Self; + + fn add(self, duration: Duration) -> Self::Output { + match self { + Self::Local(datetime) => Self::Local(datetime + duration), + Self::Utc(datetime) => Self::Utc(datetime + duration), + Self::ExplicitTZ((tz, datetime)) => Self::ExplicitTZ((tz, datetime + duration)), + Self::Date(date) => Self::Local(date.and_time(NaiveTime::default()) + duration), + } + } +} + +impl FromStr for CalDateTime { + type Err = Error; + + fn from_str(value: &str) -> Result { + if let Ok(datetime) = NaiveDateTime::parse_from_str(value, LOCAL_DATE_TIME) { + return Ok(CalDateTime::Local(datetime)); + } + if let Ok(datetime) = NaiveDateTime::parse_from_str(value, UTC_DATE_TIME) { + return Ok(CalDateTime::Utc(datetime.and_utc())); + } + if let Ok(date) = NaiveDate::parse_from_str(value, LOCAL_DATE) { + return Ok(CalDateTime::Date(date)); + } + Err(Error::Other(anyhow!("Invalid datetime format"))) + } +} + +#[test] +fn test_parse_cal_datetime() { + CalDateTime::from_str("19980118T230000").unwrap(); + CalDateTime::from_str("19980118T230000Z").unwrap(); + CalDateTime::from_str("19980118").unwrap(); +} + pub fn parse_duration(string: &str) -> Result { let captures = RE_DURATION .captures(string) @@ -45,25 +99,3 @@ fn test_parse_duration() { assert_eq!(parse_duration("PT12M").unwrap(), Duration::minutes(12)); assert_eq!(parse_duration("PT12S").unwrap(), Duration::seconds(12)); } - -pub fn parse_datetime(string: &str) -> Result { - // TODO: respect timezones - // - // Format: ^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})(?PZ)?$ - // if Z? - // UTC time - // else - // if TZID given? - // time in TZ - // else - // local time of attendee (can be different actual times for different attendees) - // BUT for this implementation will be UTC for now since this case is annoying - // (sabre-dav does same) - let (datetime, _tz_remainder) = NaiveDateTime::parse_and_remainder(string, "%Y%m%dT%H%M%S")?; - Ok(datetime) -} - -#[test] -fn test_parse_datetime() { - dbg!(parse_datetime("19960329T133000Z").unwrap()); -}