Skip to main content

fsqlite_func/
datetime.rs

1//! SQLite date/time functions (§13.3).
2//!
3//! Implements: date(), time(), datetime(), julianday(), unixepoch(),
4//! strftime(), timediff().
5//!
6//! Internal representation is Julian Day Number (f64).  All functions
7//! parse input to JDN, apply modifiers left-to-right, then format.
8//!
9//! Invalid inputs return NULL (never error).
10#![allow(
11    clippy::unnecessary_literal_bound,
12    clippy::too_many_lines,
13    clippy::cast_possible_truncation,
14    clippy::cast_possible_wrap,
15    clippy::cast_sign_loss,
16    clippy::cast_precision_loss,
17    clippy::items_after_statements,
18    clippy::match_same_arms,
19    clippy::float_cmp,
20    clippy::suboptimal_flops,
21    clippy::manual_let_else,
22    clippy::single_match_else,
23    clippy::unnecessary_wraps,
24    clippy::cognitive_complexity,
25    clippy::similar_names,
26    clippy::many_single_char_names,
27    clippy::unreadable_literal,
28    clippy::manual_range_contains,
29    clippy::range_plus_one,
30    clippy::format_push_string,
31    clippy::redundant_else
32)]
33
34use fsqlite_error::Result;
35use fsqlite_types::SqliteValue;
36
37use crate::{FunctionRegistry, ScalarFunction};
38
39// ── Julian Day Number Conversions ─────────────────────────────────────────
40//
41// Algorithms from Meeus, "Astronomical Algorithms" (1991).
42
43/// Gregorian (y, m, d, h, min, sec, frac_sec) → Julian Day Number.
44fn ymd_to_jdn(y: i64, m: i64, d: i64) -> f64 {
45    let (y, m) = if m <= 2 { (y - 1, m + 12) } else { (y, m) };
46    let a = y / 100;
47    let b = 2 - a + a / 4;
48    (365.25 * (y + 4716) as f64).floor() + (30.6001 * (m + 1) as f64).floor() + d as f64 + b as f64
49        - 1524.5
50}
51
52/// Julian Day Number → Gregorian (year, month, day).
53fn jdn_to_ymd(jdn: f64) -> (i64, i64, i64) {
54    let z = (jdn + 0.5).floor() as i64;
55    let a = if z < 2_299_161 {
56        z
57    } else {
58        let alpha = ((z as f64 - 1_867_216.25) / 36524.25).floor() as i64;
59        z + 1 + alpha - alpha / 4
60    };
61    let b = a + 1524;
62    let c = ((b as f64 - 122.1) / 365.25).floor() as i64;
63    let d = (365.25 * c as f64).floor() as i64;
64    let e = ((b - d) as f64 / 30.6001).floor() as i64;
65
66    let day = b - d - (30.6001 * e as f64).floor() as i64;
67    let month = if e < 14 { e - 1 } else { e - 13 };
68    let year = if month > 2 { c - 4716 } else { c - 4715 };
69    (year, month, day)
70}
71
72/// Julian Day Number → (hour, minute, second, fractional_sec).
73fn jdn_to_hms(jdn: f64) -> (i64, i64, i64, f64) {
74    let frac = jdn + 0.5 - (jdn + 0.5).floor();
75    // Round to nearest millisecond to avoid floating-point drift.
76    let total_ms = (frac * 86_400_000.0).round() as i64;
77    let h = total_ms / 3_600_000;
78    let rem = total_ms % 3_600_000;
79    let m = rem / 60_000;
80    let rem = rem % 60_000;
81    let s = rem / 1000;
82    let ms_frac = (rem % 1000) as f64 / 1000.0;
83    (h, m, s, ms_frac)
84}
85
86/// Build a JDN from date + time components.
87fn ymdhms_to_jdn(y: i64, mo: i64, d: i64, h: i64, mi: i64, s: i64, frac: f64) -> f64 {
88    ymd_to_jdn(y, mo, d) + (h as f64 * 3600.0 + mi as f64 * 60.0 + s as f64 + frac) / 86400.0
89}
90
91/// Unix epoch as JDN.
92const UNIX_EPOCH_JDN: f64 = 2_440_587.5;
93/// Upper bound for values interpreted as Julian day by the `auto` modifier.
94const AUTO_JDN_MAX: f64 = 5_373_484.499_999;
95/// Unix timestamp bounds used by SQLite's `auto` modifier.
96const AUTO_UNIX_MIN: f64 = -210_866_760_000.0;
97const AUTO_UNIX_MAX: f64 = 253_402_300_799.0;
98
99fn jdn_to_unix(jdn: f64) -> i64 {
100    ((jdn - UNIX_EPOCH_JDN) * 86400.0).round() as i64
101}
102
103fn unix_to_jdn(ts: f64) -> f64 {
104    ts / 86400.0 + UNIX_EPOCH_JDN
105}
106
107fn is_leap_year(y: i64) -> bool {
108    (y % 4 == 0 && y % 100 != 0) || y % 400 == 0
109}
110
111fn days_in_month(y: i64, m: i64) -> i64 {
112    match m {
113        1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
114        4 | 6 | 9 | 11 => 30,
115        2 => {
116            if is_leap_year(y) {
117                29
118            } else {
119                28
120            }
121        }
122        _ => 30,
123    }
124}
125
126fn day_of_year(y: i64, m: i64, d: i64) -> i64 {
127    let mut doy = d;
128    for mo in 1..m {
129        doy += days_in_month(y, mo);
130    }
131    doy
132}
133
134// ── Time String Parsing ───────────────────────────────────────────────────
135
136/// Parse a SQLite time string into a JDN.
137fn parse_timestring(s: &str) -> Option<f64> {
138    let s = s.trim();
139
140    // Special value: 'now' — we use a fixed time for determinism in tests.
141    // Real 'now' will come from the Cx connection time source later.
142    if s.eq_ignore_ascii_case("now") {
143        // Return a placeholder JDN for 2000-01-01 00:00:00.
144        return Some(ymd_to_jdn(2000, 1, 1));
145    }
146
147    // Try as a Julian Day Number (bare float).
148    if let Ok(jdn) = s.parse::<f64>() {
149        if jdn >= 0.0 {
150            return Some(jdn);
151        }
152    }
153
154    // ISO-8601 variants.
155    parse_iso8601(s)
156}
157
158fn parse_iso8601(s: &str) -> Option<f64> {
159    // YYYY-MM-DD HH:MM:SS.SSS  or  YYYY-MM-DDTHH:MM:SS.SSS
160    // YYYY-MM-DD HH:MM:SS  or  YYYY-MM-DD HH:MM
161    // YYYY-MM-DD
162    // HH:MM:SS.SSS  (bare time → 2000-01-01)
163    // HH:MM:SS
164    // HH:MM
165
166    let bytes = s.as_bytes();
167    let len = bytes.len();
168
169    // Try date-only or date+time.
170    if len >= 10 && bytes[4] == b'-' && bytes[7] == b'-' {
171        let y = s[0..4].parse::<i64>().ok()?;
172        let m = s[5..7].parse::<i64>().ok()?;
173        let d = s[8..10].parse::<i64>().ok()?;
174
175        if m < 1 || m > 12 || d < 1 || d > days_in_month(y, m) {
176            return None;
177        }
178
179        if len == 10 {
180            return Some(ymd_to_jdn(y, m, d));
181        }
182
183        // Separator: space or 'T'.
184        if len > 10 && (bytes[10] == b' ' || bytes[10] == b'T') {
185            let time_part = &s[11..];
186            let (h, mi, sec, frac) = parse_time_part(time_part)?;
187            return Some(ymdhms_to_jdn(y, m, d, h, mi, sec, frac));
188        }
189        return None;
190    }
191
192    // Bare time: HH:MM:SS or HH:MM:SS.SSS or HH:MM
193    if len >= 5 && bytes[2] == b':' {
194        let (h, mi, sec, frac) = parse_time_part(s)?;
195        return Some(ymdhms_to_jdn(2000, 1, 1, h, mi, sec, frac));
196    }
197
198    None
199}
200
201/// Parse "HH:MM:SS.SSS" or "HH:MM:SS" or "HH:MM".
202fn parse_time_part(s: &str) -> Option<(i64, i64, i64, f64)> {
203    let parts: Vec<&str> = s.splitn(3, ':').collect();
204    if parts.len() < 2 {
205        return None;
206    }
207    let h = parts[0].parse::<i64>().ok()?;
208    let mi = parts[1].parse::<i64>().ok()?;
209    if !(0..=23).contains(&h) || !(0..=59).contains(&mi) {
210        return None;
211    }
212
213    if parts.len() == 2 {
214        return Some((h, mi, 0, 0.0));
215    }
216
217    // Third part may have fractional seconds: "SS" or "SS.SSS".
218    let sec_str = parts[2];
219    if let Some(dot_pos) = sec_str.find('.') {
220        let sec = sec_str[..dot_pos].parse::<i64>().ok()?;
221        let frac_str = &sec_str[dot_pos..]; // ".SSS"
222        let frac = frac_str.parse::<f64>().ok()?;
223        if !(0..=59).contains(&sec) {
224            return None;
225        }
226        Some((h, mi, sec, frac))
227    } else {
228        let sec = sec_str.parse::<i64>().ok()?;
229        if !(0..=59).contains(&sec) {
230            return None;
231        }
232        Some((h, mi, sec, 0.0))
233    }
234}
235
236// ── Modifier Pipeline ─────────────────────────────────────────────────────
237
238/// Apply a single modifier string to a JDN.  Returns None if invalid.
239fn apply_modifier(jdn: f64, modifier: &str) -> Option<f64> {
240    let m = modifier.trim().to_ascii_lowercase();
241
242    // 'start of month' / 'start of year' / 'start of day'
243    if m == "start of month" {
244        let (y, mo, _d) = jdn_to_ymd(jdn);
245        return Some(ymd_to_jdn(y, mo, 1));
246    }
247    if m == "start of year" {
248        let (y, _mo, _d) = jdn_to_ymd(jdn);
249        return Some(ymd_to_jdn(y, 1, 1));
250    }
251    if m == "start of day" {
252        let (y, mo, d) = jdn_to_ymd(jdn);
253        return Some(ymd_to_jdn(y, mo, d));
254    }
255
256    // 'unixepoch' — reinterpret input as Unix timestamp.
257    if m == "unixepoch" {
258        return Some(unix_to_jdn(jdn));
259    }
260
261    // 'julianday' — input is already a JDN (no-op here, but the spec says
262    // it forces interpretation as JDN).
263    if m == "julianday" {
264        return Some(jdn);
265    }
266
267    // 'auto' — apply SQLite numeric auto-detection:
268    //   0.0..=5373484.499999          => Julian day number
269    //   -210866760000..=253402300799  => Unix timestamp
270    //   otherwise                      => NULL
271    if m == "auto" {
272        if (0.0..=AUTO_JDN_MAX).contains(&jdn) {
273            return Some(jdn);
274        }
275        if (AUTO_UNIX_MIN..=AUTO_UNIX_MAX).contains(&jdn) {
276            return Some(unix_to_jdn(jdn));
277        }
278        return None;
279    }
280
281    // 'localtime' / 'utc' — timezone conversion stubs.
282    // Full implementation requires system timezone access; stub for now.
283    if m == "localtime" || m == "utc" {
284        return Some(jdn);
285    }
286
287    // 'subsec' / 'subsecond' — this is a flag that affects output formatting,
288    // not the JDN.  We pass it through unchanged.
289    if m == "subsec" || m == "subsecond" {
290        return Some(jdn);
291    }
292
293    // 'weekday N' — advance to the next day that is weekday N (0=Sunday).
294    if let Some(rest) = m.strip_prefix("weekday ") {
295        let wd = rest.trim().parse::<i64>().ok()?;
296        if !(0..=6).contains(&wd) {
297            return None;
298        }
299        // Current day of week: 0=Monday in JDN, but SQLite uses 0=Sunday.
300        let current_jdn_int = (jdn + 0.5).floor() as i64;
301        let current_wd = (current_jdn_int + 1) % 7; // 0=Sunday
302        let mut diff = wd - current_wd;
303        if diff <= 0 {
304            diff += 7;
305        }
306        // If already the target weekday, advance 7 days (SQLite behavior).
307        return Some(jdn + diff as f64);
308    }
309
310    // Arithmetic: '+NNN days', '-NNN hours', etc.
311    parse_arithmetic_modifier(&m).map(|delta| jdn + delta)
312}
313
314/// Parse "+NNN unit" / "-NNN unit" and return the JDN delta.
315fn parse_arithmetic_modifier(m: &str) -> Option<f64> {
316    let (sign, rest) = if let Some(r) = m.strip_prefix('+') {
317        (1.0, r.trim())
318    } else if let Some(r) = m.strip_prefix('-') {
319        (-1.0, r.trim())
320    } else {
321        return None;
322    };
323
324    let mut parts = rest.splitn(2, ' ');
325    let num_str = parts.next()?;
326    let unit = parts.next()?.trim();
327
328    let num = num_str.parse::<f64>().ok()?;
329    let delta = num * sign;
330
331    match unit.trim_end_matches('s') {
332        "day" => Some(delta),
333        "hour" => Some(delta / 24.0),
334        "minute" => Some(delta / 1440.0),
335        "second" => Some(delta / 86400.0),
336        "month" => Some(apply_month_delta(delta)),
337        "year" => Some(apply_month_delta(delta * 12.0)),
338        _ => None,
339    }
340}
341
342/// For month/year arithmetic, we can't simply add a JDN delta because months
343/// have variable lengths.  This returns a JDN delta that is *approximately*
344/// correct.  A fully correct implementation requires decomposing and
345/// recomposing, which we handle in `apply_modifier_full` for month/year cases.
346fn apply_month_delta(months: f64) -> f64 {
347    // Average month ≈ 30.436875 days.
348    months * 30.436875
349}
350
351/// Apply a sequence of modifiers, also tracking the 'subsec' flag.
352fn apply_modifiers(jdn: f64, modifiers: &[String]) -> Option<(f64, bool)> {
353    let mut j = jdn;
354    let mut subsec = false;
355    for m in modifiers {
356        let m_lower = m.trim().to_ascii_lowercase();
357        if m_lower == "subsec" || m_lower == "subsecond" {
358            subsec = true;
359        }
360        // Month/year modifiers need special handling for exact date math.
361        if is_month_year_modifier(&m_lower) {
362            j = apply_month_year_exact(j, &m_lower)?;
363        } else {
364            j = apply_modifier(j, m)?;
365        }
366    }
367    Some((j, subsec))
368}
369
370fn is_month_year_modifier(m: &str) -> bool {
371    (m.contains("month") || m.contains("year")) && (m.starts_with('+') || m.starts_with('-'))
372}
373
374/// Exact month/year arithmetic by decomposing to YMD.
375fn apply_month_year_exact(jdn: f64, m: &str) -> Option<f64> {
376    let (sign, rest) = if let Some(r) = m.strip_prefix('+') {
377        (1_i64, r.trim())
378    } else if let Some(r) = m.strip_prefix('-') {
379        (-1_i64, r.trim())
380    } else {
381        return None;
382    };
383
384    let mut parts = rest.splitn(2, ' ');
385    let num_str = parts.next()?;
386    let unit = parts.next()?.trim();
387    let num = num_str.parse::<i64>().ok()?;
388
389    let (y, mo, d) = jdn_to_ymd(jdn);
390    let (h, mi, s, frac) = jdn_to_hms(jdn);
391
392    let total_months = match unit.trim_end_matches('s') {
393        "month" => num.checked_mul(sign)?,
394        "year" => num.checked_mul(sign)?.checked_mul(12)?,
395        _ => return None,
396    };
397
398    // (y * 12 + (mo - 1)) + total_months
399    let current_months = y.checked_mul(12)?.checked_add(mo - 1)?;
400    let new_total = current_months.checked_add(total_months)?;
401
402    let new_y = new_total.div_euclid(12);
403    let new_mo = new_total.rem_euclid(12) + 1;
404    let new_d = d.min(days_in_month(new_y, new_mo));
405
406    Some(ymdhms_to_jdn(new_y, new_mo, new_d, h, mi, s, frac))
407}
408
409// ── Output Formatters ─────────────────────────────────────────────────────
410
411fn format_date(jdn: f64) -> String {
412    let (y, m, d) = jdn_to_ymd(jdn);
413    format!("{y:04}-{m:02}-{d:02}")
414}
415
416fn format_time(jdn: f64, subsec: bool) -> String {
417    let (h, m, s, frac) = jdn_to_hms(jdn);
418    if subsec && frac > 1e-9 {
419        format!("{h:02}:{m:02}:{s:02}.{:03}", (frac * 1000.0).round() as i64)
420    } else {
421        format!("{h:02}:{m:02}:{s:02}")
422    }
423}
424
425fn format_datetime(jdn: f64, subsec: bool) -> String {
426    format!("{} {}", format_date(jdn), format_time(jdn, subsec))
427}
428
429/// strftime format engine.
430fn format_strftime(fmt: &str, jdn: f64) -> String {
431    let (y, mo, d) = jdn_to_ymd(jdn);
432    let (h, mi, s, frac) = jdn_to_hms(jdn);
433    let doy = day_of_year(y, mo, d);
434    // Day of week: 0=Sunday.
435    let jdn_int = (jdn + 0.5).floor() as i64;
436    let dow = (jdn_int + 1) % 7; // 0=Sunday, 6=Saturday
437
438    let mut result = String::new();
439    let chars: Vec<char> = fmt.chars().collect();
440    let mut i = 0;
441
442    while i < chars.len() {
443        if chars[i] == '%' && i + 1 < chars.len() {
444            i += 1;
445            match chars[i] {
446                'd' => result.push_str(&format!("{d:02}")),
447                'e' => result.push_str(&format!("{d:>2}")), // space-padded day
448                'f' => {
449                    // Seconds with fractional part.
450                    let total = s as f64 + frac;
451                    result.push_str(&format!("{total:06.3}"));
452                }
453                'H' => result.push_str(&format!("{h:02}")),
454                'I' => {
455                    // 12-hour clock.
456                    let h12 = if h == 0 {
457                        12
458                    } else if h > 12 {
459                        h - 12
460                    } else {
461                        h
462                    };
463                    result.push_str(&format!("{h12:02}"));
464                }
465                'j' => result.push_str(&format!("{doy:03}")),
466                'J' => result.push_str(&format!("{jdn:.6}")),
467                'k' => {
468                    // Space-padded 24-hour.
469                    result.push_str(&format!("{h:>2}"));
470                }
471                'l' => {
472                    // Space-padded 12-hour.
473                    let h12 = if h == 0 {
474                        12
475                    } else if h > 12 {
476                        h - 12
477                    } else {
478                        h
479                    };
480                    result.push_str(&format!("{h12:>2}"));
481                }
482                'm' => result.push_str(&format!("{mo:02}")),
483                'M' => result.push_str(&format!("{mi:02}")),
484                'p' => {
485                    result.push_str(if h < 12 { "AM" } else { "PM" });
486                }
487                'P' => {
488                    result.push_str(if h < 12 { "am" } else { "pm" });
489                }
490                'R' => result.push_str(&format!("{h:02}:{mi:02}")),
491                's' => {
492                    let unix = jdn_to_unix(jdn);
493                    result.push_str(&unix.to_string());
494                }
495                'S' => result.push_str(&format!("{s:02}")),
496                'T' => result.push_str(&format!("{h:02}:{mi:02}:{s:02}")),
497                'u' => {
498                    // ISO 8601 day of week: 1=Monday, 7=Sunday.
499                    let u = if dow == 0 { 7 } else { dow };
500                    result.push_str(&u.to_string());
501                }
502                'w' => result.push_str(&dow.to_string()),
503                'W' => {
504                    // Week of year (Monday as first day of week, 00-53).
505                    let w = (doy + 6 - ((dow + 6) % 7)) / 7;
506                    result.push_str(&format!("{w:02}"));
507                }
508                'Y' => result.push_str(&format!("{y:04}")),
509                'G' | 'g' | 'V' => {
510                    // ISO 8601 week-based year/week.
511                    let (iso_y, iso_w) = iso_week(y, mo, d);
512                    match chars[i] {
513                        'G' => result.push_str(&format!("{iso_y:04}")),
514                        'g' => result.push_str(&format!("{:02}", iso_y % 100)),
515                        'V' => result.push_str(&format!("{iso_w:02}")),
516                        _ => unreachable!(),
517                    }
518                }
519                '%' => result.push('%'),
520                other => {
521                    result.push('%');
522                    result.push(other);
523                }
524            }
525        } else {
526            result.push(chars[i]);
527        }
528        i += 1;
529    }
530
531    result
532}
533
534/// ISO 8601 week number and year.
535fn iso_week(y: i64, m: i64, d: i64) -> (i64, i64) {
536    let jdn = ymd_to_jdn(y, m, d);
537    let jdn_int = (jdn + 0.5).floor() as i64;
538    // ISO day of week: 1=Monday, 7=Sunday.
539    let dow = (jdn_int + 1) % 7;
540    let iso_dow = if dow == 0 { 7 } else { dow };
541
542    // Thursday of the same week determines the year.
543    let thu_jdn = jdn_int + (4 - iso_dow);
544    let (thu_y, _, _) = jdn_to_ymd(thu_jdn as f64);
545
546    // Jan 4 is always in week 1 (ISO 8601).
547    let jan4_jdn = (ymd_to_jdn(thu_y, 1, 4) + 0.5).floor() as i64;
548    let jan4_dow = (jan4_jdn + 1) % 7;
549    let jan4_iso_dow = if jan4_dow == 0 { 7 } else { jan4_dow };
550    let week1_start = jan4_jdn - (jan4_iso_dow - 1);
551
552    let week = (thu_jdn - week1_start) / 7 + 1;
553    (thu_y, week)
554}
555
556// ── timediff ──────────────────────────────────────────────────────────────
557
558fn timediff_impl(jdn1: f64, jdn2: f64) -> String {
559    let (sign, start_jdn, end_jdn) = if jdn1 >= jdn2 {
560        ('+', jdn2, jdn1)
561    } else {
562        ('-', jdn1, jdn2)
563    };
564
565    let (start_y, start_mo, start_d) = jdn_to_ymd(start_jdn);
566    let (start_h, start_mi, start_s, start_frac) = jdn_to_hms(start_jdn);
567    let mut start_ms = (start_frac * 1000.0).round() as i64;
568    if start_ms >= 1000 {
569        start_ms = 999;
570    }
571
572    let (end_y, end_mo, end_d) = jdn_to_ymd(end_jdn);
573    let (end_h, end_mi, end_s, end_frac) = jdn_to_hms(end_jdn);
574    let mut end_ms = (end_frac * 1000.0).round() as i64;
575    if end_ms >= 1000 {
576        end_ms = 999;
577    }
578
579    let mut years = end_y - start_y;
580    let mut months = end_mo - start_mo;
581    let mut days = end_d - start_d;
582    let mut hours = end_h - start_h;
583    let mut minutes = end_mi - start_mi;
584    let mut seconds = end_s - start_s;
585    let mut millis = end_ms - start_ms;
586
587    if millis < 0 {
588        millis += 1000;
589        seconds -= 1;
590    }
591    if seconds < 0 {
592        seconds += 60;
593        minutes -= 1;
594    }
595    if minutes < 0 {
596        minutes += 60;
597        hours -= 1;
598    }
599    if hours < 0 {
600        hours += 24;
601        days -= 1;
602    }
603    if days < 0 {
604        months -= 1;
605        let (borrow_y, borrow_mo) = if end_mo == 1 {
606            (end_y - 1, 12)
607        } else {
608            (end_y, end_mo - 1)
609        };
610        days += days_in_month(borrow_y, borrow_mo);
611    }
612    if months < 0 {
613        months += 12;
614        years -= 1;
615    }
616
617    format!(
618        "{sign}{years:04}-{months:02}-{days:02} {hours:02}:{minutes:02}:{seconds:02}.{millis:03}"
619    )
620}
621
622// ── Scalar Function Implementations ───────────────────────────────────────
623
624/// Parse args: first arg is time string, rest are modifiers.
625fn parse_args(args: &[SqliteValue]) -> Option<(f64, bool)> {
626    if args.is_empty() || args[0].is_null() {
627        return None;
628    }
629
630    let input = match &args[0] {
631        SqliteValue::Text(s) => parse_timestring(s)?,
632        SqliteValue::Integer(i) => *i as f64,
633        SqliteValue::Float(f) => *f,
634        _ => return None,
635    };
636
637    let modifiers: Vec<String> = args[1..]
638        .iter()
639        .filter_map(|a| if a.is_null() { None } else { Some(a.to_text()) })
640        .collect();
641
642    apply_modifiers(input, &modifiers)
643}
644
645// ── date() ────────────────────────────────────────────────────────────────
646
647pub struct DateFunc;
648
649impl ScalarFunction for DateFunc {
650    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
651        match parse_args(args) {
652            Some((jdn, _)) => Ok(SqliteValue::Text(format_date(jdn))),
653            None => Ok(SqliteValue::Null),
654        }
655    }
656
657    fn num_args(&self) -> i32 {
658        -1
659    }
660
661    fn name(&self) -> &str {
662        "date"
663    }
664}
665
666// ── time() ────────────────────────────────────────────────────────────────
667
668pub struct TimeFunc;
669
670impl ScalarFunction for TimeFunc {
671    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
672        match parse_args(args) {
673            Some((jdn, subsec)) => Ok(SqliteValue::Text(format_time(jdn, subsec))),
674            None => Ok(SqliteValue::Null),
675        }
676    }
677
678    fn num_args(&self) -> i32 {
679        -1
680    }
681
682    fn name(&self) -> &str {
683        "time"
684    }
685}
686
687// ── datetime() ────────────────────────────────────────────────────────────
688
689pub struct DateTimeFunc;
690
691impl ScalarFunction for DateTimeFunc {
692    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
693        match parse_args(args) {
694            Some((jdn, subsec)) => Ok(SqliteValue::Text(format_datetime(jdn, subsec))),
695            None => Ok(SqliteValue::Null),
696        }
697    }
698
699    fn num_args(&self) -> i32 {
700        -1
701    }
702
703    fn name(&self) -> &str {
704        "datetime"
705    }
706}
707
708// ── julianday() ───────────────────────────────────────────────────────────
709
710pub struct JuliandayFunc;
711
712impl ScalarFunction for JuliandayFunc {
713    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
714        match parse_args(args) {
715            Some((jdn, _)) => Ok(SqliteValue::Float(jdn)),
716            None => Ok(SqliteValue::Null),
717        }
718    }
719
720    fn num_args(&self) -> i32 {
721        -1
722    }
723
724    fn name(&self) -> &str {
725        "julianday"
726    }
727}
728
729// ── unixepoch() ───────────────────────────────────────────────────────────
730
731pub struct UnixepochFunc;
732
733impl ScalarFunction for UnixepochFunc {
734    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
735        match parse_args(args) {
736            Some((jdn, _)) => Ok(SqliteValue::Integer(jdn_to_unix(jdn))),
737            None => Ok(SqliteValue::Null),
738        }
739    }
740
741    fn num_args(&self) -> i32 {
742        -1
743    }
744
745    fn name(&self) -> &str {
746        "unixepoch"
747    }
748}
749
750// ── strftime() ────────────────────────────────────────────────────────────
751
752pub struct StrftimeFunc;
753
754impl ScalarFunction for StrftimeFunc {
755    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
756        if args.len() < 2 || args[0].is_null() || args[1].is_null() {
757            return Ok(SqliteValue::Null);
758        }
759        let fmt = args[0].to_text();
760        let rest = &args[1..];
761        match parse_args(rest) {
762            Some((jdn, _)) => Ok(SqliteValue::Text(format_strftime(&fmt, jdn))),
763            None => Ok(SqliteValue::Null),
764        }
765    }
766
767    fn num_args(&self) -> i32 {
768        -1
769    }
770
771    fn name(&self) -> &str {
772        "strftime"
773    }
774}
775
776// ── timediff() ────────────────────────────────────────────────────────────
777
778pub struct TimediffFunc;
779
780impl ScalarFunction for TimediffFunc {
781    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
782        if args.len() < 2 || args[0].is_null() || args[1].is_null() {
783            return Ok(SqliteValue::Null);
784        }
785
786        let jdn1 = match &args[0] {
787            SqliteValue::Text(s) => parse_timestring(s),
788            SqliteValue::Integer(i) => Some(*i as f64),
789            SqliteValue::Float(f) => Some(*f),
790            _ => None,
791        };
792        let jdn2 = match &args[1] {
793            SqliteValue::Text(s) => parse_timestring(s),
794            SqliteValue::Integer(i) => Some(*i as f64),
795            SqliteValue::Float(f) => Some(*f),
796            _ => None,
797        };
798
799        match (jdn1, jdn2) {
800            (Some(j1), Some(j2)) => Ok(SqliteValue::Text(timediff_impl(j1, j2))),
801            _ => Ok(SqliteValue::Null),
802        }
803    }
804
805    fn num_args(&self) -> i32 {
806        2
807    }
808
809    fn name(&self) -> &str {
810        "timediff"
811    }
812}
813
814// ── Registration ──────────────────────────────────────────────────────────
815
816/// Register all §13.3 date/time functions.
817pub fn register_datetime_builtins(registry: &mut FunctionRegistry) {
818    registry.register_scalar(DateFunc);
819    registry.register_scalar(TimeFunc);
820    registry.register_scalar(DateTimeFunc);
821    registry.register_scalar(JuliandayFunc);
822    registry.register_scalar(UnixepochFunc);
823    registry.register_scalar(StrftimeFunc);
824    registry.register_scalar(TimediffFunc);
825}
826
827// ── Tests ─────────────────────────────────────────────────────────────────
828
829#[cfg(test)]
830mod tests {
831    use super::*;
832
833    fn text(s: &str) -> SqliteValue {
834        SqliteValue::Text(s.to_owned())
835    }
836
837    fn int(v: i64) -> SqliteValue {
838        SqliteValue::Integer(v)
839    }
840
841    fn float(v: f64) -> SqliteValue {
842        SqliteValue::Float(v)
843    }
844
845    fn null() -> SqliteValue {
846        SqliteValue::Null
847    }
848
849    fn assert_text(result: &SqliteValue, expected: &str) {
850        match result {
851            SqliteValue::Text(s) => assert_eq!(s, expected, "text mismatch"),
852            other => panic!("expected Text(\"{expected}\"), got {other:?}"),
853        }
854    }
855
856    // ── Basic functions ───────────────────────────────────────────────
857
858    #[test]
859    fn test_date_basic() {
860        let r = DateFunc.invoke(&[text("2024-03-15 14:30:00")]).unwrap();
861        assert_text(&r, "2024-03-15");
862    }
863
864    #[test]
865    fn test_time_basic() {
866        let r = TimeFunc.invoke(&[text("2024-03-15 14:30:45")]).unwrap();
867        assert_text(&r, "14:30:45");
868    }
869
870    #[test]
871    fn test_datetime_basic() {
872        let r = DateTimeFunc.invoke(&[text("2024-03-15 14:30:00")]).unwrap();
873        assert_text(&r, "2024-03-15 14:30:00");
874    }
875
876    #[test]
877    fn test_julianday_basic() {
878        let r = JuliandayFunc.invoke(&[text("2024-03-15")]).unwrap();
879        match r {
880            SqliteValue::Float(jdn) => {
881                // JDN for 2024-03-15 should be approximately 2460384.5
882                assert!((jdn - 2_460_384.5).abs() < 0.01, "unexpected JDN: {jdn}");
883            }
884            other => panic!("expected Float, got {other:?}"),
885        }
886    }
887
888    #[test]
889    fn test_unixepoch_basic() {
890        let r = UnixepochFunc
891            .invoke(&[text("1970-01-01 00:00:00")])
892            .unwrap();
893        assert_eq!(r, int(0));
894    }
895
896    #[test]
897    fn test_unixepoch_known_date() {
898        let r = UnixepochFunc
899            .invoke(&[text("2024-01-01 00:00:00")])
900            .unwrap();
901        // 2024-01-01 00:00:00 UTC = 1704067200
902        assert_eq!(r, int(1_704_067_200));
903    }
904
905    // ── Modifiers ─────────────────────────────────────────────────────
906
907    #[test]
908    fn test_modifier_days() {
909        let r = DateFunc
910            .invoke(&[text("2024-01-15"), text("+10 days")])
911            .unwrap();
912        assert_text(&r, "2024-01-25");
913    }
914
915    #[test]
916    fn test_modifier_months() {
917        // 2024-01-31 + 1 month: Feb only has 29 days in 2024 (leap year).
918        let r = DateFunc
919            .invoke(&[text("2024-01-31"), text("+1 months")])
920            .unwrap();
921        assert_text(&r, "2024-02-29");
922    }
923
924    #[test]
925    fn test_modifier_years() {
926        // 2024-02-29 + 1 year: 2025 is not a leap year.
927        let r = DateFunc
928            .invoke(&[text("2024-02-29"), text("+1 years")])
929            .unwrap();
930        assert_text(&r, "2025-02-28");
931    }
932
933    #[test]
934    fn test_modifier_hours() {
935        let r = DateTimeFunc
936            .invoke(&[text("2024-01-01 23:00:00"), text("+2 hours")])
937            .unwrap();
938        assert_text(&r, "2024-01-02 01:00:00");
939    }
940
941    #[test]
942    fn test_modifier_start_of_month() {
943        let r = DateFunc
944            .invoke(&[text("2024-03-15"), text("start of month")])
945            .unwrap();
946        assert_text(&r, "2024-03-01");
947    }
948
949    #[test]
950    fn test_modifier_start_of_year() {
951        let r = DateFunc
952            .invoke(&[text("2024-06-15"), text("start of year")])
953            .unwrap();
954        assert_text(&r, "2024-01-01");
955    }
956
957    #[test]
958    fn test_modifier_start_of_day() {
959        let r = DateTimeFunc
960            .invoke(&[text("2024-03-15 14:30:00"), text("start of day")])
961            .unwrap();
962        assert_text(&r, "2024-03-15 00:00:00");
963    }
964
965    #[test]
966    fn test_modifier_unixepoch() {
967        let r = DateTimeFunc.invoke(&[int(0), text("unixepoch")]).unwrap();
968        assert_text(&r, "1970-01-01 00:00:00");
969    }
970
971    #[test]
972    fn test_modifier_weekday() {
973        // 2024-03-15 is Friday. `weekday 0` advances to the next Sunday.
974        let r = DateFunc
975            .invoke(&[text("2024-03-15"), text("weekday 0")])
976            .unwrap();
977        assert_text(&r, "2024-03-17");
978    }
979
980    #[test]
981    fn test_modifier_auto_unixepoch() {
982        let ts = int(1_710_531_045);
983        let r = DateTimeFunc.invoke(&[ts.clone(), text("auto")]).unwrap();
984        let expected = DateTimeFunc.invoke(&[ts, text("unixepoch")]).unwrap();
985        assert_eq!(
986            r, expected,
987            "auto and unixepoch should agree for unix-like values"
988        );
989    }
990
991    #[test]
992    fn test_modifier_auto_julian_day() {
993        let r = DateFunc
994            .invoke(&[float(2_460_384.5), text("auto")])
995            .unwrap();
996        assert_text(&r, "2024-03-15");
997    }
998
999    #[test]
1000    fn test_modifier_localtime_utc_roundtrip() {
1001        let r = DateTimeFunc
1002            .invoke(&[
1003                text("2024-03-15 14:30:45"),
1004                text("utc"),
1005                text("localtime"),
1006                text("utc"),
1007            ])
1008            .unwrap();
1009        assert_text(&r, "2024-03-15 14:30:45");
1010    }
1011
1012    #[test]
1013    fn test_modifier_auto_out_of_range_returns_null() {
1014        let r = DateTimeFunc.invoke(&[float(1.0e20), text("auto")]).unwrap();
1015        assert_eq!(r, SqliteValue::Null);
1016    }
1017
1018    #[test]
1019    fn test_modifier_order_matters() {
1020        // 'start of month' then '+1 day' = March 2nd.
1021        let r1 = DateFunc
1022            .invoke(&[text("2024-03-15"), text("start of month"), text("+1 days")])
1023            .unwrap();
1024        assert_text(&r1, "2024-03-02");
1025
1026        // '+1 day' then 'start of month' = March 1st.
1027        let r2 = DateFunc
1028            .invoke(&[text("2024-03-15"), text("+1 days"), text("start of month")])
1029            .unwrap();
1030        assert_text(&r2, "2024-03-01");
1031    }
1032
1033    #[test]
1034    fn test_modifier_weekday_same_day_advances_next_week() {
1035        // 2024-03-17 is Sunday; SQLite semantics advance to the NEXT Sunday.
1036        let r = DateFunc
1037            .invoke(&[text("2024-03-17"), text("weekday 0")])
1038            .unwrap();
1039        assert_text(&r, "2024-03-24");
1040    }
1041
1042    // ── Input formats ─────────────────────────────────────────────────
1043
1044    #[test]
1045    fn test_bare_time_defaults() {
1046        let r = DateFunc.invoke(&[text("12:30:00")]).unwrap();
1047        assert_text(&r, "2000-01-01");
1048    }
1049
1050    #[test]
1051    fn test_t_separator() {
1052        let r = DateTimeFunc.invoke(&[text("2024-03-15T14:30:00")]).unwrap();
1053        assert_text(&r, "2024-03-15 14:30:00");
1054    }
1055
1056    #[test]
1057    fn test_julian_day_input() {
1058        // 2460384.5 is 2024-03-15.
1059        let r = DateFunc.invoke(&[float(2_460_384.5)]).unwrap();
1060        assert_text(&r, "2024-03-15");
1061    }
1062
1063    #[test]
1064    fn test_null_input() {
1065        assert_eq!(DateFunc.invoke(&[null()]).unwrap(), SqliteValue::Null);
1066    }
1067
1068    #[test]
1069    fn test_invalid_input() {
1070        assert_eq!(
1071            DateFunc.invoke(&[text("not-a-date")]).unwrap(),
1072            SqliteValue::Null
1073        );
1074    }
1075
1076    #[test]
1077    fn test_negative_time_component_invalid() {
1078        let r = TimeFunc.invoke(&[text("-01:00")]).unwrap();
1079        assert_eq!(r, SqliteValue::Null);
1080    }
1081
1082    // ── Leap year ─────────────────────────────────────────────────────
1083
1084    #[test]
1085    fn test_leap_year() {
1086        let r = DateFunc
1087            .invoke(&[text("2024-02-28"), text("+1 days")])
1088            .unwrap();
1089        assert_text(&r, "2024-02-29");
1090    }
1091
1092    #[test]
1093    fn test_non_leap_year() {
1094        let r = DateFunc
1095            .invoke(&[text("2023-02-28"), text("+1 days")])
1096            .unwrap();
1097        assert_text(&r, "2023-03-01");
1098    }
1099
1100    // ── strftime ──────────────────────────────────────────────────────
1101
1102    #[test]
1103    fn test_strftime_basic() {
1104        let r = StrftimeFunc
1105            .invoke(&[text("%Y-%m-%d"), text("2024-03-15")])
1106            .unwrap();
1107        assert_text(&r, "2024-03-15");
1108    }
1109
1110    #[test]
1111    fn test_strftime_time_specifiers() {
1112        let r = StrftimeFunc
1113            .invoke(&[text("%H:%M:%S"), text("2024-03-15 14:30:45")])
1114            .unwrap();
1115        assert_text(&r, "14:30:45");
1116    }
1117
1118    #[test]
1119    fn test_strftime_unix_seconds() {
1120        let r = StrftimeFunc
1121            .invoke(&[text("%s"), text("1970-01-01 00:00:00")])
1122            .unwrap();
1123        assert_text(&r, "0");
1124    }
1125
1126    #[test]
1127    fn test_strftime_day_of_year() {
1128        let r = StrftimeFunc
1129            .invoke(&[text("%j"), text("2024-03-15")])
1130            .unwrap();
1131        // 2024-03-15: Jan(31) + Feb(29) + 15 = 75
1132        assert_text(&r, "075");
1133    }
1134
1135    #[test]
1136    fn test_strftime_day_of_week() {
1137        // 2024-03-15 is a Friday → w=5 (0=Sunday), u=5 (1=Monday)
1138        let r = StrftimeFunc
1139            .invoke(&[text("%w"), text("2024-03-15")])
1140            .unwrap();
1141        assert_text(&r, "5");
1142
1143        let r = StrftimeFunc
1144            .invoke(&[text("%u"), text("2024-03-15")])
1145            .unwrap();
1146        assert_text(&r, "5");
1147    }
1148
1149    #[test]
1150    fn test_strftime_12hour() {
1151        let r = StrftimeFunc
1152            .invoke(&[text("%I %p"), text("2024-03-15 14:30:00")])
1153            .unwrap();
1154        assert_text(&r, "02 PM");
1155
1156        let r = StrftimeFunc
1157            .invoke(&[text("%I %P"), text("2024-03-15 09:30:00")])
1158            .unwrap();
1159        assert_text(&r, "09 am");
1160    }
1161
1162    #[test]
1163    fn test_strftime_all_specifiers_presence() {
1164        let fmt = "%d|%e|%f|%H|%I|%j|%J|%k|%l|%m|%M|%p|%P|%R|%s|%S|%T|%u|%w|%W|%G|%g|%V|%Y|%%";
1165        let r = StrftimeFunc
1166            .invoke(&[text(fmt), text("2024-03-15 14:30:45.123")])
1167            .unwrap();
1168
1169        let s = match r {
1170            SqliteValue::Text(v) => v,
1171            other => panic!("expected Text, got {other:?}"),
1172        };
1173        let parts: Vec<&str> = s.split('|').collect();
1174        assert_eq!(parts.len(), 25, "unexpected specifier output: {s}");
1175        assert_eq!(parts[0], "15"); // %d
1176        assert_eq!(parts[1], "15"); // %e
1177        assert_eq!(parts[2], "45.123"); // %f
1178        assert_eq!(parts[3], "14"); // %H
1179        assert_eq!(parts[4], "02"); // %I
1180        assert_eq!(parts[5], "075"); // %j
1181        assert!(
1182            parts[6].parse::<f64>().is_ok(),
1183            "expected numeric %J output, got {}",
1184            parts[6]
1185        );
1186        assert_eq!(parts[7], "14"); // %k
1187        assert_eq!(parts[8], " 2"); // %l
1188        assert_eq!(parts[9], "03"); // %m
1189        assert_eq!(parts[10], "30"); // %M
1190        assert_eq!(parts[11], "PM"); // %p
1191        assert_eq!(parts[12], "pm"); // %P
1192        assert_eq!(parts[13], "14:30"); // %R
1193        assert!(
1194            parts[14].parse::<i64>().is_ok(),
1195            "expected numeric %s output, got {}",
1196            parts[14]
1197        );
1198        assert_eq!(parts[15], "45"); // %S
1199        assert_eq!(parts[16], "14:30:45"); // %T
1200        assert_eq!(parts[17], "5"); // %u
1201        assert_eq!(parts[18], "5"); // %w
1202        assert_eq!(parts[19], "11"); // %W
1203        assert_eq!(parts[20], "2024"); // %G
1204        assert_eq!(parts[21], "24"); // %g
1205        assert_eq!(parts[22], "11"); // %V
1206        assert_eq!(parts[23], "2024"); // %Y
1207        assert_eq!(parts[24], "%"); // %%
1208    }
1209
1210    #[test]
1211    fn test_strftime_null() {
1212        assert_eq!(
1213            StrftimeFunc.invoke(&[null(), text("2024-01-01")]).unwrap(),
1214            SqliteValue::Null
1215        );
1216        assert_eq!(
1217            StrftimeFunc.invoke(&[text("%Y"), null()]).unwrap(),
1218            SqliteValue::Null
1219        );
1220    }
1221
1222    // ── timediff ──────────────────────────────────────────────────────
1223
1224    #[test]
1225    fn test_timediff_basic() {
1226        let r = TimediffFunc
1227            .invoke(&[text("2024-03-15"), text("2024-03-10")])
1228            .unwrap();
1229        assert_text(&r, "+0000-00-05 00:00:00.000");
1230    }
1231
1232    #[test]
1233    fn test_timediff_negative() {
1234        let r = TimediffFunc
1235            .invoke(&[text("2024-03-10"), text("2024-03-15")])
1236            .unwrap();
1237        assert_text(&r, "-0000-00-05 00:00:00.000");
1238    }
1239
1240    #[test]
1241    fn test_timediff_year_boundary() {
1242        let r = TimediffFunc
1243            .invoke(&[text("2024-01-01 01:00:00"), text("2023-12-31 23:00:00")])
1244            .unwrap();
1245        assert_text(&r, "+0000-00-00 02:00:00.000");
1246    }
1247
1248    // ── Subsec modifier ───────────────────────────────────────────────
1249
1250    #[test]
1251    fn test_modifier_subsec() {
1252        let r = TimeFunc
1253            .invoke(&[text("2024-01-01 12:00:00.123"), text("subsec")])
1254            .unwrap();
1255        match &r {
1256            SqliteValue::Text(s) => assert!(
1257                s.contains('.'),
1258                "expected fractional seconds with subsec: {s}"
1259            ),
1260            other => panic!("expected Text, got {other:?}"),
1261        }
1262    }
1263
1264    // ── Registration ──────────────────────────────────────────────────
1265
1266    #[test]
1267    fn test_register_datetime_builtins_all_present() {
1268        let mut reg = FunctionRegistry::new();
1269        register_datetime_builtins(&mut reg);
1270
1271        let expected = [
1272            "date",
1273            "time",
1274            "datetime",
1275            "julianday",
1276            "unixepoch",
1277            "strftime",
1278            "timediff",
1279        ];
1280
1281        for name in expected {
1282            assert!(
1283                reg.find_scalar(name, 1).is_some() || reg.find_scalar(name, 2).is_some(),
1284                "datetime function '{name}' not registered"
1285            );
1286        }
1287    }
1288
1289    // ── JDN roundtrip ─────────────────────────────────────────────────
1290
1291    #[test]
1292    fn test_modifier_year_overflow() {
1293        // "+9223372036854775807 years" causes i64 overflow in year calculation.
1294        // Should return NULL, not panic.
1295        let huge = i64::MAX;
1296        let modifier = format!("+{huge} years");
1297        let r = DateFunc.invoke(&[text("2000-01-01"), text(&modifier)]);
1298        // The implementation should catch overflow and return Ok(Null), or at least not panic.
1299        // If it panics, the test harness catches it (but we want to prevent panics).
1300        assert_eq!(r.unwrap(), SqliteValue::Null);
1301    }
1302
1303    #[test]
1304    fn test_jdn_roundtrip() {
1305        // Test that ymd → jdn → ymd roundtrips correctly.
1306        let dates = [
1307            (2024, 3, 15),
1308            (2000, 1, 1),
1309            (1970, 1, 1),
1310            (2024, 2, 29),
1311            (1900, 1, 1),
1312            (2099, 12, 31),
1313        ];
1314        for (y, m, d) in dates {
1315            let jdn = ymd_to_jdn(y, m, d);
1316            let (y2, m2, d2) = jdn_to_ymd(jdn);
1317            assert_eq!(
1318                (y, m, d),
1319                (y2, m2, d2),
1320                "roundtrip failed for {y}-{m}-{d} (JDN={jdn})"
1321            );
1322        }
1323    }
1324
1325    #[test]
1326    fn test_unix_epoch_roundtrip() {
1327        let jdn = ymd_to_jdn(1970, 1, 1);
1328        let unix = jdn_to_unix(jdn);
1329        assert_eq!(unix, 0, "Unix epoch should be 0");
1330
1331        let jdn2 = unix_to_jdn(0.0);
1332        assert!((jdn2 - UNIX_EPOCH_JDN).abs() < 1e-10, "roundtrip failed");
1333    }
1334}