diff options
author | Hendrik Sollich <hoodie@users.noreply.github.com> | 2019-11-24 14:21:30 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-11-24 14:21:30 +0100 |
commit | cae5c8b6857341e7ff7363d0a9de861489f6a2d2 (patch) | |
tree | cc52daf76d1d8d09d2294640e5d92ab4ffe79324 | |
parent | 286e5e54b1b6078e990984a83fa0e6bbb8ddbb1a (diff) | |
parent | 1ef531f294308cfc9e5a52b3bd8a8279d41d1aeb (diff) |
Merge pull request #10 from strohel/timezone
Time zone handling: convert DateTime to UTC, NaiveDateTime to floating reference
-rw-r--r-- | Cargo.toml | 3 | ||||
-rw-r--r-- | examples/tasks.rs | 9 | ||||
-rw-r--r-- | src/components.rs | 92 | ||||
-rw-r--r-- | src/lib.rs | 14 | ||||
-rw-r--r-- | tests/calendar.rs | 32 |
5 files changed, 116 insertions, 34 deletions
@@ -26,3 +26,6 @@ chrono = "0.4" [dependencies.uuid] features = ["v4"] version = "0.5" + +[dev-dependencies] +pretty_assertions = "0.6" diff --git a/examples/tasks.rs b/examples/tasks.rs index b323123..e74f6f7 100644 --- a/examples/tasks.rs +++ b/examples/tasks.rs @@ -4,14 +4,13 @@ use chrono::*; fn main(){ let todo = Todo::new() - .starts(Local::now()) - .ends(Local::now()) + .starts(Local::now().naive_local()) + .ends(Local::now().naive_local()) .priority(12) .percent_complete(28) .status(TodoStatus::Completed) - .completed(&Local::now()) - .due(&Local::now()) - .due(&Local::now()) + .completed(Utc::now()) + .due(Local::now().with_timezone(&Utc)) .done(); println!("{}", todo.to_string()); diff --git a/src/components.rs b/src/components.rs index b4efb91..0462329 100644 --- a/src/components.rs +++ b/src/components.rs @@ -8,6 +8,52 @@ use std::collections::BTreeMap; use crate::properties::*; +/// Representation of various forms of `DATE-TIME` per +/// [RFC 5545, Section 3.3.5](https://tools.ietf.org/html/rfc5545#section-3.3.5) +/// +/// Conversions from [chrono] types are provided in form of [From] implementations, see +/// documentation of individual variants. +/// +/// In addition to readily implemented `FORM #1` and `FORM #2`, the RFC also specifies +/// `FORM #3: DATE WITH LOCAL TIME AND TIME ZONE REFERENCE`. This variant is not yet implemented. +/// Adding it will require adding support for `VTIMEZONE` and referencing it using `TZID`. +#[derive(Clone, Copy, Debug)] +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. + Floating(NaiveDateTime), + /// `FORM #2: DATE WITH UTC TIME`: rendered with Z suffix character. + /// + /// Conversion from [`chrono::DateTime<Utc>`](DateTime) results in this variant. Use + /// `date_time.with_timezone(&Utc)` to convert `date_time` from arbitrary time zone to UTC. + Utc(DateTime<Utc>), +} + +impl fmt::Display for CalendarDateTime { + /// Format date-time in RFC 5545 compliant manner. + fn fmt(self: &Self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + match self { + CalendarDateTime::Floating(naive_dt) => naive_dt.format("%Y%m%dT%H%M%S").fmt(f), + CalendarDateTime::Utc(utc_dt) => utc_dt.format("%Y%m%dT%H%M%SZ").fmt(f), + } + } +} + +/// Converts from time zone-aware UTC date-time to [CalendarDateTime::Utc]. +impl From<DateTime<Utc>> for CalendarDateTime { + fn from(dt: DateTime<Utc>) -> Self { + Self::Utc(dt) + } +} + +/// Converts from time zone-less date-time to [CalendarDateTime::Floating]. +impl From<NaiveDateTime> for CalendarDateTime { + fn from(dt: NaiveDateTime) -> Self { + Self::Floating(dt) + } +} + /// VEVENT [(RFC 5545, Section 3.6.1 )](https://tools.ietf.org/html/rfc5545#section-3.6.1) #[derive(Debug, Default)] pub struct Event { inner: InnerComponent } @@ -77,20 +123,21 @@ impl Todo { self } - - /// Set the COMPLETED `Property`, date only - pub fn due<TZ:TimeZone>(&mut self, dt: &DateTime<TZ>) -> &mut Self - where TZ::Offset: fmt::Display - { - self.add_property("DUE", dt.format("%Y%m%dT%H%M%S").to_string().as_ref()); + /// Set the DUE `Property` + /// + /// See [CalendarDateTime] for info how are different [chrono] types converted automatically. + pub fn due<T: Into<CalendarDateTime>>(&mut self, dt: T) -> &mut Self { + let calendar_dt: CalendarDateTime = dt.into(); + self.add_property("DUE", &calendar_dt.to_string()); self } - /// Set the COMPLETED `Property`, date only - pub fn completed<TZ:TimeZone>(&mut self, dt: &DateTime<TZ>) -> &mut Self - where TZ::Offset: fmt::Display - { - self.add_property("COMPLETED", dt.format("%Y%m%dT%H%M%S").to_string().as_ref()); + /// Set the COMPLETED `Property` + /// + /// Per [RFC 5545, Section 3.8.2.1](https://tools.ietf.org/html/rfc5545#section-3.8.2.1), this + /// must be a date-time in UTC format. + pub fn completed(&mut self, dt: DateTime<Utc>) -> &mut Self { + self.add_property("COMPLETED", &CalendarDateTime::Utc(dt).to_string()); self } @@ -129,7 +176,7 @@ pub trait Component { write_crlf!(out, "BEGIN:{}", Self::component_kind())?; if !self.properties().contains_key("DTSTAMP") { - let now = Local::now().format("%Y%m%dT%H%M%S"); + let now = CalendarDateTime::Utc(Utc::now()); write_crlf!(out, "DTSTAMP:{}", now)?; } @@ -174,22 +221,21 @@ pub trait Component { self } - /// Set the DTSTART `Property` - fn starts<TZ:TimeZone>(&mut self, dt: DateTime<TZ>) -> &mut Self - where TZ::Offset: fmt::Display - { - // DTSTART - self.add_property("DTSTART", dt.format("%Y%m%dT%H%M%S").to_string().as_ref()); + /// + /// See [CalendarDateTime] for info how are different [chrono] types converted automatically. + fn starts<T: Into<CalendarDateTime>>(&mut self, dt: T) -> &mut Self { + let calendar_dt = dt.into(); + self.add_property("DTSTART", &calendar_dt.to_string()); self } /// Set the DTEND `Property` - fn ends<TZ:TimeZone>(&mut self, dt: DateTime<TZ>) -> &mut Self - where TZ::Offset: fmt::Display - { - // TODO don't manually use format but the rfc method, but test timezone behaviour - self.add_property("DTEND", dt.format("%Y%m%dT%H%M%S").to_string().as_ref()); + /// + /// See [CalendarDateTime] for info how are different [chrono] types converted automatically. + fn ends<T: Into<CalendarDateTime>>(&mut self, dt: T) -> &mut Self { + let calendar_dt = dt.into(); + self.add_property("DTEND", &calendar_dt.to_string()); self } @@ -47,6 +47,18 @@ //! calendar.add(bday); //! # } //! ``` +//! +//! ## Breaking API Changes in version 0.7.0 +//! +//! - [Todo::due] and [Todo::completed] now take their date-time argument by value rather than by +//! reference +//! - [Todo::completed] now requires its [chrono::DateTime] argument to have exactly [chrono::Utc] +//! specified as its time zone as mandated by the RFC. +//! - [Component::starts], [Component::ends] and [Todo::due] now take newly introduced +//! [CalendarDateTime] (through `Into<CalendarDateTime>` indirection). This allows callers to +//! define time zone handling. Conversions from [`chrono::NaiveDateTime`] and +//! [`chrono::DateTime<Utc>`](chrono::DateTime) are provided for ergonomics, the latter also restoring API +//! compatibility in case of UTC date-times. #![warn(missing_docs, missing_copy_implementations, @@ -83,7 +95,7 @@ mod calendar; //pub mod repeats; pub use crate::properties::{Property, Parameter, Class, ValueType}; pub use crate::properties::{TodoStatus, EventStatus}; -pub use crate::components::{Event, Todo, Component}; +pub use crate::components::{CalendarDateTime, Event, Todo, Component}; pub use crate::calendar::Calendar; // TODO Calendar TimeZone VTIMEZONE STANDARD DAYLIGHT (see thunderbird exports) diff --git a/tests/calendar.rs b/tests/calendar.rs index 785bec5..badcb15 100644 --- a/tests/calendar.rs +++ b/tests/calendar.rs @@ -1,5 +1,6 @@ use chrono::prelude::*; -use icalendar::{Calendar, Class, Component, Event, EventStatus}; +use icalendar::{Calendar, Class, Component, Event, EventStatus, Todo}; +use pretty_assertions::assert_eq; const EXPECTED_CAL_CONTENT: &str = "\ BEGIN:VCALENDAR\r @@ -9,25 +10,37 @@ CALSCALE:GREGORIAN\r BEGIN:VEVENT\r CLASS:CONFIDENTIAL\r DESCRIPTION:Description\r -DTEND:20140709T091011\r +DTEND:20140709T091011Z\r DTSTAMP:20190307T181159\r -DTSTART:20140708T091011\r +DTSTART:20140708T071011Z\r LOCATION:Somewhere\r PRIORITY:10\r STATUS:TENTATIVE\r SUMMARY:summary\r UID:euid\r END:VEVENT\r +BEGIN:VTODO\r +COMPLETED:20140709T091011Z\r +DTSTAMP:20190307T181159\r +DUE:20140708T091011\r +PERCENT-COMPLETE:95\r +SUMMARY:A Todo\r +UID:todouid\r +END:VTODO\r END:VCALENDAR\r "; #[test] fn test_calendar_to_string() { let mut calendar = Calendar::new(); + let cest_date = FixedOffset::east(2 * 3600) + .ymd(2014, 7, 8) + .and_hms(9, 10, 11); + let utc_date = Utc.ymd(2014, 7, 9).and_hms(9, 10, 11); let event = Event::new() .status(EventStatus::Tentative) - .starts(Local.ymd(2014, 7, 8).and_hms(9, 10, 11)) - .ends(Local.ymd(2014, 7, 9).and_hms(9, 10, 11)) + .starts(cest_date.with_timezone(&Utc)) + .ends(utc_date) .priority(11) // converted to 10 .summary("summary") .description("Description") @@ -37,5 +50,14 @@ fn test_calendar_to_string() { .add_property("DTSTAMP", "20190307T181159") .done(); calendar.push(event); + let todo = Todo::new() + .percent_complete(95) + .due(cest_date.naive_local()) + .completed(utc_date) + .summary("A Todo") + .uid("todouid") + .add_property("DTSTAMP", "20190307T181159") + .done(); + calendar.push(todo); assert_eq!(calendar.to_string(), EXPECTED_CAL_CONTENT); } |