summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorHendrik Sollich <hoodie@users.noreply.github.com>2019-11-24 14:21:30 +0100
committerGitHub <noreply@github.com>2019-11-24 14:21:30 +0100
commitcae5c8b6857341e7ff7363d0a9de861489f6a2d2 (patch)
treecc52daf76d1d8d09d2294640e5d92ab4ffe79324
parent286e5e54b1b6078e990984a83fa0e6bbb8ddbb1a (diff)
parent1ef531f294308cfc9e5a52b3bd8a8279d41d1aeb (diff)
Merge pull request #10 from strohel/timezone
Time zone handling: convert DateTime to UTC, NaiveDateTime to floating reference
-rw-r--r--Cargo.toml3
-rw-r--r--examples/tasks.rs9
-rw-r--r--src/components.rs92
-rw-r--r--src/lib.rs14
-rw-r--r--tests/calendar.rs32
5 files changed, 116 insertions, 34 deletions
diff --git a/Cargo.toml b/Cargo.toml
index e98133f..c18efea 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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
}
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 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);
}