use crate::{
block::Block,
date_or_date_time::{DateIntersectError, DateOrDateTime, EventOverlap},
rrule::{RRule, RRuleParseError},
vevent_iterator::VEventIterator,
TzIdDateTime,
};
use chrono::{DateTime, Datelike, FixedOffset, Local, NaiveDateTime, TimeZone, Utc};
use std::{num::ParseIntError, ops::Range};
use thiserror::Error;
#[derive(Error, Debug)]
pub enum VEventFormatError {
#[error("Missing mandatory colon (block {block:?})")]
MissingColon { block: Block },
#[error("Missing mandatory semicolon (block {block:?})")]
MissingSemicolon { block: Block },
#[error("Missing mandatory field {field:?}. Block:\n{block:?}")]
MissingMandatoryField { block: Block, field: String },
#[error("Error parsing SEQUENCE number {block:?}. Error: {error}")]
SequenceParseIntError { block: Block, error: ParseIntError },
#[error("RRule parse error")]
RRuleParseError(#[from] RRuleParseError),
#[error("TzIdDateTime parse error")]
TzIdDateTimeFormatError(#[from] crate::TzIdDateTimeFormatError),
#[error("Chrono parse error")]
ChronoParseError(#[from] chrono::ParseError),
}
impl VEventFormatError {
pub fn missing_colon(block: Block) -> Self {
VEventFormatError::MissingColon { block }
}
pub fn missing_semicolon(block: Block) -> Self {
VEventFormatError::MissingSemicolon { block }
}
pub fn missing_mandatory_field(block: Block, field: impl Into<String>) -> Self {
VEventFormatError::MissingMandatoryField {
field: field.into(),
block,
}
}
pub fn sequence_parse_int_error(block: Block, error: ParseIntError) -> Self {
VEventFormatError::SequenceParseIntError { block, error }
}
}
#[derive(Debug, Clone)]
pub struct VEvent {
pub dt_created: DateOrDateTime,
pub dt_last_modified: DateOrDateTime,
pub dt_start: DateOrDateTime,
pub dt_end: DateOrDateTime,
pub dt_stamp: DateOrDateTime,
pub summary: String,
pub description: Option<String>,
pub rrule: Option<RRule>,
pub exdates: Vec<TzIdDateTime>,
pub sequence: u32,
pub status: Option<String>,
pub organizer: Option<String>,
pub google_conference_url: Option<String>,
}
#[derive(Debug, Clone)]
pub struct OccurrenceResult {
pub occurrence: Range<DateOrDateTime>,
pub event_overlap: EventOverlap,
}
fn midnight(d: DateTime<Utc>) -> DateTime<Utc> {
Utc.with_ymd_and_hms(d.year(), d.month(), d.day(), 0, 0, 0)
.unwrap()
}
impl VEvent {
pub fn first_occurrence(&self) -> DateOrDateTime {
self.dt_start
}
pub fn next_occurrence_since(
&self,
dt: DateOrDateTime,
) -> Result<Option<OccurrenceResult>, DateIntersectError> {
for occurrence in self.into_iter() {
let event_overlap = {
if let (DateOrDateTime::WholeDay(wd_start), DateOrDateTime::WholeDay(wd_end)) =
(occurrence.start, occurrence.end)
{
dt.intersects(
DateOrDateTime::DateTime(midnight(wd_start)),
DateOrDateTime::DateTime(midnight(wd_end) - chrono::Duration::seconds(1)),
)?
} else {
dt.intersects(occurrence.start, occurrence.end)?
}
};
log::debug!("event_overlap == {:?} ==> {:?}", occurrence, event_overlap);
match event_overlap {
EventOverlap::FinishesPast => {} _ => {
return Ok(Some(OccurrenceResult {
occurrence,
event_overlap,
}));
}
}
}
Ok(None)
}
}
impl TryFrom<Block> for VEvent {
type Error = VEventFormatError;
fn try_from(block: Block) -> Result<Self, Self::Error> {
let mut dt_created = None;
let mut dt_last_modified = None;
let mut dt_start: Option<DateOrDateTime> = None;
let mut dt_end = None;
let mut dt_stamp = None;
let mut summary = None;
let mut description = None;
let mut rrule = None;
let mut exdates = Vec::new();
let mut sequence = None;
let mut status = None;
let mut organizer = None;
let mut google_conference_url = None;
for line in block.inner_lines.iter() {
let idx_colon = line.find(':').unwrap_or(line.len());
let tag = &line[0..idx_colon];
let extra = if idx_colon + 1 < line.len() {
Some(&line[idx_colon + 1..])
} else {
None
};
match tag {
"LAST-MODIFIED" => {
dt_last_modified =
Some(string_to_date_or_datetime(extra.ok_or_else(|| {
VEventFormatError::missing_colon(block.clone())
})?)?);
}
"DTSTART" => {
dt_start = Some(DateOrDateTime::DateTime(string_to_datetime(
extra.ok_or_else(|| VEventFormatError::missing_colon(block.clone()))?,
)?));
}
"DTEND" => {
dt_end =
Some(string_to_date_or_datetime(extra.ok_or_else(|| {
VEventFormatError::missing_colon(block.clone())
})?)?);
}
"CREATED" => {
dt_created =
Some(string_to_date_or_datetime(extra.ok_or_else(|| {
VEventFormatError::missing_colon(block.clone())
})?)?);
}
"DTSTAMP" => {
dt_stamp =
Some(string_to_date_or_datetime(extra.ok_or_else(|| {
VEventFormatError::missing_colon(block.clone())
})?)?);
}
"SUMMARY" => {
summary = Some(
extra
.ok_or_else(|| VEventFormatError::missing_colon(block.clone()))?
.to_string(),
);
}
"DESCRIPTION" => description = extra.map(|e| e.to_string()),
"SEQUENCE" => {
sequence = extra.map(|e| e.parse::<u32>()).transpose().map_err(|e| {
VEventFormatError::sequence_parse_int_error(block.clone(), e)
})?;
}
"RRULE" => {
rrule = Some(
extra
.ok_or_else(|| VEventFormatError::missing_colon(block.clone()))?
.parse::<RRule>()?,
);
}
"STATUS" => {
status = Some(
extra
.ok_or_else(|| VEventFormatError::missing_colon(block.clone()))?
.to_string(),
);
}
"X-GOOGLE-CONFERENCE" => {
google_conference_url = extra.map(|e| e.to_string());
}
_ => {} }
let idx_semicolon = line.find(';').unwrap_or(line.len());
let tag = &line[0..idx_semicolon];
let extra = if idx_semicolon + 1 < line.len() {
Some(&line[idx_semicolon + 1..])
} else {
None
};
match tag {
"ORGANIZER" => {
organizer = Some(
extra
.ok_or_else(|| VEventFormatError::missing_colon(block.clone()))?
.to_string(),
);
}
"EXDATE" => {
let extra =
extra.ok_or_else(|| VEventFormatError::missing_semicolon(block.clone()))?;
log::trace!("parsing EXDATE ==> {}", extra);
if let Some(colon_idx) = extra.find(':') {
let prefix = &extra[0..=colon_idx];
let values_str = &extra[colon_idx + 1..];
for val in values_str.split(',').filter(|s| !s.is_empty()) {
let reconstructed = format!("{}{}", prefix, val);
exdates.push(TzIdDateTime::try_from(reconstructed.as_str())?);
}
} else {
exdates.push(TzIdDateTime::try_from(extra)?);
}
}
"DTSTART" => {
dt_start = Some(
extra
.map(to_tziddate_or_date)
.transpose()?
.ok_or_else(|| VEventFormatError::missing_semicolon(block.clone()))?,
);
}
"DTEND" => {
dt_end = Some(
extra
.map(to_tziddate_or_date)
.transpose()?
.ok_or_else(|| VEventFormatError::missing_semicolon(block.clone()))?,
);
}
_ => {} }
}
let dt_start = dt_start
.ok_or_else(|| VEventFormatError::missing_mandatory_field(block.clone(), "DTSTART"))?;
Ok(VEvent {
dt_last_modified: dt_last_modified.ok_or_else(|| {
VEventFormatError::missing_mandatory_field(block.clone(), "LAST-MODIFIED")
})?,
dt_start,
dt_end: dt_end.unwrap_or(dt_start), dt_created: dt_created.ok_or_else(|| {
VEventFormatError::missing_mandatory_field(block.clone(), "CREATED")
})?,
dt_stamp: dt_stamp.ok_or_else(|| {
VEventFormatError::missing_mandatory_field(block.clone(), "DTSTAMP")
})?,
summary: summary.ok_or_else(|| {
VEventFormatError::missing_mandatory_field(block.clone(), "SUMMARY")
})?,
description,
rrule,
exdates,
sequence: sequence.ok_or_else(|| {
VEventFormatError::missing_mandatory_field(block.clone(), "SEQUENCE")
})?,
status,
organizer,
google_conference_url,
})
}
}
impl<'a> IntoIterator for &'a VEvent {
type Item = Range<DateOrDateTime>;
type IntoIter = VEventIterator<'a>;
fn into_iter(self) -> Self::IntoIter {
VEventIterator::new(self)
}
}
pub(crate) fn string_to_date_or_datetime(s: &str) -> Result<DateOrDateTime, chrono::ParseError> {
Ok(if s.len() == 8 {
let date = string_to_date(s)?;
DateOrDateTime::WholeDay(
Utc.with_ymd_and_hms(date.year(), date.month(), date.day(), 0, 0, 0)
.unwrap(),
)
} else {
DateOrDateTime::DateTime(string_to_datetime(s)?)
})
}
fn string_to_datetime(s: &str) -> Result<DateTime<Utc>, chrono::ParseError> {
Ok(if s.ends_with('Z') {
DateTime::<FixedOffset>::parse_from_str(s, "%Y%m%dT%H%M%S%#z")?.with_timezone(&Utc)
} else {
let a = NaiveDateTime::parse_from_str(s, "%Y%m%dT%H%M%S")?;
let tz_offset = Local::now().offset().to_owned();
tz_offset
.from_local_datetime(&a)
.unwrap()
.with_timezone(&Utc)
})
}
fn string_to_date(s: &str) -> Result<DateTime<Utc>, chrono::ParseError> {
Ok(Utc.from_utc_datetime(&NaiveDateTime::parse_from_str(
&format!("{s}T000000"),
"%Y%m%dT%H%M%S",
)?))
}
fn to_tziddate_or_date(
s: &str,
) -> Result<DateOrDateTime, crate::tzid_date_time::TzIdDateTimeFormatError> {
Ok(s.parse::<TzIdDateTime>()?.date_time)
}