Skip to main content

jpx_core/extensions/
datetime.rs

1//! Date and time functions.
2
3use 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
14/// Register datetime functions filtered by the enabled set.
15pub 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    // epoch_ms is an alias for now_millis (common name)
104    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
119// now() -> number
120defn!(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
130// now_millis() -> number
131defn!(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
141// parse_date(string, format?) -> number | null
142defn!(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            // Custom format provided
152            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            // Try common formats
159            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
175// format_date(timestamp, format) -> string
176defn!(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
193// date_add(timestamp, amount, unit) -> number
194defn!(
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
228// date_diff(ts1, ts2, unit) -> number
229defn!(
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
258// timezone_convert(timestamp, from_tz, to_tz) -> string
259defn!(
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        // Parse timezone strings
274        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        // Parse the input timestamp (try multiple formats)
282        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        // Interpret the naive datetime in the source timezone
298        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        // Convert to target timezone
304        let to_dt = from_dt.with_timezone(&to_tz);
305
306        // Format as ISO string without timezone suffix
307        Ok(Value::String(to_dt.format("%Y-%m-%dT%H:%M:%S").to_string()))
308    }
309}
310
311// is_weekend(timestamp) -> boolean
312defn!(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
332// is_weekday(timestamp) -> boolean
333defn!(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
353// business_days_between(ts1, ts2) -> number
354defn!(
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        // Ensure we iterate from earlier to later date
377        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        // If original order was reversed, return negative count
395        let result = if ts1 > ts2 { -count } else { count };
396
397        Ok(number_value(result as f64))
398    }
399}
400
401// relative_time(timestamp) -> string
402defn!(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        // Determine the unit and value
419        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
448// quarter(timestamp) -> number
449defn!(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
469/// Helper function to parse a date value that can be either a string or a number (timestamp).
470/// Returns the Unix timestamp as i64, or None if parsing fails.
471fn 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            // Try RFC3339 first
476            if let Ok(dt) = DateTime::parse_from_rfc3339(s) {
477                return Some(dt.timestamp());
478            }
479            // Try ISO datetime without timezone
480            if let Ok(dt) = NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S") {
481                return Some(dt.and_utc().timestamp());
482            }
483            // Try date only
484            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
495// is_after(date1, date2) -> boolean
496defn!(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
512// is_before(date1, date2) -> boolean
513defn!(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
529// is_between(date, start, end) -> boolean
530defn!(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
547// time_ago(date) -> string
548defn!(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        // Determine the unit and value
564        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
594// =============================================================================
595// from_epoch(seconds) -> string
596// =============================================================================
597
598defn!(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
613// =============================================================================
614// from_epoch_ms(milliseconds) -> string
615// =============================================================================
616
617defn!(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
636// =============================================================================
637// to_epoch(datetime) -> number
638// =============================================================================
639
640defn!(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
653// =============================================================================
654// to_epoch_ms(datetime) -> number
655// =============================================================================
656
657defn!(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
673// =============================================================================
674// duration_since(datetime) -> object
675// =============================================================================
676
677defn!(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        // Calculate components
691        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        // Build human-readable string
700        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        // Build result object
731        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
755// =============================================================================
756// start_of_day(datetime) -> string
757// =============================================================================
758
759defn!(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
778// =============================================================================
779// end_of_day(datetime) -> string
780// =============================================================================
781
782defn!(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
799// =============================================================================
800// start_of_week(datetime) -> string
801// =============================================================================
802
803defn!(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        // Calculate days since Monday (Monday = 0)
816        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
826// =============================================================================
827// start_of_month(datetime) -> string
828// =============================================================================
829
830defn!(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
856// =============================================================================
857// start_of_year(datetime) -> string
858// =============================================================================
859
860defn!(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
884// =============================================================================
885// is_same_day(datetime1, datetime2) -> boolean
886// =============================================================================
887
888defn!(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
918// =============================================================================
919// parse_datetime(date_string) -> string
920// =============================================================================
921
922defn!(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
940// =============================================================================
941// parse_natural_date(expression) -> string
942// =============================================================================
943
944defn!(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 chrono_english::parse_date_string(input, Utc::now(), chrono_english::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}