use std::fmt;
use std::ops::{Add, AddAssign, Sub, SubAssign};
use std::str::FromStr;
use chrono::{DateTime, Datelike, LocalResult, NaiveDate, NaiveDateTime, TimeZone, Timelike};
use chrono_tz::{Tz, UTC};
use derive_more::Display;
use eyre::{eyre, Result};
use num_traits::ToPrimitive;
use serde::de::{self, Visitor};
use serde::{Deserialize, Serialize};
use crate::date::{Date, Day};
use crate::duration::Duration;
use crate::op::{TOp, TimeOp};
#[derive(Debug, Eq, PartialEq, Hash, Copy, Clone, Display, Ord, PartialOrd)]
#[display(fmt = "{}", t)]
pub struct Time {
t: DateTime<Tz>,
}
impl Time {
#[must_use]
pub const fn new(t: DateTime<Tz>) -> Self {
Self { t }
}
#[must_use]
pub fn zero(tz: Tz) -> Self {
Time::from_utc_secs(0, tz)
}
pub fn from_ymd_str(s: &str, tz: Tz) -> Result<Self> {
Self::from_local_date_str(s, "%Y/%m/%d", tz)
}
pub fn from_local_date_str(s: &str, fmt: &str, tz: Tz) -> Result<Self> {
let d = tz.from_local_date(&NaiveDate::parse_from_str(s, fmt)?);
let d = d.single().ok_or_else(|| eyre!("no single representation for {}", s))?;
Ok(Self::from_date(d))
}
pub fn from_local_iso(s: &str, tz: Tz) -> Result<Self> {
let utc_secs = DateTime::parse_from_rfc3339(s)?.timestamp();
Ok(Self::from_utc_secs(utc_secs, tz))
}
pub fn from_local_datetime(d: NaiveDateTime, tz: Tz) -> Result<Self> {
let dt = tz.from_local_datetime(&d);
let dt = dt.single().ok_or_else(|| eyre!("no single representation for {}", d))?;
Ok(Self::new(dt))
}
pub fn from_local_datetime_str(s: &str, fmt: &str, tz: Tz) -> Result<Self> {
Self::from_local_datetime(NaiveDateTime::parse_from_str(s, fmt)?, tz)
}
pub fn from_date(d: impl Into<chrono::Date<Tz>>) -> Self {
Self { t: d.into().and_hms(0, 0, 0) }
}
#[must_use]
pub fn from_utc_secs(utc_secs: i64, tz: Tz) -> Self {
tz.from_utc_datetime(&NaiveDateTime::from_timestamp(utc_secs, 0)).into()
}
#[must_use]
pub fn to_iso(self) -> String {
self.t.to_rfc3339()
}
#[must_use]
pub fn tz(&self) -> Tz {
self.t.timezone()
}
#[must_use]
pub fn with_tz(&self, tz: Tz) -> Self {
self.t.with_timezone(&tz).into()
}
#[must_use]
pub fn ymd(&self) -> Self {
Self::from_date(self.date())
}
#[must_use]
pub fn date(&self) -> Date {
self.t.date().into()
}
#[must_use]
pub fn with_date(&self, d: impl Into<chrono::Date<Tz>>) -> Self {
let d = d.into();
let mut t = self.t.time();
loop {
let localdt = d.naive_local().and_time(t);
let v = self.tz().from_local_datetime(&localdt);
match v {
LocalResult::None => {}
LocalResult::Single(dt) => return Self::new(dt),
LocalResult::Ambiguous(min, max) => {
return Self::new(if min.offset() == d.offset() { min } else { max });
}
};
t += chrono::Duration::hours(1);
}
}
#[must_use]
pub fn as_f64(self) -> f64 {
self.utc_secs() as f64
}
#[must_use]
pub fn utc_secs(&self) -> i64 {
self.t.with_timezone(&UTC).timestamp()
}
#[must_use]
pub fn op(op: TOp, n: i64) -> TimeOp {
TimeOp::new(op, n)
}
#[must_use]
pub fn with_sec(self, s: u32) -> Self {
self.t.with_second(s.clamp(0, 59)).unwrap().into()
}
#[must_use]
pub fn add_secs(self, secs: i64) -> Self {
(self.t + chrono::Duration::seconds(secs)).into()
}
#[must_use]
pub fn with_min(self, m: u32) -> Self {
self.t.with_minute(m.clamp(0, 59)).unwrap().into()
}
#[must_use]
pub fn add_mins(self, mins: i64) -> Self {
(self.t + chrono::Duration::minutes(mins)).into()
}
#[must_use]
pub fn with_hour(self, h: u32) -> Self {
self.t.with_hour(h.clamp(0, 23)).unwrap().into()
}
#[must_use]
pub fn add_hours(self, h: i64) -> Self {
(self.t + chrono::Duration::hours(h)).into()
}
#[must_use]
pub fn day(&self) -> u32 {
self.t.day()
}
#[must_use]
pub fn with_day(self, d: u32) -> Self {
self.with_date(self.date().with_day(d))
}
#[must_use]
pub fn add_days(self, d: i32) -> Self {
self.with_date(self.date().add_days(d))
}
#[must_use]
pub fn weekday(&self) -> Day {
self.date().weekday()
}
#[must_use]
pub fn month_name(&self) -> String {
self.date().month_name()
}
#[must_use]
pub fn month0(&self) -> u32 {
self.t.month0()
}
#[must_use]
pub fn month(&self) -> u32 {
self.t.month()
}
#[must_use]
pub fn with_month(self, m: u32) -> Self {
self.with_date(self.date().with_month(m))
}
#[must_use]
pub fn add_months(self, m: i32) -> Self {
self.with_date(self.date().add_months(m))
}
#[must_use]
pub fn year(&self) -> i32 {
self.t.year()
}
#[must_use]
pub fn with_year(self, y: i32) -> Self {
self.with_date(self.date().with_year(y))
}
#[must_use]
pub fn add_years(self, y: i32) -> Self {
self.with_date(self.date().add_years(y))
}
}
impl Default for Time {
fn default() -> Self {
Time::zero(UTC)
}
}
impl From<DateTime<Tz>> for Time {
fn from(v: DateTime<Tz>) -> Self {
Self::new(v)
}
}
impl From<Time> for DateTime<Tz> {
fn from(v: Time) -> Self {
v.t
}
}
impl From<chrono::Date<Tz>> for Time {
fn from(v: chrono::Date<Tz>) -> Self {
Self::from_date(v)
}
}
impl From<&chrono::Date<Tz>> for Time {
fn from(v: &chrono::Date<Tz>) -> Self {
Self::from_date(*v)
}
}
impl From<Date> for Time {
fn from(v: Date) -> Self {
Self::from_date(v)
}
}
impl From<&Date> for Time {
fn from(v: &Date) -> Self {
Self::from_date(*v)
}
}
impl From<Time> for f64 {
fn from(v: Time) -> Self {
v.as_f64()
}
}
impl Sub<Time> for Time {
type Output = Duration;
fn sub(self, t: Time) -> Self::Output {
Duration::new(self.utc_secs() - t.utc_secs())
}
}
impl Sub<Duration> for Time {
type Output = Time;
fn sub(self, d: Duration) -> Self::Output {
Self::from_utc_secs(self.utc_secs() - d.secs(), self.t.timezone())
}
}
impl SubAssign<Duration> for Time {
fn sub_assign(&mut self, d: Duration) {
*self = *self - d;
}
}
impl Add<Duration> for Time {
type Output = Time;
fn add(self, d: Duration) -> Self::Output {
Self::from_utc_secs(self.utc_secs() + d.secs(), self.t.timezone())
}
}
impl AddAssign<Duration> for Time {
fn add_assign(&mut self, d: Duration) {
*self = *self + d;
}
}
impl<'a> Deserialize<'a> for Time {
fn deserialize<D: serde::Deserializer<'a>>(d: D) -> Result<Self, D::Error> {
struct TimeVisitor;
impl Visitor<'_> for TimeVisitor {
type Value = Time;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("iso8601 string and timezone name")
}
fn visit_str<E>(self, v: &str) -> Result<Time, E>
where
E: de::Error,
{
let mut s = v.split_whitespace();
let iso8601 = s
.next()
.ok_or_else(|| eyre!("missing iso8601 timestamp"))
.map_err(E::custom)?;
let tz = s.next().ok_or_else(|| eyre!("missing timezone")).map_err(E::custom)?;
let tz = Tz::from_str(tz).map_err(E::custom)?;
let time = Time::from_local_iso(iso8601, tz).map_err(E::custom)?;
Ok(time)
}
}
d.deserialize_string(TimeVisitor)
}
}
impl Serialize for Time {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
s.serialize_str(&(self.to_iso() + " " + self.tz().name()))
}
}
impl ToPrimitive for Time {
fn to_i64(&self) -> Option<i64> {
Some(self.utc_secs())
}
fn to_u64(&self) -> Option<u64> {
self.utc_secs().to_u64()
}
fn to_f64(&self) -> Option<f64> {
Some(Time::as_f64(*self))
}
}
#[cfg(test)]
mod tests {
use chrono::NaiveDateTime;
use chrono_tz::Australia::Sydney;
use chrono_tz::US::Eastern;
use super::*;
#[test]
fn set_date_with_different_daylight_savings() {
let t = Time::new(Eastern.ymd(1994, 10, 27).and_hms(1, 44, 35));
let d = Eastern.ymd(1994, 10, 27);
assert_eq!(t, t.with_date(d));
let t = Time::new(Eastern.ymd(1994, 4, 30).and_hms(1, 29, 11));
let d = Eastern.ymd(1994, 10, 30);
assert_eq!(Time::new(UTC.ymd(1994, 10, 30).and_hms(5, 29, 11)), t.with_date(d));
}
#[test]
fn nonexistent_set_date() {
let t = Time::new(Eastern.ymd(2017, 3, 5).and_hms(2, 57, 12));
let d = Eastern.ymd(2017, 3, 12);
assert_eq!(Time::new(UTC.ymd(2017, 3, 12).and_hms(7, 57, 12)), t.with_date(d));
}
#[test]
fn date_tz_conversion() -> Result<()> {
let expected = Time::from_date(Sydney.ymd(2018, 1, 30));
let d_str = "30 Jan 2018";
assert_eq!(expected, Time::from_ymd_str("2018/1/30", Sydney)?);
assert_eq!(expected, Time::from_local_date_str(d_str, "%d %b %Y", Sydney)?);
Ok(())
}
#[test]
fn datetime_tz_conversion() -> Result<()> {
let expected = Time::new(Sydney.ymd(2018, 1, 30).and_hms(6, 4, 57));
let dt_str = "30 Jan 2018 06:04:57";
assert_eq!(
expected,
Time::from_local_datetime(
NaiveDateTime::parse_from_str(dt_str, "%d %b %Y %H:%M:%S")?,
Sydney
)?
);
assert_eq!(expected, Time::from_local_datetime_str(dt_str, "%d %b %Y %H:%M:%S", Sydney)?);
Ok(())
}
#[test]
fn iso_tz_conversion() -> Result<()> {
let expected = Time::new(Sydney.ymd(2018, 1, 30).and_hms(6, 4, 57));
assert_eq!(expected, Time::from_local_iso("2018-01-30T06:04:57+11:00", Sydney)?);
assert_eq!(expected, Time::from_local_iso(&expected.to_iso(), Sydney)?);
Ok(())
}
#[test]
fn tz_change() {
let time = Time::new(Sydney.ymd(2018, 1, 30).and_hms(6, 4, 57));
assert_eq!(time.utc_secs(), time.with_tz(Eastern).utc_secs());
}
}