Skip to content

Commit

Permalink
feat: add better api and examples for dates with timezones
Browse files Browse the repository at this point in the history
  • Loading branch information
hoodie committed Feb 12, 2023
1 parent 8279c82 commit 6245b27
Show file tree
Hide file tree
Showing 8 changed files with 199 additions and 5 deletions.
15 changes: 15 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,21 @@ jobs:
command: test
args: --features parser -- --nocapture

test_with_tz:
name: Test Suite (with parser)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- uses: actions-rs/cargo@v1
with:
command: test
args: --features chrono-tz

fmt:
name: Rustfmt
runs-on: ubuntu-latest
Expand Down
6 changes: 6 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ parser = ["nom"]
serde = { version = "1.0", optional = true, features = ["derive"] }
serde_json = { version = "1.0", optional = true }
iso8601 = "0.5"
chrono-tz = {version = "0.8.1", optional = true }

[dependencies.chrono]
version = "0.4"
Expand Down Expand Up @@ -72,3 +73,8 @@ required-features = ["parser"]
name = "parsed_property"
path = "examples/custom_property_parsed.rs"
required-features = ["parser"]

[[example]]
name = "timezone"
path = "examples/timezone.rs"
required-features = ["chrono-tz"]
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,19 @@ let my_calendar = Calendar::new()
.push(
// add an all-day event
Event::new()
.all_day(NaiveDate::from_ymd(2016, 3, 15))
.all_day(NaiveDate::from_ymd_opt(2016, 3, 15).unwrap())
.summary("My Birthday")
.description("Hey, I'm gonna have a party\nBYOB: Bring your own beer.\nHendrik")
.done(),
)
.push(
// local event with timezone
Event::new()
.starts(CalendarDateTime::from_ymd_hm_tzid(2023, 3, 15, 18, 45, Berlin).unwrap())
.summary("Birthday Party")
.description("I'm gonna have a party\nBYOB: Bring your own beer.\nHendrik")
.done(),
)
.done();

println!("{}", my_calendar);
Expand Down
23 changes: 23 additions & 0 deletions examples/readme.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use chrono::*;
#[cfg(feature = "chrono-tz")]
use chrono_tz::Europe::Berlin;
use icalendar::*;

fn main() {
Expand Down Expand Up @@ -36,6 +38,27 @@ fn main() {
.description("Hey, I'm gonna have a party\nBYOB: Bring your own beer.\nHendrik")
.done(),
)
.push(
// local event with timezone
Event::new()
.starts({
#[cfg(feature = "chrono-tz")]
{
CalendarDateTime::from_ymd_hm_tzid(2023, 3, 15, 18, 45, Berlin).unwrap()
}
#[cfg(not(feature = "chrono-tz"))]
{
// probably not when you think
NaiveDate::from_ymd_opt(2016, 3, 15)
.unwrap()
.and_hms_opt(18, 45, 0)
.unwrap()
}
})
.summary("Birthday Party")
.description("I'm gonna have a party\nBYOB: Bring your own beer.\nHendrik")
.done(),
)
.done();

println!("{}", my_calendar);
Expand Down
40 changes: 40 additions & 0 deletions examples/timezone.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#![cfg(feature = "chrono-tz")]
use chrono::*;
use chrono_tz::Europe::Berlin;
use icalendar::*;

fn main() {
// lets make sure everybody arrives at the expected time
let my_calendar = Calendar::new()
.push(
Event::new()
.starts(CalendarDateTime::from_ymd_hm_tzid(2023, 3, 15, 18, 45, Berlin).unwrap())
.description("I'm gonna have a party\nBYOB: Bring your own beer.\nHendrik")
.done(),
)
.push(
Event::new()
.starts(
CalendarDateTime::from_ymd_hm_tzid(2023, 3, 15, 18, 45, Berlin)
.and_then(|cdt| cdt.try_into_utc())
.unwrap(),
)
.description("I'm gonna have a party\nBYOB: Bring your own beer.\nHendrik")
.done(),
)
.push(
Event::new()
.starts(CalendarDateTime::from_date_time(
chrono_tz::Europe::Berlin
.with_ymd_and_hms(2023, 3, 15, 18, 45, 0)
.single()
.unwrap(),
))
.summary("My Birthday")
.description("I'm gonna have a party\nBYOB: Bring your own beer.\nHendrik")
.done(),
)
.done();

println!("{}", my_calendar);
}
2 changes: 1 addition & 1 deletion src/components.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ mod todo;
mod venue;

use alarm::*;
pub use date_time::{CalendarDateTime, DatePerhapsTime};
use date_time::{CalendarDateTime, DatePerhapsTime};
pub use event::*;
pub use other::*;
pub use todo::*;
Expand Down
102 changes: 100 additions & 2 deletions src/components/date_time.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#![allow(dead_code, unused)]
use std::str::FromStr;

use chrono::*;
use chrono::{Date, DateTime, Duration, NaiveDate, NaiveDateTime, Offset, TimeZone as _, Utc};

use crate::{Property, ValueType};

Expand Down Expand Up @@ -47,6 +48,9 @@ pub enum CalendarDateTime {
/// `FORM #1: DATE WITH LOCAL TIME`: floating, follows current time-zone of the attendee.
///
/// Conversion from [`chrono::NaiveDateTime`] results in this variant.
///
/// ## Note
/// finding this in a calendar is a red flag, datetimes should end in `'Z'` for `UTC` or have a `TZID` property
Floating(NaiveDateTime),

/// `FORM #2: DATE WITH UTC TIME`: rendered with Z suffix character.
Expand Down Expand Up @@ -112,6 +116,68 @@ impl CalendarDateTime {
pub(crate) fn from_naive_string(s: &str) -> Option<Self> {
parse_naive_date_time(s).map(CalendarDateTime::Floating)
}

/// attempts to convert the into UTC
#[cfg(feature = "chrono-tz")]
pub fn try_into_utc(&self) -> Option<DateTime<Utc>> {
match self {
CalendarDateTime::Floating(_) => None, // we shouldn't guess here
CalendarDateTime::Utc(inner) => Some(*inner),
CalendarDateTime::WithTimezone { date_time, tzid } => tzid
.parse::<chrono_tz::Tz>()
.ok()
.and_then(|tz| tz.from_local_datetime(date_time).single())
.map(|tz| tz.with_timezone(&Utc)),
}
}

#[cfg(feature = "chrono-tz")]
pub(crate) fn with_timezone(dt: NaiveDateTime, tz_id: chrono_tz::Tz) -> Self {
Self::WithTimezone {
date_time: dt,
tzid: tz_id.name().to_owned(),
}
}

/// will return [`None`] if date is not valid
#[cfg(feature = "chrono-tz")]
pub fn from_ymd_hm_tzid(
year: i32,
month: u32,
day: u32,
hour: u32,
min: u32,
tz_id: chrono_tz::Tz,
) -> Option<Self> {
NaiveDate::from_ymd_opt(year, month, day)
.and_then(|date| date.and_hms_opt(hour, min, 0))
.zip(Some(tz_id))
.map(|(dt, tz)| Self::with_timezone(dt, tz))
}

/// Create a new instance with the given timezone
#[cfg(feature = "chrono-tz")]
pub fn from_date_time<TZ: chrono::TimeZone<Offset = O>, O: chrono_tz::OffsetName>(
dt: DateTime<TZ>,
) -> Self {
Self::WithTimezone {
date_time: dt.naive_local(),
tzid: dt.offset().tz_id().to_owned(),
}
}
}

/// will return [`None`] if date is not valid
#[cfg(feature = "chrono-tz")]
pub fn ymd_hm_tzid(
year: i32,
month: u32,
day: u32,
hour: u32,
min: u32,
tz_id: chrono_tz::Tz,
) -> Option<CalendarDateTime> {
CalendarDateTime::from_ymd_hm_tzid(year, month, day, hour, min, tz_id)
}

/// Converts from time zone-aware UTC date-time to [`CalendarDateTime::Utc`].
Expand All @@ -121,6 +187,17 @@ impl From<DateTime<Utc>> for CalendarDateTime {
}
}

// impl<TZ: chrono::TimeZone<Offset = O>, O: chrono_tz::OffsetName> From<DateTime<TZ>>
// for CalendarDateTime
// {
// fn from(date_time: DateTime<TZ>) -> Self {
// CalendarDateTime::WithTimezone {
// date_time: date_time.naive_local(),
// tzid: date_time.offset().tz_id().to_owned(),
// }
// }
// }

/// Converts from time zone-less date-time to [`CalendarDateTime::Floating`].
impl From<NaiveDateTime> for CalendarDateTime {
fn from(dt: NaiveDateTime) -> Self {
Expand Down Expand Up @@ -168,6 +245,17 @@ impl DatePerhapsTime {
}
}

#[cfg(feature = "chrono-tz")]
pub fn with_timezone<T: chrono::TimeZone + chrono_tz::OffsetName>(
dt: DateTime<T>,
) -> DatePerhapsTime {
CalendarDateTime::WithTimezone {
date_time: dt.naive_local(),
tzid: dt.timezone().tz_id().to_owned(),
}
.into()
}

impl From<CalendarDateTime> for DatePerhapsTime {
fn from(dt: CalendarDateTime) -> Self {
Self::DateTime(dt)
Expand All @@ -176,10 +264,20 @@ impl From<CalendarDateTime> for DatePerhapsTime {

impl From<DateTime<Utc>> for DatePerhapsTime {
fn from(dt: DateTime<Utc>) -> Self {
Self::DateTime(dt.into())
Self::DateTime(CalendarDateTime::Utc(dt))
}
}

// CANT HAVE NICE THINGS until specializations are stable
// OR: breaking change and make this the default
// impl<TZ: chrono::TimeZone<Offset = O>, O: chrono_tz::OffsetName> From<DateTime<TZ>>
// for DatePerhapsTime
// {
// fn from(date_time: DateTime<TZ>) -> Self {
// Self::DateTime(CalendarDateTime::from(date_time))
// }
// }

#[allow(deprecated)]
impl From<Date<Utc>> for DatePerhapsTime {
fn from(dt: Date<Utc>) -> Self {
Expand Down
6 changes: 5 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,9 +104,13 @@ pub use crate::{
calendar::{Calendar, CalendarComponent},
components::{
alarm::{Alarm, Related, Trigger},
CalendarDateTime, Component, DatePerhapsTime, Event, EventLike, Todo, Venue,
date_time::{CalendarDateTime, DatePerhapsTime},
Component, Event, EventLike, Todo, Venue,
},
properties::{Class, EventStatus, Parameter, Property, TodoStatus, ValueType},
};

#[cfg(feature = "chrono-tz")]
pub use crate::components::date_time::ymd_hm_tzid;

// TODO Calendar TimeZone VTIMEZONE STANDARD DAYLIGHT (see thunderbird exports)

0 comments on commit 6245b27

Please sign in to comment.