iocore 3.1.0

IOCore is a safe library for unix CLI tools and Systems programming. IOCore provides the [`iocore::Path`] abstraction of file-system paths designed to replace most [`std::path`] and [`std::fs`] operations with practical methods, other abstractions include: - handling file-system permissions via [`iocore::PathPermissions`] powered by the crate [`trilobyte`]. - handling file-system timestamps via [`iocore::PathTimestamps`] granularly via [`iocore::PathDateTime`]. IOCore provides the [`iocore::walk_dir`] function and its companion trait [`iocore::WalkProgressHandler`] which traverses file-systems quickly via threads. IOcore provides [`iocore::User`] which provides unix user information such as uid, path to home etc. The module [`iocore::env`] provides [`iocore::env:args`] returns a [`Vec<String>`] from [`std::env:args`], and [`iocore::env:var`] that returns environment variables as string.
Documentation
use std::fmt::Display;
use std::str::FromStr;
use std::string::ToString;
use std::time::SystemTime;

use chrono::format::SecondsFormat;
use chrono::{DateTime, FixedOffset, Local, NaiveDateTime, Utc};
use filetime::FileTime;
use serde::{Deserialize, Deserializer, Serialize, Serializer};

use crate::{traceback, Error};

#[derive(Clone)]
pub struct PathDateTime {
    t: DateTime<Local>,
}

impl From<SystemTime> for PathDateTime {
    fn from(st: SystemTime) -> PathDateTime {
        PathDateTime { t: st.into() }
    }
}

impl Display for PathDateTime {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "{}", self.to_rfc3339())
    }
}

impl std::fmt::Debug for PathDateTime {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "PathDateTime[{}]", self.to_rfc3339())
    }
}

impl Serialize for PathDateTime {
    fn serialize<S>(&self, ser: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        self.t.serialize(ser)
    }
}

impl<'de> Deserialize<'de> for PathDateTime {
    fn deserialize<D>(de: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        Ok(PathDateTime::from(DateTime::<Local>::deserialize(de)?))
    }
}
impl From<DateTime<Local>> for PathDateTime {
    fn from(t: DateTime<Local>) -> PathDateTime {
        PathDateTime { t }
    }
}

impl Into<DateTime<Local>> for PathDateTime {
    fn into(self) -> DateTime<Local> {
        (&self).t.clone()
    }
}

impl PathDateTime {
    pub fn human_friendly(&self, t: Option<DateTime<Local>>) -> String {
        let day = self.t.format("%e").to_string().trim().to_string();
        let fmt = if self.t.format("%Y").to_string()
            == t.unwrap_or(Local::now()).format("%Y").to_string()
        {
            format!("%h {: <2} %H:%M", day)
        } else {
            format!("%h {: <2} %Y", day)
        }
        .to_string();
        self.t.format(fmt.as_str()).to_string()
    }

    pub fn to_rfc3339(&self) -> String {
        self.t.to_rfc3339_opts(SecondsFormat::Nanos, true)
    }

    pub fn from_datetime_utc(datetime: &DateTime<Utc>) -> PathDateTime {
        PathDateTime::from_datetime(&datetime.with_timezone(&Local))
    }

    pub fn from_datetime_fixed_offset(datetime: &DateTime<FixedOffset>) -> PathDateTime {
        PathDateTime::from_datetime(&datetime.with_timezone(&Local))
    }

    pub fn from_datetime(t: &DateTime<Local>) -> PathDateTime {
        let t = t.clone();
        PathDateTime { t }
    }

    pub fn from_timestamp(secs: i64, nsecs: u32) -> PathDateTime {
        PathDateTime::from_datetime_utc(
            &DateTime::from_timestamp(secs, nsecs)
                .expect(&format!("chrono::DateTime<Utc> from {} secs and {} nsecs", secs, nsecs)),
        )
    }

    pub fn parse_from_str(s: &str, fmt: &str) -> Result<PathDateTime, Error> {
        Ok(PathDateTime::from_datetime_utc(
            &NaiveDateTime::parse_from_str(s, fmt)
                .map_err(|error| {
                    traceback!(ParseError, "error parsing '{}' with format '{}': {}", s, fmt, error)
                })?
                .and_utc(),
        ))
    }

    pub fn local_datetime(&self) -> DateTime<Local> {
        self.t.with_timezone(&Local)
    }

    pub fn utc_datetime(&self) -> DateTime<Utc> {
        self.t.with_timezone(&Utc)
    }

    pub fn timestamp(&self) -> (i64, u32) {
        (self.t.timestamp(), self.t.timestamp_subsec_nanos())
    }

    pub fn to_array(&self) -> [u16; 6] {
        [
            u16::from_str(&self.utc_datetime().format("%Y").to_string()).unwrap(),
            u16::from_str(&self.utc_datetime().format("%m").to_string()).unwrap(),
            u16::from_str(&self.utc_datetime().format("%d").to_string()).unwrap(),
            u16::from_str(&self.utc_datetime().format("%H").to_string()).unwrap(),
            u16::from_str(&self.utc_datetime().format("%M").to_string()).unwrap(),
            u16::from_str(&self.utc_datetime().format("%S").to_string()).unwrap(),
        ]
    }

    pub fn to_bytes<'a>(&self) -> Vec<u8> {
        let mut bytes = Vec::<u8>::new();
        for data in self.to_array() {
            bytes.extend(data.to_be_bytes())
        }
        bytes
    }

    pub fn filetime(&self) -> FileTime {
        FileTime::from_unix_time(self.t.timestamp(), self.t.timestamp_subsec_nanos())
    }
}
impl std::cmp::PartialEq for PathDateTime {
    fn eq(&self, other: &Self) -> bool {
        self.utc_datetime().eq(&other.utc_datetime())
    }
}
impl std::cmp::Eq for PathDateTime {}
impl std::cmp::PartialOrd for PathDateTime {
    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
        self.utc_datetime().partial_cmp(&other.utc_datetime())
    }
}
impl std::cmp::Ord for PathDateTime {
    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
        self.utc_datetime().cmp(&other.utc_datetime())
    }
}

impl std::hash::Hash for PathDateTime {
    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
        self.utc_datetime().hash(state);
        self.local_datetime().hash(state);
        self.to_bytes().hash(state);
    }
}

#[cfg(test)]
mod tests {
    use k9::assert_equal;
    use chrono::{DateTime, FixedOffset, Local, NaiveDate, TimeZone};

    use crate::PathDateTime;

    #[test]
    fn test_from_datetime_utc() {
        let datetime = DateTime::from_timestamp(1742346763, 0).unwrap();
        if std::env::var("TZ").unwrap_or_default() == "UTC" {
            assert_equal!(
                PathDateTime::from_datetime_utc(&datetime).to_string(),
                "2025-03-19T01:12:43.000000000Z"
            );
        } else {
            assert_equal!(
                PathDateTime::from_datetime_utc(&datetime).to_string(),
                "2025-03-18T22:12:43.000000000-03:00"
            );
        }
    }
    #[test]
    fn test_from_datetime_fixedoffset() {
        let datetime = FixedOffset::east_opt(0)
            .unwrap()
            .from_local_datetime(
                &NaiveDate::from_ymd_opt(2025, 3, 19)
                    .unwrap()
                    .and_hms_nano_opt(1, 12, 43, 0)
                    .unwrap(),
            )
            .unwrap();
        if std::env::var("TZ").unwrap_or_default() == "UTC" {
            assert_equal!(
                PathDateTime::from_datetime_fixed_offset(&datetime).to_string(),
                "2025-03-19T01:12:43.000000000Z"
            );
        } else {
            assert_equal!(
                PathDateTime::from_datetime_fixed_offset(&datetime).to_string(),
                "2025-03-18T22:12:43.000000000-03:00"
            );
        }
    }
    #[test]
    fn test_from_datetime() {
        let datetime = DateTime::from_timestamp(1742346763, 0).unwrap().with_timezone(&Local);
        if std::env::var("TZ").unwrap_or_default() == "UTC" {
            assert_equal!(
                PathDateTime::from_datetime(&datetime).to_string(),
                "2025-03-19T01:12:43.000000000Z"
            );
        } else {
            assert_equal!(
                PathDateTime::from_datetime(&datetime).to_string(),
                "2025-03-18T22:12:43.000000000-03:00"
            );
        }
    }
    #[test]
    fn test_from_timestamp() {
        if std::env::var("TZ").unwrap_or_default() == "UTC" {
            assert_equal!(
                PathDateTime::from_timestamp(1742346763, 0).to_string(),
                "2025-03-19T01:12:43.000000000Z"
            );
        } else {
            assert_equal!(
                PathDateTime::from_timestamp(1742346763, 0).to_string(),
                "2025-03-18T22:12:43.000000000-03:00"
            );
        }
    }
    #[test]
    fn test_parse_from_str() {
        if std::env::var("TZ").unwrap_or_default() == "UTC" {
            assert_equal!(
                PathDateTime::parse_from_str("2025-03-19T01:12:43", "%Y-%m-%dT%H:%M:%S")
                    .unwrap()
                    .to_string(),
                "2025-03-19T01:12:43.000000000Z"
            );
            assert_equal!(
                PathDateTime::parse_from_str("2025-03-19T01:12:43", "%Y-%m-%dT%H:%M:%S")
                    .unwrap()
                    .to_string(),
                "2025-03-19T01:12:43.000000000Z"
            );
            assert_equal!(
                PathDateTime::parse_from_str(
                    "2025-03-19T01:12:43.123456789Z",
                    "%Y-%m-%dT%H:%M:%S.%fZ"
                )
                .unwrap()
                .to_string(),
                "2025-03-19T01:12:43.123456789Z"
            );
        } else {
            assert_equal!(
                PathDateTime::parse_from_str("2025-03-19T01:12:43", "%Y-%m-%dT%H:%M:%S")
                    .unwrap()
                    .to_string(),
                "2025-03-18T22:12:43.000000000-03:00"
            );

            assert_equal!(
                PathDateTime::parse_from_str("2025-03-19 01:12:43", "%Y-%m-%d %H:%M:%S")
                    .unwrap()
                    .to_string(),
                "2025-03-18T22:12:43.000000000-03:00"
            );
            assert_equal!(
                PathDateTime::parse_from_str(
                    "2025-03-19T01:12:43.123456789Z",
                    "%Y-%m-%dT%H:%M:%S.%fZ"
                )
                .unwrap()
                .to_string(),
                "2025-03-18T22:12:43.123456789-03:00"
            );
        }
    }
    #[test]
    fn test_to_array() {
        let datetime = DateTime::from_timestamp(1742346763, 0).unwrap();
        if std::env::var("TZ").unwrap_or_default() == "UTC" {
            assert_equal!(
                PathDateTime::from_datetime_utc(&datetime).to_array(),
                [2025, 3, 19, 1, 12, 43]
            );
        } else {
            assert_equal!(
                PathDateTime::from_datetime_utc(&datetime).to_array(),
                [2025, 3, 19, 1, 12, 43]
            );
        }
    }
    #[test]
    fn test_to_bytes() {
        let datetime = DateTime::from_timestamp(1742346763, 0).unwrap();
        if std::env::var("TZ").unwrap_or_default() == "UTC" {
            assert_eq!(
                PathDateTime::from_datetime_utc(&datetime).to_bytes(),
                vec![7, 233, 0, 3, 0, 19, 0, 1, 0, 12, 0, 43],
            );
        } else {
            assert_eq!(
                PathDateTime::from_datetime_utc(&datetime).to_bytes(),
                vec![7, 233, 0, 3, 0, 19, 0, 1, 0, 12, 0, 43],
            );
        }
    }
    #[test]
    fn test_local_datetime() {
        let datetime = DateTime::from_timestamp(1742346763, 0).unwrap();

        if std::env::var("TZ").unwrap_or_default() == "UTC" {
            assert_equal!(
                PathDateTime::from_datetime_utc(&datetime).local_datetime().to_string(),
                "2025-03-19 01:12:43 +00:00"
            );
        } else {
            assert_equal!(
                PathDateTime::from_datetime_utc(&datetime).local_datetime().to_string(),
                "2025-03-18 22:12:43 -03:00"
            );
        }
    }
    #[test]
    fn test_utc_datetime() {
        let datetime = DateTime::from_timestamp(1742346763, 0).unwrap();

        if std::env::var("TZ").unwrap_or_default() == "UTC" {
            assert_equal!(
                PathDateTime::from_datetime_utc(&datetime).utc_datetime().to_string(),
                "2025-03-19 01:12:43 UTC"
            );
        } else {
            assert_equal!(
                PathDateTime::from_datetime_utc(&datetime).utc_datetime().to_string(),
                "2025-03-19 01:12:43 UTC"
            );
        }
    }
    #[test]
    fn test_timestamp() {
        let datetime = DateTime::from_timestamp(1742346763, 123456789).unwrap();

        if std::env::var("TZ").unwrap_or_default() == "UTC" {
            assert_eq!(
                PathDateTime::from_datetime_utc(&datetime).timestamp(),
                (1742346763, 123456789),
            );
        } else {
            assert_eq!(
                PathDateTime::from_datetime_utc(&datetime).timestamp(),
                (1742346763, 123456789),
            );
        }
    }
}