billecta 1.14.0

Generated Billecta API
Documentation
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);

/// A Datetime using Stockholm as TZ. Either CET or CEST.
#[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#""2019-01-07 15:41:26+01:00""#).expect("deserialize"),
        //     DateTime::ymd_hms(2019, 01, 07, 15, 41, 26),
        // );

        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),
        );
    }
}