atom-engine 5.0.2

A component-oriented template engine built on Tera with props, slots, and provide/inject context
Documentation
use crate::types::value::Value;
use chrono::{DateTime, Datelike, Duration, Timelike, Utc};

pub fn date_format(dt: &DateTime<Utc>, pattern: &str) -> Value {
    let result = pattern
        .replace("YYYY", &dt.format("%Y").to_string())
        .replace("MM", &dt.format("%m").to_string())
        .replace("DD", &dt.format("%d").to_string())
        .replace("HH", &dt.format("%H").to_string())
        .replace("mm", &dt.format("%M").to_string())
        .replace("ss", &dt.format("%S").to_string());
    Value::Str(result)
}

pub fn date_to_iso(dt: &DateTime<Utc>) -> Value {
    Value::Str(dt.to_rfc3339())
}

pub fn date_to_unix(dt: &DateTime<Utc>) -> Value {
    Value::Num(dt.timestamp() as f64)
}

pub fn date_add(dt: &DateTime<Utc>, n: i64, unit: &str) -> Value {
    let duration = match unit {
        "years" => Duration::days(n * 365),
        "months" => Duration::days(n * 30),
        "weeks" => Duration::weeks(n),
        "days" => Duration::days(n),
        "hours" => Duration::hours(n),
        "minutes" => Duration::minutes(n),
        "seconds" => Duration::seconds(n),
        _ => Duration::days(n),
    };
    Value::Date(*dt + duration)
}

pub fn date_sub(dt: &DateTime<Utc>, n: i64, unit: &str) -> Value {
    date_add(dt, -n, unit)
}

pub fn date_start_of(dt: &DateTime<Utc>, unit: &str) -> Value {
    match unit {
        "year" => Value::Date(
            dt.with_month(1)
                .unwrap()
                .with_day(1)
                .unwrap()
                .with_hour(0)
                .unwrap()
                .with_minute(0)
                .unwrap()
                .with_second(0)
                .unwrap(),
        ),
        "month" => Value::Date(
            dt.with_day(1)
                .unwrap()
                .with_hour(0)
                .unwrap()
                .with_minute(0)
                .unwrap()
                .with_second(0)
                .unwrap(),
        ),
        "day" => Value::Date(
            dt.with_hour(0)
                .unwrap()
                .with_minute(0)
                .unwrap()
                .with_second(0)
                .unwrap(),
        ),
        "hour" => Value::Date(dt.with_minute(0).unwrap().with_second(0).unwrap()),
        _ => Value::Date(*dt),
    }
}

pub fn date_end_of(dt: &DateTime<Utc>, unit: &str) -> Value {
    match unit {
        "year" => Value::Date(
            dt.with_month(12)
                .unwrap()
                .with_day(31)
                .unwrap()
                .with_hour(23)
                .unwrap()
                .with_minute(59)
                .unwrap()
                .with_second(59)
                .unwrap(),
        ),
        "month" => {
            let last_day = (*dt - Duration::days(1)).day();
            Value::Date(
                dt.with_day(last_day)
                    .unwrap()
                    .with_hour(23)
                    .unwrap()
                    .with_minute(59)
                    .unwrap()
                    .with_second(59)
                    .unwrap(),
            )
        }
        "day" => Value::Date(
            dt.with_hour(23)
                .unwrap()
                .with_minute(59)
                .unwrap()
                .with_second(59)
                .unwrap(),
        ),
        _ => Value::Date(*dt),
    }
}

pub fn date_is_before(dt: &DateTime<Utc>, other: &DateTime<Utc>) -> Value {
    Value::Bool(dt < other)
}

pub fn date_is_after(dt: &DateTime<Utc>, other: &DateTime<Utc>) -> Value {
    Value::Bool(dt > other)
}

pub fn date_is_same(dt: &DateTime<Utc>, other: &DateTime<Utc>, _unit: Option<&str>) -> Value {
    Value::Bool(dt.timestamp() == other.timestamp())
}

pub fn date_diff(dt: &DateTime<Utc>, other: &DateTime<Utc>, unit: &str) -> Value {
    let diff = (*dt - *other).num_seconds();
    let result = match unit {
        "years" => diff / (365 * 24 * 60 * 60),
        "months" => diff / (30 * 24 * 60 * 60),
        "days" => diff / (24 * 60 * 60),
        "hours" => diff / (60 * 60),
        "minutes" => diff / 60,
        "seconds" => diff,
        _ => diff,
    };
    Value::Num(result as f64)
}

pub fn date_to_relative(dt: &DateTime<Utc>) -> Value {
    let now = Utc::now();
    let diff = now.signed_duration_since(*dt);

    if diff.num_seconds() < 60 {
        Value::Str("just now".to_string())
    } else if diff.num_minutes() < 60 {
        let m = diff.num_minutes();
        Value::Str(format!("{} minute{} ago", m, if m == 1 { "" } else { "s" }))
    } else if diff.num_hours() < 24 {
        let h = diff.num_hours();
        Value::Str(format!("{} hour{} ago", h, if h == 1 { "" } else { "s" }))
    } else if diff.num_days() < 30 {
        let d = diff.num_days();
        Value::Str(format!("{} day{} ago", d, if d == 1 { "" } else { "s" }))
    } else if diff.num_days() < 365 {
        Value::Str(format!("{} months ago", diff.num_days() / 30))
    } else {
        Value::Str(format!("{} years ago", diff.num_days() / 365))
    }
}

pub fn now() -> Value {
    Value::Date(Utc::now())
}

pub fn today() -> Value {
    Value::Date(
        Utc::now()
            .date_naive()
            .and_hms_opt(0, 0, 0)
            .unwrap()
            .and_utc(),
    )
}

pub fn parse_date(s: &str, _fmt: Option<&str>) -> Value {
    if let Ok(dt) = DateTime::parse_from_rfc3339(s) {
        Value::Date(dt.with_timezone(&Utc))
    } else if let Ok(naive) = chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d") {
        Value::Date(naive.and_hms_opt(0, 0, 0).unwrap().and_utc())
    } else {
        Value::Null
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_now() {
        let result = now();
        assert!(matches!(result, Value::Date(_)));
    }

    #[test]
    fn test_today() {
        let result = today();
        assert!(matches!(result, Value::Date(_)));
    }

    #[test]
    fn test_date_format() {
        let dt = Utc::now();
        let result = date_format(&dt, "YYYY-MM-DD");
        assert!(matches!(result, Value::Str(_)));
    }

    #[test]
    fn test_date_to_iso() {
        let dt = Utc::now();
        let result = date_to_iso(&dt);
        assert!(matches!(result, Value::Str(_)));
    }

    #[test]
    fn test_date_add() {
        let dt = Utc::now();
        let result = date_add(&dt, 1, "days");
        assert!(matches!(result, Value::Date(_)));
    }

    #[test]
    fn test_parse_date() {
        let result = parse_date("2024-01-15", None);
        assert!(matches!(result, Value::Date(_)));
    }
}