ukraine 1.1.0

Glory to Ukraine. Library for transliterating Ukrainian Cyrillic text into Latin script representation
Documentation
// Date and time formatting utilities that follow Ukrainian linguistic standards.
//

use chrono::{Datelike, Local, NaiveDate, NaiveDateTime, NaiveTime, Timelike};

/// Formatting style for representing a calendar date.
#[derive(Debug, Clone, Copy, Default)]
pub enum DateFormatStyle {
    /// Long human-friendly form (`7 серпня 2024 року`).
    #[default]
    Long,
    /// Numeric form (`07.08.2024`).
    Numeric,
}

/// Formatting style for representing a clock time.
#[derive(Debug, Clone, Copy, Default)]
pub enum TimeFormatStyle {
    /// Twenty-four-hour format (`14:05`).
    #[default]
    TwentyFourHour,
    /// Twenty-four-hour format without seconds (`14:05`).
    TwentyFourHourNoSeconds,
    /// Twelve-hour format with Ukrainian period markers (`02:05 дп`).
    TwelveHour,
    /// Twelve-hour format without seconds and with Ukrainian period markers (`02:05 дп`).
    TwelveHourNoSeconds,
}


/// Options for formatting combined date and time strings.
#[derive(Debug, Clone, Copy)]
pub struct FormatOptions {
    pub date_style: DateFormatStyle,
    pub time_style: TimeFormatStyle,
    /// Whether to insert `о` ("at") between date and time segments.
    pub include_time_preposition: bool,
}

impl Default for FormatOptions {
    fn default() -> Self {
        Self {
            date_style: DateFormatStyle::Long,
            time_style: TimeFormatStyle::TwentyFourHourNoSeconds,
            include_time_preposition: true,
        }
    }
}

const MONTHS_GENITIVE: [&str; 12] = [
    "січня",
    "лютого",
    "березня",
    "квітня",
    "травня",
    "червня",
    "липня",
    "серпня",
    "вересня",
    "жовтня",
    "листопада",
    "грудня",
];

/// Format a `NaiveDate` into a Ukrainian string in the requested style.
pub fn format_date(date: NaiveDate, style: DateFormatStyle) -> String {
    match style {
        DateFormatStyle::Long => {
            let month_idx = (date.month0() as usize).min(MONTHS_GENITIVE.len() - 1);
            format!(
                "{} {} {} року",
                date.day(),
                MONTHS_GENITIVE[month_idx],
                date.year()
            )
        }
        DateFormatStyle::Numeric => {
            format!("{:02}.{:02}.{}", date.day(), date.month(), date.year())
        }
    }
}

/// Format a `NaiveTime` into a Ukrainian string in the requested style.
pub fn format_time(time: NaiveTime, style: TimeFormatStyle) -> String {
    match style {
        TimeFormatStyle::TwentyFourHour => format!("{:02}:{:02}:{:02}", time.hour(), time.minute(), time.second()),
        TimeFormatStyle::TwelveHour => {
            let (is_pm, hour12) = time.hour12();
            let designator = if is_pm { "вечора" } else { "ранку" };
            format!("{:02}:{:02}:{:02} {}", hour12, time.minute(), time.second(), designator)
        }
        TimeFormatStyle::TwentyFourHourNoSeconds => {
            format!("{:02}:{:02}", time.hour(), time.minute())
        }
        TimeFormatStyle::TwelveHourNoSeconds => {
            let (is_pm, hour12) = time.hour12();
            let designator = if is_pm { "вечора" } else { "ранку" };
            format!("{:02}:{:02} {}", hour12, time.minute(), designator)
        }
    }
}

/// Format a full `NaiveDateTime` using the provided [`FormatOptions`].
pub fn format_datetime(datetime: NaiveDateTime, opts: FormatOptions) -> String {
    let date_part = format_date(datetime.date(), opts.date_style);
    let time_part = format_time(datetime.time(), opts.time_style);

    if opts.include_time_preposition {
        format!("{date_part} о {time_part}")
    } else {
        format!("{date_part} {time_part}")
    }
}

/// Convenience wrapper that formats the current local time.
pub fn format_local_now(opts: FormatOptions) -> String {
    let now = Local::now().naive_local();
    format_datetime(now, opts)
}

#[cfg(test)]
mod tests {
    use super::*;
    use chrono::{NaiveDate, NaiveDateTime, NaiveTime};

    #[test]
    fn formats_long_date() {
        let date = NaiveDate::from_ymd_opt(2024, 8, 24).unwrap();
        assert_eq!(
            format_date(date, DateFormatStyle::Long),
            "24 серпня 2024 року"
        );
    }

    #[test]
    fn formats_numeric_date() {
        let date = NaiveDate::from_ymd_opt(2024, 1, 9).unwrap();
        assert_eq!(format_date(date, DateFormatStyle::Numeric), "09.01.2024");
    }

    #[test]
    fn formats_datetime_with_preposition() {
        let datetime = NaiveDateTime::new(
            NaiveDate::from_ymd_opt(2023, 11, 21).unwrap(),
            NaiveTime::from_hms_opt(17, 45, 0).unwrap(),
        );
        assert_eq!(
            format_datetime(datetime, FormatOptions::default()),
            "21 листопада 2023 року о 17:45"
        );
    }

    #[test]
    fn formats_12_pm_hour_time() {
        let time = NaiveTime::from_hms_opt(23, 59, 59).unwrap();
        assert_eq!(
            format_time(time, TimeFormatStyle::TwelveHour),
            "11:59:59 вечора"
        );
    }

    #[test]
    fn formats_12_pm_hour_time_no_seconds() {
        let time = NaiveTime::from_hms_opt(22, 59, 00).unwrap();
        assert_eq!(
            format_time(time, TimeFormatStyle::TwelveHourNoSeconds),
            "10:59 вечора"
        );
    }

    #[test]
    fn formats_12_am_hour_time() {
        let time = NaiveTime::from_hms_opt(8, 10, 59).unwrap();
        assert_eq!(
            format_time(time, TimeFormatStyle::TwelveHour),
            "08:10:59 ранку"
        );
    }

    #[test]
    fn formats_24_hour_time() {
        let time = NaiveTime::from_hms_opt(23, 59, 59).unwrap();
        assert_eq!(
            format_time(time, TimeFormatStyle::TwentyFourHour),
            "23:59:59"
        );
    }

    #[test]
    fn formats_24_hour_time_no_seconds() {
        let time = NaiveTime::from_hms_opt(23, 59, 59).unwrap();
        assert_eq!(
            format_time(time, TimeFormatStyle::TwentyFourHourNoSeconds),
            "23:59"
        );
    }
}