summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMatěj Laitl <matej@laitl.cz>2019-11-09 19:09:36 +0100
committerMatěj Laitl <matej@laitl.cz>2019-11-17 02:03:50 +0100
commit1ef531f294308cfc9e5a52b3bd8a8279d41d1aeb (patch)
treecc52daf76d1d8d09d2294640e5d92ab4ffe79324
parentb55004b8db378503a68056562084b42cf85cbe77 (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.rs9
-rw-r--r--src/components.rs92
-rw-r--r--src/lib.rs14
-rw-r--r--tests/calendar.rs12
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
}
diff --git a/src/lib.rs b/src/lib.rs
index fe9ee3b..31916b1 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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")