Skip to main content

i3status_rs/formatting/formatter/
datetime.rs

1use chrono::format::{Fixed, Item, StrftimeItems};
2use chrono::{DateTime, Local, Locale, TimeZone};
3use chrono_tz::{OffsetName as _, Tz};
4
5use std::fmt::Display;
6use std::sync::LazyLock;
7
8use super::*;
9
10make_log_macro!(error, "datetime");
11
12const DEFAULT_DATETIME_FORMAT: &str = "%a %d/%m %R";
13
14pub static DEFAULT_DATETIME_FORMATTER: LazyLock<DatetimeFormatter> =
15    LazyLock::new(|| DatetimeFormatter::new(Some(DEFAULT_DATETIME_FORMAT), None, None).unwrap());
16
17#[derive(Debug)]
18pub enum DatetimeFormatter {
19    Chrono {
20        items: Vec<Item<'static>>,
21        locale: Option<Locale>,
22    },
23    #[cfg(feature = "icu_calendar")]
24    Icu {
25        fieldset: icu_datetime::fieldsets::enums::CompositeDateTimeFieldSet,
26        locale: icu_locale::Locale,
27    },
28}
29
30impl DatetimeFormatter {
31    pub(super) fn from_args(args: &[Arg]) -> Result<Self> {
32        let mut format = None;
33        let mut locale = None;
34        let mut precision = None;
35        for arg in args {
36            match arg.key {
37                "format" | "f" => {
38                    format = Some(arg.val.error("format must be specified")?);
39                }
40                "locale" | "l" => {
41                    locale = Some(arg.val.error("locale must be specified")?);
42                }
43                "precision" | "p" => {
44                    precision = Some(arg.val.error("precision must be specified")?);
45                }
46                other => {
47                    return Err(Error::new(format!(
48                        "Unknown argument for 'datetime': '{other}'"
49                    )));
50                }
51            }
52        }
53        Self::new(format, locale, precision)
54    }
55
56    fn new(format: Option<&str>, locale: Option<&str>, precision: Option<&str>) -> Result<Self> {
57        let (items, locale) = match locale {
58            Some(locale) => {
59                #[cfg(feature = "icu_calendar")]
60                let Ok(locale) = locale.try_into() else {
61                    // try with icu4x
62                    use icu_datetime::fieldsets::{
63                        self,
64                        enums::{CompositeDateTimeFieldSet, DateAndTimeFieldSet, DateFieldSet},
65                    };
66                    use icu_datetime::options::{Length, TimePrecision};
67                    use std::str::FromStr as _;
68
69                    let precision = match precision {
70                        Some("seconds" | "second" | "s") => Some(TimePrecision::Second),
71                        Some("minutes" | "minute" | "m") => Some(TimePrecision::Minute),
72                        Some("hours" | "hour" | "h") => Some(TimePrecision::Hour),
73                        None => None,
74                        _ => Err(Error::new("Invalid precision value"))?,
75                    };
76                    let locale = icu_locale::Locale::from_str(locale)
77                        .ok()
78                        .error("invalid locale")?;
79                    let fieldset = match format {
80                        Some("full") => match precision {
81                            Some(precision) => {
82                                CompositeDateTimeFieldSet::DateTime(DateAndTimeFieldSet::YMDET(
83                                    fieldsets::YMDET::long().with_time_precision(precision),
84                                ))
85                            }
86                            None => CompositeDateTimeFieldSet::Date(DateFieldSet::YMDE(
87                                fieldsets::YMDE::long(),
88                            )),
89                        },
90                        length => {
91                            let length = match length {
92                                Some("short") => Length::Short,
93                                Some("medium") => Length::Medium,
94                                Some("long") | None => Length::Long,
95                                _ => Err(Error::new("Invalid length value"))?,
96                            };
97                            match precision {
98                                Some(precision) => {
99                                    CompositeDateTimeFieldSet::DateTime(DateAndTimeFieldSet::YMDT(
100                                        fieldsets::YMDT::for_length(length)
101                                            .with_time_precision(precision),
102                                    ))
103                                }
104                                None => CompositeDateTimeFieldSet::Date(DateFieldSet::YMD(
105                                    fieldsets::YMD::for_length(length),
106                                )),
107                            }
108                        }
109                    };
110
111                    return Ok(Self::Icu { locale, fieldset });
112                };
113                #[cfg(not(feature = "icu_calendar"))]
114                let locale = locale.try_into().ok().error("invalid locale")?;
115                if precision.is_some() {
116                    return Err(Error::new(
117                        "`precision` is only available for icu datetimes",
118                    ));
119                }
120                (
121                    StrftimeItems::new_with_locale(
122                        format.unwrap_or(DEFAULT_DATETIME_FORMAT),
123                        locale,
124                    ),
125                    Some(locale),
126                )
127            }
128            None => {
129                if precision.is_some() {
130                    return Err(Error::new(
131                        "`precision` is only available for icu datetimes",
132                    ));
133                }
134                (
135                    StrftimeItems::new(format.unwrap_or(DEFAULT_DATETIME_FORMAT)),
136                    None,
137                )
138            }
139        };
140
141        Ok(Self::Chrono {
142            items: items.parse_to_owned().error(format!(
143                "Invalid format: \"{}\"",
144                format.unwrap_or(DEFAULT_DATETIME_FORMAT)
145            ))?,
146            locale,
147        })
148    }
149}
150
151pub(crate) trait TimezoneName {
152    fn timezone_name(datetime: &DateTime<Self>) -> Result<Item<'_>>
153    where
154        Self: TimeZone;
155}
156
157impl TimezoneName for Tz {
158    fn timezone_name(datetime: &DateTime<Tz>) -> Result<Item<'_>> {
159        Ok(Item::Literal(
160            datetime
161                .offset()
162                .abbreviation()
163                .error("Timezone name unknown")?,
164        ))
165    }
166}
167
168impl TimezoneName for Local {
169    fn timezone_name(datetime: &DateTime<Local>) -> Result<Item<'_>> {
170        let tz_name = iana_time_zone::get_timezone().error("Could not get local timezone")?;
171        let tz = tz_name
172            .parse::<Tz>()
173            .error("Could not parse local timezone")?;
174        Tz::timezone_name(&datetime.with_timezone(&tz)).map(|x| x.to_owned())
175    }
176}
177
178fn borrow_item<'a>(item: &'a Item) -> Item<'a> {
179    match item {
180        Item::Literal(s) => Item::Literal(s),
181        Item::OwnedLiteral(s) => Item::Literal(s),
182        Item::Space(s) => Item::Space(s),
183        Item::OwnedSpace(s) => Item::Space(s),
184        Item::Numeric(n, p) => Item::Numeric(n.clone(), *p),
185        Item::Fixed(f) => Item::Fixed(f.clone()),
186        Item::Error => Item::Error,
187    }
188}
189
190impl Formatter for DatetimeFormatter {
191    fn format(&self, val: &Value, _config: &SharedConfig) -> Result<String, FormatError> {
192        #[allow(clippy::unnecessary_wraps)]
193        fn for_generic_datetime<T>(
194            this: &DatetimeFormatter,
195            datetime: DateTime<T>,
196        ) -> Result<String, FormatError>
197        where
198            T: TimeZone + TimezoneName,
199            T::Offset: Display,
200        {
201            Ok(match this {
202                DatetimeFormatter::Chrono { items, locale } => {
203                    let new_items = items.iter().map(|item| match item {
204                        Item::Fixed(Fixed::TimezoneName) => match T::timezone_name(&datetime) {
205                            Ok(name) => name,
206                            Err(e) => {
207                                error!("{e}");
208                                Item::Fixed(Fixed::TimezoneName)
209                            }
210                        },
211                        item => borrow_item(item),
212                    });
213                    match *locale {
214                        Some(locale) => datetime
215                            .format_localized_with_items(new_items, locale)
216                            .to_string(),
217                        None => datetime.format_with_items(new_items).to_string(),
218                    }
219                }
220                #[cfg(feature = "icu_calendar")]
221                DatetimeFormatter::Icu {
222                    locale,
223                    fieldset: length,
224                } => {
225                    use chrono::{Datelike as _, Timelike as _};
226                    let datetime = icu_datetime::input::DateTime {
227                        date: icu_datetime::input::Date::try_new_iso(
228                            datetime.year(),
229                            datetime.month() as u8,
230                            datetime.day() as u8,
231                        )
232                        .error("Current date should be a valid date")?,
233                        time: icu_datetime::input::Time::try_new(
234                            datetime.hour() as u8,
235                            datetime.minute() as u8,
236                            datetime.second() as u8,
237                            datetime.nanosecond(),
238                        )
239                        .error("Current time should be a valid time")?,
240                    };
241                    let dft = icu_datetime::DateTimeFormatter::try_new(locale.into(), *length)
242                        .ok()
243                        .error("locale should be present in compiled data")?;
244                    dft.format(&datetime).to_string()
245                }
246            })
247        }
248        match val {
249            Value::Datetime(datetime, timezone) => match timezone {
250                Some(tz) => for_generic_datetime(self, datetime.with_timezone(tz)),
251                None => for_generic_datetime(self, datetime.with_timezone(&Local)),
252            },
253            other => Err(FormatError::IncompatibleFormatter {
254                ty: other.type_name(),
255                fmt: "datetime",
256            }),
257        }
258    }
259}