temporal_core 0.0.1

An Intl-first date time library
Documentation
use std::{str::FromStr, fmt::Debug};

use crate::iso::{IsoDate, IsoTime, self, parse_time, parse_sign};

mod iana_generated;
mod timezone_impl;

pub trait TimeZoneProtocol {
    fn id(&self) -> String;
    fn get_second_offset(&self, seconds_since_epoch: i64) -> i64;
    fn get_possible_seconds(&self, date: IsoDate, time: IsoTime) -> Vec<i64>;
}

use iana_generated::Tz;

use self::timezone_impl::TimeSpans;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum TimezoneInner {
    Tz(Tz),
    Fixed(i32),
}

#[derive(Clone, Copy, PartialEq, Eq)]
pub struct TimeZone(TimezoneInner);

impl TimeZone {
    const UTC: Self = Self(TimezoneInner::Tz(Tz::UTC));

    pub fn extract_from_iso_date(iso: &str) -> Result<Self, TimeZoneParseError> {
        use TimeZoneParseError::*;
        let i = iso::parse(iso).ok_or(InvalidIsoString)?;
        if let Some(x) = i.timezone_name {
            return x.parse();
        }
        Ok(match i.timezone_offset.ok_or(InvalidIsoString)? {
            iso::IsoOffset::Z => Self::UTC,
            iso::IsoOffset::Numeric(x) => Self(TimezoneInner::Fixed(x.to_seconds())),
        })
    }
}

impl Debug for TimeZone {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_tuple("TimeZone").field(&self.id()).finish()
    }
}

#[derive(Debug, PartialEq, Eq)]
pub enum TimeZoneParseError {
    InvalidTimeZone,
    InvalidIsoString,
    OutOfRange,
    SubSecondOffset(i64),
}

impl TimeZoneParseError {
    pub fn is_sub_second(&self) -> Option<i64> {
        match self {
            Self::SubSecondOffset(x) => Some(*x),
            _ => None
        }
    }
}

impl FromStr for TimeZone {
    type Err = TimeZoneParseError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        use TimeZoneParseError::*;
        let mut it = s.chars().peekable();
        if let Ok(t) = Tz::from_str_insensitive(s) {
            return Ok(Self(TimezoneInner::Tz(t)));
        }
        if let Some(c) = it.next() {
            if let Some(is_neg) = parse_sign(c) {
                if let Some(t) = parse_time(&mut it, true) {
                    if it.next().is_some() {
                        return Err(InvalidIsoString);
                    }
                    if t.has_nanosecond() {
                        let t = t.to_nanosecond();
                        let r = if is_neg { -t } else { t };
                        return Err(SubSecondOffset(r));
                    }
                    let t = t.to_second();
                    let r = if is_neg { -t } else { t };
                    return Ok(Self(TimezoneInner::Fixed(r)));
                }
                return Err(InvalidIsoString);
            }
        }
        Err(InvalidTimeZone)
    }
}

impl TimeZoneProtocol for TimeZone {
    fn id(&self) -> String {
        match &self.0 {
            TimezoneInner::Tz(x) => x.name().to_string(),
            TimezoneInner::Fixed(x) => {
                let sign = if *x < 0 { '-' } else { '+' };
                let x = x.abs();
                let secs = x % 60;
                let x = x / 60;
                let mins = x % 60;
                let hours = x / 60;
                if secs == 0 {
                    format!("{}{:02}:{:02}", sign, hours, mins)
                } else {
                    format!("{}{:02}:{:02}:{:02}", sign, hours, mins, secs)    
                }
            },
        }
    }

    fn get_second_offset(&self, seconds_since_epoch: i64) -> i64 {
        match &self.0 {
            TimezoneInner::Tz(x) => x
                .timespans()
                .select_with_sec(seconds_since_epoch)
                .second_offset(),
            TimezoneInner::Fixed(x) => (*x).into(),
        }
    }

    fn get_possible_seconds(&self, date: IsoDate, time: IsoTime) -> Vec<i64> {
        todo!()
    }
}

#[cfg(test)]
mod tests {
    use crate::{TimeZone, TimeZoneProtocol, timezone::TimeZoneParseError};

    #[test]
    fn simple_parse() {
        let tz: TimeZone = "+01:00".parse().unwrap();
        assert_eq!(tz.get_second_offset(0), 3600);
    }

    #[test]
    fn parse_without_colon() {
        let tz: TimeZone = "+0330".parse().unwrap();
        assert_eq!(tz.get_second_offset(0), 12600);
    }

    #[test]
    fn parse_sub_second() {
        let tz: Result<TimeZone, TimeZoneParseError> = "-03:30:00.000000001".parse();
        assert_eq!(tz, Err(TimeZoneParseError::SubSecondOffset(-12600_000000001)));
    }
}