1use jiff::fmt::temporal::{DateTimeParser, Pieces};
2use jiff::tz::TimeZone;
3use jiff::{Timestamp, Zoned};
4use tera::{Kwargs, Number, State, TeraResult, Value};
5
6static PARSER: DateTimeParser = DateTimeParser::new();
7
8fn parse_to_zoned(val: &Value, tz: Option<TimeZone>) -> TeraResult<Zoned> {
10 let default_tz = tz.unwrap_or(TimeZone::UTC);
11 if let Some(s) = val.as_str() {
12 PARSER
13 .parse_zoned(s)
14 .or_else(|_| {
15 PARSER.parse_timestamp(s).map(|t| {
16 let zone = Pieces::parse(s)
17 .ok()
18 .and_then(|p| p.to_numeric_offset())
19 .map_or_else(|| default_tz.clone(), TimeZone::fixed);
20 t.to_zoned(zone)
21 })
22 })
23 .or_else(|_| {
24 PARSER
25 .parse_datetime(s)
26 .and_then(|d| d.to_zoned(default_tz.clone()))
27 })
28 .or_else(|_| PARSER.parse_date(s).and_then(|d| d.to_zoned(default_tz)))
29 .map_err(|e| {
30 tera::Error::message(format!(
31 "The string {s} cannot be parsed as a valid date: {e}"
32 ))
33 })
34 } else if let Some(Number::Integer(ts)) = val.as_number() {
35 let ts = i64::try_from(ts)
36 .map_err(|_| tera::Error::message(format!("Invalid timestamp: {ts}")))?;
37 Timestamp::new(ts, 0)
38 .map(|t| t.to_zoned(default_tz))
39 .map_err(|e| tera::Error::message(format!("Invalid timestamp: {e}")))
40 } else {
41 Err(tera::Error::message(format!(
42 "Invalid value: expected a string or integer, got {}",
43 val.name()
44 )))
45 }
46}
47
48pub fn now(kwargs: Kwargs, _: &State) -> TeraResult<Value> {
56 let tz_str = kwargs.get::<&str>("timezone")?.unwrap_or("UTC");
57 let timezone = TimeZone::get(tz_str)
58 .map_err(|_| tera::Error::message(format!("Unknown timezone: {tz_str}")))?;
59 let now = Zoned::now().with_time_zone(timezone);
60 Ok(Value::from(now.to_string()))
61}
62
63pub fn date(val: &Value, kwargs: Kwargs, _: &State) -> TeraResult<String> {
74 let format = kwargs.get::<&str>("format")?.unwrap_or("%Y-%m-%d");
75 let timezone = match kwargs.get::<&str>("timezone")? {
76 Some(t) => Some(
77 TimeZone::get(t).map_err(|_| tera::Error::message(format!("Unknown timezone: {t}")))?,
78 ),
79 None => None,
80 };
81
82 let mut zoned = parse_to_zoned(val, timezone.clone())?;
83 if let Some(tz) = timezone {
84 zoned = zoned.with_time_zone(tz);
85 }
86
87 jiff::fmt::strtime::format(format, &zoned)
88 .map_err(|e| tera::Error::message(format!("Invalid date format `{format}`: {e}")))
89}
90
91pub fn is_before(val: &Value, kwargs: Kwargs, _: &State) -> TeraResult<bool> {
100 let other = kwargs.must_get::<&Value>("other")?;
101 let inclusive = kwargs.get::<bool>("inclusive")?.unwrap_or(false);
102 let val_zoned = parse_to_zoned(val, None)?;
103 let other_zoned = parse_to_zoned(other, None)?;
104 if inclusive {
105 Ok(val_zoned <= other_zoned)
106 } else {
107 Ok(val_zoned < other_zoned)
108 }
109}
110
111pub fn is_after(val: &Value, kwargs: Kwargs, _: &State) -> TeraResult<bool> {
120 let other = kwargs.must_get::<&Value>("other")?;
121 let inclusive = kwargs.get::<bool>("inclusive")?.unwrap_or(false);
122 let val_zoned = parse_to_zoned(val, None)?;
123 let other_zoned = parse_to_zoned(other, None)?;
124 if inclusive {
125 Ok(val_zoned >= other_zoned)
126 } else {
127 Ok(val_zoned > other_zoned)
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134 use std::sync::Arc;
135 use tera::value::Map;
136 use tera::{Context, Kwargs, State};
137
138 #[test]
140 fn test_ok_date() {
141 let inputs = vec![
142 (Value::from(1482720453), None, None, "2016-12-26"),
143 (
144 Value::from(1482720453),
145 Some("%Y-%m-%d %H:%M"),
146 None,
147 "2016-12-26 02:47",
148 ),
149 (
151 Value::from("1985-04-12T23:20:50.52Z"),
152 None,
153 None,
154 "1985-04-12",
155 ),
156 (
158 Value::from("1996-12-19T16:39:57[-08:00]"),
159 Some("%Y-%m-%d %z"),
160 None,
161 "1996-12-19 -0800",
162 ),
163 (
165 Value::from("1996-12-19T16:39:57-08:00"),
166 Some("%Y-%m-%d %H:%M %z"),
167 None,
168 "1996-12-19 16:39 -0800",
169 ),
170 (
172 Value::from("2017-03-05"),
173 Some("%a, %d %b %Y %H:%M:%S %z"),
174 None,
175 "Sun, 05 Mar 2017 00:00:00 +0000",
176 ),
177 (
179 Value::from("2017-03-05T00:00:00.602"),
180 Some("%a, %d %b %Y %H:%M:%S"),
181 None,
182 "Sun, 05 Mar 2017 00:00:00",
183 ),
184 (
186 Value::from("2019-09-19T01:48:44.581Z"),
187 None,
188 Some("America/New_York"),
189 "2019-09-18",
190 ),
191 (
192 Value::from(1648252203),
193 None,
194 Some("Europe/Berlin"),
195 "2022-03-26",
196 ),
197 ];
198
199 for (value, format, timezone, expected) in inputs {
200 let mut map = Map::new();
201 if let Some(f) = format {
202 map.insert("format".into(), f.into());
203 }
204 if let Some(tz) = timezone {
205 map.insert("timezone".into(), tz.into());
206 }
207 let kwargs = Kwargs::new(Arc::new(map));
208 let ctx = Context::new();
209 let res = date(&value, kwargs, &State::new(&ctx)).unwrap();
210 assert_eq!(expected, res);
211 }
212 }
213
214 #[test]
215 fn test_bad_date_call() {
216 let inputs = vec![
217 (Value::from(1482720453), Some("%1"), None),
218 (Value::from(1482720453), Some("%+S"), None),
219 (
220 Value::from("2019-09-19T01:48:44.581Z"),
221 Some("%+S"),
222 Some("Narnia"),
223 ),
224 ];
225
226 for (value, format, timezone) in inputs {
227 let mut map = Map::new();
228 if let Some(f) = format {
229 map.insert("format".into(), f.into());
230 }
231 if let Some(tz) = timezone {
232 map.insert("timezone".into(), tz.into());
233 }
234 let kwargs = Kwargs::new(Arc::new(map));
235 let ctx = Context::new();
236 let res = date(&value, kwargs, &State::new(&ctx));
237 println!("{res:?}");
238 assert!(res.is_err());
239 }
240 }
241
242 #[test]
243 fn test_register() {
244 let mut tera = tera::Tera::default();
245 tera.register_filter("date", date);
246 tera.register_test("before", is_before);
247 tera.register_test("after", is_after);
248 tera.register_function("now", now);
249 }
250
251 #[test]
252 fn test_is_before() {
253 let ctx = Context::new();
254 let state = State::new(&ctx);
255
256 let cases: Vec<(Value, Value, bool, bool)> = vec![
258 (Value::from(500), Value::from(1000), false, true),
259 (Value::from(1000), Value::from(500), false, false),
260 (
261 Value::from("2024-01-01"),
262 Value::from("2024-06-01"),
263 false,
264 true,
265 ),
266 (
267 Value::from("2024-06-01"),
268 Value::from("2024-01-01"),
269 false,
270 false,
271 ),
272 (Value::from(1000), Value::from("2020-01-01"), false, true), (
274 Value::from("2024-01-01"),
275 Value::from("2024-01-01"),
276 false,
277 false,
278 ), (
280 Value::from("2024-01-01"),
281 Value::from("2024-01-01"),
282 true,
283 true,
284 ), ];
286
287 for (val, other, inclusive, expected) in cases {
288 let mut map = Map::new();
289 map.insert("other".into(), other);
290 if inclusive {
291 map.insert("inclusive".into(), Value::from(true));
292 }
293 let kwargs = Kwargs::new(Arc::new(map));
294 assert_eq!(is_before(&val, kwargs, &state).unwrap(), expected);
295 }
296 }
297
298 #[test]
299 fn test_is_after() {
300 let ctx = Context::new();
301 let state = State::new(&ctx);
302
303 let cases: Vec<(Value, Value, bool, bool)> = vec![
305 (Value::from(1000), Value::from(500), false, true),
306 (Value::from(500), Value::from(1000), false, false),
307 (
308 Value::from("2024-06-01"),
309 Value::from("2024-01-01"),
310 false,
311 true,
312 ),
313 (
314 Value::from("2024-01-01"),
315 Value::from("2024-06-01"),
316 false,
317 false,
318 ),
319 (Value::from("2020-01-01"), Value::from(1000), false, true), (
321 Value::from("2024-01-01"),
322 Value::from("2024-01-01"),
323 false,
324 false,
325 ), (
327 Value::from("2024-01-01"),
328 Value::from("2024-01-01"),
329 true,
330 true,
331 ), ];
333
334 for (val, other, inclusive, expected) in cases {
335 let mut map = Map::new();
336 map.insert("other".into(), other);
337 if inclusive {
338 map.insert("inclusive".into(), Value::from(true));
339 }
340 let kwargs = Kwargs::new(Arc::new(map));
341 assert_eq!(is_after(&val, kwargs, &state).unwrap(), expected);
342 }
343 }
344}