/* Copyright 2020-2023, Savanni D'Gerinel This file is part of the Luminescent Dreams Tools. Luminescent Dreams Tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Luminescent Dreams Tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Lumeto. If not, see . */ extern crate chrono; extern crate chrono_tz; use chrono::SecondsFormat; use chrono_tz::Etc::UTC; use serde::de::{self, Deserialize, Deserializer, Visitor}; use serde::ser::{Serialize, Serializer}; use std::{fmt, str::FromStr}; /// This is a wrapper around date time objects, using timezones from the chroon-tz database and /// providing string representation and parsing of the form " ", i.e., /// "2019-05-15T14:30:00Z US/Central". The to_string method, and serde serialization will /// produce a string of this format. The parser will accept an RFC3339-only string of the forms /// "2019-05-15T14:30:00Z", "2019-05-15T14:30:00+00:00", and also an "RFC3339 Timezone Name" /// string. /// /// The function here is to generate as close to unambiguous time/date strings, (for earth's /// gravitational frame of reference), as possible. Clumping together the time, offset from UTC, /// and the named time zone allows future parsers to know the exact interpretation of the time in /// the frame of reference of the original recording. #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct DateTimeTz(pub chrono::DateTime); impl DateTimeTz { pub fn map(&self, f: F) -> DateTimeTz where F: FnOnce(chrono::DateTime) -> chrono::DateTime, { DateTimeTz(f(self.0)) } pub fn to_string(&self) -> String { if self.0.timezone() == UTC { self.0.to_rfc3339_opts(SecondsFormat::Secs, true) } else { format!( "{} {}", self.0 .with_timezone(&chrono_tz::Etc::UTC) .to_rfc3339_opts(SecondsFormat::Secs, true,), self.0.timezone().name() ) } } } impl std::str::FromStr for DateTimeTz { type Err = chrono::ParseError; fn from_str(s: &str) -> Result { let v: Vec<&str> = s.split_terminator(" ").collect(); if v.len() == 2 { let tz = v[1].parse::().unwrap(); chrono::DateTime::parse_from_rfc3339(v[0]).map(|ts| DateTimeTz(ts.with_timezone(&tz))) } else { chrono::DateTime::parse_from_rfc3339(v[0]).map(|ts| DateTimeTz(ts.with_timezone(&UTC))) } } } impl From> for DateTimeTz { fn from(dt: chrono::DateTime) -> DateTimeTz { DateTimeTz(dt) } } struct DateTimeTzVisitor; impl<'de> Visitor<'de> for DateTimeTzVisitor { type Value = DateTimeTz; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str("a string date time representation that can be parsed") } fn visit_str(self, s: &str) -> Result { DateTimeTz::from_str(s).or(Err(E::custom(format!( "string is not a parsable datetime representation" )))) } } impl Serialize for DateTimeTz { fn serialize(&self, serializer: S) -> Result { serializer.serialize_str(&self.to_string()) } } impl<'de> Deserialize<'de> for DateTimeTz { fn deserialize>(deserializer: D) -> Result { deserializer.deserialize_str(DateTimeTzVisitor) } } #[cfg(test)] mod test { extern crate serde_json; use super::*; use chrono::TimeZone; use chrono_tz::America::Phoenix; use chrono_tz::Etc::UTC; use chrono_tz::US::{Arizona, Central}; use std::str::FromStr; #[test] fn it_creates_timestamp_with_z() { let t = DateTimeTz(UTC.ymd(2019, 5, 15).and_hms(12, 0, 0)); assert_eq!(t.to_string(), "2019-05-15T12:00:00Z"); } #[test] fn it_parses_utc_rfc3339_z() { let t = DateTimeTz::from_str("2019-05-15T12:00:00Z").unwrap(); assert_eq!(t, DateTimeTz(UTC.ymd(2019, 5, 15).and_hms(12, 0, 0))); } #[test] fn it_parses_rfc3339_with_offset() { let t = DateTimeTz::from_str("2019-05-15T12:00:00-06:00").unwrap(); assert_eq!(t, DateTimeTz(UTC.ymd(2019, 5, 15).and_hms(18, 0, 0))); } #[test] fn it_parses_rfc3339_with_tz() { let t = DateTimeTz::from_str("2019-06-15T19:00:00Z US/Arizona").unwrap(); assert_eq!(t, DateTimeTz(UTC.ymd(2019, 6, 15).and_hms(19, 0, 0))); assert_eq!(t, DateTimeTz(Arizona.ymd(2019, 6, 15).and_hms(12, 0, 0))); assert_eq!(t, DateTimeTz(Central.ymd(2019, 6, 15).and_hms(14, 0, 0))); assert_eq!(t.to_string(), "2019-06-15T19:00:00Z US/Arizona"); } #[derive(Serialize)] struct DemoStruct { id: String, dt: DateTimeTz, } // I used Arizona here specifically because large parts of Arizona do not honor DST, and so // that adds in more ambiguity of the -0700 offset with Pacific time. #[test] fn it_json_serializes() { let t = DateTimeTz::from_str("2019-06-15T19:00:00Z America/Phoenix").unwrap(); assert_eq!( serde_json::to_string(&t).unwrap(), "\"2019-06-15T19:00:00Z America/Phoenix\"" ); let demo = DemoStruct { id: String::from("abcdefg"), dt: t, }; assert_eq!( serde_json::to_string(&demo).unwrap(), "{\"id\":\"abcdefg\",\"dt\":\"2019-06-15T19:00:00Z America/Phoenix\"}" ); } #[test] fn it_json_parses() { let t = serde_json::from_str::("\"2019-06-15T19:00:00Z America/Phoenix\"").unwrap(); assert_eq!(t, DateTimeTz(Phoenix.ymd(2019, 6, 15).and_hms(12, 0, 0))); } }