Repository: cobalt-org/liquid-rust
Pull request #366 (2019-12-09 18:33:44)

refactor(value): Use a newtype for Date/DateTime
BREAKING CHANGE: `Date` and `DateTime` are now newtypes.

- [ ] Tests created for any new feature or regression tests for bugfixes.



Patch:
diff --git a/liquid-value/src/date.rs b/liquid-value/src/date.rs
new file mode 100644
index 0000000..4e7a89f
--- /dev/null
+++ b/liquid-value/src/date.rs
@@ -0,0 +1,416 @@
+use std::fmt;
+
+use chrono::TimeZone;
+
+/// The time zone with fixed offset, from UTC-23:59:59 to UTC+23:59:59.
+pub type FixedOffset = chrono::FixedOffset;
+
+/// Liquid's native date + time type.
+#[derive(
+    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
+)]
+#[serde(transparent)]
+pub struct DateTime {
+    #[serde(with = "friendly_date_time")]
+    inner: DateTimeImpl,
+}
+
+type DateTimeImpl = chrono::DateTime<chrono::FixedOffset>;
+
+impl DateTime {
+    /// Create a `DateTime` from the current moment.
+    pub fn now() -> Self {
+        let d = chrono::Utc::now().with_timezone(&chrono::FixedOffset::east(0));
+        Self::with_chrono(d)
+    }
+
+    /// Convert a `str` to `Self`
+    pub fn from_str(other: &str) -> Option<Self> {
+        parse_date_time(other).map(|d| Self { inner: d })
+    }
+
+    /// Replace fields with `other`.
+    pub fn with_date(self, other: Date) -> Self {
+        use chrono::{NaiveDateTime, NaiveTime};
+        let other = DateTimeImpl::from_utc(
+            NaiveDateTime::new(other.inner, NaiveTime::from_hms(0, 0, 0)),
+            *self.inner.offset(),
+        );
+        Self::with_chrono(other)
+    }
+
+    /// Changes the associated time zone. This does not change the actual DateTime (but will change the string representation).
+    pub fn with_timezone(&self, tz: &chrono::FixedOffset) -> Self {
+        Self::with_chrono(self.inner.with_timezone(tz))
+    }
+
+    /// Retrieves a date component.
+    pub fn date(&self) -> Date {
+        Date::with_chrono(self.inner.naive_utc().date())
+    }
+
+    /// Formats the combined date and time with the specified format string. See the
+    /// chrono::format::strftime module on the supported escape sequences.
+    pub fn format<'a>(&self, fmt: &'a str) -> impl fmt::Display + 'a {
+        self.inner.format(fmt)
+    }
+
+    fn with_chrono(inner: DateTimeImpl) -> Self {
+        Self { inner }
+    }
+}
+
+impl Default for DateTime {
+    fn default() -> Self {
+        let d = chrono::Utc
+            .timestamp(0, 0)
+            .with_timezone(&chrono::FixedOffset::east(0));
+        DateTime::with_chrono(d)
+    }
+}
+
+impl fmt::Display for DateTime {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        self.inner.format(DATE_TIME_FORMAT).fmt(f)
+    }
+}
+
+impl chrono::Datelike for DateTime {
+    #[inline]
+    fn year(&self) -> i32 {
+        self.inner.year()
+    }
+    #[inline]
+    fn month(&self) -> u32 {
+        self.inner.month()
+    }
+    #[inline]
+    fn month0(&self) -> u32 {
+        self.inner.month0()
+    }
+    #[inline]
+    fn day(&self) -> u32 {
+        self.inner.day()
+    }
+    #[inline]
+    fn day0(&self) -> u32 {
+        self.inner.day0()
+    }
+    #[inline]
+    fn ordinal(&self) -> u32 {
+        self.inner.ordinal()
+    }
+    #[inline]
+    fn ordinal0(&self) -> u32 {
+        self.inner.ordinal0()
+    }
+    #[inline]
+    fn weekday(&self) -> chrono::Weekday {
+        self.inner.weekday()
+    }
+    #[inline]
+    fn iso_week(&self) -> chrono::IsoWeek {
+        self.inner.iso_week()
+    }
+
+    #[inline]
+    fn with_year(&self, year: i32) -> Option<Self> {
+        self.inner.with_year(year).map(|d| Self::with_chrono(d))
+    }
+
+    #[inline]
+    fn with_month(&self, month: u32) -> Option<Self> {
+        self.inner.with_month(month).map(|d| Self::with_chrono(d))
+    }
+
+    #[inline]
+    fn with_month0(&self, month0: u32) -> Option<Self> {
+        self.inner.with_month0(month0).map(|d| Self::with_chrono(d))
+    }
+
+    #[inline]
+    fn with_day(&self, day: u32) -> Option<Self> {
+        self.inner.with_day(day).map(|d| Self::with_chrono(d))
+    }
+
+    #[inline]
+    fn with_day0(&self, day0: u32) -> Option<Self> {
+        self.inner.with_day(day0).map(|d| Self::with_chrono(d))
+    }
+
+    #[inline]
+    fn with_ordinal(&self, ordinal: u32) -> Option<Self> {
+        self.inner
+            .with_ordinal(ordinal)
+            .map(|d| Self::with_chrono(d))
+    }
+
+    #[inline]
+    fn with_ordinal0(&self, ordinal0: u32) -> Option<Self> {
+        self.inner
+            .with_ordinal0(ordinal0)
+            .map(|d| Self::with_chrono(d))
+    }
+}
+
+impl chrono::Timelike for DateTime {
+    #[inline]
+    fn hour(&self) -> u32 {
+        self.inner.hour()
+    }
+    #[inline]
+    fn minute(&self) -> u32 {
+        self.inner.minute()
+    }
+    #[inline]
+    fn second(&self) -> u32 {
+        self.inner.second()
+    }
+    #[inline]
+    fn nanosecond(&self) -> u32 {
+        self.inner.nanosecond()
+    }
+
+    #[inline]
+    fn with_hour(&self, hour: u32) -> Option<Self> {
+        self.inner.with_hour(hour).map(|d| Self::with_chrono(d))
+    }
+
+    #[inline]
+    fn with_minute(&self, min: u32) -> Option<Self> {
+        self.inner.with_minute(min).map(|d| Self::with_chrono(d))
+    }
+
+    #[inline]
+    fn with_second(&self, sec: u32) -> Option<Self> {
+        self.inner.with_second(sec).map(|d| Self::with_chrono(d))
+    }
+
+    #[inline]
+    fn with_nanosecond(&self, nano: u32) -> Option<Self> {
+        self.inner
+            .with_nanosecond(nano)
+            .map(|d| Self::with_chrono(d))
+    }
+}
+
+const DATE_TIME_FORMAT: &str = "%Y-%m-%d %H:%M:%S %z";
+
+mod friendly_date_time {
+    use super::*;
+    use serde::{self, Deserialize, Deserializer, Serializer};
+
+    pub(crate) fn serialize<S>(date: &DateTimeImpl, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: Serializer,
+    {
+        let s = date.to_string();
+        serializer.serialize_str(&s)
+    }
+
+    pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result<DateTimeImpl, D::Error>
+    where
+        D: Deserializer<'de>,
+    {
+        let s = String::deserialize(deserializer)?;
+        DateTimeImpl::parse_from_str(&s, DATE_TIME_FORMAT).map_err(serde::de::Error::custom)
+    }
+}
+
+fn parse_date_time(s: &str) -> Option<DateTimeImpl> {
+    match s {
+        "now" => {
+            use chrono::offset;
+            let now = offset::Utc::now();
+            let now = now.naive_utc();
+            let now = DateTimeImpl::from_utc(now, offset::FixedOffset::east(0));
+            Some(now)
+        }
+        _ => {
+            let formats = ["%d %B %Y %H:%M:%S %z", "%Y-%m-%d %H:%M:%S %z"];
+            formats
+                .iter()
+                .filter_map(|f| DateTimeImpl::parse_from_str(s, f).ok())
+                .next()
+        }
+    }
+}
+
+/// Liquid's native date only type.
+#[derive(
+    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
+)]
+#[serde(transparent)]
+pub struct Date {
+    #[serde(with = "friendly_date")]
+    inner: DateImpl,
+}
+
+type DateImpl = chrono::NaiveDate;
+
+impl Date {
+    /// Makes a new NaiveDate from the calendar date (year, month and day).
+    ///
+    /// Panics on the out-of-range date, invalid month and/or day.
+    pub fn from_ymd(year: i32, month: u32, day: u32) -> Self {
+        Self::with_chrono(DateImpl::from_ymd(year, month, day))
+    }
+
+    /// Convert a `str` to `Self`
+    pub fn from_str(other: &str) -> Option<Self> {
+        parse_date(other).map(|d| Self { inner: d })
+    }
+
+    fn with_chrono(inner: DateImpl) -> Self {
+        Self { inner }
+    }
+}
+
+impl fmt::Display for Date {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        self.inner.format(DATE_FORMAT).fmt(f)
+    }
+}
+
+impl chrono::Datelike for Date {
+    #[inline]
+    fn year(&self) -> i32 {
+        self.inner.year()
+    }
+    #[inline]
+    fn month(&self) -> u32 {
+        self.inner.month()
+    }
+    #[inline]
+    fn month0(&self) -> u32 {
+        self.inner.month0()
+    }
+    #[inline]
+    fn day(&self) -> u32 {
+        self.inner.day()
+    }
+    #[inline]
+    fn day0(&self) -> u32 {
+        self.inner.day0()
+    }
+    #[inline]
+    fn ordinal(&self) -> u32 {
+        self.inner.ordinal()
+    }
+    #[inline]
+    fn ordinal0(&self) -> u32 {
+        self.inner.ordinal0()
+    }
+    #[inline]
+    fn weekday(&self) -> chrono::Weekday {
+        self.inner.weekday()
+    }
+    #[inline]
+    fn iso_week(&self) -> chrono::IsoWeek {
+        self.inner.iso_week()
+    }
+
+    #[inline]
+    fn with_year(&self, year: i32) -> Option<Self> {
+        self.inner.with_year(year).map(|d| Self::with_chrono(d))
+    }
+
+    #[inline]
+    fn with_month(&self, month: u32) -> Option<Self> {
+        self.inner.with_month(month).map(|d| Self::with_chrono(d))
+    }
+
+    #[inline]
+    fn with_month0(&self, month0: u32) -> Option<Self> {
+        self.inner.with_month0(month0).map(|d| Self::with_chrono(d))
+    }
+
+    #[inline]
+    fn with_day(&self, day: u32) -> Option<Self> {
+        self.inner.with_day(day).map(|d| Self::with_chrono(d))
+    }
+
+    #[inline]
+    fn with_day0(&self, day0: u32) -> Option<Self> {
+        self.inner.with_day(day0).map(|d| Self::with_chrono(d))
+    }
+
+    #[inline]
+    fn with_ordinal(&self, ordinal: u32) -> Option<Self> {
+        self.inner
+            .with_ordinal(ordinal)
+            .map(|d| Self::with_chrono(d))
+    }
+
+    #[inline]
+    fn with_ordinal0(&self, ordinal0: u32) -> Option<Self> {
+        self.inner
+            .with_ordinal0(ordinal0)
+            .map(|d| Self::with_chrono(d))
+    }
+}
+
+const DATE_FORMAT: &str = "%Y-%m-%d";
+
+mod friendly_date {
+    use super::*;
+    use serde::{self, Deserialize, Deserializer, Serializer};
+
+    pub(crate) fn serialize<S>(date: &DateImpl, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: Serializer,
+    {
+        let s = date.to_string();
+        serializer.serialize_str(&s)
+    }
+
+    pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result<DateImpl, D::Error>
+    where
+        D: Deserializer<'de>,
+    {
+        let s = String::deserialize(deserializer)?;
+        DateImpl::parse_from_str(&s, DATE_FORMAT).map_err(serde::de::Error::custom)
+    }
+}
+
+fn parse_date(s: &str) -> Option<DateImpl> {
+    match s {
+        "today" => {
+            use chrono::offset::Utc;
+            Some(Utc::today().naive_utc())
+        }
+        _ => {
+            let formats = ["%d %B %Y", "%Y-%m-%d"];
+            formats
+                .iter()
+                .filter_map(|f| DateImpl::parse_from_str(s, f).ok())
+                .next()
+        }
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use super::*;
+    #[test]
+    fn parse_date_time_empty_is_bad() {
+        assert!(parse_date_time("").is_none());
+        assert!(parse_date("").is_none());
+    }
+
+    #[test]
+    fn parse_date_time_bad() {
+        assert!(parse_date_time("aaaaa").is_none());
+        assert!(parse_date("aaaaa").is_none());
+    }
+
+    #[test]
+    fn parse_date_time_now() {
+        assert!(parse_date_time("now").is_some());
+    }
+
+    #[test]
+    fn parse_date_today() {
+        assert!(parse_date("today").is_some());
+    }
+}
diff --git a/liquid-value/src/lib.rs b/liquid-value/src/lib.rs
index 78b17c3..b207a9b 100644
--- a/liquid-value/src/lib.rs
+++ b/liquid-value/src/lib.rs
@@ -10,17 +10,20 @@ extern crate serde;
 #[macro_use]
 mod macros;
 
-pub mod map;
+mod date;
 mod path;
 mod scalar;
 mod ser;
 mod values;
 
+pub mod map;
+
 /// Liquid Processing Errors.
 pub mod error {
     pub use liquid_error::*;
 }
 
+pub use crate::date::*;
 pub use crate::path::*;
 pub use crate::scalar::*;
 pub use crate::ser::*;
diff --git a/liquid-value/src/scalar.rs b/liquid-value/src/scalar.rs
index 3da6b4a..5191d02 100644
--- a/liquid-value/src/scalar.rs
+++ b/liquid-value/src/scalar.rs
@@ -2,13 +2,7 @@ use std::borrow;
 use std::cmp::Ordering;
 use std::fmt;
 
-use chrono;
-
-/// Liquid's native date + time type.
-pub type DateTime = chrono::DateTime<chrono::FixedOffset>;
-
-/// Liquid's native date only type.
-pub type Date = chrono::NaiveDate;
+use crate::{Date, DateTime};
 
 /// A Liquid scalar value
 #[derive(Clone, Debug, Serialize, Deserialize)]
@@ -24,9 +18,7 @@ enum ScalarCowEnum<'s> {
     Integer(i32),
     Float(f64),
     Bool(bool),
-    #[serde(with = "friendly_date_time")]
     DateTime(DateTime),
-    #[serde(with = "friendly_date")]
     Date(Date),
     Str(borrow::Cow<'s, str>),
 }
@@ -73,10 +65,8 @@ impl<'s> ScalarCow<'s> {
             ScalarCowEnum::Integer(ref x) => borrow::Cow::Owned(x.to_string()),
             ScalarCowEnum::Float(ref x) => borrow::Cow::Owned(x.to_string()),
             ScalarCowEnum::Bool(ref x) => borrow::Cow::Owned(x.to_string()),
-            ScalarCowEnum::DateTime(ref x) => {
-                borrow::Cow::Owned(x.format(DATE_TIME_FORMAT).to_string())
-            }
-            ScalarCowEnum::Date(ref x) => borrow::Cow::Owned(x.format(DATE_FORMAT).to_string()),
+            ScalarCowEnum::DateTime(ref x) => borrow::Cow::Owned(x.to_string()),
+            ScalarCowEnum::Date(ref x) => borrow::Cow::Owned(x.to_string()),
             ScalarCowEnum::Str(ref x) => borrow::Cow::Borrowed(x.as_ref()),
         }
     }
@@ -88,7 +78,7 @@ impl<'s> ScalarCow<'s> {
             ScalarCowEnum::Float(x) => x.to_string(),
             ScalarCowEnum::Bool(x) => x.to_string(),
             ScalarCowEnum::DateTime(x) => x.to_string(),
-            ScalarCowEnum::Date(x) => x.format(DATE_FORMAT).to_string(),
+            ScalarCowEnum::Date(x) => x.to_string(),
             ScalarCowEnum::Str(x) => x.into_owned(),
         }
     }
@@ -124,7 +114,7 @@ impl<'s> ScalarCow<'s> {
     pub fn to_date_time(&self) -> Option<DateTime> {
         match self.0 {
             ScalarCowEnum::DateTime(ref x) => Some(*x),
-            ScalarCowEnum::Str(ref x) => parse_date_time(x.as_ref()),
+            ScalarCowEnum::Str(ref x) => DateTime::from_str(x.as_ref()),
             _ => None,
         }
     }
@@ -132,9 +122,9 @@ impl<'s> ScalarCow<'s> {
     /// Interpret as a date time, if possible
     pub fn to_date(&self) -> Option<Date> {
         match self.0 {
-            ScalarCowEnum::DateTime(ref x) => Some(x.naive_utc().date()),
+            ScalarCowEnum::DateTime(ref x) => Some(x.date()),
             ScalarCowEnum::Date(ref x) => Some(*x),
-            ScalarCowEnum::Str(ref x) => parse_date(x.as_ref()),
+            ScalarCowEnum::Str(ref x) => Date::from_str(x.as_ref()),
             _ => None,
         }
     }
@@ -351,8 +341,8 @@ impl<'s> fmt::Display for ScalarSource<'s> {
             ScalarCowEnum::Integer(ref x) => write!(f, "{}", x),
             ScalarCowEnum::Float(ref x) => write!(f, "{}", x),
             ScalarCowEnum::Bool(ref x) => write!(f, "{}", x),
-            ScalarCowEnum::DateTime(ref x) => write!(f, "{}", x.format(DATE_TIME_FORMAT)),
-            ScalarCowEnum::Date(ref x) => write!(f, "{}", x.format(DATE_FORMAT)),
+            ScalarCowEnum::DateTime(ref x) => write!(f, "{}", x),
+            ScalarCowEnum::Date(ref x) => write!(f, "{}", x),
             ScalarCowEnum::Str(ref x) => write!(f, r#""{}""#, x),
         }
     }
@@ -369,21 +359,13 @@ impl<'s> fmt::Display for ScalarRendered<'s> {
             ScalarCowEnum::Integer(ref x) => write!(f, "{}", x),
             ScalarCowEnum::Float(ref x) => write!(f, "{}", x),
             ScalarCowEnum::Bool(ref x) => write!(f, "{}", x),
-            ScalarCowEnum::DateTime(ref x) => write!(f, "{}", x.format(DATE_TIME_FORMAT)),
-            ScalarCowEnum::Date(ref x) => write!(f, "{}", x.format(DATE_FORMAT)),
+            ScalarCowEnum::DateTime(ref x) => write!(f, "{}", x),
+            ScalarCowEnum::Date(ref x) => write!(f, "{}", x),
             ScalarCowEnum::Str(ref x) => write!(f, "{}", x),
         }
     }
 }
 
-fn date_to_datetime(date: Date, offset: chrono::FixedOffset) -> DateTime {
-    use chrono::{NaiveDateTime, NaiveTime};
-    DateTime::from_utc(
-        NaiveDateTime::new(date, NaiveTime::from_hms(0, 0, 0)),
-        offset,
-    )
-}
-
 fn scalar_eq<'s>(lhs: &ScalarCow<'s>, rhs: &ScalarCow<'s>) -> bool {
     match (&lhs.0, &rhs.0) {
         (&ScalarCowEnum::Integer(x), &ScalarCowEnum::Integer(y)) => x == y,
@@ -393,12 +375,8 @@ fn scalar_eq<'s>(lhs: &ScalarCow<'s>, rhs: &ScalarCow<'s>) -> bool {
         (&ScalarCowEnum::Bool(x), &ScalarCowEnum::Bool(y)) => x == y,
         (&ScalarCowEnum::DateTime(x), &ScalarCowEnum::DateTime(y)) => x == y,
         (&ScalarCowEnum::Date(x), &ScalarCowEnum::Date(y)) => x == y,
-        (&ScalarCowEnum::DateTime(x), &ScalarCowEnum::Date(y)) => {
-            x == date_to_datetime(y, *x.offset())
-        }
-        (&ScalarCowEnum::Date(x), &ScalarCowEnum::DateTime(y)) => {
-            date_to_datetime(x, *y.offset()) == y
-        }
+        (&ScalarCowEnum::DateTime(x), &ScalarCowEnum::Date(y)) => x == x.with_date(y),
+        (&ScalarCowEnum::Date(x), &ScalarCowEnum::DateTime(y)) => y.with_date(x) == y,
         (&ScalarCowEnum::Str(ref x), &ScalarCowEnum::Str(ref y)) => x == y,
         // encode Ruby truthiness: all values except false and nil are true
         (_, &ScalarCowEnum::Bool(b)) | (&ScalarCowEnum::Bool(b), _) => b,
@@ -415,98 +393,13 @@ fn scalar_cmp<'s>(lhs: &ScalarCow<'s>, rhs: &ScalarCow<'s>) -> Option<Ordering>
         (&ScalarCowEnum::Bool(x), &ScalarCowEnum::Bool(y)) => x.partial_cmp(&y),
         (&ScalarCowEnum::DateTime(x), &ScalarCowEnum::DateTime(y)) => x.partial_cmp(&y),
         (&ScalarCowEnum::Date(x), &ScalarCowEnum::Date(y)) => x.partial_cmp(&y),
-        (&ScalarCowEnum::DateTime(x), &ScalarCowEnum::Date(y)) => {
-            x.partial_cmp(&date_to_datetime(y, *x.offset()))
-        }
-        (&ScalarCowEnum::Date(x), &ScalarCowEnum::DateTime(y)) => {
-            date_to_datetime(x, *y.offset()).partial_cmp(&y)
-        }
+        (&ScalarCowEnum::DateTime(x), &ScalarCowEnum::Date(y)) => x.partial_cmp(&x.with_date(y)),
+        (&ScalarCowEnum::Date(x), &ScalarCowEnum::DateTime(y)) => y.with_date(x).partial_cmp(&y),
         (&ScalarCowEnum::Str(ref x), &ScalarCowEnum::Str(ref y)) => x.partial_cmp(y),
         _ => None,
     }
 }
 
-const DATE_TIME_FORMAT: &str = "%Y-%m-%d %H:%M:%S %z";
-
-mod friendly_date_time {
-    use super::*;
-    use serde::{self, Deserialize, Deserializer, Serializer};
-
-    pub(crate) fn serialize<S>(date: &DateTime, serializer: S) -> Result<S::Ok, S::Error>
-    where
-        S: Serializer,
-    {
-        let s = date.format(DATE_TIME_FORMAT).to_string();
-        serializer.serialize_str(&s)
-    }
-
-    pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result<DateTime, D::Error>
-    where
-        D: Deserializer<'de>,
-    {
-        let s = String::deserialize(deserializer)?;
-        DateTime::parse_from_str(&s, DATE_TIME_FORMAT).map_err(serde::de::Error::custom)
-    }
-}
-
-const DATE_FORMAT: &str = "%Y-%m-%d";
-
-mod friendly_date {
-    use super::*;
-    use serde::{self, Deserialize, Deserializer, Serializer};
-
-    pub(crate) fn serialize<S>(date: &Date, serializer: S) -> Result<S::Ok, S::Error>
-    where
-        S: Serializer,
-    {
-        let s = date.format(DATE_FORMAT).to_string();
-        serializer.serialize_str(&s)
-    }
-
-    pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result<Date, D::Error>
-    where
-        D: Deserializer<'de>,
-    {
-        let s = String::deserialize(deserializer)?;
-        chrono::NaiveDate::parse_from_str(&s, DATE_FORMAT).map_err(serde::de::Error::custom)
-    }
-}
-
-fn parse_date_time(s: &str) -> Option<DateTime> {
-    match s {
-        "now" => {
-            use chrono::offset;
-            let now = offset::Utc::now();
-            let now = now.naive_utc();
-            let now = DateTime::from_utc(now, offset::FixedOffset::east(0));
-            Some(now)
-        }
-        _ => {
-            let formats = ["%d %B %Y %H:%M:%S %z", "%Y-%m-%d %H:%M:%S %z"];
-            formats
-                .iter()
-                .filter_map(|f| DateTime::parse_from_str(s, f).ok())
-                .next()
-        }
-    }
-}
-
-fn parse_date(s: &str) -> Option<Date> {
-    match s {
-        "today" => {
-            use chrono::offset::Utc;
-            Some(Utc::today().naive_utc())
-        }
-        _ => {
-            let formats = ["%d %B %Y", "%Y-%m-%d"];
-            formats
-                .iter()
-                .filter_map(|f| Date::parse_from_str(s, f).ok())
-                .next()
-        }
-    }
-}
-
 #[cfg(test)]
 mod test {
     use super::*;
@@ -720,26 +613,4 @@ mod test {
         assert_eq!(empty, TRUE);
         assert!(empty.is_truthy());
     }
-
-    #[test]
-    fn parse_date_time_empty_is_bad() {
-        assert!(parse_date_time("").is_none());
-        assert!(parse_date("").is_none());
-    }
-
-    #[test]
-    fn parse_date_time_bad() {
-        assert!(parse_date_time("aaaaa").is_none());
-        assert!(parse_date("aaaaa").is_none());
-    }
-
-    #[test]
-    fn parse_date_time_now() {
-        assert!(parse_date_time("now").is_some());
-    }
-
-    #[test]
-    fn parse_date_today() {
-        assert!(parse_date("today").is_some());
-    }
 }
diff --git a/src/filters/extra/date.rs b/src/filters/extra/date.rs
index e4609ab..e0c171d 100644
--- a/src/filters/extra/date.rs
+++ b/src/filters/extra/date.rs
@@ -47,11 +47,9 @@ impl Filter for DateInTzFilter {
 
         let timezone = FixedOffset::east(args.timezone * 3600);
 
-        Ok(Value::scalar(
-            date.with_timezone(&timezone)
-                .format(args.format.as_ref())
-                .to_string(),
-        ))
+        let formatter = date.with_timezone(&timezone).format(args.format.as_ref());
+        let date = formatter.to_string();
+        Ok(Value::scalar(date))
     }
 }
 


Test patch:
diff --git a/tests/derive_macros.rs b/tests/derive_macros.rs
index 6043cb4..82ccff8 100644
--- a/tests/derive_macros.rs
+++ b/tests/derive_macros.rs
@@ -153,9 +153,9 @@ pub fn test_derive_mixed_filter_ok() {
         ))
         .unwrap();
     let expected = concat!(
-        "<a: 5; b: false; c: 4.3, d: 2019-02-08 15:34:25 -08:00, e: 2019-02-08, f: str, type: 0>\n",
-        "<a: None; b: false; c: None, d: 2019-02-08 15:34:25 -08:00, e: 2019-02-08, f: None, type: 0>\n",
-        "<a: 5; b: false; c: 4.3, d: 2019-02-08 15:34:25 -08:00, e: 2019-02-08, f: str, type: 0>"
+        "<a: 5; b: false; c: 4.3, d: 2019-02-08 15:34:25 -0800, e: 2019-02-08, f: str, type: 0>\n",
+        "<a: None; b: false; c: None, d: 2019-02-08 15:34:25 -0800, e: 2019-02-08, f: None, type: 0>\n",
+        "<a: 5; b: false; c: 4.3, d: 2019-02-08 15:34:25 -0800, e: 2019-02-08, f: str, type: 0>"
     );
 
     let globals = liquid::value::Object::new();