timens 0.1.8

Simple and efficient library for timestamp and date manipulation.
Documentation
#[cfg(feature = "binio")]
use binprot::macros::{BinProtRead, BinProtWrite};

#[cfg(feature = "with-chrono")]
use chrono::{TimeZone, Timelike};

use crate::{date, ofday};
use crate::{Date, OfDay, Span, Tz, TzError, TzParseError};
use std::ops::{Add, AddAssign, Rem, Sub, SubAssign};
use std::str::FromStr;

#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "binio", derive(BinProtRead, BinProtWrite))]
pub struct Time(i64);

impl std::fmt::Debug for Time {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
        let (date, ofday) = self.to_date_ofday_gmt();
        write!(f, "{} {}Z", date, ofday)
    }
}

impl std::fmt::Display for Time {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
        write!(f, "{:?}", self)
    }
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub enum TimeParseError {
    NoSpace,
    DateError(date::DateError),
    OfDayError(ofday::ParseOfDayError),
    NoZone,
    ExpectedIntInZone(std::num::ParseIntError),
    TzError(TzError),
    TzParseError(TzParseError),
}

impl std::fmt::Display for TimeParseError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{:?}", self)
    }
}

impl std::error::Error for TimeParseError {}

impl std::convert::From<TzError> for TimeParseError {
    fn from(tz_error: TzError) -> Self {
        TimeParseError::TzError(tz_error)
    }
}

impl std::convert::From<TzParseError> for TimeParseError {
    fn from(tz_error: TzParseError) -> Self {
        TimeParseError::TzParseError(tz_error)
    }
}

impl std::convert::From<std::num::ParseIntError> for TimeParseError {
    fn from(int_error: std::num::ParseIntError) -> Self {
        TimeParseError::ExpectedIntInZone(int_error)
    }
}

impl std::convert::From<date::DateError> for TimeParseError {
    fn from(date_error: date::DateError) -> Self {
        TimeParseError::DateError(date_error)
    }
}

impl std::convert::From<ofday::ParseOfDayError> for TimeParseError {
    fn from(ofday_error: ofday::ParseOfDayError) -> Self {
        TimeParseError::OfDayError(ofday_error)
    }
}

fn parse_zone_offset(s: &str) -> Result<Span, TimeParseError> {
    match s.split(':').collect::<Vec<_>>()[..] {
        [] => Err(TimeParseError::NoZone),
        [hour] => {
            let hour = u8::from_str(hour)? as i64;
            Ok(Span::HR * hour)
        }
        [hour, minute] => {
            let hour = u8::from_str(hour)? as i64;
            let minute = u8::from_str(minute)? as i64;
            Ok(Span::HR * hour + Span::MIN * minute)
        }
        [hour, minute, second] => {
            let hour = u8::from_str(hour)? as i64;
            let minute = u8::from_str(minute)? as i64;
            let second = u8::from_str(second)? as i64;
            Ok(Span::HR * hour + Span::MIN * minute + Span::SEC * second)
        }
        _ => Err(TimeParseError::NoZone),
    }
}

impl Time {
    fn parse_ofday_with_zone(ofday_with_zone: &str, date: Date) -> Result<Self, TimeParseError> {
        match ofday_with_zone.split_once('Z') {
            Some((ofday, z)) if z.is_empty() => {
                let ofday = OfDay::from_str(ofday)?;
                return Ok(Self::of_date_ofday_gmt(date, ofday));
            }
            Some(_) | None => (),
        };
        if let Some((ofday, zone_offset)) = ofday_with_zone.split_once('+') {
            let ofday = OfDay::from_str(ofday)?;
            let zone_offset = parse_zone_offset(zone_offset)?;
            return Ok(Self::of_date_ofday_gmt(date, ofday) + zone_offset);
        }
        if let Some((ofday, zone_offset)) = ofday_with_zone.split_once('-') {
            let ofday = OfDay::from_str(ofday)?;
            let zone_offset = parse_zone_offset(zone_offset)?;
            return Ok(Self::of_date_ofday_gmt(date, ofday) - zone_offset);
        }
        if let Some((ofday, tz)) = ofday_with_zone.split_once(' ') {
            let ofday = OfDay::from_str(ofday)?;
            let tz = Tz::from_str(tz)?;
            return Ok(Self::of_date_ofday(date, ofday, tz)?);
        }
        Err(TimeParseError::NoZone)
    }
}

impl std::str::FromStr for Time {
    type Err = TimeParseError;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.split_once(' ') {
            None => Err(TimeParseError::NoSpace),
            Some((date, ofday_with_zone)) => {
                let date = Date::from_str(date)?;
                Self::parse_ofday_with_zone(ofday_with_zone, date)
            }
        }
    }
}

impl Add<Span> for Time {
    type Output = Self;

    fn add(self, other: Span) -> Self {
        Self(self.0 + other.to_int_ns())
    }
}

impl AddAssign<Span> for Time {
    fn add_assign(&mut self, other: Span) {
        self.0 += other.to_int_ns()
    }
}

impl Sub<Span> for Time {
    type Output = Self;

    fn sub(self, other: Span) -> Self {
        Self(self.0 - other.to_int_ns())
    }
}

impl SubAssign<Span> for Time {
    fn sub_assign(&mut self, other: Span) {
        self.0 -= other.to_int_ns()
    }
}

impl Sub for Time {
    type Output = Span;

    fn sub(self, other: Self) -> Span {
        Span::of_int_ns(self.0 - other.0)
    }
}

impl Rem<Span> for Time {
    type Output = Span;

    fn rem(self, other: Span) -> Span {
        Span::of_int_ns(self.0 % other.to_int_ns())
    }
}

impl Time {
    pub const EPOCH: Self = Self(0);

    pub fn now() -> Self {
        let now = std::time::SystemTime::now();
        let dt = now.duration_since(std::time::UNIX_EPOCH).expect("system time before Unix epoch");
        let dt = Span::of_int_sec(dt.as_secs() as i64) + Span::of_int_ns(dt.subsec_nanos() as i64);
        Self::of_span_since_epoch(dt)
    }

    pub fn to_span_since_epoch(self) -> Span {
        Span::of_int_ns(self.0)
    }

    pub fn of_span_since_epoch(span: Span) -> Self {
        Self(span.to_int_ns())
    }

    pub fn to_int_ns_since_epoch(self) -> i64 {
        self.0
    }

    pub fn of_int_ns_since_epoch(ns: i64) -> Self {
        Self(ns)
    }

    pub fn to_date_ofday(self, tz: Tz) -> (Date, OfDay) {
        let offset = tz.tz_info().offset(self);
        let ns_since_epoch = self.0 + offset.to_int_ns();
        let day_ns = Span::DAY.to_int_ns();
        let days = ns_since_epoch.div_euclid(day_ns);
        let ofday = ns_since_epoch.rem_euclid(day_ns);
        let date = Date::of_days_since_epoch(days as i32);
        (date, OfDay::of_ns_since_midnight(ofday))
    }

    pub fn to_date_ofday_gmt(self) -> (Date, OfDay) {
        let day_ns = Span::DAY.to_int_ns();
        let days = self.0.div_euclid(day_ns);
        let ofday = self.0.rem_euclid(day_ns);
        let date = Date::of_days_since_epoch(days as i32);
        (date, OfDay::of_ns_since_midnight(ofday))
    }

    pub fn to_date(self, tz: Tz) -> Date {
        let offset = tz.tz_info().offset(self);
        let ns_since_epoch = self.0 + offset.to_int_ns();
        let days = ns_since_epoch.div_euclid(Span::DAY.to_int_ns());
        Date::of_days_since_epoch(days as i32)
    }

    pub fn to_ofday(self, tz: Tz) -> OfDay {
        let offset = tz.tz_info().offset(self);
        let ns_since_epoch = self.0 + offset.to_int_ns();
        let ofday = ns_since_epoch.rem_euclid(Span::DAY.to_int_ns());
        OfDay::of_ns_since_midnight(ofday)
    }

    pub fn of_date_ofday(date: Date, ofday: OfDay, tz: Tz) -> Result<Self, TzError> {
        tz.tz_info().date_ofday_to_time(date, ofday)
    }

    pub fn of_date_ofday_gmt(date: Date, ofday: OfDay) -> Self {
        let gmt_ns = (date - Date::UNIX_EPOCH) as i64 * Span::DAY.to_int_ns();
        Time(gmt_ns + ofday.to_ns_since_midnight())
    }

    pub fn to_string_gmt(self) -> String {
        format!("{:?}", self)
    }

    pub fn write_tz<W: std::fmt::Write>(self, w: &mut W, tz: Tz) -> Result<(), std::fmt::Error> {
        let offset_sec = tz.tz_info().find(self).total_offset_sec();
        let ns_since_epoch = self.0 + offset_sec as i64 * Span::SEC.to_int_ns();
        let day_ns = Span::DAY.to_int_ns();
        let days = ns_since_epoch.div_euclid(day_ns);
        let ofday = OfDay::of_ns_since_midnight(ns_since_epoch.rem_euclid(day_ns));
        let date = Date::of_days_since_epoch(days as i32);
        if offset_sec == 0 {
            write!(w, "{} {}Z", date, ofday)
        } else {
            let (abs_offset, sign) =
                if offset_sec < 0 { (-offset_sec, '-') } else { (offset_sec, '+') };
            let offset_sec = abs_offset % 60;
            let abs_offset = abs_offset / 60;
            let offset_min = abs_offset % 60;
            let offset_hr = abs_offset / 60;
            write!(w, "{} {}{}{:02}:{:02}", date, ofday, sign, offset_hr, offset_min)?;
            if offset_sec != 0 {
                write!(w, ":{:02}", offset_sec)?;
            }
            Ok(())
        }
    }

    pub fn to_string_tz(self, tz: Tz) -> String {
        let mut s = String::new();
        self.write_tz(&mut s, tz).unwrap();
        s
    }

    pub fn prev_multiple(self, rhs: Span) -> Self {
        Self::of_span_since_epoch(self.to_span_since_epoch().prev_multiple(rhs))
    }

    pub fn next_multiple(self, rhs: Span) -> Self {
        Self::of_span_since_epoch(self.to_span_since_epoch().next_multiple(rhs))
    }
}

#[cfg(feature = "with-chrono")]
impl Time {
    pub fn to_naive_datetime(self) -> chrono::NaiveDateTime {
        let day_ns = Span::DAY.to_int_ns();
        let sec = self.0.div_euclid(day_ns);
        let ns = self.0.rem_euclid(day_ns);
        chrono::NaiveDateTime::from_timestamp(sec, ns as u32)
    }

    pub fn to_datetime(self, tz: &chrono_tz::Tz) -> Option<chrono::DateTime<chrono_tz::Tz>> {
        match chrono_tz::UTC.from_local_datetime(&self.to_naive_datetime()) {
            chrono::LocalResult::None | chrono::LocalResult::Ambiguous(_, _) => None,
            chrono::LocalResult::Single(t) => Some(t.with_timezone(tz)),
        }
    }

    pub fn to_ofday_string_no_trailing_zeros(self, tz: &chrono_tz::Tz) -> String {
        match self.to_datetime(tz) {
            None => format!("unable to format for timezone {} {:?}", self.to_naive_datetime(), tz),
            Some(t) => {
                let t = t.time();
                let hr = t.hour();
                let min = t.minute();
                let sec = t.second();
                let ns = t.nanosecond();
                if ns == 0 {
                    format!("{:02}:{:02}:{:02}", hr, min, sec)
                } else {
                    let mut ns = ns;
                    let mut ns_width = 9;
                    while ns % 10 == 0 {
                        ns /= 10;
                        ns_width -= 1;
                    }
                    format!(
                        "{:02}:{:02}:{:02}.{:0ns_width$}",
                        hr,
                        min,
                        sec,
                        ns,
                        ns_width = ns_width
                    )
                }
            }
        }
    }
}

#[cfg(feature = "sexp")]
mod sexp {
    use rsexp::Sexp;
    impl rsexp::SexpOf for crate::Time {
        fn sexp_of(&self) -> Sexp {
            let (date, ofday) = self.to_date_ofday_gmt();
            let ofday = format!("{}Z", ofday);
            rsexp::SexpOf::sexp_of(&(date.to_string(), ofday))
        }
    }

    impl rsexp::OfSexp for crate::Time {
        fn of_sexp(sexp: &Sexp) -> Result<Self, rsexp::IntoSexpError> {
            match sexp {
                Sexp::List(ref list) => match list[..] {
                    [Sexp::Atom(ref date), Sexp::Atom(ref ofday_with_zone)] => {
                        let date: crate::Date = String::from_utf8_lossy(date).parse().map_err(
                            |err: crate::DateError| rsexp::IntoSexpError::StringConversionError {
                                err: err.to_string(),
                            },
                        )?;
                        let time = Self::parse_ofday_with_zone(
                            &String::from_utf8_lossy(ofday_with_zone),
                            date,
                        )
                        .map_err(|err| {
                            rsexp::IntoSexpError::StringConversionError { err: err.to_string() }
                        })?;
                        Ok(time)
                    }
                    _ => Err(rsexp::IntoSexpError::ListLengthMismatch {
                        type_: "time",
                        list_len: list.len(),
                        expected_len: 2,
                    }),
                },
                Sexp::Atom(_) => Err(rsexp::IntoSexpError::ExpectedListGotAtom { type_: "time" }),
            }
        }
    }
}