1use icu_calendar::{Gregorian, Iso};
2use icu_datetime::fieldsets::enums::CompositeFieldSet;
3use icu_datetime::pattern::{DateTimePattern, FixedCalendarDateTimeNames};
4use icu_locale::Locale;
5use icu_time::zone::models::AtTime;
6use icu_time::{TimeZoneInfo, ZonedDateTime};
7use jiff::fmt::temporal::{DateTimeParser, Pieces};
8use jiff::tz::TimeZone;
9use jiff::{Timestamp, Zoned};
10use jiff_icu::ConvertFrom;
11use tera::{Kwargs, Number, State, TeraResult, Value};
12use writeable::TryWriteable;
13
14static PARSER: DateTimeParser = DateTimeParser::new();
15
16fn parse_to_zoned(val: &Value, tz: Option<TimeZone>) -> TeraResult<Zoned> {
18 let default_tz = tz.unwrap_or(TimeZone::UTC);
19 if let Some(s) = val.as_str() {
20 PARSER
21 .parse_zoned(s)
22 .or_else(|_| {
23 PARSER.parse_timestamp(s).map(|t| {
24 let zone = Pieces::parse(s)
25 .ok()
26 .and_then(|p| p.to_numeric_offset())
27 .map_or_else(|| default_tz.clone(), TimeZone::fixed);
28 t.to_zoned(zone)
29 })
30 })
31 .or_else(|_| {
32 PARSER
33 .parse_datetime(s)
34 .and_then(|d| d.to_zoned(default_tz.clone()))
35 })
36 .or_else(|_| PARSER.parse_date(s).and_then(|d| d.to_zoned(default_tz)))
37 .map_err(|e| {
38 tera::Error::message(format!(
39 "The string {s} cannot be parsed as a valid date: {e}"
40 ))
41 })
42 } else if let Some(Number::Integer(ts)) = val.as_number() {
43 let ts = i64::try_from(ts)
44 .map_err(|_| tera::Error::message(format!("Invalid timestamp: {ts}")))?;
45 Timestamp::new(ts, 0)
46 .map(|t| t.to_zoned(default_tz))
47 .map_err(|e| tera::Error::message(format!("Invalid timestamp: {e}")))
48 } else {
49 Err(tera::Error::message(format!(
50 "Invalid value: expected a string or integer, got {}",
51 val.name()
52 )))
53 }
54}
55
56pub fn now(kwargs: Kwargs, _: &State) -> TeraResult<Value> {
64 let tz_str = kwargs.get::<&str>("timezone")?.unwrap_or("UTC");
65 let timezone = TimeZone::get(tz_str)
66 .map_err(|_| tera::Error::message(format!("Unknown timezone: {tz_str}")))?;
67 let now = Zoned::now().with_time_zone(timezone);
68 Ok(Value::from(now.to_string()))
69}
70
71pub fn date(val: &Value, kwargs: Kwargs, _: &State) -> TeraResult<String> {
91 let format = kwargs.get::<&str>("format")?;
92 let locale = kwargs.get::<&str>("locale")?;
93 let timezone = match kwargs.get::<&str>("timezone")? {
94 Some(t) => Some(
95 TimeZone::get(t).map_err(|_| tera::Error::message(format!("Unknown timezone: {t}")))?,
96 ),
97 None => None,
98 };
99
100 let mut zoned = parse_to_zoned(val, timezone.clone())?;
101 if let Some(tz) = timezone {
102 zoned = zoned.with_time_zone(tz);
103 }
104
105 match locale {
106 Some(locale_str) => {
109 let Some(fmt) = format else {
110 return Err(tera::Error::message(
111 "Setting `locale` requires setting a `format` as well",
112 ));
113 };
114
115 if fmt.contains('%') {
120 return Err(tera::Error::message(format!(
121 "Invalid date format `{fmt}`: strftime formats cannot be localized, \
122 use a UTS-35 pattern instead (eg `d MMMM y`)"
123 )));
124 }
125 let locale = Locale::try_from_str(&locale_str.replace('_', "-"))
127 .map_err(|_| tera::Error::message(format!("Invalid locale: {locale_str}")))?;
128 let pattern = DateTimePattern::try_from_pattern_str(fmt).map_err(|e| {
129 tera::Error::message(format!("Invalid UTS-35 date format `{fmt}`: {e}"))
130 })?;
131 let mut names =
132 FixedCalendarDateTimeNames::<Gregorian, CompositeFieldSet>::try_new(locale.into())
133 .map_err(|e| {
134 tera::Error::message(format!(
135 "Failed to load data for locale {locale_str}: {e}"
136 ))
137 })?;
138 let formatter = names.include_for_pattern(&pattern).map_err(|e| {
139 tera::Error::message(format!(
140 "Invalid date format `{fmt}` for locale {locale_str}: {e}"
141 ))
142 })?;
143
144 let iso = ZonedDateTime::<Iso, TimeZoneInfo<AtTime>>::convert_from(&zoned);
145 let zdt = ZonedDateTime {
146 date: iso.date.to_calendar(Gregorian),
147 time: iso.time,
148 zone: iso.zone,
149 };
150
151 formatter
152 .format(&zdt)
153 .try_write_to_string()
154 .map(|s| s.into_owned())
155 .map_err(|(e, _)| {
156 tera::Error::message(format!("Failed to format date with `{fmt}`: {e}"))
157 })
158 }
159 None => {
160 let fmt = format.unwrap_or("%Y-%m-%d");
161 jiff::fmt::strtime::format(fmt, &zoned)
162 .map_err(|e| tera::Error::message(format!("Invalid date format `{fmt}`: {e}")))
163 }
164 }
165}
166
167pub fn is_before(val: &Value, kwargs: Kwargs, _: &State) -> TeraResult<bool> {
176 let other = kwargs.must_get::<&Value>("other")?;
177 let inclusive = kwargs.get::<bool>("inclusive")?.unwrap_or(false);
178 let val_zoned = parse_to_zoned(val, None)?;
179 let other_zoned = parse_to_zoned(other, None)?;
180 if inclusive {
181 Ok(val_zoned <= other_zoned)
182 } else {
183 Ok(val_zoned < other_zoned)
184 }
185}
186
187pub fn is_after(val: &Value, kwargs: Kwargs, _: &State) -> TeraResult<bool> {
196 let other = kwargs.must_get::<&Value>("other")?;
197 let inclusive = kwargs.get::<bool>("inclusive")?.unwrap_or(false);
198 let val_zoned = parse_to_zoned(val, None)?;
199 let other_zoned = parse_to_zoned(other, None)?;
200 if inclusive {
201 Ok(val_zoned >= other_zoned)
202 } else {
203 Ok(val_zoned > other_zoned)
204 }
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210 use std::sync::Arc;
211 use tera::value::Map;
212 use tera::{Context, Kwargs, State};
213
214 #[test]
216 fn test_ok_date() {
217 let inputs = vec![
218 (Value::from(1482720453), None, None, None, "2016-12-26"),
219 (
220 Value::from(1482720453),
221 Some("%Y-%m-%d %H:%M"),
222 None,
223 None,
224 "2016-12-26 02:47",
225 ),
226 (
228 Value::from("1985-04-12T23:20:50.52Z"),
229 None,
230 None,
231 None,
232 "1985-04-12",
233 ),
234 (
236 Value::from("1996-12-19T16:39:57[-08:00]"),
237 Some("%Y-%m-%d %z"),
238 None,
239 None,
240 "1996-12-19 -0800",
241 ),
242 (
244 Value::from("1996-12-19T16:39:57-08:00"),
245 Some("%Y-%m-%d %H:%M %z"),
246 None,
247 None,
248 "1996-12-19 16:39 -0800",
249 ),
250 (
252 Value::from("2017-03-05"),
253 Some("%a, %d %b %Y %H:%M:%S %z"),
254 None,
255 None,
256 "Sun, 05 Mar 2017 00:00:00 +0000",
257 ),
258 (
260 Value::from("2017-03-05T00:00:00.602"),
261 Some("%a, %d %b %Y %H:%M:%S"),
262 None,
263 None,
264 "Sun, 05 Mar 2017 00:00:00",
265 ),
266 (
268 Value::from("2019-09-19T01:48:44.581Z"),
269 None,
270 None,
271 Some("America/New_York"),
272 "2019-09-18",
273 ),
274 (
275 Value::from(1648252203),
276 None,
277 None,
278 Some("Europe/Berlin"),
279 "2022-03-26",
280 ),
281 (
283 Value::from("2026-08-24"),
284 Some("d MMMM y"),
285 Some("fr"),
286 None,
287 "24 août 2026",
288 ),
289 (
290 Value::from("2026-08-24"),
291 Some("d MMMM y"),
292 Some("fr-FR"),
293 None,
294 "24 août 2026",
295 ),
296 (
297 Value::from("2026-08-24"),
298 Some("d MMMM y"),
299 Some("fr_FR"),
300 None,
301 "24 août 2026",
302 ),
303 (
304 Value::from("2026-08-24"),
305 Some("EEEE d MMMM y"),
306 Some("fr_FR"),
307 None,
308 "lundi 24 août 2026",
309 ),
310 (
311 Value::from("2026-08-24"),
312 Some("y年M月d日"),
313 Some("ja"),
314 None,
315 "2026年8月24日",
316 ),
317 (
318 Value::from(1787574924),
319 Some("d MMMM y à HH:mm"),
320 Some("fr"),
321 Some("Europe/Paris"),
322 "24 août 2026 à 14:35",
323 ),
324 (
325 Value::from("2026-08-24T14:35:00"),
326 Some("MMMM d y [zzz]"),
327 Some("it"),
328 Some("America/New_York"),
329 "agosto 24 2026 [GMT-4]",
330 ),
331 ];
332
333 for (value, format, locale, timezone, expected) in inputs {
334 let mut map = Map::new();
335 if let Some(f) = format {
336 map.insert("format".into(), f.into());
337 }
338 if let Some(tz) = timezone {
339 map.insert("timezone".into(), tz.into());
340 }
341 if let Some(l) = locale {
342 map.insert("locale".into(), l.into());
343 }
344 let kwargs = Kwargs::new(Arc::new(map));
345 let ctx = Context::new();
346 let res = date(&value, kwargs, &State::new(&ctx)).unwrap();
347 assert_eq!(expected, res);
348 }
349 }
350
351 #[test]
352 fn test_bad_date_call() {
353 let inputs = vec![
354 (Value::from(1482720453), Some("%1"), None, None),
355 (Value::from(1482720453), Some("%+S"), None, None),
356 (
357 Value::from("2019-09-19T01:48:44.581Z"),
358 Some("%+S"),
359 Some("Narnia"),
360 None,
361 ),
362 (Value::from(1482720453), Some("MMMM"), None, Some("unknown")),
364 (Value::from(1482720453), None, None, Some("fr")),
366 (Value::from(1482720453), Some("%Y-%m-%d"), None, Some("fr")),
368 ];
369
370 for (value, format, timezone, locale) in inputs {
371 let mut map = Map::new();
372 if let Some(f) = format {
373 map.insert("format".into(), f.into());
374 }
375 if let Some(tz) = timezone {
376 map.insert("timezone".into(), tz.into());
377 }
378 if let Some(l) = locale {
379 map.insert("locale".into(), l.into());
380 }
381 let kwargs = Kwargs::new(Arc::new(map));
382 let ctx = Context::new();
383 let res = date(&value, kwargs, &State::new(&ctx));
384 println!("{res:?}");
385 assert!(res.is_err());
386 }
387 }
388
389 #[test]
390 fn test_register() {
391 let mut tera = tera::Tera::default();
392 tera.register_filter("date", date);
393 tera.register_test("before", is_before);
394 tera.register_test("after", is_after);
395 tera.register_function("now", now);
396 }
397
398 #[test]
399 fn test_is_before() {
400 let ctx = Context::new();
401 let state = State::new(&ctx);
402
403 let cases: Vec<(Value, Value, bool, bool)> = vec![
405 (Value::from(500), Value::from(1000), false, true),
406 (Value::from(1000), Value::from(500), false, false),
407 (
408 Value::from("2024-01-01"),
409 Value::from("2024-06-01"),
410 false,
411 true,
412 ),
413 (
414 Value::from("2024-06-01"),
415 Value::from("2024-01-01"),
416 false,
417 false,
418 ),
419 (Value::from(1000), Value::from("2020-01-01"), false, true), (
421 Value::from("2024-01-01"),
422 Value::from("2024-01-01"),
423 false,
424 false,
425 ), (
427 Value::from("2024-01-01"),
428 Value::from("2024-01-01"),
429 true,
430 true,
431 ), ];
433
434 for (val, other, inclusive, expected) in cases {
435 let mut map = Map::new();
436 map.insert("other".into(), other);
437 if inclusive {
438 map.insert("inclusive".into(), Value::from(true));
439 }
440 let kwargs = Kwargs::new(Arc::new(map));
441 assert_eq!(is_before(&val, kwargs, &state).unwrap(), expected);
442 }
443 }
444
445 #[test]
446 fn test_is_after() {
447 let ctx = Context::new();
448 let state = State::new(&ctx);
449
450 let cases: Vec<(Value, Value, bool, bool)> = vec![
452 (Value::from(1000), Value::from(500), false, true),
453 (Value::from(500), Value::from(1000), false, false),
454 (
455 Value::from("2024-06-01"),
456 Value::from("2024-01-01"),
457 false,
458 true,
459 ),
460 (
461 Value::from("2024-01-01"),
462 Value::from("2024-06-01"),
463 false,
464 false,
465 ),
466 (Value::from("2020-01-01"), Value::from(1000), false, true), (
468 Value::from("2024-01-01"),
469 Value::from("2024-01-01"),
470 false,
471 false,
472 ), (
474 Value::from("2024-01-01"),
475 Value::from("2024-01-01"),
476 true,
477 true,
478 ), ];
480
481 for (val, other, inclusive, expected) in cases {
482 let mut map = Map::new();
483 map.insert("other".into(), other);
484 if inclusive {
485 map.insert("inclusive".into(), Value::from(true));
486 }
487 let kwargs = Kwargs::new(Arc::new(map));
488 assert_eq!(is_after(&val, kwargs, &state).unwrap(), expected);
489 }
490 }
491}