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