diff options
author | Matěj Laitl <matej@laitl.cz> | 2019-11-09 19:09:36 +0100 |
---|---|---|
committer | Matěj Laitl <matej@laitl.cz> | 2019-11-17 02:03:50 +0100 |
commit | 1ef531f294308cfc9e5a52b3bd8a8279d41d1aeb (patch) | |
tree | cc52daf76d1d8d09d2294640e5d92ab4ffe79324 | |
parent | b55004b8db378503a68056562084b42cf85cbe77 (diff) |
Time zone handling: accept only DateTime<Utc> and NaiveDateTime
This is a breaking API change.
- [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>`] are provided for ergonomics, the latter also restoring API
compatibility in case of UTC date-times.
Note that we now implement 2 of the 3 DATE-TIME variants defined by the RFC.
The third variant can be implemented in the future.
Fixes #2.
-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 | 12 |
4 files changed, 92 insertions, 35 deletions
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 3d885f8..badcb15 100644 --- a/tests/calendar.rs +++ b/tests/calendar.rs @@ -10,9 +10,9 @@ 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 @@ -20,7 +20,7 @@ SUMMARY:summary\r UID:euid\r END:VEVENT\r BEGIN:VTODO\r -COMPLETED:20140709T091011\r +COMPLETED:20140709T091011Z\r DTSTAMP:20190307T181159\r DUE:20140708T091011\r PERCENT-COMPLETE:95\r @@ -39,7 +39,7 @@ fn test_calendar_to_string() { let utc_date = Utc.ymd(2014, 7, 9).and_hms(9, 10, 11); let event = Event::new() .status(EventStatus::Tentative) - .starts(cest_date) + .starts(cest_date.with_timezone(&Utc)) .ends(utc_date) .priority(11) // converted to 10 .summary("summary") @@ -52,8 +52,8 @@ fn test_calendar_to_string() { calendar.push(event); let todo = Todo::new() .percent_complete(95) - .due(&cest_date) - .completed(&utc_date) + .due(cest_date.naive_local()) + .completed(utc_date) .summary("A Todo") .uid("todouid") .add_property("DTSTAMP", "20190307T181159") |