mirror of
https://github.com/lennart-k/rustical.git
synced 2025-12-14 03:32:15 +00:00
Move ical-related stuff to dedicated rustical_ical crate
This commit is contained in:
18
crates/ical/Cargo.toml
Normal file
18
crates/ical/Cargo.toml
Normal file
@@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "rustical_ical"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
description.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[dependencies]
|
||||
chrono.workspace = true
|
||||
chrono-tz.workspace = true
|
||||
thiserror.workspace = true
|
||||
derive_more.workspace = true
|
||||
rustical_xml.workspace = true
|
||||
ical.workspace = true
|
||||
lazy_static.workspace = true
|
||||
regex.workspace = true
|
||||
strum.workspace = true
|
||||
strum_macros.workspace = true
|
||||
9
crates/ical/src/lib.rs
Normal file
9
crates/ical/src/lib.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
pub mod rrule;
|
||||
|
||||
mod property_ext;
|
||||
pub use property_ext::*;
|
||||
|
||||
mod timestamp;
|
||||
mod timezone;
|
||||
pub use timestamp::*;
|
||||
pub use timezone::*;
|
||||
46
crates/ical/src/property_ext.rs
Normal file
46
crates/ical/src/property_ext.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
use ical::{generator::IcalEvent, property::Property};
|
||||
|
||||
pub trait IcalProperty {
|
||||
fn get_param(&self, name: &str) -> Option<Vec<&str>>;
|
||||
fn get_value_type(&self) -> Option<&str>;
|
||||
fn get_tzid(&self) -> Option<&str>;
|
||||
}
|
||||
|
||||
impl IcalProperty for ical::property::Property {
|
||||
fn get_param(&self, name: &str) -> Option<Vec<&str>> {
|
||||
self.params
|
||||
.as_ref()?
|
||||
.iter()
|
||||
.find(|(key, _)| name == key)
|
||||
.map(|(_, value)| value.iter().map(String::as_str).collect())
|
||||
}
|
||||
|
||||
fn get_value_type(&self) -> Option<&str> {
|
||||
self.get_param("VALUE")
|
||||
.and_then(|params| params.into_iter().next())
|
||||
}
|
||||
|
||||
fn get_tzid(&self) -> Option<&str> {
|
||||
self.get_param("TZID")
|
||||
.and_then(|params| params.into_iter().next())
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ComponentMut {
|
||||
fn remove_property(&mut self, name: &str);
|
||||
fn set_property(&mut self, prop: Property);
|
||||
fn push_property(&mut self, prop: Property);
|
||||
}
|
||||
|
||||
impl ComponentMut for IcalEvent {
|
||||
fn remove_property(&mut self, name: &str) {
|
||||
self.properties.retain(|prop| prop.name != name);
|
||||
}
|
||||
fn set_property(&mut self, prop: Property) {
|
||||
self.remove_property(&prop.name);
|
||||
self.push_property(prop);
|
||||
}
|
||||
fn push_property(&mut self, prop: Property) {
|
||||
self.properties.push(prop);
|
||||
}
|
||||
}
|
||||
56
crates/ical/src/rrule/iter.rs
Normal file
56
crates/ical/src/rrule/iter.rs
Normal file
@@ -0,0 +1,56 @@
|
||||
use crate::CalDateTime;
|
||||
|
||||
use super::{RecurrenceLimit, RecurrenceRule};
|
||||
|
||||
impl RecurrenceRule {
|
||||
pub fn between(
|
||||
&self,
|
||||
start: CalDateTime,
|
||||
end: Option<CalDateTime>,
|
||||
limit: Option<usize>,
|
||||
) -> Vec<CalDateTime> {
|
||||
let start = start;
|
||||
// Terrible code, should clean this up later.
|
||||
let mut end = end;
|
||||
if let Some(RecurrenceLimit::Until(until)) = &self.limit {
|
||||
let mut _end = end.unwrap_or(until.clone());
|
||||
if until.utc() < _end.utc() {
|
||||
_end = until.clone();
|
||||
}
|
||||
end = Some(_end);
|
||||
}
|
||||
let mut count = if let Some(RecurrenceLimit::Count(count)) = &self.limit {
|
||||
*count
|
||||
} else {
|
||||
2048
|
||||
};
|
||||
if let Some(limit) = limit {
|
||||
count = count.min(limit)
|
||||
}
|
||||
|
||||
let mut datetimes = vec![start.clone()];
|
||||
let mut datetime_utc = start.utc();
|
||||
while datetimes.len() < count {
|
||||
if let Some(end) = &end {
|
||||
if datetime_utc > end.utc() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
datetimes.push(datetime_utc.into());
|
||||
}
|
||||
|
||||
datetimes
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{CalDateTime, rrule::RecurrenceRule};
|
||||
|
||||
#[test]
|
||||
fn test_between() {
|
||||
let rrule = RecurrenceRule::parse("FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-1").unwrap();
|
||||
let start = CalDateTime::parse("20250516T133000Z", None).unwrap();
|
||||
assert_eq!(rrule.between(start, None, Some(4)), vec![]);
|
||||
}
|
||||
}
|
||||
266
crates/ical/src/rrule/mod.rs
Normal file
266
crates/ical/src/rrule/mod.rs
Normal file
@@ -0,0 +1,266 @@
|
||||
use super::{CalDateTime, CalDateTimeError};
|
||||
use std::{num::ParseIntError, str::FromStr};
|
||||
use strum_macros::EnumString;
|
||||
|
||||
mod iter;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ParserError {
|
||||
#[error("Missing RRULE FREQ")]
|
||||
MissingFrequency,
|
||||
#[error("Invalid RRULE part: {0}")]
|
||||
InvalidPart(String),
|
||||
#[error(transparent)]
|
||||
StrumError(#[from] strum::ParseError),
|
||||
#[error(transparent)]
|
||||
ParseIntError(#[from] ParseIntError),
|
||||
#[error(transparent)]
|
||||
CalDateTimeError(#[from] CalDateTimeError),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, EnumString, Default, PartialEq)]
|
||||
#[strum(serialize_all = "UPPERCASE")]
|
||||
pub enum RecurrenceFrequency {
|
||||
Secondly,
|
||||
Minutely,
|
||||
Hourly,
|
||||
#[default]
|
||||
Daily,
|
||||
Weekly,
|
||||
Monthly,
|
||||
Yearly,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, EnumString, PartialEq)]
|
||||
#[strum(serialize_all = "UPPERCASE")]
|
||||
pub enum Weekday {
|
||||
Mo,
|
||||
Tu,
|
||||
We,
|
||||
Th,
|
||||
Fr,
|
||||
Sa,
|
||||
Su,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum RecurrenceLimit {
|
||||
Count(usize),
|
||||
Until(CalDateTime),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq)]
|
||||
pub struct RecurrenceRule {
|
||||
// Self-explanatory
|
||||
pub frequency: RecurrenceFrequency,
|
||||
pub limit: Option<RecurrenceLimit>,
|
||||
// Repeat every n-th time
|
||||
pub interval: usize,
|
||||
|
||||
pub bysecond: Option<Vec<usize>>,
|
||||
pub byminute: Option<Vec<usize>>,
|
||||
pub byhour: Option<Vec<usize>>,
|
||||
pub byday: Option<Vec<(Option<i64>, Weekday)>>,
|
||||
pub bymonthday: Option<Vec<i8>>,
|
||||
pub byyearday: Option<Vec<i64>>,
|
||||
pub byweekno: Option<Vec<i8>>,
|
||||
pub bymonth: Option<Vec<i8>>,
|
||||
pub week_start: Option<Weekday>,
|
||||
// Selects the n-th occurence within an a recurrence rule
|
||||
pub bysetpos: Option<Vec<i64>>,
|
||||
}
|
||||
|
||||
impl RecurrenceRule {
|
||||
pub fn parse(rule: &str) -> Result<Self, ParserError> {
|
||||
let mut frequency = None;
|
||||
let mut limit = None;
|
||||
let mut interval = 1;
|
||||
let mut bysecond = None;
|
||||
let mut byminute = None;
|
||||
let mut byhour = None;
|
||||
let mut byday = None;
|
||||
let mut bymonthday = None;
|
||||
let mut byyearday = None;
|
||||
let mut byweekno = None;
|
||||
let mut bymonth = None;
|
||||
let mut week_start = None;
|
||||
let mut bysetpos = None;
|
||||
|
||||
for part in rule.split(';') {
|
||||
match part
|
||||
.split_once('=')
|
||||
.ok_or(ParserError::InvalidPart(part.to_owned()))?
|
||||
{
|
||||
("FREQ", val) => {
|
||||
frequency = Some(RecurrenceFrequency::from_str(val)?);
|
||||
}
|
||||
("COUNT", val) => limit = Some(RecurrenceLimit::Count(val.parse()?)),
|
||||
("UNTIL", val) => {
|
||||
limit = Some(RecurrenceLimit::Until(CalDateTime::parse(val, None)?))
|
||||
}
|
||||
("INTERVAL", val) => interval = val.parse()?,
|
||||
("BYSECOND", val) => {
|
||||
bysecond = Some(
|
||||
val.split(',')
|
||||
.map(|val| val.parse())
|
||||
.collect::<Result<Vec<_>, _>>()?,
|
||||
);
|
||||
}
|
||||
("BYMINUTE", val) => {
|
||||
byminute = Some(
|
||||
val.split(',')
|
||||
.map(|val| val.parse())
|
||||
.collect::<Result<Vec<_>, _>>()?,
|
||||
);
|
||||
}
|
||||
("BYHOUR", val) => {
|
||||
byhour = Some(
|
||||
val.split(',')
|
||||
.map(|val| val.parse())
|
||||
.collect::<Result<Vec<_>, _>>()?,
|
||||
);
|
||||
}
|
||||
("BYDAY", val) => {
|
||||
byday = Some(
|
||||
val.split(',')
|
||||
.map(|val| {
|
||||
assert!(val.len() >= 2);
|
||||
let weekday =
|
||||
Weekday::from_str(val.get((val.len() - 2)..).unwrap())?;
|
||||
let prefix = if val.len() > 2 {
|
||||
Some(val.get(..(val.len() - 2)).unwrap().parse()?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Ok((prefix, weekday))
|
||||
})
|
||||
.collect::<Result<Vec<_>, ParserError>>()?,
|
||||
);
|
||||
}
|
||||
("BYMONTHDAY", val) => {
|
||||
bymonthday = Some(
|
||||
val.split(',')
|
||||
.map(|val| val.parse())
|
||||
.collect::<Result<Vec<_>, _>>()?,
|
||||
);
|
||||
}
|
||||
("BYYEARDAY", val) => {
|
||||
byyearday = Some(
|
||||
val.split(',')
|
||||
.map(|val| val.parse())
|
||||
.collect::<Result<Vec<_>, _>>()?,
|
||||
);
|
||||
}
|
||||
("BYWEEKNO", val) => {
|
||||
byweekno = Some(
|
||||
val.split(',')
|
||||
.map(|val| val.parse())
|
||||
.collect::<Result<Vec<_>, _>>()?,
|
||||
);
|
||||
}
|
||||
("BYMONTH", val) => {
|
||||
bymonth = Some(
|
||||
val.split(',')
|
||||
.map(|val| val.parse())
|
||||
.collect::<Result<Vec<_>, _>>()?,
|
||||
);
|
||||
}
|
||||
("WKST", val) => week_start = Some(Weekday::from_str(val)?),
|
||||
("BYSETPOS", val) => {
|
||||
bysetpos = Some(
|
||||
val.split(',')
|
||||
.map(|val| val.parse())
|
||||
.collect::<Result<Vec<_>, _>>()?,
|
||||
);
|
||||
}
|
||||
(name, val) => panic!("Cannot handle {name}={val}"),
|
||||
}
|
||||
}
|
||||
Ok(Self {
|
||||
frequency: frequency.ok_or(ParserError::MissingFrequency)?,
|
||||
limit,
|
||||
interval,
|
||||
bysecond,
|
||||
byminute,
|
||||
byhour,
|
||||
byday,
|
||||
bymonthday,
|
||||
byyearday,
|
||||
byweekno,
|
||||
bymonth,
|
||||
week_start,
|
||||
bysetpos,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
CalDateTime,
|
||||
rrule::{RecurrenceFrequency, RecurrenceLimit, Weekday},
|
||||
};
|
||||
|
||||
use super::{ParserError, RecurrenceRule};
|
||||
|
||||
#[test]
|
||||
fn parse_recurrence_rule() -> Result<(), ParserError> {
|
||||
assert_eq!(
|
||||
RecurrenceRule::parse("FREQ=DAILY;UNTIL=20250516T133000Z;INTERVAL=3")?,
|
||||
RecurrenceRule {
|
||||
frequency: RecurrenceFrequency::Daily,
|
||||
limit: Some(RecurrenceLimit::Until(
|
||||
CalDateTime::parse("20250516T133000Z", None).unwrap()
|
||||
)),
|
||||
interval: 3,
|
||||
..Default::default()
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
RecurrenceRule::parse("FREQ=WEEKLY;COUNT=4;INTERVAL=2;BYDAY=TU,TH,SU")?,
|
||||
RecurrenceRule {
|
||||
frequency: RecurrenceFrequency::Weekly,
|
||||
limit: Some(RecurrenceLimit::Count(4)),
|
||||
interval: 2,
|
||||
byday: Some(vec![
|
||||
(None, Weekday::Tu),
|
||||
(None, Weekday::Th),
|
||||
(None, Weekday::Su),
|
||||
]),
|
||||
..Default::default()
|
||||
}
|
||||
);
|
||||
// Example: Last workday of the month
|
||||
assert_eq!(
|
||||
RecurrenceRule::parse("FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-1")?,
|
||||
RecurrenceRule {
|
||||
frequency: RecurrenceFrequency::Monthly,
|
||||
byday: Some(vec![
|
||||
(None, Weekday::Mo),
|
||||
(None, Weekday::Tu),
|
||||
(None, Weekday::We),
|
||||
(None, Weekday::Th),
|
||||
(None, Weekday::Fr),
|
||||
]),
|
||||
bysetpos: Some(vec![-1]),
|
||||
..Default::default()
|
||||
}
|
||||
);
|
||||
|
||||
// Every last Sunday of March
|
||||
assert_eq!(
|
||||
RecurrenceRule::parse("FREQ=YEARLY;UNTIL=20370329T010000Z;BYDAY=-1SU;BYMONTH=3")?,
|
||||
RecurrenceRule {
|
||||
frequency: RecurrenceFrequency::Yearly,
|
||||
limit: Some(RecurrenceLimit::Until(
|
||||
CalDateTime::parse("20370329T010000Z", None).unwrap()
|
||||
)),
|
||||
byday: Some(vec![(Some(-1), Weekday::Su)]),
|
||||
bymonth: Some(vec![3]),
|
||||
..Default::default()
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
290
crates/ical/src/timestamp.rs
Normal file
290
crates/ical/src/timestamp.rs
Normal file
@@ -0,0 +1,290 @@
|
||||
use crate::IcalProperty;
|
||||
|
||||
use super::timezone::CalTimezone;
|
||||
use chrono::{DateTime, Duration, Local, NaiveDate, NaiveDateTime, NaiveTime, Utc};
|
||||
use chrono_tz::Tz;
|
||||
use derive_more::derive::Deref;
|
||||
use ical::{
|
||||
parser::{Component, ical::component::IcalTimeZone},
|
||||
property::Property,
|
||||
};
|
||||
use lazy_static::lazy_static;
|
||||
use rustical_xml::{ValueDeserialize, ValueSerialize};
|
||||
use std::{collections::HashMap, ops::Add};
|
||||
|
||||
lazy_static! {
|
||||
static ref RE_DURATION: regex::Regex = regex::Regex::new(r"^(?<sign>[+-])?P((?P<W>\d+)W)?((?P<D>\d+)D)?(T((?P<H>\d+)H)?((?P<M>\d+)M)?((?P<S>\d+)S)?)?$").unwrap();
|
||||
|
||||
static ref RE_VCARD_DATE_MM_DD: regex::Regex =
|
||||
regex::Regex::new(r"^--(?<m>\d{2})(?<d>\d{2})$").unwrap();
|
||||
}
|
||||
|
||||
const LOCAL_DATE_TIME: &str = "%Y%m%dT%H%M%S";
|
||||
const UTC_DATE_TIME: &str = "%Y%m%dT%H%M%SZ";
|
||||
pub const LOCAL_DATE: &str = "%Y%m%d";
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum CalDateTimeError {
|
||||
#[error(
|
||||
"Timezone has X-LIC-LOCATION property to specify a timezone from the Olson database, however its value {0} is invalid"
|
||||
)]
|
||||
InvalidOlson(String),
|
||||
#[error("TZID {0} does not refer to a valid timezone")]
|
||||
InvalidTZID(String),
|
||||
#[error("Timestamp doesn't exist because of gap in local time")]
|
||||
LocalTimeGap,
|
||||
#[error("Datetime string {0} has an invalid format")]
|
||||
InvalidDatetimeFormat(String),
|
||||
#[error("Could not parse datetime {0}")]
|
||||
ParseError(String),
|
||||
#[error("Duration string {0} has an invalid format")]
|
||||
InvalidDurationFormat(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deref, PartialEq)]
|
||||
pub struct UtcDateTime(DateTime<Utc>);
|
||||
|
||||
impl ValueDeserialize for UtcDateTime {
|
||||
fn deserialize(val: &str) -> Result<Self, rustical_xml::XmlError> {
|
||||
let input = <String as ValueDeserialize>::deserialize(val)?;
|
||||
Ok(Self(
|
||||
NaiveDateTime::parse_from_str(&input, UTC_DATE_TIME)
|
||||
.map_err(|_| {
|
||||
rustical_xml::XmlError::InvalidValue(rustical_xml::ParseValueError::Other(
|
||||
"Could not parse as UTC timestamp".to_owned(),
|
||||
))
|
||||
})?
|
||||
.and_utc(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl ValueSerialize for UtcDateTime {
|
||||
fn serialize(&self) -> String {
|
||||
format!("{}", self.0.format(UTC_DATE_TIME))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum CalDateTime {
|
||||
// Form 1, example: 19980118T230000 -> Local
|
||||
// Form 2, example: 19980119T070000Z -> UTC
|
||||
// Form 3, example: TZID=America/New_York:19980119T020000 -> Olson
|
||||
// https://en.wikipedia.org/wiki/Tz_database
|
||||
DateTime(DateTime<CalTimezone>),
|
||||
Date(NaiveDate),
|
||||
}
|
||||
|
||||
impl From<DateTime<Local>> for CalDateTime {
|
||||
fn from(value: DateTime<Local>) -> Self {
|
||||
CalDateTime::DateTime(value.with_timezone(&CalTimezone::Local))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DateTime<Utc>> for CalDateTime {
|
||||
fn from(value: DateTime<Utc>) -> Self {
|
||||
CalDateTime::DateTime(value.with_timezone(&CalTimezone::Utc))
|
||||
}
|
||||
}
|
||||
|
||||
impl Add<Duration> for CalDateTime {
|
||||
type Output = Self;
|
||||
|
||||
fn add(self, duration: Duration) -> Self::Output {
|
||||
match self {
|
||||
Self::DateTime(datetime) => Self::DateTime(datetime + duration),
|
||||
Self::Date(date) => Self::DateTime(
|
||||
date.and_time(NaiveTime::default())
|
||||
.and_local_timezone(CalTimezone::Local)
|
||||
.earliest()
|
||||
.expect("Local timezone has constant offset")
|
||||
+ duration,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CalDateTime {
|
||||
pub fn parse_prop(
|
||||
prop: &Property,
|
||||
timezones: &HashMap<String, IcalTimeZone>,
|
||||
) -> Result<Option<Self>, CalDateTimeError> {
|
||||
let prop_value = if let Some(value) = prop.value.as_ref() {
|
||||
value
|
||||
} else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
// Use the TZID parameter from the property
|
||||
let timezone = if let Some(tzid) = prop.get_tzid() {
|
||||
if let Some(timezone) = timezones.get(tzid) {
|
||||
// X-LIC-LOCATION is often used to refer to a standardised timezone from the Olson
|
||||
// database
|
||||
if let Some(olson_name) = timezone
|
||||
.get_property("X-LIC-LOCATION")
|
||||
.map(|prop| prop.value.to_owned())
|
||||
.unwrap_or_default()
|
||||
{
|
||||
if let Ok(tz) = olson_name.parse::<Tz>() {
|
||||
Some(tz)
|
||||
} else {
|
||||
return Err(CalDateTimeError::InvalidOlson(olson_name));
|
||||
}
|
||||
} else {
|
||||
// If the TZID matches a name from the Olson database (e.g. Europe/Berlin) we
|
||||
// guess that we can just use it
|
||||
tzid.parse::<Tz>().ok()
|
||||
// TODO: If None: Too bad, we need to manually parse it
|
||||
// For now it's just treated as localtime
|
||||
}
|
||||
} else {
|
||||
// TZID refers to timezone that does not exist
|
||||
return Err(CalDateTimeError::InvalidTZID(tzid.to_string()));
|
||||
}
|
||||
} else {
|
||||
// No explicit timezone specified.
|
||||
// This is valid and will be localtime or UTC depending on the value
|
||||
None
|
||||
};
|
||||
|
||||
Self::parse(prop_value, timezone).map(Some)
|
||||
}
|
||||
|
||||
pub fn format(&self) -> String {
|
||||
match self {
|
||||
Self::DateTime(datetime) => match datetime.timezone() {
|
||||
CalTimezone::Utc => datetime.format(UTC_DATE_TIME).to_string(),
|
||||
_ => datetime.format(LOCAL_DATE_TIME).to_string(),
|
||||
},
|
||||
Self::Date(date) => date.format(LOCAL_DATE).to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn date(&self) -> NaiveDate {
|
||||
match self {
|
||||
Self::DateTime(datetime) => datetime.date_naive(),
|
||||
Self::Date(date) => date.to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse(value: &str, timezone: Option<Tz>) -> Result<Self, CalDateTimeError> {
|
||||
if let Ok(datetime) = NaiveDateTime::parse_from_str(value, LOCAL_DATE_TIME) {
|
||||
if let Some(timezone) = timezone {
|
||||
return Ok(CalDateTime::DateTime(
|
||||
datetime
|
||||
.and_local_timezone(timezone.into())
|
||||
.earliest()
|
||||
.ok_or(CalDateTimeError::LocalTimeGap)?,
|
||||
));
|
||||
}
|
||||
return Ok(CalDateTime::DateTime(
|
||||
datetime
|
||||
.and_local_timezone(CalTimezone::Local)
|
||||
.earliest()
|
||||
.ok_or(CalDateTimeError::LocalTimeGap)?,
|
||||
));
|
||||
}
|
||||
|
||||
if let Ok(datetime) = NaiveDateTime::parse_from_str(value, UTC_DATE_TIME) {
|
||||
return Ok(datetime.and_utc().into());
|
||||
}
|
||||
if let Ok(date) = NaiveDate::parse_from_str(value, LOCAL_DATE) {
|
||||
return Ok(CalDateTime::Date(date));
|
||||
}
|
||||
|
||||
if let Ok(date) = NaiveDate::parse_from_str(value, "%Y-%m-%d") {
|
||||
return Ok(CalDateTime::Date(date));
|
||||
}
|
||||
if let Ok(date) = NaiveDate::parse_from_str(value, "%Y%m%d") {
|
||||
return Ok(CalDateTime::Date(date));
|
||||
}
|
||||
if let Some(captures) = RE_VCARD_DATE_MM_DD.captures(value) {
|
||||
// Because 1972 is a leap year
|
||||
let year = 1972;
|
||||
// Cannot fail because of the regex
|
||||
let month = captures.name("m").unwrap().as_str().parse().ok().unwrap();
|
||||
let day = captures.name("d").unwrap().as_str().parse().ok().unwrap();
|
||||
|
||||
return Ok(CalDateTime::Date(
|
||||
NaiveDate::from_ymd_opt(year, month, day)
|
||||
.ok_or(CalDateTimeError::ParseError(value.to_string()))?,
|
||||
));
|
||||
}
|
||||
|
||||
Err(CalDateTimeError::InvalidDatetimeFormat(value.to_string()))
|
||||
}
|
||||
|
||||
pub fn utc(&self) -> DateTime<Utc> {
|
||||
match &self {
|
||||
CalDateTime::DateTime(datetime) => datetime.to_utc(),
|
||||
CalDateTime::Date(date) => date.and_time(NaiveTime::default()).and_utc(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CalDateTime> for DateTime<Utc> {
|
||||
fn from(value: CalDateTime) -> Self {
|
||||
value.utc()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_duration(string: &str) -> Result<Duration, CalDateTimeError> {
|
||||
let captures = RE_DURATION
|
||||
.captures(string)
|
||||
.ok_or(CalDateTimeError::InvalidDurationFormat(string.to_string()))?;
|
||||
|
||||
let mut duration = Duration::zero();
|
||||
if let Some(weeks) = captures.name("W") {
|
||||
duration += Duration::weeks(weeks.as_str().parse().unwrap());
|
||||
}
|
||||
if let Some(days) = captures.name("D") {
|
||||
duration += Duration::days(days.as_str().parse().unwrap());
|
||||
}
|
||||
if let Some(hours) = captures.name("H") {
|
||||
duration += Duration::hours(hours.as_str().parse().unwrap());
|
||||
}
|
||||
if let Some(minutes) = captures.name("M") {
|
||||
duration += Duration::minutes(minutes.as_str().parse().unwrap());
|
||||
}
|
||||
if let Some(seconds) = captures.name("S") {
|
||||
duration += Duration::seconds(seconds.as_str().parse().unwrap());
|
||||
}
|
||||
if let Some(sign) = captures.name("sign") {
|
||||
if sign.as_str() == "-" {
|
||||
duration = -duration;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(duration)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{CalDateTime, parse_duration};
|
||||
use chrono::{Duration, NaiveDate};
|
||||
|
||||
#[test]
|
||||
fn test_parse_duration() {
|
||||
assert_eq!(parse_duration("P12W").unwrap(), Duration::weeks(12));
|
||||
assert_eq!(parse_duration("P12D").unwrap(), Duration::days(12));
|
||||
assert_eq!(parse_duration("PT12H").unwrap(), Duration::hours(12));
|
||||
assert_eq!(parse_duration("PT12M").unwrap(), Duration::minutes(12));
|
||||
assert_eq!(parse_duration("PT12S").unwrap(), Duration::seconds(12));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vcard_date() {
|
||||
assert_eq!(
|
||||
CalDateTime::parse("19850412", None).unwrap(),
|
||||
CalDateTime::Date(NaiveDate::from_ymd_opt(1985, 4, 12).unwrap())
|
||||
);
|
||||
assert_eq!(
|
||||
CalDateTime::parse("1985-04-12", None).unwrap(),
|
||||
CalDateTime::Date(NaiveDate::from_ymd_opt(1985, 4, 12).unwrap())
|
||||
);
|
||||
assert_eq!(
|
||||
CalDateTime::parse("--0412", None).unwrap(),
|
||||
CalDateTime::Date(NaiveDate::from_ymd_opt(1972, 4, 12).unwrap())
|
||||
);
|
||||
}
|
||||
}
|
||||
86
crates/ical/src/timezone.rs
Normal file
86
crates/ical/src/timezone.rs
Normal file
@@ -0,0 +1,86 @@
|
||||
use chrono::{Local, NaiveDate, NaiveDateTime, TimeZone, Utc};
|
||||
use chrono_tz::Tz;
|
||||
use derive_more::{Display, From};
|
||||
|
||||
#[derive(Debug, Clone, From)]
|
||||
pub enum CalTimezone {
|
||||
Local,
|
||||
Utc,
|
||||
Olson(Tz),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Display)]
|
||||
pub enum CalTimezoneOffset {
|
||||
Local(chrono::FixedOffset),
|
||||
Utc(chrono::Utc),
|
||||
Olson(chrono_tz::TzOffset),
|
||||
}
|
||||
|
||||
impl chrono::Offset for CalTimezoneOffset {
|
||||
fn fix(&self) -> chrono::FixedOffset {
|
||||
match self {
|
||||
Self::Local(local) => local.fix(),
|
||||
Self::Utc(utc) => utc.fix(),
|
||||
Self::Olson(olson) => olson.fix(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TimeZone for CalTimezone {
|
||||
type Offset = CalTimezoneOffset;
|
||||
|
||||
fn from_offset(offset: &Self::Offset) -> Self {
|
||||
match offset {
|
||||
CalTimezoneOffset::Local(_) => Self::Local,
|
||||
CalTimezoneOffset::Utc(_) => Self::Utc,
|
||||
CalTimezoneOffset::Olson(offset) => Self::Olson(Tz::from_offset(offset)),
|
||||
}
|
||||
}
|
||||
|
||||
fn offset_from_local_date(&self, local: &NaiveDate) -> chrono::MappedLocalTime<Self::Offset> {
|
||||
match self {
|
||||
Self::Local => Local
|
||||
.offset_from_local_date(local)
|
||||
.map(CalTimezoneOffset::Local),
|
||||
Self::Utc => Utc
|
||||
.offset_from_local_date(local)
|
||||
.map(CalTimezoneOffset::Utc),
|
||||
Self::Olson(tz) => tz
|
||||
.offset_from_local_date(local)
|
||||
.map(CalTimezoneOffset::Olson),
|
||||
}
|
||||
}
|
||||
|
||||
fn offset_from_local_datetime(
|
||||
&self,
|
||||
local: &NaiveDateTime,
|
||||
) -> chrono::MappedLocalTime<Self::Offset> {
|
||||
match self {
|
||||
Self::Local => Local
|
||||
.offset_from_local_datetime(local)
|
||||
.map(CalTimezoneOffset::Local),
|
||||
Self::Utc => Utc
|
||||
.offset_from_local_datetime(local)
|
||||
.map(CalTimezoneOffset::Utc),
|
||||
Self::Olson(tz) => tz
|
||||
.offset_from_local_datetime(local)
|
||||
.map(CalTimezoneOffset::Olson),
|
||||
}
|
||||
}
|
||||
|
||||
fn offset_from_utc_datetime(&self, utc: &NaiveDateTime) -> Self::Offset {
|
||||
match self {
|
||||
Self::Local => CalTimezoneOffset::Local(Local.offset_from_utc_datetime(utc)),
|
||||
Self::Utc => CalTimezoneOffset::Utc(Utc.offset_from_utc_datetime(utc)),
|
||||
Self::Olson(tz) => CalTimezoneOffset::Olson(tz.offset_from_utc_datetime(utc)),
|
||||
}
|
||||
}
|
||||
|
||||
fn offset_from_utc_date(&self, utc: &NaiveDate) -> Self::Offset {
|
||||
match self {
|
||||
Self::Local => CalTimezoneOffset::Local(Local.offset_from_utc_date(utc)),
|
||||
Self::Utc => CalTimezoneOffset::Utc(Utc.offset_from_utc_date(utc)),
|
||||
Self::Olson(tz) => CalTimezoneOffset::Olson(tz.offset_from_utc_date(utc)),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user