1use std::collections::HashSet;
4
5use chrono::{DateTime, Datelike, NaiveDateTime, TimeDelta, TimeZone, Utc, Weekday};
6use chrono_tz::Tz;
7use serde_json::Value;
8
9use crate::functions::{Function, custom_error, number_value};
10use crate::interpreter::SearchResult;
11use crate::registry::register_if_enabled;
12use crate::{Context, Runtime, arg, defn};
13
14pub fn register_filtered(runtime: &mut Runtime, enabled: &HashSet<&str>) {
16 register_if_enabled(runtime, "now_millis", enabled, Box::new(NowMillisFn::new()));
19 register_if_enabled(runtime, "parse_date", enabled, Box::new(ParseDateFn::new()));
20 register_if_enabled(
21 runtime,
22 "format_date",
23 enabled,
24 Box::new(FormatDateFn::new()),
25 );
26 register_if_enabled(runtime, "date_add", enabled, Box::new(DateAddFn::new()));
27 register_if_enabled(runtime, "date_diff", enabled, Box::new(DateDiffFn::new()));
28 register_if_enabled(
29 runtime,
30 "timezone_convert",
31 enabled,
32 Box::new(TimezoneConvertFn::new()),
33 );
34 register_if_enabled(runtime, "is_weekend", enabled, Box::new(IsWeekendFn::new()));
35 register_if_enabled(runtime, "is_weekday", enabled, Box::new(IsWeekdayFn::new()));
36 register_if_enabled(
37 runtime,
38 "business_days_between",
39 enabled,
40 Box::new(BusinessDaysBetweenFn::new()),
41 );
42 register_if_enabled(
43 runtime,
44 "relative_time",
45 enabled,
46 Box::new(RelativeTimeFn::new()),
47 );
48 register_if_enabled(runtime, "quarter", enabled, Box::new(QuarterFn::new()));
49 register_if_enabled(runtime, "is_after", enabled, Box::new(IsAfterFn::new()));
50 register_if_enabled(runtime, "is_before", enabled, Box::new(IsBeforeFn::new()));
51 register_if_enabled(runtime, "is_between", enabled, Box::new(IsBetweenFn::new()));
52 register_if_enabled(runtime, "time_ago", enabled, Box::new(TimeAgoFn::new()));
53 register_if_enabled(runtime, "from_epoch", enabled, Box::new(FromEpochFn::new()));
54 register_if_enabled(
55 runtime,
56 "from_epoch_ms",
57 enabled,
58 Box::new(FromEpochMsFn::new()),
59 );
60 register_if_enabled(runtime, "to_epoch", enabled, Box::new(ToEpochFn::new()));
61 register_if_enabled(
62 runtime,
63 "to_epoch_ms",
64 enabled,
65 Box::new(ToEpochMsFn::new()),
66 );
67 register_if_enabled(
68 runtime,
69 "duration_since",
70 enabled,
71 Box::new(DurationSinceFn::new()),
72 );
73 register_if_enabled(
74 runtime,
75 "start_of_day",
76 enabled,
77 Box::new(StartOfDayFn::new()),
78 );
79 register_if_enabled(runtime, "end_of_day", enabled, Box::new(EndOfDayFn::new()));
80 register_if_enabled(
81 runtime,
82 "start_of_week",
83 enabled,
84 Box::new(StartOfWeekFn::new()),
85 );
86 register_if_enabled(
87 runtime,
88 "start_of_month",
89 enabled,
90 Box::new(StartOfMonthFn::new()),
91 );
92 register_if_enabled(
93 runtime,
94 "start_of_year",
95 enabled,
96 Box::new(StartOfYearFn::new()),
97 );
98 register_if_enabled(
99 runtime,
100 "is_same_day",
101 enabled,
102 Box::new(IsSameDayFn::new()),
103 );
104 register_if_enabled(runtime, "epoch_ms", enabled, Box::new(NowMillisFn::new()));
106 register_if_enabled(
107 runtime,
108 "parse_datetime",
109 enabled,
110 Box::new(ParseDatetimeFn::new()),
111 );
112 register_if_enabled(
113 runtime,
114 "parse_natural_date",
115 enabled,
116 Box::new(ParseNaturalDateFn::new()),
117 );
118}
119
120defn!(NowMillisFn, vec![], None);
122
123impl Function for NowMillisFn {
124 fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
125 self.signature.validate(args, ctx)?;
126 let ts = Utc::now().timestamp_millis();
127 Ok(number_value(ts as f64))
128 }
129}
130
131defn!(ParseDateFn, vec![arg!(string)], Some(arg!(string)));
133
134impl Function for ParseDateFn {
135 fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
136 self.signature.validate(args, ctx)?;
137
138 let s = args[0].as_str().unwrap();
139
140 if args.len() > 1 {
141 let format = args[1].as_str().unwrap();
143 match NaiveDateTime::parse_from_str(s, format) {
144 Ok(dt) => Ok(number_value(dt.and_utc().timestamp() as f64)),
145 Err(_) => Ok(Value::Null),
146 }
147 } else {
148 if let Ok(dt) = DateTime::parse_from_rfc3339(s) {
150 return Ok(number_value(dt.timestamp() as f64));
151 }
152 if let Ok(dt) = NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S") {
153 return Ok(number_value(dt.and_utc().timestamp() as f64));
154 }
155 if let Ok(dt) =
156 NaiveDateTime::parse_from_str(&format!("{}T00:00:00", s), "%Y-%m-%dT%H:%M:%S")
157 {
158 return Ok(number_value(dt.and_utc().timestamp() as f64));
159 }
160 Ok(Value::Null)
161 }
162 }
163}
164
165defn!(FormatDateFn, vec![arg!(number), arg!(string)], None);
167
168impl Function for FormatDateFn {
169 fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
170 self.signature.validate(args, ctx)?;
171
172 let ts = args[0].as_f64().unwrap();
173 let format = args[1].as_str().unwrap();
174
175 let dt = Utc.timestamp_opt(ts as i64, 0);
176 match dt {
177 chrono::LocalResult::Single(dt) => Ok(Value::String(dt.format(format).to_string())),
178 _ => Ok(Value::Null),
179 }
180 }
181}
182
183defn!(
185 DateAddFn,
186 vec![arg!(number), arg!(number), arg!(string)],
187 None
188);
189
190impl Function for DateAddFn {
191 fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
192 self.signature.validate(args, ctx)?;
193
194 let ts = args[0].as_f64().unwrap();
195 let amount = args[1].as_f64().unwrap();
196 let unit = args[2].as_str().unwrap();
197
198 let duration = match unit.to_lowercase().as_str() {
199 "seconds" | "second" | "s" => TimeDelta::seconds(amount as i64),
200 "minutes" | "minute" | "m" => TimeDelta::minutes(amount as i64),
201 "hours" | "hour" | "h" => TimeDelta::hours(amount as i64),
202 "days" | "day" | "d" => TimeDelta::days(amount as i64),
203 "weeks" | "week" | "w" => TimeDelta::weeks(amount as i64),
204 _ => return Err(custom_error(ctx, &format!("invalid time unit: {}", unit))),
205 };
206
207 let dt = Utc.timestamp_opt(ts as i64, 0);
208 match dt {
209 chrono::LocalResult::Single(dt) => {
210 let new_dt = dt + duration;
211 Ok(number_value(new_dt.timestamp() as f64))
212 }
213 _ => Ok(Value::Null),
214 }
215 }
216}
217
218defn!(
220 DateDiffFn,
221 vec![arg!(number), arg!(number), arg!(string)],
222 None
223);
224
225impl Function for DateDiffFn {
226 fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
227 self.signature.validate(args, ctx)?;
228
229 let ts1 = args[0].as_f64().unwrap();
230 let ts2 = args[1].as_f64().unwrap();
231 let unit = args[2].as_str().unwrap();
232
233 let diff_seconds = (ts1 - ts2) as i64;
234
235 let result = match unit.to_lowercase().as_str() {
236 "seconds" | "second" | "s" => diff_seconds as f64,
237 "minutes" | "minute" | "m" => diff_seconds as f64 / 60.0,
238 "hours" | "hour" | "h" => diff_seconds as f64 / 3600.0,
239 "days" | "day" | "d" => diff_seconds as f64 / 86400.0,
240 "weeks" | "week" | "w" => diff_seconds as f64 / 604800.0,
241 _ => return Err(custom_error(ctx, &format!("invalid time unit: {}", unit))),
242 };
243
244 Ok(number_value(result))
245 }
246}
247
248defn!(
250 TimezoneConvertFn,
251 vec![arg!(string), arg!(string), arg!(string)],
252 None
253);
254
255impl Function for TimezoneConvertFn {
256 fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
257 self.signature.validate(args, ctx)?;
258
259 let timestamp_str = args[0].as_str().unwrap();
260 let from_tz_str = args[1].as_str().unwrap();
261 let to_tz_str = args[2].as_str().unwrap();
262
263 let from_tz: Tz = from_tz_str
265 .parse()
266 .map_err(|_| custom_error(ctx, &format!("invalid timezone: {}", from_tz_str)))?;
267 let to_tz: Tz = to_tz_str
268 .parse()
269 .map_err(|_| custom_error(ctx, &format!("invalid timezone: {}", to_tz_str)))?;
270
271 let naive_dt =
273 if let Ok(dt) = NaiveDateTime::parse_from_str(timestamp_str, "%Y-%m-%dT%H:%M:%S") {
274 dt
275 } else if let Ok(dt) = NaiveDateTime::parse_from_str(
276 &format!("{}T00:00:00", timestamp_str),
277 "%Y-%m-%dT%H:%M:%S",
278 ) {
279 dt
280 } else {
281 return Err(custom_error(
282 ctx,
283 &format!("invalid timestamp format: {}", timestamp_str),
284 ));
285 };
286
287 let from_dt = from_tz
289 .from_local_datetime(&naive_dt)
290 .single()
291 .ok_or_else(|| custom_error(ctx, "ambiguous or invalid local time"))?;
292
293 let to_dt = from_dt.with_timezone(&to_tz);
295
296 Ok(Value::String(to_dt.format("%Y-%m-%dT%H:%M:%S").to_string()))
298 }
299}
300
301defn!(IsWeekendFn, vec![arg!(number)], None);
303
304impl Function for IsWeekendFn {
305 fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
306 self.signature.validate(args, ctx)?;
307
308 let ts = args[0].as_f64().unwrap();
309 let dt = Utc.timestamp_opt(ts as i64, 0);
310
311 match dt {
312 chrono::LocalResult::Single(dt) => {
313 let weekday = dt.weekday();
314 let is_weekend = weekday == Weekday::Sat || weekday == Weekday::Sun;
315 Ok(Value::Bool(is_weekend))
316 }
317 _ => Ok(Value::Null),
318 }
319 }
320}
321
322defn!(IsWeekdayFn, vec![arg!(number)], None);
324
325impl Function for IsWeekdayFn {
326 fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
327 self.signature.validate(args, ctx)?;
328
329 let ts = args[0].as_f64().unwrap();
330 let dt = Utc.timestamp_opt(ts as i64, 0);
331
332 match dt {
333 chrono::LocalResult::Single(dt) => {
334 let weekday = dt.weekday();
335 let is_weekday = weekday != Weekday::Sat && weekday != Weekday::Sun;
336 Ok(Value::Bool(is_weekday))
337 }
338 _ => Ok(Value::Null),
339 }
340 }
341}
342
343defn!(
345 BusinessDaysBetweenFn,
346 vec![arg!(number), arg!(number)],
347 None
348);
349
350impl Function for BusinessDaysBetweenFn {
351 fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
352 self.signature.validate(args, ctx)?;
353
354 let ts1 = args[0].as_f64().unwrap() as i64;
355 let ts2 = args[1].as_f64().unwrap() as i64;
356
357 let dt1 = match Utc.timestamp_opt(ts1, 0) {
358 chrono::LocalResult::Single(dt) => dt,
359 _ => return Ok(Value::Null),
360 };
361 let dt2 = match Utc.timestamp_opt(ts2, 0) {
362 chrono::LocalResult::Single(dt) => dt,
363 _ => return Ok(Value::Null),
364 };
365
366 let (start, end) = if dt1 <= dt2 {
368 (dt1.date_naive(), dt2.date_naive())
369 } else {
370 (dt2.date_naive(), dt1.date_naive())
371 };
372
373 let mut count = 0i64;
374 let mut current = start;
375
376 while current < end {
377 let weekday = current.weekday();
378 if weekday != Weekday::Sat && weekday != Weekday::Sun {
379 count += 1;
380 }
381 current = current.succ_opt().unwrap_or(current);
382 }
383
384 let result = if ts1 > ts2 { -count } else { count };
386
387 Ok(number_value(result as f64))
388 }
389}
390
391defn!(RelativeTimeFn, vec![arg!(number)], None);
393
394impl Function for RelativeTimeFn {
395 fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
396 self.signature.validate(args, ctx)?;
397
398 let ts = args[0].as_f64().unwrap() as i64;
399 let now = Utc::now().timestamp();
400 let diff = ts - now;
401
402 let (abs_diff, is_future) = if diff >= 0 {
403 (diff, true)
404 } else {
405 (-diff, false)
406 };
407
408 let (value, unit_singular, unit_plural) = if abs_diff < 60 {
410 (abs_diff, "second", "seconds")
411 } else if abs_diff < 3600 {
412 (abs_diff / 60, "minute", "minutes")
413 } else if abs_diff < 86400 {
414 (abs_diff / 3600, "hour", "hours")
415 } else if abs_diff < 2592000 {
416 (abs_diff / 86400, "day", "days")
417 } else if abs_diff < 31536000 {
418 (abs_diff / 2592000, "month", "months")
419 } else {
420 (abs_diff / 31536000, "year", "years")
421 };
422
423 let unit = if value == 1 {
424 unit_singular
425 } else {
426 unit_plural
427 };
428 let result = if is_future {
429 format!("in {} {}", value, unit)
430 } else {
431 format!("{} {} ago", value, unit)
432 };
433
434 Ok(Value::String(result))
435 }
436}
437
438defn!(QuarterFn, vec![arg!(number)], None);
440
441impl Function for QuarterFn {
442 fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
443 self.signature.validate(args, ctx)?;
444
445 let ts = args[0].as_f64().unwrap();
446 let dt = Utc.timestamp_opt(ts as i64, 0);
447
448 match dt {
449 chrono::LocalResult::Single(dt) => {
450 let month = dt.month();
451 let quarter = ((month - 1) / 3) + 1;
452 Ok(number_value(quarter as f64))
453 }
454 _ => Ok(Value::Null),
455 }
456 }
457}
458
459fn parse_date_value(value: &Value) -> Option<i64> {
462 match value {
463 Value::Number(n) => n.as_f64().map(|f| f as i64),
464 Value::String(s) => {
465 if let Ok(dt) = DateTime::parse_from_rfc3339(s) {
467 return Some(dt.timestamp());
468 }
469 if let Ok(dt) = NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S") {
471 return Some(dt.and_utc().timestamp());
472 }
473 if let Ok(dt) =
475 NaiveDateTime::parse_from_str(&format!("{}T00:00:00", s), "%Y-%m-%dT%H:%M:%S")
476 {
477 return Some(dt.and_utc().timestamp());
478 }
479 None
480 }
481 _ => None,
482 }
483}
484
485defn!(IsAfterFn, vec![arg!(any), arg!(any)], None);
487
488impl Function for IsAfterFn {
489 fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
490 self.signature.validate(args, ctx)?;
491
492 let ts1 = parse_date_value(&args[0]);
493 let ts2 = parse_date_value(&args[1]);
494
495 match (ts1, ts2) {
496 (Some(t1), Some(t2)) => Ok(Value::Bool(t1 > t2)),
497 _ => Ok(Value::Null),
498 }
499 }
500}
501
502defn!(IsBeforeFn, vec![arg!(any), arg!(any)], None);
504
505impl Function for IsBeforeFn {
506 fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
507 self.signature.validate(args, ctx)?;
508
509 let ts1 = parse_date_value(&args[0]);
510 let ts2 = parse_date_value(&args[1]);
511
512 match (ts1, ts2) {
513 (Some(t1), Some(t2)) => Ok(Value::Bool(t1 < t2)),
514 _ => Ok(Value::Null),
515 }
516 }
517}
518
519defn!(IsBetweenFn, vec![arg!(any), arg!(any), arg!(any)], None);
521
522impl Function for IsBetweenFn {
523 fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
524 self.signature.validate(args, ctx)?;
525
526 let ts = parse_date_value(&args[0]);
527 let start = parse_date_value(&args[1]);
528 let end = parse_date_value(&args[2]);
529
530 match (ts, start, end) {
531 (Some(t), Some(s), Some(e)) => Ok(Value::Bool(t >= s && t <= e)),
532 _ => Ok(Value::Null),
533 }
534 }
535}
536
537defn!(TimeAgoFn, vec![arg!(any)], None);
539
540impl Function for TimeAgoFn {
541 fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
542 self.signature.validate(args, ctx)?;
543
544 let ts = match parse_date_value(&args[0]) {
545 Some(t) => t,
546 None => return Ok(Value::Null),
547 };
548
549 let now = Utc::now().timestamp();
550 let diff = now - ts;
551 let abs_diff = diff.abs();
552
553 let (value, unit_singular, unit_plural) = if abs_diff < 60 {
555 (abs_diff, "second", "seconds")
556 } else if abs_diff < 3600 {
557 (abs_diff / 60, "minute", "minutes")
558 } else if abs_diff < 86400 {
559 (abs_diff / 3600, "hour", "hours")
560 } else if abs_diff < 2592000 {
561 (abs_diff / 86400, "day", "days")
562 } else if abs_diff < 31536000 {
563 (abs_diff / 2592000, "month", "months")
564 } else {
565 (abs_diff / 31536000, "year", "years")
566 };
567
568 let unit = if value == 1 {
569 unit_singular
570 } else {
571 unit_plural
572 };
573
574 let result = if diff < 0 {
575 format!("in {} {}", value, unit)
576 } else {
577 format!("{} {} ago", value, unit)
578 };
579
580 Ok(Value::String(result))
581 }
582}
583
584defn!(FromEpochFn, vec![arg!(number)], None);
589
590impl Function for FromEpochFn {
591 fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
592 self.signature.validate(args, ctx)?;
593
594 let epoch = args[0].as_f64().unwrap() as i64;
595
596 match DateTime::from_timestamp(epoch, 0) {
597 Some(dt) => Ok(Value::String(dt.format("%Y-%m-%dT%H:%M:%SZ").to_string())),
598 None => Ok(Value::Null),
599 }
600 }
601}
602
603defn!(FromEpochMsFn, vec![arg!(number)], None);
608
609impl Function for FromEpochMsFn {
610 fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
611 self.signature.validate(args, ctx)?;
612
613 let epoch_ms = args[0].as_f64().unwrap() as i64;
614 let seconds = epoch_ms.div_euclid(1000);
619 let nanos = (epoch_ms.rem_euclid(1000) * 1_000_000) as u32;
620
621 match DateTime::from_timestamp(seconds, nanos) {
622 Some(dt) => Ok(Value::String(
623 dt.format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string(),
624 )),
625 None => Ok(Value::Null),
626 }
627 }
628}
629
630defn!(ToEpochFn, vec![arg!(any)], None);
635
636impl Function for ToEpochFn {
637 fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
638 self.signature.validate(args, ctx)?;
639
640 match parse_date_value(&args[0]) {
641 Some(ts) => Ok(number_value(ts as f64)),
642 None => Ok(Value::Null),
643 }
644 }
645}
646
647defn!(ToEpochMsFn, vec![arg!(any)], None);
652
653impl Function for ToEpochMsFn {
654 fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
655 self.signature.validate(args, ctx)?;
656
657 match parse_date_value(&args[0]) {
658 Some(ts) => {
659 let ts_ms = ts * 1000;
660 Ok(number_value(ts_ms as f64))
661 }
662 None => Ok(Value::Null),
663 }
664 }
665}
666
667defn!(DurationSinceFn, vec![arg!(any)], None);
672
673impl Function for DurationSinceFn {
674 fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
675 self.signature.validate(args, ctx)?;
676
677 let ts = match parse_date_value(&args[0]) {
678 Some(t) => t,
679 None => return Ok(Value::Null),
680 };
681 let now = Utc::now().timestamp();
682 let diff = now - ts;
683
684 let is_future = diff < 0;
686 let abs_diff = diff.abs();
687
688 let days = abs_diff / 86400;
689 let hours = (abs_diff % 86400) / 3600;
690 let minutes = (abs_diff % 3600) / 60;
691 let seconds = abs_diff % 60;
692
693 let human = if days > 0 {
695 if days == 1 {
696 "1 day".to_string()
697 } else {
698 format!("{} days", days)
699 }
700 } else if hours > 0 {
701 if hours == 1 {
702 "1 hour".to_string()
703 } else {
704 format!("{} hours", hours)
705 }
706 } else if minutes > 0 {
707 if minutes == 1 {
708 "1 minute".to_string()
709 } else {
710 format!("{} minutes", minutes)
711 }
712 } else if seconds == 1 {
713 "1 second".to_string()
714 } else {
715 format!("{} seconds", seconds)
716 };
717
718 let human_with_direction = if is_future {
719 format!("in {}", human)
720 } else {
721 format!("{} ago", human)
722 };
723
724 let mut map = serde_json::Map::new();
726 map.insert(
727 "seconds".to_string(),
728 Value::Number(serde_json::Number::from(abs_diff)),
729 );
730 map.insert(
731 "minutes".to_string(),
732 Value::Number(serde_json::Number::from(abs_diff / 60)),
733 );
734 map.insert(
735 "hours".to_string(),
736 Value::Number(serde_json::Number::from(abs_diff / 3600)),
737 );
738 map.insert(
739 "days".to_string(),
740 Value::Number(serde_json::Number::from(abs_diff / 86400)),
741 );
742 map.insert("is_future".to_string(), Value::Bool(is_future));
743 map.insert("human".to_string(), Value::String(human_with_direction));
744
745 Ok(Value::Object(map))
746 }
747}
748
749defn!(StartOfDayFn, vec![arg!(any)], None);
754
755impl Function for StartOfDayFn {
756 fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
757 self.signature.validate(args, ctx)?;
758
759 let ts = match parse_date_value(&args[0]) {
760 Some(t) => t,
761 None => return Ok(Value::Null),
762 };
763 let dt = match DateTime::from_timestamp(ts, 0) {
764 Some(dt) => dt,
765 None => return Ok(Value::Null),
766 };
767 let start = dt.date_naive().and_hms_opt(0, 0, 0).unwrap().and_utc();
768
769 Ok(Value::String(
770 start.format("%Y-%m-%dT%H:%M:%SZ").to_string(),
771 ))
772 }
773}
774
775defn!(EndOfDayFn, vec![arg!(any)], None);
780
781impl Function for EndOfDayFn {
782 fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
783 self.signature.validate(args, ctx)?;
784
785 let ts = match parse_date_value(&args[0]) {
786 Some(t) => t,
787 None => return Ok(Value::Null),
788 };
789 let dt = match DateTime::from_timestamp(ts, 0) {
790 Some(dt) => dt,
791 None => return Ok(Value::Null),
792 };
793 let end = dt.date_naive().and_hms_opt(23, 59, 59).unwrap().and_utc();
794
795 Ok(Value::String(end.format("%Y-%m-%dT%H:%M:%SZ").to_string()))
796 }
797}
798
799defn!(StartOfWeekFn, vec![arg!(any)], None);
804
805impl Function for StartOfWeekFn {
806 fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
807 self.signature.validate(args, ctx)?;
808
809 let ts = match parse_date_value(&args[0]) {
810 Some(t) => t,
811 None => return Ok(Value::Null),
812 };
813 let dt = match DateTime::from_timestamp(ts, 0) {
814 Some(dt) => dt,
815 None => return Ok(Value::Null),
816 };
817
818 let days_since_monday = dt.weekday().num_days_from_monday();
820 let monday = dt.date_naive() - chrono::Duration::days(days_since_monday as i64);
821 let start = monday.and_hms_opt(0, 0, 0).unwrap().and_utc();
822
823 Ok(Value::String(
824 start.format("%Y-%m-%dT%H:%M:%SZ").to_string(),
825 ))
826 }
827}
828
829defn!(StartOfMonthFn, vec![arg!(any)], None);
834
835impl Function for StartOfMonthFn {
836 fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
837 self.signature.validate(args, ctx)?;
838
839 let ts = match parse_date_value(&args[0]) {
840 Some(t) => t,
841 None => return Ok(Value::Null),
842 };
843 let dt = match DateTime::from_timestamp(ts, 0) {
844 Some(dt) => dt,
845 None => return Ok(Value::Null),
846 };
847
848 let start = dt
849 .date_naive()
850 .with_day(1)
851 .unwrap()
852 .and_hms_opt(0, 0, 0)
853 .unwrap()
854 .and_utc();
855
856 Ok(Value::String(
857 start.format("%Y-%m-%dT%H:%M:%SZ").to_string(),
858 ))
859 }
860}
861
862defn!(StartOfYearFn, vec![arg!(any)], None);
867
868impl Function for StartOfYearFn {
869 fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
870 self.signature.validate(args, ctx)?;
871
872 let ts = match parse_date_value(&args[0]) {
873 Some(t) => t,
874 None => return Ok(Value::Null),
875 };
876 let dt = match DateTime::from_timestamp(ts, 0) {
877 Some(dt) => dt,
878 None => return Ok(Value::Null),
879 };
880
881 let start = chrono::NaiveDate::from_ymd_opt(dt.year(), 1, 1)
882 .unwrap()
883 .and_hms_opt(0, 0, 0)
884 .unwrap()
885 .and_utc();
886
887 Ok(Value::String(
888 start.format("%Y-%m-%dT%H:%M:%SZ").to_string(),
889 ))
890 }
891}
892
893defn!(IsSameDayFn, vec![arg!(any), arg!(any)], None);
898
899impl Function for IsSameDayFn {
900 fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
901 self.signature.validate(args, ctx)?;
902
903 let ts1 = match parse_date_value(&args[0]) {
904 Some(t) => t,
905 None => return Ok(Value::Null),
906 };
907 let ts2 = match parse_date_value(&args[1]) {
908 Some(t) => t,
909 None => return Ok(Value::Null),
910 };
911
912 let dt1 = match DateTime::from_timestamp(ts1, 0) {
913 Some(dt) => dt,
914 None => return Ok(Value::Null),
915 };
916 let dt2 = match DateTime::from_timestamp(ts2, 0) {
917 Some(dt) => dt,
918 None => return Ok(Value::Null),
919 };
920
921 let same_day = dt1.date_naive() == dt2.date_naive();
922
923 Ok(Value::Bool(same_day))
924 }
925}
926
927defn!(ParseDatetimeFn, vec![arg!(string)], None);
932
933impl Function for ParseDatetimeFn {
934 fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
935 self.signature.validate(args, ctx)?;
936
937 let input = args[0].as_str().unwrap();
938
939 match dateparser::parse_with_timezone(input, &Utc) {
940 Ok(dt) => {
941 let iso = dt.format("%Y-%m-%dT%H:%M:%SZ").to_string();
942 Ok(Value::String(iso))
943 }
944 Err(_) => Ok(Value::Null),
945 }
946 }
947}
948
949defn!(ParseNaturalDateFn, vec![arg!(string)], None);
954
955impl Function for ParseNaturalDateFn {
956 fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
957 self.signature.validate(args, ctx)?;
958
959 let input = args[0].as_str().unwrap();
960
961 match interim::parse_date_string(input, Utc::now(), interim::Dialect::Us) {
962 Ok(dt) => {
963 let iso = dt.format("%Y-%m-%dT%H:%M:%SZ").to_string();
964 Ok(Value::String(iso))
965 }
966 Err(_) => Ok(Value::Null),
967 }
968 }
969}
970
971#[cfg(test)]
972mod tests {
973 use crate::Runtime;
974 use chrono::Utc;
975 use serde_json::json;
976
977 fn setup_runtime() -> Runtime {
978 Runtime::builder()
979 .with_standard()
980 .with_all_extensions()
981 .build()
982 }
983
984 #[test]
985 fn test_now() {
986 let runtime = setup_runtime();
987 let expr = runtime.compile("now()").unwrap();
988 let result = expr.search(&json!(null)).unwrap();
989 let ts = result.as_f64().unwrap();
990 assert!(ts > 1577836800.0);
992 }
993
994 #[test]
995 fn test_start_of_out_of_range_timestamp_is_null_not_panic() {
996 let runtime = setup_runtime();
999 for f in [
1000 "start_of_day",
1001 "end_of_day",
1002 "start_of_week",
1003 "start_of_month",
1004 "start_of_year",
1005 ] {
1006 let expr = runtime.compile(&format!("{f}(`1e19`)")).unwrap();
1007 assert_eq!(expr.search(&json!(null)).unwrap(), json!(null), "{f}");
1008 }
1009 }
1010
1011 #[test]
1012 fn test_now_millis() {
1013 let runtime = setup_runtime();
1014 let expr = runtime.compile("epoch_ms()").unwrap();
1016 let result = expr.search(&json!(null)).unwrap();
1017 let ts = result.as_f64().unwrap();
1018 assert!(ts > 1577836800000.0);
1020 }
1021
1022 #[test]
1023 fn test_format_date() {
1024 let runtime = setup_runtime();
1025 let expr = runtime
1027 .compile("format_date(`1720000000`, '%Y-%m-%d')")
1028 .unwrap();
1029 let result = expr.search(&json!(null)).unwrap();
1030 assert_eq!(result.as_str().unwrap(), "2024-07-03");
1031 }
1032
1033 #[test]
1034 fn test_format_date_with_time() {
1035 let runtime = setup_runtime();
1036 let expr = runtime
1038 .compile("format_date(`0`, '%Y-%m-%dT%H:%M:%S')")
1039 .unwrap();
1040 let result = expr.search(&json!(null)).unwrap();
1041 assert_eq!(result.as_str().unwrap(), "1970-01-01T00:00:00");
1042 }
1043
1044 #[test]
1045 fn test_parse_date_iso() {
1046 let runtime = setup_runtime();
1047 let data = json!("1970-01-01T00:00:00Z");
1048 let expr = runtime.compile("parse_date(@)").unwrap();
1049 let result = expr.search(&data).unwrap();
1050 assert_eq!(result.as_f64().unwrap(), 0.0);
1051 }
1052
1053 #[test]
1054 fn test_parse_date_date_only() {
1055 let runtime = setup_runtime();
1056 let data = json!("2024-07-03");
1057 let expr = runtime.compile("parse_date(@)").unwrap();
1058 let result = expr.search(&data).unwrap();
1059 assert_eq!(result.as_f64().unwrap(), 1719964800.0);
1061 }
1062
1063 #[test]
1064 fn test_parse_date_with_format() {
1065 let runtime = setup_runtime();
1066 let data = json!("03/07/2024 00:00:00");
1068 let expr = runtime
1069 .compile("parse_date(@, '%d/%m/%Y %H:%M:%S')")
1070 .unwrap();
1071 let result = expr.search(&data).unwrap();
1072 assert_eq!(result.as_f64().unwrap(), 1719964800.0);
1074 }
1075
1076 #[test]
1077 fn test_parse_date_invalid() {
1078 let runtime = setup_runtime();
1079 let data = json!("not a date");
1080 let expr = runtime.compile("parse_date(@)").unwrap();
1081 let result = expr.search(&data).unwrap();
1082 assert!(result.is_null());
1083 }
1084
1085 #[test]
1086 fn test_date_add_days() {
1087 let runtime = setup_runtime();
1088 let expr = runtime
1090 .compile("date_add(`1720000000`, `7`, 'days')")
1091 .unwrap();
1092 let result = expr.search(&json!(null)).unwrap();
1093 assert_eq!(result.as_f64().unwrap(), 1720604800.0);
1094 }
1095
1096 #[test]
1097 fn test_date_add_hours() {
1098 let runtime = setup_runtime();
1099 let expr = runtime
1100 .compile("date_add(`1720000000`, `24`, 'hours')")
1101 .unwrap();
1102 let result = expr.search(&json!(null)).unwrap();
1103 assert_eq!(result.as_f64().unwrap(), 1720086400.0);
1104 }
1105
1106 #[test]
1107 fn test_date_add_negative() {
1108 let runtime = setup_runtime();
1109 let expr = runtime
1111 .compile("date_add(`1720000000`, `-1`, 'day')")
1112 .unwrap();
1113 let result = expr.search(&json!(null)).unwrap();
1114 assert_eq!(result.as_f64().unwrap(), 1719913600.0);
1115 }
1116
1117 #[test]
1118 fn test_date_diff_days() {
1119 let runtime = setup_runtime();
1120 let expr = runtime
1122 .compile("date_diff(`1720604800`, `1720000000`, 'days')")
1123 .unwrap();
1124 let result = expr.search(&json!(null)).unwrap();
1125 assert_eq!(result.as_f64().unwrap(), 7.0);
1126 }
1127
1128 #[test]
1129 fn test_date_diff_hours() {
1130 let runtime = setup_runtime();
1131 let expr = runtime
1132 .compile("date_diff(`1720086400`, `1720000000`, 'hours')")
1133 .unwrap();
1134 let result = expr.search(&json!(null)).unwrap();
1135 assert_eq!(result.as_f64().unwrap(), 24.0);
1136 }
1137
1138 #[test]
1139 fn test_date_diff_negative() {
1140 let runtime = setup_runtime();
1141 let expr = runtime
1143 .compile("date_diff(`1720000000`, `1720604800`, 'days')")
1144 .unwrap();
1145 let result = expr.search(&json!(null)).unwrap();
1146 assert_eq!(result.as_f64().unwrap(), -7.0);
1147 }
1148
1149 #[test]
1150 fn test_date_add_invalid_unit() {
1151 let runtime = setup_runtime();
1152 let expr = runtime
1153 .compile("date_add(`1720000000`, `1`, 'invalid')")
1154 .unwrap();
1155 let result = expr.search(&json!(null));
1156 assert!(result.is_err());
1157 }
1158
1159 #[test]
1160 fn test_timezone_convert_ny_to_london() {
1161 let runtime = setup_runtime();
1162 let data = json!("2024-01-15T10:00:00");
1163 let expr = runtime
1164 .compile("timezone_convert(@, 'America/New_York', 'Europe/London')")
1165 .unwrap();
1166 let result = expr.search(&data).unwrap();
1167 assert_eq!(result.as_str().unwrap(), "2024-01-15T15:00:00");
1169 }
1170
1171 #[test]
1172 fn test_timezone_convert_tokyo_to_la() {
1173 let runtime = setup_runtime();
1174 let data = json!("2024-07-15T09:00:00");
1175 let expr = runtime
1176 .compile("timezone_convert(@, 'Asia/Tokyo', 'America/Los_Angeles')")
1177 .unwrap();
1178 let result = expr.search(&data).unwrap();
1179 assert_eq!(result.as_str().unwrap(), "2024-07-14T17:00:00");
1181 }
1182
1183 #[test]
1184 fn test_timezone_convert_invalid_tz() {
1185 let runtime = setup_runtime();
1186 let data = json!("2024-01-15T10:00:00");
1187 let expr = runtime
1188 .compile("timezone_convert(@, 'Invalid/Zone', 'Europe/London')")
1189 .unwrap();
1190 let result = expr.search(&data);
1191 assert!(result.is_err());
1192 }
1193
1194 #[test]
1195 fn test_is_weekend_saturday() {
1196 let runtime = setup_runtime();
1197 let expr = runtime.compile("is_weekend(`1705104000`)").unwrap();
1199 let result = expr.search(&json!(null)).unwrap();
1200 assert!(result.as_bool().unwrap());
1201 }
1202
1203 #[test]
1204 fn test_is_weekend_sunday() {
1205 let runtime = setup_runtime();
1206 let expr = runtime.compile("is_weekend(`1705190400`)").unwrap();
1208 let result = expr.search(&json!(null)).unwrap();
1209 assert!(result.as_bool().unwrap());
1210 }
1211
1212 #[test]
1213 fn test_is_weekend_monday() {
1214 let runtime = setup_runtime();
1215 let expr = runtime.compile("is_weekend(`1705276800`)").unwrap();
1217 let result = expr.search(&json!(null)).unwrap();
1218 assert!(!result.as_bool().unwrap());
1219 }
1220
1221 #[test]
1222 fn test_is_weekday_monday() {
1223 let runtime = setup_runtime();
1224 let expr = runtime.compile("is_weekday(`1705276800`)").unwrap();
1226 let result = expr.search(&json!(null)).unwrap();
1227 assert!(result.as_bool().unwrap());
1228 }
1229
1230 #[test]
1231 fn test_is_weekday_saturday() {
1232 let runtime = setup_runtime();
1233 let expr = runtime.compile("is_weekday(`1705104000`)").unwrap();
1235 let result = expr.search(&json!(null)).unwrap();
1236 assert!(!result.as_bool().unwrap());
1237 }
1238
1239 #[test]
1240 fn test_business_days_between() {
1241 let runtime = setup_runtime();
1242 let expr = runtime
1246 .compile("business_days_between(`1704067200`, `1705276800`)")
1247 .unwrap();
1248 let result = expr.search(&json!(null)).unwrap();
1249 assert_eq!(result.as_f64().unwrap(), 10.0);
1250 }
1251
1252 #[test]
1253 fn test_business_days_between_reversed() {
1254 let runtime = setup_runtime();
1255 let expr = runtime
1257 .compile("business_days_between(`1705276800`, `1704067200`)")
1258 .unwrap();
1259 let result = expr.search(&json!(null)).unwrap();
1260 assert_eq!(result.as_f64().unwrap(), -10.0);
1261 }
1262
1263 #[test]
1264 fn test_business_days_between_same_day() {
1265 let runtime = setup_runtime();
1266 let expr = runtime
1267 .compile("business_days_between(`1705276800`, `1705276800`)")
1268 .unwrap();
1269 let result = expr.search(&json!(null)).unwrap();
1270 assert_eq!(result.as_f64().unwrap(), 0.0);
1271 }
1272
1273 #[test]
1274 fn test_quarter_q1() {
1275 let runtime = setup_runtime();
1276 let expr = runtime.compile("quarter(`1705276800`)").unwrap();
1278 let result = expr.search(&json!(null)).unwrap();
1279 assert_eq!(result.as_f64().unwrap(), 1.0);
1280 }
1281
1282 #[test]
1283 fn test_quarter_q2() {
1284 let runtime = setup_runtime();
1285 let expr = runtime.compile("quarter(`1713139200`)").unwrap();
1287 let result = expr.search(&json!(null)).unwrap();
1288 assert_eq!(result.as_f64().unwrap(), 2.0);
1289 }
1290
1291 #[test]
1292 fn test_quarter_q3() {
1293 let runtime = setup_runtime();
1294 let expr = runtime.compile("quarter(`1721001600`)").unwrap();
1296 let result = expr.search(&json!(null)).unwrap();
1297 assert_eq!(result.as_f64().unwrap(), 3.0);
1298 }
1299
1300 #[test]
1301 fn test_quarter_q4() {
1302 let runtime = setup_runtime();
1303 let expr = runtime.compile("quarter(`1728950400`)").unwrap();
1305 let result = expr.search(&json!(null)).unwrap();
1306 assert_eq!(result.as_f64().unwrap(), 4.0);
1307 }
1308
1309 #[test]
1310 fn test_relative_time_past() {
1311 let runtime = setup_runtime();
1312 let one_year_ago = Utc::now().timestamp() - 31536000;
1314 let expr_str = format!("relative_time(`{}`)", one_year_ago);
1315 let expr = runtime.compile(&expr_str).unwrap();
1316 let result = expr.search(&json!(null)).unwrap();
1317 assert!(result.as_str().unwrap().contains("ago"));
1318 }
1319
1320 #[test]
1321 fn test_relative_time_future() {
1322 let runtime = setup_runtime();
1323 let one_day_future = Utc::now().timestamp() + 86400;
1325 let expr_str = format!("relative_time(`{}`)", one_day_future);
1326 let expr = runtime.compile(&expr_str).unwrap();
1327 let result = expr.search(&json!(null)).unwrap();
1328 assert!(result.as_str().unwrap().starts_with("in "));
1329 }
1330
1331 #[test]
1334 fn test_is_after_with_timestamps() {
1335 let runtime = setup_runtime();
1336 let expr = runtime
1338 .compile("is_after(`1720000000`, `1710000000`)")
1339 .unwrap();
1340 let result = expr.search(&json!(null)).unwrap();
1341 assert!(result.as_bool().unwrap());
1342 }
1343
1344 #[test]
1345 fn test_is_after_with_timestamps_false() {
1346 let runtime = setup_runtime();
1347 let expr = runtime
1349 .compile("is_after(`1710000000`, `1720000000`)")
1350 .unwrap();
1351 let result = expr.search(&json!(null)).unwrap();
1352 assert!(!result.as_bool().unwrap());
1353 }
1354
1355 #[test]
1356 fn test_is_after_with_date_strings() {
1357 let runtime = setup_runtime();
1358 let data = json!({"d1": "2024-07-15", "d2": "2024-01-01"});
1359 let expr = runtime.compile("is_after(d1, d2)").unwrap();
1360 let result = expr.search(&data).unwrap();
1361 assert!(result.as_bool().unwrap());
1362 }
1363
1364 #[test]
1365 fn test_is_after_with_iso_strings() {
1366 let runtime = setup_runtime();
1367 let data = json!({"d1": "2024-07-15T10:30:00Z", "d2": "2024-07-15T08:00:00Z"});
1368 let expr = runtime.compile("is_after(d1, d2)").unwrap();
1369 let result = expr.search(&data).unwrap();
1370 assert!(result.as_bool().unwrap());
1371 }
1372
1373 #[test]
1374 fn test_is_after_mixed_types() {
1375 let runtime = setup_runtime();
1376 let data = json!({"d": "2024-01-01"});
1378 let expr = runtime.compile("is_after(`1720000000`, d)").unwrap();
1379 let result = expr.search(&data).unwrap();
1380 assert!(result.as_bool().unwrap());
1381 }
1382
1383 #[test]
1384 fn test_is_after_equal_dates() {
1385 let runtime = setup_runtime();
1386 let expr = runtime
1387 .compile("is_after(`1720000000`, `1720000000`)")
1388 .unwrap();
1389 let result = expr.search(&json!(null)).unwrap();
1390 assert!(!result.as_bool().unwrap());
1391 }
1392
1393 #[test]
1394 fn test_is_after_invalid_date() {
1395 let runtime = setup_runtime();
1396 let data = json!({"d": "not-a-date"});
1397 let expr = runtime.compile("is_after(d, `1720000000`)").unwrap();
1398 let result = expr.search(&data).unwrap();
1399 assert!(result.is_null());
1400 }
1401
1402 #[test]
1405 fn test_is_before_with_timestamps() {
1406 let runtime = setup_runtime();
1407 let expr = runtime
1409 .compile("is_before(`1710000000`, `1720000000`)")
1410 .unwrap();
1411 let result = expr.search(&json!(null)).unwrap();
1412 assert!(result.as_bool().unwrap());
1413 }
1414
1415 #[test]
1416 fn test_is_before_with_timestamps_false() {
1417 let runtime = setup_runtime();
1418 let expr = runtime
1420 .compile("is_before(`1720000000`, `1710000000`)")
1421 .unwrap();
1422 let result = expr.search(&json!(null)).unwrap();
1423 assert!(!result.as_bool().unwrap());
1424 }
1425
1426 #[test]
1427 fn test_is_before_with_date_strings() {
1428 let runtime = setup_runtime();
1429 let data = json!({"d1": "2024-01-01", "d2": "2024-07-15"});
1430 let expr = runtime.compile("is_before(d1, d2)").unwrap();
1431 let result = expr.search(&data).unwrap();
1432 assert!(result.as_bool().unwrap());
1433 }
1434
1435 #[test]
1436 fn test_is_before_equal_dates() {
1437 let runtime = setup_runtime();
1438 let expr = runtime
1439 .compile("is_before(`1720000000`, `1720000000`)")
1440 .unwrap();
1441 let result = expr.search(&json!(null)).unwrap();
1442 assert!(!result.as_bool().unwrap());
1443 }
1444
1445 #[test]
1448 fn test_is_between_with_timestamps_true() {
1449 let runtime = setup_runtime();
1450 let expr = runtime
1452 .compile("is_between(`1715000000`, `1710000000`, `1720000000`)")
1453 .unwrap();
1454 let result = expr.search(&json!(null)).unwrap();
1455 assert!(result.as_bool().unwrap());
1456 }
1457
1458 #[test]
1459 fn test_is_between_with_timestamps_false() {
1460 let runtime = setup_runtime();
1461 let expr = runtime
1463 .compile("is_between(`1700000000`, `1710000000`, `1720000000`)")
1464 .unwrap();
1465 let result = expr.search(&json!(null)).unwrap();
1466 assert!(!result.as_bool().unwrap());
1467 }
1468
1469 #[test]
1470 fn test_is_between_with_date_strings() {
1471 let runtime = setup_runtime();
1472 let data = json!({"d": "2024-06-15", "start": "2024-01-01", "end": "2024-12-31"});
1473 let expr = runtime.compile("is_between(d, start, end)").unwrap();
1474 let result = expr.search(&data).unwrap();
1475 assert!(result.as_bool().unwrap());
1476 }
1477
1478 #[test]
1479 fn test_is_between_inclusive_start() {
1480 let runtime = setup_runtime();
1481 let expr = runtime
1483 .compile("is_between(`1710000000`, `1710000000`, `1720000000`)")
1484 .unwrap();
1485 let result = expr.search(&json!(null)).unwrap();
1486 assert!(result.as_bool().unwrap());
1487 }
1488
1489 #[test]
1490 fn test_is_between_inclusive_end() {
1491 let runtime = setup_runtime();
1492 let expr = runtime
1494 .compile("is_between(`1720000000`, `1710000000`, `1720000000`)")
1495 .unwrap();
1496 let result = expr.search(&json!(null)).unwrap();
1497 assert!(result.as_bool().unwrap());
1498 }
1499
1500 #[test]
1501 fn test_is_between_outside_range() {
1502 let runtime = setup_runtime();
1503 let expr = runtime
1505 .compile("is_between(`1730000000`, `1710000000`, `1720000000`)")
1506 .unwrap();
1507 let result = expr.search(&json!(null)).unwrap();
1508 assert!(!result.as_bool().unwrap());
1509 }
1510
1511 #[test]
1514 fn test_time_ago_with_timestamp() {
1515 let runtime = setup_runtime();
1516 let one_hour_ago = Utc::now().timestamp() - 3600;
1518 let expr_str = format!("time_ago(`{}`)", one_hour_ago);
1519 let expr = runtime.compile(&expr_str).unwrap();
1520 let result = expr.search(&json!(null)).unwrap();
1521 assert_eq!(result.as_str().unwrap(), "1 hour ago");
1522 }
1523
1524 #[test]
1525 fn test_time_ago_with_date_string() {
1526 let runtime = setup_runtime();
1527 let data = json!("2020-01-01");
1529 let expr = runtime.compile("time_ago(@)").unwrap();
1530 let result = expr.search(&data).unwrap();
1531 assert!(result.as_str().unwrap().contains("years ago"));
1532 }
1533
1534 #[test]
1535 fn test_time_ago_plural() {
1536 let runtime = setup_runtime();
1537 let two_days_ago = Utc::now().timestamp() - 172800;
1539 let expr_str = format!("time_ago(`{}`)", two_days_ago);
1540 let expr = runtime.compile(&expr_str).unwrap();
1541 let result = expr.search(&json!(null)).unwrap();
1542 assert_eq!(result.as_str().unwrap(), "2 days ago");
1543 }
1544
1545 #[test]
1546 fn test_time_ago_singular() {
1547 let runtime = setup_runtime();
1548 let one_day_ago = Utc::now().timestamp() - 86400;
1550 let expr_str = format!("time_ago(`{}`)", one_day_ago);
1551 let expr = runtime.compile(&expr_str).unwrap();
1552 let result = expr.search(&json!(null)).unwrap();
1553 assert_eq!(result.as_str().unwrap(), "1 day ago");
1554 }
1555
1556 #[test]
1557 fn test_time_ago_future() {
1558 let runtime = setup_runtime();
1559 let one_day_future = Utc::now().timestamp() + 86400;
1561 let expr_str = format!("time_ago(`{}`)", one_day_future);
1562 let expr = runtime.compile(&expr_str).unwrap();
1563 let result = expr.search(&json!(null)).unwrap();
1564 assert!(result.as_str().unwrap().starts_with("in "));
1565 }
1566
1567 #[test]
1568 fn test_time_ago_invalid_date() {
1569 let runtime = setup_runtime();
1570 let data = json!("not-a-date");
1571 let expr = runtime.compile("time_ago(@)").unwrap();
1572 let result = expr.search(&data).unwrap();
1573 assert!(result.is_null());
1574 }
1575
1576 #[test]
1577 fn test_from_epoch() {
1578 let runtime = setup_runtime();
1579 let expr = runtime.compile("from_epoch(`1702425600`)").unwrap();
1581 let result = expr.search(&json!(null)).unwrap();
1582 assert_eq!(result.as_str().unwrap(), "2023-12-13T00:00:00Z");
1583 }
1584
1585 #[test]
1586 fn test_from_epoch_ms() {
1587 let runtime = setup_runtime();
1588 let expr = runtime.compile("from_epoch_ms(`1702425600500`)").unwrap();
1590 let result = expr.search(&json!(null)).unwrap();
1591 assert_eq!(result.as_str().unwrap(), "2023-12-13T00:00:00.500Z");
1592 }
1593
1594 #[test]
1595 fn test_from_epoch_ms_negative() {
1596 let runtime = setup_runtime();
1597 let expr = runtime.compile("from_epoch_ms(`-500`)").unwrap();
1601 let result = expr.search(&json!(null)).unwrap();
1602 assert_eq!(result.as_str().unwrap(), "1969-12-31T23:59:59.500Z");
1603 }
1604
1605 #[test]
1606 fn test_to_epoch() {
1607 let runtime = setup_runtime();
1608 let data = json!("2023-12-13T00:00:00Z");
1609 let expr = runtime.compile("to_epoch(@)").unwrap();
1610 let result = expr.search(&data).unwrap();
1611 assert_eq!(result.as_f64().unwrap() as i64, 1702425600);
1612 }
1613
1614 #[test]
1615 fn test_to_epoch_ms() {
1616 let runtime = setup_runtime();
1617 let data = json!("2023-12-13T00:00:00Z");
1618 let expr = runtime.compile("to_epoch_ms(@)").unwrap();
1619 let result = expr.search(&data).unwrap();
1620 assert_eq!(result.as_f64().unwrap() as i64, 1702425600000);
1621 }
1622
1623 #[test]
1624 fn test_to_epoch_from_number() {
1625 let runtime = setup_runtime();
1626 let expr = runtime.compile("to_epoch(`1702425600`)").unwrap();
1628 let result = expr.search(&json!(null)).unwrap();
1629 assert_eq!(result.as_f64().unwrap() as i64, 1702425600);
1630 }
1631
1632 #[test]
1633 fn test_duration_since() {
1634 let runtime = setup_runtime();
1635 let two_days_ago = Utc::now().timestamp() - 172800;
1637 let expr_str = format!("duration_since(`{}`)", two_days_ago);
1638 let expr = runtime.compile(&expr_str).unwrap();
1639 let result = expr.search(&json!(null)).unwrap();
1640 let obj = result.as_object().unwrap();
1641 assert_eq!(obj.get("days").unwrap().as_i64().unwrap(), 2);
1642 assert!(!obj.get("is_future").unwrap().as_bool().unwrap());
1643 assert!(
1644 obj.get("human")
1645 .unwrap()
1646 .as_str()
1647 .unwrap()
1648 .contains("2 days ago")
1649 );
1650 }
1651
1652 #[test]
1653 fn test_start_of_day() {
1654 let runtime = setup_runtime();
1655 let data = json!("2023-12-13T15:30:45Z");
1656 let expr = runtime.compile("start_of_day(@)").unwrap();
1657 let result = expr.search(&data).unwrap();
1658 assert_eq!(result.as_str().unwrap(), "2023-12-13T00:00:00Z");
1659 }
1660
1661 #[test]
1662 fn test_end_of_day() {
1663 let runtime = setup_runtime();
1664 let data = json!("2023-12-13T15:30:45Z");
1665 let expr = runtime.compile("end_of_day(@)").unwrap();
1666 let result = expr.search(&data).unwrap();
1667 assert_eq!(result.as_str().unwrap(), "2023-12-13T23:59:59Z");
1668 }
1669
1670 #[test]
1671 fn test_start_of_week() {
1672 let runtime = setup_runtime();
1673 let data = json!("2023-12-13T15:30:45Z");
1675 let expr = runtime.compile("start_of_week(@)").unwrap();
1676 let result = expr.search(&data).unwrap();
1677 assert_eq!(result.as_str().unwrap(), "2023-12-11T00:00:00Z");
1679 }
1680
1681 #[test]
1682 fn test_start_of_month() {
1683 let runtime = setup_runtime();
1684 let data = json!("2023-12-13T15:30:45Z");
1685 let expr = runtime.compile("start_of_month(@)").unwrap();
1686 let result = expr.search(&data).unwrap();
1687 assert_eq!(result.as_str().unwrap(), "2023-12-01T00:00:00Z");
1688 }
1689
1690 #[test]
1691 fn test_start_of_year() {
1692 let runtime = setup_runtime();
1693 let data = json!("2023-12-13T15:30:45Z");
1694 let expr = runtime.compile("start_of_year(@)").unwrap();
1695 let result = expr.search(&data).unwrap();
1696 assert_eq!(result.as_str().unwrap(), "2023-01-01T00:00:00Z");
1697 }
1698
1699 #[test]
1700 fn test_is_same_day_true() {
1701 let runtime = setup_runtime();
1702 let expr = runtime
1703 .compile(r#"is_same_day(`"2023-12-13T10:00:00Z"`, `"2023-12-13T23:00:00Z"`)"#)
1704 .unwrap();
1705 let result = expr.search(&json!(null)).unwrap();
1706 assert!(result.as_bool().unwrap());
1707 }
1708
1709 #[test]
1710 fn test_is_same_day_false() {
1711 let runtime = setup_runtime();
1712 let expr = runtime
1713 .compile(r#"is_same_day(`"2023-12-13T10:00:00Z"`, `"2023-12-14T10:00:00Z"`)"#)
1714 .unwrap();
1715 let result = expr.search(&json!(null)).unwrap();
1716 assert!(!result.as_bool().unwrap());
1717 }
1718
1719 #[test]
1720 fn test_epoch_ms_alias() {
1721 let runtime = setup_runtime();
1722 let expr = runtime.compile("epoch_ms()").unwrap();
1724 let result = expr.search(&json!(null)).unwrap();
1725 let ts = result.as_f64().unwrap() as i64;
1726 assert!(ts > 1700000000000);
1728 }
1729
1730 #[test]
1735 fn test_parse_datetime_iso_date() {
1736 let runtime = setup_runtime();
1737 let data = json!("2024-01-15");
1738 let expr = runtime.compile("parse_datetime(@)").unwrap();
1739 let result = expr.search(&data).unwrap();
1740 let s = result.as_str().unwrap();
1741 assert!(s.contains("2024-01-15"), "Expected 2024-01-15 in {}", s);
1742 assert!(s.ends_with('Z'), "Expected UTC timezone marker in {}", s);
1743 }
1744
1745 #[test]
1746 fn test_parse_datetime_iso_datetime() {
1747 let runtime = setup_runtime();
1748 let data = json!("2024-01-15T10:30:00Z");
1749 let expr = runtime.compile("parse_datetime(@)").unwrap();
1750 let result = expr.search(&data).unwrap();
1751 let s = result.as_str().unwrap();
1752 assert_eq!(s, "2024-01-15T10:30:00Z");
1753 }
1754
1755 #[test]
1756 fn test_parse_datetime_iso_with_offset() {
1757 let runtime = setup_runtime();
1758 let data = json!("2024-01-15T10:30:00+05:00");
1759 let expr = runtime.compile("parse_datetime(@)").unwrap();
1760 let result = expr.search(&data).unwrap();
1761 let s = result.as_str().unwrap();
1762 assert!(s.contains("2024-01-15"), "Expected 2024-01-15 in {}", s);
1764 assert!(s.ends_with('Z'), "Expected UTC timezone marker in {}", s);
1765 }
1766
1767 #[test]
1768 fn test_parse_datetime_human_month_day_year() {
1769 let runtime = setup_runtime();
1770 let data = json!("Jan 15, 2024");
1771 let expr = runtime.compile("parse_datetime(@)").unwrap();
1772 let result = expr.search(&data).unwrap();
1773 let s = result.as_str().unwrap();
1774 assert!(s.contains("2024-01-15"), "Expected 2024-01-15 in {}", s);
1775 }
1776
1777 #[test]
1778 fn test_parse_datetime_human_full_month() {
1779 let runtime = setup_runtime();
1780 let data = json!("January 15, 2024");
1781 let expr = runtime.compile("parse_datetime(@)").unwrap();
1782 let result = expr.search(&data).unwrap();
1783 let s = result.as_str().unwrap();
1784 assert!(s.contains("2024-01-15"), "Expected 2024-01-15 in {}", s);
1785 }
1786
1787 #[test]
1788 fn test_parse_datetime_us_format() {
1789 let runtime = setup_runtime();
1790 let data = json!("01/15/2024");
1791 let expr = runtime.compile("parse_datetime(@)").unwrap();
1792 let result = expr.search(&data).unwrap();
1793 let s = result.as_str().unwrap();
1794 assert!(s.contains("2024"), "Expected year 2024 in {}", s);
1795 }
1796
1797 #[test]
1798 fn test_parse_datetime_rfc2822() {
1799 let runtime = setup_runtime();
1800 let data = json!("Mon, 15 Jan 2024 10:30:00 +0000");
1801 let expr = runtime.compile("parse_datetime(@)").unwrap();
1802 let result = expr.search(&data).unwrap();
1803 let s = result.as_str().unwrap();
1804 assert!(s.contains("2024-01-15"), "Expected 2024-01-15 in {}", s);
1805 }
1806
1807 #[test]
1808 fn test_parse_datetime_unix_timestamp() {
1809 let runtime = setup_runtime();
1810 let data = json!("1705363200");
1812 let expr = runtime.compile("parse_datetime(@)").unwrap();
1813 let result = expr.search(&data).unwrap();
1814 let s = result.as_str().unwrap();
1815 assert!(s.contains("2024-01-16"), "Expected 2024-01-16 in {}", s);
1816 }
1817
1818 #[test]
1819 fn test_parse_datetime_invalid() {
1820 let runtime = setup_runtime();
1821 let data = json!("not a date at all xyz123");
1822 let expr = runtime.compile("parse_datetime(@)").unwrap();
1823 let result = expr.search(&data).unwrap();
1824 assert!(result.is_null(), "Invalid date should return null");
1825 }
1826
1827 #[test]
1828 fn test_parse_datetime_empty() {
1829 let runtime = setup_runtime();
1830 let data = json!("");
1831 let expr = runtime.compile("parse_datetime(@)").unwrap();
1832 let result = expr.search(&data).unwrap();
1833 assert!(result.is_null(), "Empty string should return null");
1834 }
1835
1836 #[test]
1841 fn test_parse_natural_date_yesterday() {
1842 let runtime = setup_runtime();
1843 let data = json!("yesterday");
1844 let expr = runtime.compile("parse_natural_date(@)").unwrap();
1845 let result = expr.search(&data).unwrap();
1846 let s = result.as_str().unwrap();
1847 assert!(s.ends_with('Z'), "Expected UTC timezone marker in {}", s);
1848 assert!(
1849 s.contains('T'),
1850 "Expected ISO format with T separator in {}",
1851 s
1852 );
1853 }
1854
1855 #[test]
1856 fn test_parse_natural_date_tomorrow() {
1857 let runtime = setup_runtime();
1858 let data = json!("tomorrow");
1859 let expr = runtime.compile("parse_natural_date(@)").unwrap();
1860 let result = expr.search(&data).unwrap();
1861 let s = result.as_str().unwrap();
1862 assert!(s.ends_with('Z'), "Expected UTC timezone marker in {}", s);
1863 }
1864
1865 #[test]
1866 fn test_parse_natural_date_days_ago() {
1867 let runtime = setup_runtime();
1868 let data = json!("3 days ago");
1869 let expr = runtime.compile("parse_natural_date(@)").unwrap();
1870 let result = expr.search(&data).unwrap();
1871 let s = result.as_str().unwrap();
1872 assert!(s.ends_with('Z'), "Expected UTC timezone marker in {}", s);
1873 }
1874
1875 #[test]
1876 fn test_parse_natural_date_weeks_ago() {
1877 let runtime = setup_runtime();
1878 let data = json!("2 weeks ago");
1879 let expr = runtime.compile("parse_natural_date(@)").unwrap();
1880 let result = expr.search(&data).unwrap();
1881 let s = result.as_str().unwrap();
1882 assert!(s.ends_with('Z'), "Expected UTC timezone marker in {}", s);
1883 }
1884
1885 #[test]
1886 fn test_parse_natural_date_next_weekday() {
1887 let runtime = setup_runtime();
1888 let data = json!("next friday");
1889 let expr = runtime.compile("parse_natural_date(@)").unwrap();
1890 let result = expr.search(&data).unwrap();
1891 let s = result.as_str().unwrap();
1892 assert!(s.ends_with('Z'), "Expected UTC timezone marker in {}", s);
1893 }
1894
1895 #[test]
1896 fn test_parse_natural_date_last_weekday() {
1897 let runtime = setup_runtime();
1898 let data = json!("last monday");
1899 let expr = runtime.compile("parse_natural_date(@)").unwrap();
1900 let result = expr.search(&data).unwrap();
1901 let s = result.as_str().unwrap();
1902 assert!(s.ends_with('Z'), "Expected UTC timezone marker in {}", s);
1903 }
1904
1905 #[test]
1906 fn test_parse_natural_date_hours() {
1907 let runtime = setup_runtime();
1908 let data = json!("5 hours ago");
1909 let expr = runtime.compile("parse_natural_date(@)").unwrap();
1910 let result = expr.search(&data).unwrap();
1911 let s = result.as_str().unwrap();
1912 assert!(s.ends_with('Z'), "Expected UTC timezone marker in {}", s);
1913 }
1914
1915 #[test]
1916 fn test_parse_natural_date_invalid() {
1917 let runtime = setup_runtime();
1918 let data = json!("not a natural date expression");
1919 let expr = runtime.compile("parse_natural_date(@)").unwrap();
1920 let result = expr.search(&data).unwrap();
1921 assert!(result.is_null(), "Invalid expression should return null");
1922 }
1923}