i3status_rs/formatting/formatter/
datetime.rs1use 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 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}