use std::fmt;
use serde::Deserialize;
use time::{OffsetDateTime, PrimitiveDateTime};
use time_tz::{OffsetDateTimeExt, PrimitiveDateTimeExt, Tz};
use crate::Date;
static STOCKHOLM: &Tz = time_tz::timezones::db::europe::STOCKHOLM;
pub const BILLECTA_STANDARD_FORMAT: &[time::format_description::FormatItem] = time::macros::format_description!(
"[year]-[month]-[day] [hour]:[minute]:[second]+[offset_hour]:[offset_minute]"
);
pub const BILLECTA_SAFE_FORMAT: &[time::format_description::FormatItem] = time::macros::format_description!(
"[year]-[month]-[day][optional [T]][optional [ ]][hour]:[minute]:[second]+[offset_hour]:[offset_minute]"
);
pub const BILLECTA_NO_TZ_FORMAT: &[time::format_description::FormatItem] = time::macros::format_description!(
"[year]-[month]-[day]T[hour]:[minute]:[second][optional [.[subsecond]]]"
);
time::serde::format_description!(format, OffsetDateTime, BILLECTA_STANDARD_FORMAT);
#[derive(Debug, PartialEq, Eq, Clone, Copy, PartialOrd, Ord, serde::Serialize)]
pub struct DateTime(#[serde(with = "format")] pub time::OffsetDateTime);
pub trait ToStockholmTz {
fn to_stockholm(&self) -> time::OffsetDateTime;
}
impl ToStockholmTz for time::OffsetDateTime {
fn to_stockholm(&self) -> time::OffsetDateTime {
self.to_timezone(STOCKHOLM)
}
}
impl ToStockholmTz for time::PrimitiveDateTime {
fn to_stockholm(&self) -> time::OffsetDateTime {
self.assume_timezone(STOCKHOLM)
.take_first()
.expect("Invalid time in Stockholm")
}
}
impl DateTime {
pub fn date(self) -> Date {
Date::from(self.0.date())
}
pub fn ymd_hms(year: i32, month: u8, day: u8, hour: u8, minute: u8, second: u8) -> Self {
Self(
time::Date::from_calendar_date(year, month.try_into().expect("month"), day)
.expect("date")
.with_hms(hour, minute, second)
.expect("time")
.to_stockholm(),
)
}
pub fn start_of_month(self) -> Self {
let dt = self.0.to_stockholm();
Self(
time::Date::from_calendar_date(dt.year(), dt.month(), 1)
.expect("date")
.with_hms(0, 0, 0)
.expect("invalid time")
.to_stockholm(),
)
}
pub fn end_of_month(self) -> Self {
let dt = self.0.to_stockholm();
let mut day = 31;
loop {
if let Ok(date) = time::Date::from_calendar_date(dt.year(), dt.month(), day) {
return Self(date.midnight().to_stockholm());
}
day -= 1;
}
}
pub fn next_month_start(self) -> Self {
let dt = self.0.to_stockholm();
let month = dt.month();
let year = if month == time::Month::December {
dt.year() + 1
} else {
dt.year()
};
Self(
time::Date::from_calendar_date(year, month.next(), 1)
.expect("date")
.with_hms(0, 0, 0)
.expect("invalid time")
.to_stockholm(),
)
}
pub fn prev_month_start(self) -> Self {
let dt = self.0.to_stockholm();
let month = dt.month();
let year = if month == time::Month::January {
dt.year() - 1
} else {
dt.year()
};
Self(
time::Date::from_calendar_date(year, month.previous(), 1)
.expect("date")
.with_hms(0, 0, 0)
.expect("invalid time")
.to_stockholm(),
)
}
}
impl fmt::Display for DateTime {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let res = self.0.format(BILLECTA_STANDARD_FORMAT);
write!(f, "{}", res.as_deref().unwrap_or("-"))
}
}
impl From<time::PrimitiveDateTime> for DateTime {
fn from(dt: time::PrimitiveDateTime) -> Self {
Self(dt.to_stockholm())
}
}
impl From<time::OffsetDateTime> for DateTime {
fn from(dt: time::OffsetDateTime) -> Self {
Self(dt.to_stockholm())
}
}
impl From<DateTime> for time::OffsetDateTime {
fn from(dt: DateTime) -> time::OffsetDateTime {
dt.0
}
}
struct Visitor;
impl serde::de::Visitor<'_> for Visitor {
type Value = DateTime;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str(r#"a datetime string on the form: "2013-12-30 20:59:59+01:00""#)
}
fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
OffsetDateTime::parse(s, &BILLECTA_SAFE_FORMAT)
.or_else(|_| {
PrimitiveDateTime::parse(s, &BILLECTA_NO_TZ_FORMAT).map(|pdt| pdt.to_stockholm())
})
.map(DateTime)
.map_err(E::custom)
}
}
impl<'de> Deserialize<'de> for DateTime {
fn deserialize<D>(deserializer: D) -> Result<DateTime, D::Error>
where
D: serde::de::Deserializer<'de>,
{
deserializer.deserialize_str(Visitor)
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json as js;
#[test]
fn serialize_date_time() {
assert_eq!(
js::to_string(&DateTime::ymd_hms(2019, 1, 23, 12, 30, 0)).expect("Serializing"),
r#""2019-01-23 12:30:00+01:00""#,
);
assert_eq!(
js::to_string(&DateTime::ymd_hms(2019, 12, 31, 23, 59, 59)).expect("Serializing"),
r#""2019-12-31 23:59:59+01:00""#,
);
}
#[test]
fn deserialize_date_time() {
assert_eq!(
js::from_str::<DateTime>(r#""2023-02-01T06:47:07+01:00""#).expect("deserialize"),
DateTime::ymd_hms(2023, 2, 1, 6, 47, 7)
);
assert_eq!(
js::from_str::<DateTime>(r#""2019-02-26 23:59:59+01:00""#).expect("deserialize"),
DateTime::ymd_hms(2019, 2, 26, 23, 59, 59),
);
assert_eq!(
js::from_str::<DateTime>(r#""2019-02-27 00:00:00+00:00""#).expect("deserialize"),
DateTime::ymd_hms(2019, 2, 27, 1, 0, 0),
);
assert_eq!(
js::from_str::<DateTime>(r#""2019-05-30 13:40:13+02:00""#).expect("deserialize"),
DateTime::ymd_hms(2019, 5, 30, 13, 40, 13),
);
assert_eq!(
js::from_str::<DateTime>(r#""2019-05-30T00:00:00""#).expect("deserialize"),
DateTime::ymd_hms(2019, 5, 30, 0, 0, 0),
);
assert_eq!(
js::from_str::<DateTime>(r#""2019-11-08T10:46:29""#).expect("deserialize"),
DateTime::ymd_hms(2019, 11, 8, 10, 46, 29),
);
}
}