1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
use chrono::format::{Item, StrftimeItems};
use chrono::{DateTime, Local, Locale, TimeZone};
use once_cell::sync::Lazy;

use std::fmt::Display;

use super::*;

const DEFAULT_DATETIME_FORMAT: &str = "%a %d/%m %R";

pub static DEFAULT_DATETIME_FORMATTER: Lazy<DatetimeFormatter> =
    Lazy::new(|| DatetimeFormatter::new(Some(DEFAULT_DATETIME_FORMAT), None).unwrap());

#[derive(Debug)]
pub enum DatetimeFormatter {
    Chrono {
        items: Vec<Item<'static>>,
        locale: Option<Locale>,
    },
    #[cfg(feature = "icu_calendar")]
    Icu {
        length: icu_datetime::options::length::Date,
        locale: icu_locid::Locale,
    },
}

fn make_static_item(item: Item<'_>) -> Item<'static> {
    match item {
        Item::Literal(str) => Item::OwnedLiteral(str.into()),
        Item::OwnedLiteral(boxed) => Item::OwnedLiteral(boxed),
        Item::Space(str) => Item::OwnedSpace(str.into()),
        Item::OwnedSpace(boxed) => Item::OwnedSpace(boxed),
        Item::Numeric(numeric, pad) => Item::Numeric(numeric, pad),
        Item::Fixed(fixed) => Item::Fixed(fixed),
        Item::Error => Item::Error,
    }
}

impl DatetimeFormatter {
    pub(super) fn from_args(args: &[Arg]) -> Result<Self> {
        let mut format = None;
        let mut locale = None;
        for arg in args {
            match arg.key {
                "format" | "f" => {
                    format = Some(arg.val);
                }
                "locale" | "l" => {
                    locale = Some(arg.val);
                }
                other => {
                    return Err(Error::new(format!(
                        "Unknown argument for 'datetime': '{other}'"
                    )));
                }
            }
        }
        Self::new(format, locale)
    }

    fn new(format: Option<&str>, locale: Option<&str>) -> Result<Self> {
        let (items, locale) = match locale {
            Some(locale) => {
                #[cfg(feature = "icu_calendar")]
                let Ok(locale) = locale.try_into() else {
                    use std::str::FromStr as _;
                    // try with icu4x
                    let locale = icu_locid::Locale::from_str(locale)
                        .ok()
                        .error("invalid locale")?;
                    let length = match format {
                        Some("full") => icu_datetime::options::length::Date::Full,
                        None | Some("long") => icu_datetime::options::length::Date::Long,
                        Some("medium") => icu_datetime::options::length::Date::Medium,
                        Some("short") => icu_datetime::options::length::Date::Short,
                        _ => return Err(Error::new("Unknown format option for icu based locale")),
                    };
                    return Ok(Self::Icu { locale, length });
                };
                #[cfg(not(feature = "icu_calendar"))]
                let locale = locale.try_into().ok().error("invalid locale")?;
                (
                    StrftimeItems::new_with_locale(
                        format.unwrap_or(DEFAULT_DATETIME_FORMAT),
                        locale,
                    ),
                    Some(locale),
                )
            }
            None => (
                StrftimeItems::new(format.unwrap_or(DEFAULT_DATETIME_FORMAT)),
                None,
            ),
        };

        Ok(Self::Chrono {
            items: items.map(make_static_item).collect(),
            locale,
        })
    }
}

impl Formatter for DatetimeFormatter {
    fn format(&self, val: &Value, _config: &SharedConfig) -> Result<String, FormatError> {
        #[allow(clippy::unnecessary_wraps)]
        fn for_generic_datetime<T>(
            this: &DatetimeFormatter,
            datetime: DateTime<T>,
        ) -> Result<String, FormatError>
        where
            T: TimeZone,
            T::Offset: Display,
        {
            Ok(match this {
                DatetimeFormatter::Chrono { items, locale } => match *locale {
                    Some(locale) => datetime.format_localized_with_items(items.iter(), locale),
                    None => datetime.format_with_items(items.iter()),
                }
                .to_string(),
                #[cfg(feature = "icu_calendar")]
                DatetimeFormatter::Icu { locale, length } => {
                    use chrono::Datelike as _;
                    let date = icu_calendar::Date::try_new_iso_date(
                        datetime.year_ce().1 as i32,
                        datetime.month() as u8,
                        datetime.day() as u8,
                    )
                    .ok()
                    .error("Current date should be a valid date")?;
                    let date = date.to_any();
                    let dft =
                        icu_datetime::DateFormatter::try_new_with_length(&locale.into(), *length)
                            .ok()
                            .error("locale should be present in compiled data")?;
                    dft.format_to_string(&date)
                        .ok()
                        .error("formatting date using icu failed")?
                }
            })
        }
        match val {
            Value::Datetime(datetime, timezone) => match timezone {
                Some(tz) => for_generic_datetime(self, datetime.with_timezone(tz)),
                None => for_generic_datetime(self, datetime.with_timezone(&Local)),
            },
            other => Err(FormatError::IncompatibleFormatter {
                ty: other.type_name(),
                fmt: "datetime",
            }),
        }
    }
}