1#![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 std::{
35 borrow::Cow,
36 fmt::{Arguments, Write as _},
37};
38
39use fsqlite_error::Result;
40use fsqlite_types::SqliteValue;
41
42use crate::{FunctionRegistry, ScalarFunction};
43
44fn utc_offset_for_local_datetime(y: i32, mo: u32, d: u32, h: u32, mi: u32, s: u32) -> i64 {
55 use chrono::{Local, NaiveDate, NaiveDateTime, NaiveTime, TimeZone};
56 let date = NaiveDate::from_ymd_opt(y, mo, d).unwrap_or_default();
57 let time = NaiveTime::from_hms_opt(h, mi, s).unwrap_or_default();
58 let naive = NaiveDateTime::new(date, time);
59 match Local.from_local_datetime(&naive).earliest() {
60 Some(dt) => dt.offset().local_minus_utc() as i64,
61 None => 0, }
63}
64
65fn utc_offset_for_utc_datetime(y: i32, mo: u32, d: u32, h: u32, mi: u32, s: u32) -> i64 {
72 use chrono::{Local, NaiveDate, NaiveDateTime, NaiveTime, TimeZone, Utc};
73 let date = NaiveDate::from_ymd_opt(y, mo, d).unwrap_or_default();
74 let time = NaiveTime::from_hms_opt(h, mi, s).unwrap_or_default();
75 let naive = NaiveDateTime::new(date, time);
76 let utc_dt = Utc.from_utc_datetime(&naive);
77 let local_dt = utc_dt.with_timezone(&Local);
78 local_dt.offset().local_minus_utc() as i64
79}
80
81fn utc_offset_for_utc_jdn(jdn: f64) -> i64 {
83 let (y, mo, d) = jdn_to_ymd(jdn);
84 let (h, mi, s, _frac) = jdn_to_hms(jdn);
85 utc_offset_for_utc_datetime(y as i32, mo as u32, d as u32, h as u32, mi as u32, s as u32)
86}
87
88fn utc_offset_for_local_jdn(jdn: f64) -> i64 {
90 let (y, mo, d) = jdn_to_ymd(jdn);
91 let (h, mi, s, _frac) = jdn_to_hms(jdn);
92 utc_offset_for_local_datetime(y as i32, mo as u32, d as u32, h as u32, mi as u32, s as u32)
93}
94
95fn ymd_to_jdn(y: i64, m: i64, d: i64) -> f64 {
101 let (y, m) = if m <= 2 {
102 (y.saturating_sub(1), m.saturating_add(12))
103 } else {
104 (y, m)
105 };
106 let a = y / 100;
107 let b = 2_i64.saturating_sub(a).saturating_add(a / 4);
108 (365.25 * y.saturating_add(4716) as f64).floor()
109 + (30.6001 * m.saturating_add(1) as f64).floor()
110 + d as f64
111 + b as f64
112 - 1524.5
113}
114
115fn jdn_to_ymd(jdn: f64) -> (i64, i64, i64) {
122 let z = (jdn + 0.5).floor() as i64;
123 let a = if z < 2_299_161 {
124 z
125 } else {
126 let alpha = ((z as f64 - 1_867_216.25) / 36524.25).floor() as i64;
127 z.saturating_add(1)
128 .saturating_add(alpha)
129 .saturating_sub(alpha / 4)
130 };
131 let b = a.saturating_add(1524);
132 let c = ((b as f64 - 122.1) / 365.25).floor() as i64;
133 let d = (365.25 * c as f64).floor() as i64;
134 let e = ((b.saturating_sub(d)) as f64 / 30.6001).floor() as i64;
135
136 let day = b
137 .saturating_sub(d)
138 .saturating_sub((30.6001 * e as f64).floor() as i64);
139 let month = if e < 14 {
140 e.saturating_sub(1)
141 } else {
142 e.saturating_sub(13)
143 };
144 let year = if month > 2 {
145 c.saturating_sub(4716)
146 } else {
147 c.saturating_sub(4715)
148 };
149 (year, month, day)
150}
151
152fn jdn_to_hms(jdn: f64) -> (i64, i64, i64, f64) {
154 let frac = jdn + 0.5 - (jdn + 0.5).floor();
155 let total_ms = (frac * 86_400_000.0).round() as i64;
157 let h = total_ms / 3_600_000;
158 let rem = total_ms % 3_600_000;
159 let m = rem / 60_000;
160 let rem = rem % 60_000;
161 let s = rem / 1000;
162 let ms_frac = (rem % 1000) as f64 / 1000.0;
163 (h, m, s, ms_frac)
164}
165
166fn ymdhms_to_jdn(y: i64, mo: i64, d: i64, h: i64, mi: i64, s: i64, frac: f64) -> f64 {
168 ymd_to_jdn(y, mo, d) + (h as f64 * 3600.0 + mi as f64 * 60.0 + s as f64 + frac) / 86400.0
169}
170
171const UNIX_EPOCH_JDN: f64 = 2_440_587.5;
173const AUTO_JDN_MAX: f64 = 5_373_484.499_999;
175const AUTO_UNIX_MIN: f64 = -210_866_760_000.0;
177const AUTO_UNIX_MAX: f64 = 253_402_300_799.0;
178
179fn jdn_to_unix(jdn: f64) -> i64 {
180 ((jdn - UNIX_EPOCH_JDN) * 86400.0).round() as i64
181}
182
183fn unix_to_jdn(ts: f64) -> f64 {
184 ts / 86400.0 + UNIX_EPOCH_JDN
185}
186
187fn is_leap_year(y: i64) -> bool {
188 (y % 4 == 0 && y % 100 != 0) || y % 400 == 0
189}
190
191fn days_in_month(y: i64, m: i64) -> i64 {
192 match m {
193 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
194 4 | 6 | 9 | 11 => 30,
195 2 => {
196 if is_leap_year(y) {
197 29
198 } else {
199 28
200 }
201 }
202 _ => 30,
203 }
204}
205
206fn day_of_year(y: i64, m: i64, d: i64) -> i64 {
207 let mut doy = d;
208 for mo in 1..m {
209 doy = doy.saturating_add(days_in_month(y, mo));
210 }
211 doy
212}
213
214fn parse_timestring(s: &str) -> Option<f64> {
218 let s = s.trim();
219
220 if s.eq_ignore_ascii_case("now") {
226 use std::time::{SystemTime, UNIX_EPOCH};
227 let secs = SystemTime::now()
228 .duration_since(UNIX_EPOCH)
229 .unwrap_or_default()
230 .as_secs_f64();
231 return Some(2_440_587.5 + secs / 86_400.0);
233 }
234
235 if let Ok(jdn) = s.parse::<f64>() {
238 if jdn >= 0.0 && jdn.is_finite() {
239 return Some(jdn);
240 }
241 }
242
243 parse_iso8601(s)
245}
246
247fn parse_iso8601(s: &str) -> Option<f64> {
248 let bytes = s.as_bytes();
256 let len = bytes.len();
257
258 if len >= 10 && bytes[4] == b'-' && bytes[7] == b'-' {
260 let y = s[0..4].parse::<i64>().ok()?;
261 let m = s[5..7].parse::<i64>().ok()?;
262 let d = s[8..10].parse::<i64>().ok()?;
263
264 if m < 1 || m > 12 || d < 1 || d > 31 {
265 return None;
266 }
267
268 if len == 10 {
269 return Some(ymd_to_jdn(y, m, d));
270 }
271
272 if len > 10 && (bytes[10] == b' ' || bytes[10] == b'T') {
274 let time_part = &s[11..];
275 let (h, mi, sec, frac, tz_offset_min) = parse_time_part_with_tz(time_part)?;
276 let jdn = ymdhms_to_jdn(y, m, d, h, mi, sec, frac);
277 return Some(jdn - (tz_offset_min as f64) / 1440.0);
280 }
281 return None;
282 }
283
284 if len >= 5 && bytes[2] == b':' {
286 let (h, mi, sec, frac, tz_offset_min) = parse_time_part_with_tz(s)?;
287 let jdn = ymdhms_to_jdn(2000, 1, 1, h, mi, sec, frac);
288 return Some(jdn - (tz_offset_min as f64) / 1440.0);
289 }
290
291 None
292}
293
294fn split_tz_suffix(s: &str) -> Option<(&str, i64)> {
302 if let Some(stripped) = s.strip_suffix('Z').or_else(|| s.strip_suffix('z')) {
304 return Some((stripped, 0));
305 }
306
307 let bytes = s.as_bytes();
316 for width in [6usize, 5, 3] {
318 if bytes.len() < width + 1 {
319 continue;
320 }
321 let split_at = bytes.len() - width;
322 let sign_byte = bytes[split_at];
323 if sign_byte != b'+' && sign_byte != b'-' {
324 continue;
325 }
326 let tz_part = &s[split_at..];
327 if let Some(offset) = parse_tz_offset(tz_part) {
328 return Some((&s[..split_at], offset));
329 }
330 }
331
332 Some((s, 0))
334}
335
336fn parse_tz_offset(tz: &str) -> Option<i64> {
339 let bytes = tz.as_bytes();
340 if bytes.is_empty() {
341 return None;
342 }
343 let sign: i64 = match bytes[0] {
344 b'+' => 1,
345 b'-' => -1,
346 _ => return None,
347 };
348 let rest = &tz[1..];
349 let (hours, minutes) = match rest.len() {
350 5 if rest.as_bytes()[2] == b':' => (
352 rest[0..2].parse::<i64>().ok()?,
353 rest[3..5].parse::<i64>().ok()?,
354 ),
355 4 => (
357 rest[0..2].parse::<i64>().ok()?,
358 rest[2..4].parse::<i64>().ok()?,
359 ),
360 2 => (rest.parse::<i64>().ok()?, 0),
362 _ => return None,
363 };
364 if !(0..=23).contains(&hours) || !(0..=59).contains(&minutes) {
365 return None;
366 }
367 Some(sign * (hours * 60 + minutes))
368}
369
370fn parse_time_part_with_tz(s: &str) -> Option<(i64, i64, i64, f64, i64)> {
373 let (time_only, tz_offset_min) = split_tz_suffix(s)?;
374 let (h, mi, sec, frac) = parse_time_part(time_only)?;
375 Some((h, mi, sec, frac, tz_offset_min))
376}
377
378fn parse_time_part(s: &str) -> Option<(i64, i64, i64, f64)> {
380 let [h_tens, h_ones, b':', mi_tens, mi_ones, rest @ ..] = s.as_bytes() else {
381 return None;
382 };
383 let h = parse_two_ascii_digits(*h_tens, *h_ones)?;
388 let mi = parse_two_ascii_digits(*mi_tens, *mi_ones)?;
389 if !(0..=23).contains(&h) || !(0..=59).contains(&mi) {
390 return None;
391 }
392
393 match rest {
398 [] => Some((h, mi, 0, 0.0)),
399 [b':', sec_tens, sec_ones] => {
400 let sec = parse_two_ascii_digits(*sec_tens, *sec_ones)?;
401 if !(0..=59).contains(&sec) {
402 return None;
403 }
404 Some((h, mi, sec, 0.0))
405 }
406 [b':', sec_tens, sec_ones, b'.', ..] => {
407 let sec = parse_two_ascii_digits(*sec_tens, *sec_ones)?;
408 if !(0..=59).contains(&sec) {
409 return None;
410 }
411 let frac = s.get(8..)?.parse::<f64>().ok()?;
412 Some((h, mi, sec, frac))
413 }
414 _ => None,
415 }
416}
417
418#[inline]
419fn parse_two_ascii_digits(tens: u8, ones: u8) -> Option<i64> {
420 if tens.is_ascii_digit() && ones.is_ascii_digit() {
421 Some(i64::from((tens - b'0') * 10 + (ones - b'0')))
422 } else {
423 None
424 }
425}
426
427fn apply_modifier(jdn: f64, modifier: &str) -> Option<f64> {
431 let m = modifier.trim().to_ascii_lowercase();
432
433 if m == "start of month" {
435 let (y, mo, _d) = jdn_to_ymd(jdn);
436 return Some(ymd_to_jdn(y, mo, 1));
437 }
438 if m == "start of year" {
439 let (y, _mo, _d) = jdn_to_ymd(jdn);
440 return Some(ymd_to_jdn(y, 1, 1));
441 }
442 if m == "start of day" {
443 let (y, mo, d) = jdn_to_ymd(jdn);
444 return Some(ymd_to_jdn(y, mo, d));
445 }
446
447 if m == "unixepoch" {
449 return Some(unix_to_jdn(jdn));
450 }
451
452 if m == "julianday" {
455 return Some(jdn);
456 }
457
458 if m == "auto" {
463 if (0.0..=AUTO_JDN_MAX).contains(&jdn) {
464 return Some(jdn);
465 }
466 if (AUTO_UNIX_MIN..=AUTO_UNIX_MAX).contains(&jdn) {
467 return Some(unix_to_jdn(jdn));
468 }
469 return None;
470 }
471
472 if m == "localtime" {
475 let offset = utc_offset_for_utc_jdn(jdn);
476 return Some(jdn + offset as f64 / 86400.0);
477 }
478 if m == "utc" {
481 let offset = utc_offset_for_local_jdn(jdn);
482 return Some(jdn - offset as f64 / 86400.0);
483 }
484
485 if m == "subsec" || m == "subsecond" {
488 return Some(jdn);
489 }
490
491 if let Some(rest) = m.strip_prefix("weekday ") {
493 let wd = rest.trim().parse::<i64>().ok()?;
494 if !(0..=6).contains(&wd) {
495 return None;
496 }
497 let current_jdn_int = (jdn + 0.5).floor() as i64;
499 let current_wd = (current_jdn_int + 1) % 7; let mut diff = wd - current_wd;
501 if diff < 0 {
502 diff += 7;
503 }
504 return Some(jdn + diff as f64);
506 }
507
508 parse_arithmetic_modifier(&m).map(|delta| jdn + delta)
510}
511
512fn parse_arithmetic_modifier(m: &str) -> Option<f64> {
514 let (sign, rest) = if let Some(r) = m.strip_prefix('+') {
515 (1.0, r.trim())
516 } else {
517 let r = m.strip_prefix('-')?;
518 (-1.0, r.trim())
519 };
520
521 let mut parts = rest.splitn(2, ' ');
522 let num_str = parts.next()?;
523 let unit = parts.next()?.trim();
524
525 let num = num_str.parse::<f64>().ok().filter(|f| f.is_finite())?;
527 let delta = num * sign;
528
529 match unit.trim_end_matches('s') {
530 "day" => Some(delta),
531 "hour" => Some(delta / 24.0),
532 "minute" => Some(delta / 1440.0),
533 "second" => Some(delta / 86400.0),
534 "month" => Some(apply_month_delta(delta)),
535 "year" => Some(apply_month_delta(delta * 12.0)),
536 _ => None,
537 }
538}
539
540fn apply_month_delta(months: f64) -> f64 {
545 months * 30.436875
547}
548
549fn apply_modifiers(jdn: f64, modifiers: &[String]) -> Option<(f64, bool)> {
551 let mut j = jdn;
552 let mut subsec = false;
553 for m in modifiers {
554 let m_lower = m.trim().to_ascii_lowercase();
555 if m_lower == "subsec" || m_lower == "subsecond" {
556 subsec = true;
557 }
558 if is_month_year_modifier(&m_lower) {
564 match apply_month_year_exact(j, &m_lower) {
565 Ok(new_jdn) => {
566 j = new_jdn?;
567 continue;
568 }
569 Err(()) => {
570 }
572 }
573 }
574 j = apply_modifier(j, m)?;
575 }
576 Some((j, subsec))
577}
578
579fn is_month_year_modifier(m: &str) -> bool {
580 (m.contains("month") || m.contains("year")) && (m.starts_with('+') || m.starts_with('-'))
581}
582
583fn apply_month_year_exact(jdn: f64, m: &str) -> std::result::Result<Option<f64>, ()> {
588 let (sign, rest) = if let Some(r) = m.strip_prefix('+') {
589 (1_i64, r.trim())
590 } else if let Some(r) = m.strip_prefix('-') {
591 (-1_i64, r.trim())
592 } else {
593 return Err(());
594 };
595
596 let mut parts = rest.splitn(2, ' ');
597 let num_str = parts.next().ok_or(())?;
598 let unit = parts.next().ok_or(())?.trim();
599
600 let num = if let Ok(n) = num_str.parse::<i64>() {
602 n
603 } else if let Ok(f) = num_str.parse::<f64>() {
604 if f.fract() == 0.0 && f >= i64::MIN as f64 && f <= i64::MAX as f64 {
605 f as i64
606 } else {
607 return Err(());
608 }
609 } else {
610 return Err(());
611 };
612
613 let (y, mo, d) = jdn_to_ymd(jdn);
614 let (h, mi, s, frac) = jdn_to_hms(jdn);
615
616 let total_months = match unit.trim_end_matches('s') {
617 "month" => {
618 if let Some(val) = num.checked_mul(sign) {
619 val
620 } else {
621 return Ok(None);
622 }
623 }
624 "year" => {
625 if let Some(val) = num.checked_mul(sign).and_then(|v| v.checked_mul(12)) {
626 val
627 } else {
628 return Ok(None);
629 }
630 }
631 _ => return Err(()),
632 };
633
634 let current_months = if let Some(val) = y.checked_mul(12).and_then(|v| v.checked_add(mo - 1)) {
636 val
637 } else {
638 return Ok(None);
639 };
640 let new_total = if let Some(val) = current_months.checked_add(total_months) {
641 val
642 } else {
643 return Ok(None);
644 };
645
646 let new_y = new_total.div_euclid(12);
647 let new_mo = new_total.rem_euclid(12) + 1;
648 Ok(Some(ymdhms_to_jdn(new_y, new_mo, d, h, mi, s, frac)))
651}
652
653fn format_date(jdn: f64) -> String {
656 let (y, m, d) = jdn_to_ymd(jdn);
657 format!("{y:04}-{m:02}-{d:02}")
658}
659
660fn format_time(jdn: f64, subsec: bool) -> String {
661 let (h, m, s, frac) = jdn_to_hms(jdn);
662 if subsec && frac > 1e-9 {
663 format!("{h:02}:{m:02}:{s:02}.{:03}", (frac * 1000.0).round() as i64)
664 } else {
665 format!("{h:02}:{m:02}:{s:02}")
666 }
667}
668
669fn format_datetime(jdn: f64, subsec: bool) -> String {
670 format!("{} {}", format_date(jdn), format_time(jdn, subsec))
671}
672
673#[inline]
674fn push_format(result: &mut String, args: Arguments<'_>) {
675 let _ = result.write_fmt(args);
676}
677
678#[inline]
679fn push_zero_padded_2(result: &mut String, value: i64) {
680 if (0..=99).contains(&value) {
681 let value = value as u8;
682 result.push(char::from(b'0' + value / 10));
683 result.push(char::from(b'0' + value % 10));
684 } else {
685 push_format(result, format_args!("{value:02}"));
686 }
687}
688
689#[inline]
690fn push_space_padded_2(result: &mut String, value: i64) {
691 if (0..=99).contains(&value) {
692 let value = value as u8;
693 if value >= 10 {
694 result.push(char::from(b'0' + value / 10));
695 } else {
696 result.push(' ');
697 }
698 result.push(char::from(b'0' + value % 10));
699 } else {
700 push_format(result, format_args!("{value:>2}"));
701 }
702}
703
704#[inline]
705fn push_zero_padded_3(result: &mut String, value: i64) {
706 if (0..=999).contains(&value) {
707 let value = value as u16;
708 result.push(char::from(b'0' + (value / 100) as u8));
709 result.push(char::from(b'0' + ((value / 10) % 10) as u8));
710 result.push(char::from(b'0' + (value % 10) as u8));
711 } else {
712 push_format(result, format_args!("{value:03}"));
713 }
714}
715
716#[inline]
717fn push_zero_padded_4(result: &mut String, value: i64) {
718 if (0..=9999).contains(&value) {
719 let value = value as u16;
720 result.push(char::from(b'0' + (value / 1000) as u8));
721 result.push(char::from(b'0' + ((value / 100) % 10) as u8));
722 result.push(char::from(b'0' + ((value / 10) % 10) as u8));
723 result.push(char::from(b'0' + (value % 10) as u8));
724 } else {
725 push_format(result, format_args!("{value:04}"));
726 }
727}
728
729fn format_strftime(fmt: &str, jdn: f64) -> String {
731 let (y, mo, d) = jdn_to_ymd(jdn);
732 let (h, mi, s, frac) = jdn_to_hms(jdn);
733 let doy = day_of_year(y, mo, d);
734 let jdn_int = (jdn + 0.5).floor() as i64;
736 let dow = (jdn_int + 1) % 7; let mut result = String::with_capacity(fmt.len().saturating_add(8));
739 let bytes = fmt.as_bytes();
740 let mut i = 0;
741 let mut literal_start = 0;
742
743 while i < bytes.len() {
744 if bytes[i] != b'%' || i + 1 >= bytes.len() {
745 i += 1;
746 continue;
747 }
748
749 result.push_str(&fmt[literal_start..i]);
750
751 let spec_suffix = &fmt[i + 1..];
752 let Some(spec) = spec_suffix.chars().next() else {
753 break;
754 };
755 i += 1 + spec.len_utf8();
756 literal_start = i;
757
758 match spec {
759 'd' => push_zero_padded_2(&mut result, d),
760 'e' => push_space_padded_2(&mut result, d),
761 'f' => {
762 let total = s as f64 + frac;
764 push_format(&mut result, format_args!("{total:06.3}"));
765 }
766 'H' => push_zero_padded_2(&mut result, h),
767 'I' => {
768 let h12 = if h == 0 {
770 12
771 } else if h > 12 {
772 h - 12
773 } else {
774 h
775 };
776 push_zero_padded_2(&mut result, h12);
777 }
778 'j' => push_zero_padded_3(&mut result, doy),
779 'J' => {
780 push_format(&mut result, format_args!("{jdn:.15}"));
782 while result.as_bytes().last() == Some(&b'0') {
783 result.pop();
784 }
785 if result.as_bytes().last() == Some(&b'.') {
786 result.pop();
787 }
788 }
789 'k' => {
790 push_space_padded_2(&mut result, h);
792 }
793 'l' => {
794 let h12 = if h == 0 {
796 12
797 } else if h > 12 {
798 h - 12
799 } else {
800 h
801 };
802 push_space_padded_2(&mut result, h12);
803 }
804 'm' => push_zero_padded_2(&mut result, mo),
805 'M' => push_zero_padded_2(&mut result, mi),
806 'p' => {
807 result.push_str(if h < 12 { "AM" } else { "PM" });
808 }
809 'P' => {
810 result.push_str(if h < 12 { "am" } else { "pm" });
811 }
812 'R' => {
813 push_zero_padded_2(&mut result, h);
814 result.push(':');
815 push_zero_padded_2(&mut result, mi);
816 }
817 's' => {
818 let unix = jdn_to_unix(jdn);
819 push_format(&mut result, format_args!("{unix}"));
820 }
821 'S' => push_zero_padded_2(&mut result, s),
822 'T' => {
823 push_zero_padded_2(&mut result, h);
824 result.push(':');
825 push_zero_padded_2(&mut result, mi);
826 result.push(':');
827 push_zero_padded_2(&mut result, s);
828 }
829 'u' => {
830 let u = if dow == 0 { 7 } else { dow };
832 push_format(&mut result, format_args!("{u}"));
833 }
834 'w' => push_format(&mut result, format_args!("{dow}")),
835 'W' => {
836 let w = (doy + 6 - ((dow + 6) % 7)) / 7;
838 push_zero_padded_2(&mut result, w);
839 }
840 'Y' => push_zero_padded_4(&mut result, y),
841 'G' | 'g' | 'V' => {
842 let (iso_y, iso_w) = iso_week(y, mo, d);
844 match spec {
845 'G' => push_zero_padded_4(&mut result, iso_y),
846 'g' => push_zero_padded_2(&mut result, iso_y % 100),
847 'V' => push_zero_padded_2(&mut result, iso_w),
848 _ => unreachable!(),
849 }
850 }
851 '%' => result.push('%'),
852 other => {
853 result.push('%');
854 result.push(other);
855 }
856 }
857 }
858
859 if literal_start < fmt.len() {
860 result.push_str(&fmt[literal_start..]);
861 }
862
863 result
864}
865
866fn iso_week(y: i64, m: i64, d: i64) -> (i64, i64) {
868 let jdn = ymd_to_jdn(y, m, d);
869 let jdn_int = (jdn + 0.5).floor() as i64;
870 let dow = (jdn_int + 1) % 7;
872 let iso_dow = if dow == 0 { 7 } else { dow };
873
874 let thu_jdn = jdn_int + (4 - iso_dow);
876 let (thu_y, _, _) = jdn_to_ymd(thu_jdn as f64);
877
878 let jan4_jdn = (ymd_to_jdn(thu_y, 1, 4) + 0.5).floor() as i64;
880 let jan4_dow = (jan4_jdn + 1) % 7;
881 let jan4_iso_dow = if jan4_dow == 0 { 7 } else { jan4_dow };
882 let week1_start = jan4_jdn - (jan4_iso_dow - 1);
883
884 let week = (thu_jdn - week1_start) / 7 + 1;
885 (thu_y, week)
886}
887
888fn timediff_impl(jdn1: f64, jdn2: f64) -> String {
891 let (sign, start_jdn, end_jdn) = if jdn1 >= jdn2 {
892 ('+', jdn2, jdn1)
893 } else {
894 ('-', jdn1, jdn2)
895 };
896
897 let (start_y, start_mo, start_d) = jdn_to_ymd(start_jdn);
898 let (start_h, start_mi, mut start_s, start_frac) = jdn_to_hms(start_jdn);
899 let mut start_ms = (start_frac * 1000.0).round() as i64;
900 if start_ms >= 1000 {
901 start_ms = 0;
902 start_s += 1;
903 }
904
905 let (end_y, end_mo, end_d) = jdn_to_ymd(end_jdn);
906 let (end_h, end_mi, mut end_s, end_frac) = jdn_to_hms(end_jdn);
907 let mut end_ms = (end_frac * 1000.0).round() as i64;
908 if end_ms >= 1000 {
909 end_ms = 0;
910 end_s += 1;
911 }
912
913 let mut years = end_y - start_y;
914 let mut months = end_mo - start_mo;
915 let mut days = end_d - start_d;
916 let mut hours = end_h - start_h;
917 let mut minutes = end_mi - start_mi;
918 let mut seconds = end_s - start_s;
919 let mut millis = end_ms - start_ms;
920
921 if millis < 0 {
922 millis += 1000;
923 seconds -= 1;
924 }
925 if seconds < 0 {
926 seconds += 60;
927 minutes -= 1;
928 }
929 if minutes < 0 {
930 minutes += 60;
931 hours -= 1;
932 }
933 if hours < 0 {
934 hours += 24;
935 days -= 1;
936 }
937 if days < 0 {
938 months -= 1;
939 let (borrow_y, borrow_mo) = if end_mo == 1 {
940 (end_y - 1, 12)
941 } else {
942 (end_y, end_mo - 1)
943 };
944 days += days_in_month(borrow_y, borrow_mo);
945 }
946 if months < 0 {
947 months += 12;
948 years -= 1;
949 }
950
951 format!(
952 "{sign}{years:04}-{months:02}-{days:02} {hours:02}:{minutes:02}:{seconds:02}.{millis:03}"
953 )
954}
955
956fn parse_args(args: &[SqliteValue]) -> Option<(f64, bool)> {
960 if args.is_empty() || args[0].is_null() {
961 return None;
962 }
963
964 let input = match &args[0] {
965 SqliteValue::Text(s) => parse_timestring(s)?,
966 SqliteValue::Integer(i) => *i as f64,
967 SqliteValue::Float(f) => *f,
968 _ => return None,
969 };
970
971 if args[1..].iter().any(SqliteValue::is_null) {
974 return None;
975 }
976 let modifiers: Vec<String> = args[1..].iter().map(SqliteValue::to_text).collect();
977
978 apply_modifiers(input, &modifiers)
979}
980
981pub struct DateFunc;
984
985impl ScalarFunction for DateFunc {
986 fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
987 match parse_args(args) {
988 Some((jdn, _)) => Ok(SqliteValue::Text(format_date(jdn).into())),
989 None => Ok(SqliteValue::Null),
990 }
991 }
992
993 fn num_args(&self) -> i32 {
994 -1
995 }
996
997 fn name(&self) -> &str {
998 "date"
999 }
1000}
1001
1002pub struct TimeFunc;
1005
1006impl ScalarFunction for TimeFunc {
1007 fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1008 match parse_args(args) {
1009 Some((jdn, subsec)) => Ok(SqliteValue::Text(format_time(jdn, subsec).into())),
1010 None => Ok(SqliteValue::Null),
1011 }
1012 }
1013
1014 fn num_args(&self) -> i32 {
1015 -1
1016 }
1017
1018 fn name(&self) -> &str {
1019 "time"
1020 }
1021}
1022
1023pub struct DateTimeFunc;
1026
1027impl ScalarFunction for DateTimeFunc {
1028 fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1029 match parse_args(args) {
1030 Some((jdn, subsec)) => Ok(SqliteValue::Text(format_datetime(jdn, subsec).into())),
1031 None => Ok(SqliteValue::Null),
1032 }
1033 }
1034
1035 fn num_args(&self) -> i32 {
1036 -1
1037 }
1038
1039 fn name(&self) -> &str {
1040 "datetime"
1041 }
1042}
1043
1044pub struct JuliandayFunc;
1047
1048impl ScalarFunction for JuliandayFunc {
1049 fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1050 match parse_args(args) {
1051 Some((jdn, _)) => Ok(SqliteValue::Float(jdn)),
1052 None => Ok(SqliteValue::Null),
1053 }
1054 }
1055
1056 fn num_args(&self) -> i32 {
1057 -1
1058 }
1059
1060 fn name(&self) -> &str {
1061 "julianday"
1062 }
1063}
1064
1065pub struct UnixepochFunc;
1068
1069impl ScalarFunction for UnixepochFunc {
1070 fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1071 match parse_args(args) {
1072 Some((jdn, _)) => Ok(SqliteValue::Integer(jdn_to_unix(jdn))),
1073 None => Ok(SqliteValue::Null),
1074 }
1075 }
1076
1077 fn num_args(&self) -> i32 {
1078 -1
1079 }
1080
1081 fn name(&self) -> &str {
1082 "unixepoch"
1083 }
1084}
1085
1086pub struct StrftimeFunc;
1089
1090impl ScalarFunction for StrftimeFunc {
1091 fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1092 if args.len() < 2 || args[0].is_null() || args[1].is_null() {
1093 return Ok(SqliteValue::Null);
1094 }
1095 let rest = &args[1..];
1096 match parse_args(rest) {
1097 Some((jdn, _)) => {
1098 let fmt = match args[0].as_text_str() {
1099 Some(text) => Cow::Borrowed(text),
1100 None => Cow::Owned(args[0].to_text()),
1101 };
1102 Ok(SqliteValue::Text(format_strftime(fmt.as_ref(), jdn).into()))
1103 }
1104 None => Ok(SqliteValue::Null),
1105 }
1106 }
1107
1108 fn num_args(&self) -> i32 {
1109 -1
1110 }
1111
1112 fn name(&self) -> &str {
1113 "strftime"
1114 }
1115}
1116
1117pub struct TimediffFunc;
1120
1121impl ScalarFunction for TimediffFunc {
1122 fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1123 if args.len() < 2 || args[0].is_null() || args[1].is_null() {
1124 return Ok(SqliteValue::Null);
1125 }
1126
1127 let jdn1 = match &args[0] {
1128 SqliteValue::Text(s) => parse_timestring(s),
1129 SqliteValue::Integer(i) => Some(*i as f64),
1130 SqliteValue::Float(f) => Some(*f),
1131 _ => None,
1132 };
1133 let jdn2 = match &args[1] {
1134 SqliteValue::Text(s) => parse_timestring(s),
1135 SqliteValue::Integer(i) => Some(*i as f64),
1136 SqliteValue::Float(f) => Some(*f),
1137 _ => None,
1138 };
1139
1140 match (jdn1, jdn2) {
1141 (Some(j1), Some(j2)) => Ok(SqliteValue::Text(timediff_impl(j1, j2).into())),
1142 _ => Ok(SqliteValue::Null),
1143 }
1144 }
1145
1146 fn num_args(&self) -> i32 {
1147 2
1148 }
1149
1150 fn name(&self) -> &str {
1151 "timediff"
1152 }
1153}
1154
1155pub fn register_datetime_builtins(registry: &mut FunctionRegistry) {
1159 registry.register_scalar(DateFunc);
1160 registry.register_scalar(TimeFunc);
1161 registry.register_scalar(DateTimeFunc);
1162 registry.register_scalar(JuliandayFunc);
1163 registry.register_scalar(UnixepochFunc);
1164 registry.register_scalar(StrftimeFunc);
1165 registry.register_scalar(TimediffFunc);
1166}
1167
1168#[cfg(test)]
1171mod tests {
1172 use super::*;
1173
1174 fn text(s: &str) -> SqliteValue {
1175 SqliteValue::Text(s.into())
1176 }
1177
1178 fn int(v: i64) -> SqliteValue {
1179 SqliteValue::Integer(v)
1180 }
1181
1182 fn float(v: f64) -> SqliteValue {
1183 SqliteValue::Float(v)
1184 }
1185
1186 fn null() -> SqliteValue {
1187 SqliteValue::Null
1188 }
1189
1190 fn assert_text(result: &SqliteValue, expected: &str) {
1191 match result {
1192 SqliteValue::Text(s) => assert_eq!(s.as_ref(), expected, "text mismatch"),
1193 other => panic!("expected Text(\"{expected}\"), got {other:?}"),
1194 }
1195 }
1196
1197 #[test]
1200 fn test_date_basic() {
1201 let r = DateFunc.invoke(&[text("2024-03-15 14:30:00")]).unwrap();
1202 assert_text(&r, "2024-03-15");
1203 }
1204
1205 #[test]
1206 fn test_time_basic() {
1207 let r = TimeFunc.invoke(&[text("2024-03-15 14:30:45")]).unwrap();
1208 assert_text(&r, "14:30:45");
1209 }
1210
1211 #[test]
1212 fn test_datetime_basic() {
1213 let r = DateTimeFunc.invoke(&[text("2024-03-15 14:30:00")]).unwrap();
1214 assert_text(&r, "2024-03-15 14:30:00");
1215 }
1216
1217 #[test]
1218 fn test_julianday_basic() {
1219 let r = JuliandayFunc.invoke(&[text("2024-03-15")]).unwrap();
1220 match r {
1221 SqliteValue::Float(jdn) => {
1222 assert!((jdn - 2_460_384.5).abs() < 0.01, "unexpected JDN: {jdn}");
1224 }
1225 other => panic!("expected Float, got {other:?}"),
1226 }
1227 }
1228
1229 fn julianday_float(input: &str) -> f64 {
1237 match JuliandayFunc.invoke(&[text(input)]).unwrap() {
1238 SqliteValue::Float(v) => v,
1239 other => panic!("expected Float, got {other:?} for input {input:?}"),
1240 }
1241 }
1242
1243 fn assert_jdn_close(actual: f64, expected: f64, ctx: &str) {
1244 assert!(
1246 (actual - expected).abs() < 1e-6,
1247 "JDN mismatch for {ctx}: got {actual}, expected {expected}"
1248 );
1249 }
1250
1251 #[test]
1252 fn test_julianday_rfc3339_z_suffix() {
1253 let naive = julianday_float("2026-04-07 16:00:00");
1255 assert_jdn_close(julianday_float("2026-04-07T16:00:00Z"), naive, "T...Z");
1256 assert_jdn_close(
1257 julianday_float("2026-04-07T16:00:00z"),
1258 naive,
1259 "lowercase z",
1260 );
1261 }
1262
1263 #[test]
1264 fn test_julianday_rfc3339_zero_offset() {
1265 let naive = julianday_float("2026-04-07 16:00:00");
1266 assert_jdn_close(
1267 julianday_float("2026-04-07T16:00:00+00:00"),
1268 naive,
1269 "+00:00",
1270 );
1271 assert_jdn_close(
1272 julianday_float("2026-04-07T16:00:00-00:00"),
1273 naive,
1274 "-00:00",
1275 );
1276 }
1277
1278 #[test]
1279 fn test_julianday_rfc3339_positive_offset() {
1280 let base = julianday_float("2026-04-07 16:00:00");
1282 let expected = base - 1.0 / 24.0;
1283 assert_jdn_close(
1284 julianday_float("2026-04-07T16:00:00+01:00"),
1285 expected,
1286 "+01:00",
1287 );
1288 }
1289
1290 #[test]
1291 fn test_julianday_rfc3339_negative_offset() {
1292 let base = julianday_float("2026-04-07 16:00:00");
1294 let expected = base + 5.0 / 24.0;
1295 assert_jdn_close(
1296 julianday_float("2026-04-07T16:00:00-05:00"),
1297 expected,
1298 "-05:00",
1299 );
1300 }
1301
1302 #[test]
1303 fn test_julianday_rfc3339_half_hour_offset() {
1304 let base = julianday_float("2026-04-07 16:00:00");
1306 let expected = base - 5.5 / 24.0;
1307 assert_jdn_close(
1308 julianday_float("2026-04-07T16:00:00+05:30"),
1309 expected,
1310 "+05:30",
1311 );
1312 }
1313
1314 #[test]
1315 fn test_julianday_rfc3339_compact_offsets() {
1316 let base = julianday_float("2026-04-07 16:00:00");
1318 assert_jdn_close(
1319 julianday_float("2026-04-07T16:00:00+0100"),
1320 base - 1.0 / 24.0,
1321 "+0100",
1322 );
1323 assert_jdn_close(
1324 julianday_float("2026-04-07T16:00:00-0530"),
1325 base + 5.5 / 24.0,
1326 "-0530",
1327 );
1328 assert_jdn_close(
1329 julianday_float("2026-04-07T16:00:00+09"),
1330 base - 9.0 / 24.0,
1331 "+09",
1332 );
1333 }
1334
1335 #[test]
1336 fn test_julianday_rfc3339_fractional_seconds_with_tz() {
1337 let base = julianday_float("2026-04-07 16:00:00.500");
1339 assert_jdn_close(
1340 julianday_float("2026-04-07T16:00:00.500Z"),
1341 base,
1342 "fractional + Z",
1343 );
1344 assert_jdn_close(
1345 julianday_float("2026-04-07T16:00:00.500+01:00"),
1346 base - 1.0 / 24.0,
1347 "fractional + +01:00",
1348 );
1349 }
1350
1351 #[test]
1352 fn test_date_and_time_rfc3339_round_trip() {
1353 assert_text(
1356 &DateFunc
1357 .invoke(&[text("2026-04-07T16:00:00+05:00")])
1358 .unwrap(),
1359 "2026-04-07",
1361 );
1362 assert_text(
1363 &TimeFunc
1364 .invoke(&[text("2026-04-07T16:00:00+05:00")])
1365 .unwrap(),
1366 "11:00:00",
1367 );
1368 assert_text(
1369 &DateTimeFunc
1370 .invoke(&[text("2026-04-07T16:00:00+05:00")])
1371 .unwrap(),
1372 "2026-04-07 11:00:00",
1373 );
1374 }
1375
1376 #[test]
1377 fn test_julianday_rfc3339_invalid_offsets_return_null() {
1378 for bad in &[
1380 "2026-04-07T16:00:00+25:00", "2026-04-07T16:00:00+01:99", "2026-04-07T16:00:00+1", "2026-04-07T16:00:00+123", ] {
1385 let result = JuliandayFunc.invoke(&[text(bad)]).unwrap();
1386 assert_eq!(
1387 result,
1388 SqliteValue::Null,
1389 "expected NULL for malformed offset {bad:?}, got {result:?}"
1390 );
1391 }
1392 }
1393
1394 #[test]
1395 fn test_julianday_rejects_malformed_time_fields() {
1396 for bad in &[
1400 "+01:00", "-05:30", "+12:30:00", "12:+30:00", "12:30:+45", "12:30:+45.123", "0:00:00", "12:0:00", "12:30:0", "123:00:00", "12:345:00", ] {
1412 let result = JuliandayFunc.invoke(&[text(bad)]).unwrap();
1413 assert_eq!(
1414 result,
1415 SqliteValue::Null,
1416 "expected NULL for signed time field {bad:?}, got {result:?}"
1417 );
1418 }
1419 }
1420
1421 #[test]
1422 fn test_unixepoch_basic() {
1423 let r = UnixepochFunc
1424 .invoke(&[text("1970-01-01 00:00:00")])
1425 .unwrap();
1426 assert_eq!(r, int(0));
1427 }
1428
1429 #[test]
1430 fn test_unixepoch_known_date() {
1431 let r = UnixepochFunc
1432 .invoke(&[text("2024-01-01 00:00:00")])
1433 .unwrap();
1434 assert_eq!(r, int(1_704_067_200));
1436 }
1437
1438 #[test]
1441 fn test_modifier_days() {
1442 let r = DateFunc
1443 .invoke(&[text("2024-01-15"), text("+10 days")])
1444 .unwrap();
1445 assert_text(&r, "2024-01-25");
1446 }
1447
1448 #[test]
1449 fn test_modifier_months() {
1450 let r = DateFunc
1453 .invoke(&[text("2024-01-31"), text("+1 months")])
1454 .unwrap();
1455 assert_text(&r, "2024-03-02");
1456 }
1457
1458 #[test]
1459 fn test_modifier_years() {
1460 let r = DateFunc
1463 .invoke(&[text("2024-02-29"), text("+1 years")])
1464 .unwrap();
1465 assert_text(&r, "2025-03-01");
1466 }
1467
1468 #[test]
1469 fn test_modifier_hours() {
1470 let r = DateTimeFunc
1471 .invoke(&[text("2024-01-01 23:00:00"), text("+2 hours")])
1472 .unwrap();
1473 assert_text(&r, "2024-01-02 01:00:00");
1474 }
1475
1476 #[test]
1477 fn test_modifier_start_of_month() {
1478 let r = DateFunc
1479 .invoke(&[text("2024-03-15"), text("start of month")])
1480 .unwrap();
1481 assert_text(&r, "2024-03-01");
1482 }
1483
1484 #[test]
1485 fn test_modifier_start_of_year() {
1486 let r = DateFunc
1487 .invoke(&[text("2024-06-15"), text("start of year")])
1488 .unwrap();
1489 assert_text(&r, "2024-01-01");
1490 }
1491
1492 #[test]
1493 fn test_modifier_start_of_day() {
1494 let r = DateTimeFunc
1495 .invoke(&[text("2024-03-15 14:30:00"), text("start of day")])
1496 .unwrap();
1497 assert_text(&r, "2024-03-15 00:00:00");
1498 }
1499
1500 #[test]
1501 fn test_modifier_unixepoch() {
1502 let r = DateTimeFunc.invoke(&[int(0), text("unixepoch")]).unwrap();
1503 assert_text(&r, "1970-01-01 00:00:00");
1504 }
1505
1506 #[test]
1507 fn test_modifier_weekday() {
1508 let r = DateFunc
1510 .invoke(&[text("2024-03-15"), text("weekday 0")])
1511 .unwrap();
1512 assert_text(&r, "2024-03-17");
1513 }
1514
1515 #[test]
1516 fn test_modifier_auto_unixepoch() {
1517 let ts = int(1_710_531_045);
1518 let r = DateTimeFunc.invoke(&[ts.clone(), text("auto")]).unwrap();
1519 let expected = DateTimeFunc.invoke(&[ts, text("unixepoch")]).unwrap();
1520 assert_eq!(
1521 r, expected,
1522 "auto and unixepoch should agree for unix-like values"
1523 );
1524 }
1525
1526 #[test]
1527 fn test_modifier_auto_julian_day() {
1528 let r = DateFunc
1529 .invoke(&[float(2_460_384.5), text("auto")])
1530 .unwrap();
1531 assert_text(&r, "2024-03-15");
1532 }
1533
1534 #[test]
1535 fn test_modifier_localtime_utc_roundtrip() {
1536 let r = DateTimeFunc
1538 .invoke(&[text("2024-03-15 14:30:45"), text("localtime"), text("utc")])
1539 .unwrap();
1540 assert_text(&r, "2024-03-15 14:30:45");
1541 }
1542
1543 #[test]
1544 fn test_modifier_localtime_shifts_value() {
1545 let offset = utc_offset_for_utc_jdn(ymdhms_to_jdn(2024, 3, 15, 12, 0, 0, 0.0));
1547 if offset != 0 {
1548 let r = DateTimeFunc
1549 .invoke(&[text("2024-03-15 12:00:00"), text("localtime")])
1550 .unwrap();
1551 let shifted = match &r {
1553 SqliteValue::Text(s) => s.clone(),
1554 _ => panic!("expected text"),
1555 };
1556 assert_ne!(&*shifted, "2024-03-15 12:00:00");
1557 }
1558 }
1559
1560 #[test]
1561 fn test_modifier_auto_out_of_range_returns_null() {
1562 let r = DateTimeFunc.invoke(&[float(1.0e20), text("auto")]).unwrap();
1563 assert_eq!(r, SqliteValue::Null);
1564 }
1565
1566 #[test]
1567 fn test_modifier_order_matters() {
1568 let r1 = DateFunc
1570 .invoke(&[text("2024-03-15"), text("start of month"), text("+1 days")])
1571 .unwrap();
1572 assert_text(&r1, "2024-03-02");
1573
1574 let r2 = DateFunc
1576 .invoke(&[text("2024-03-15"), text("+1 days"), text("start of month")])
1577 .unwrap();
1578 assert_text(&r2, "2024-03-01");
1579 }
1580
1581 #[test]
1582 fn test_modifier_weekday_same_day_is_noop() {
1583 let r = DateFunc
1585 .invoke(&[text("2024-03-17"), text("weekday 0")])
1586 .unwrap();
1587 assert_text(&r, "2024-03-17");
1588 }
1589
1590 #[test]
1593 fn test_bare_time_defaults() {
1594 let r = DateFunc.invoke(&[text("12:30:00")]).unwrap();
1595 assert_text(&r, "2000-01-01");
1596 }
1597
1598 #[test]
1599 fn test_t_separator() {
1600 let r = DateTimeFunc.invoke(&[text("2024-03-15T14:30:00")]).unwrap();
1601 assert_text(&r, "2024-03-15 14:30:00");
1602 }
1603
1604 #[test]
1605 fn test_julian_day_input() {
1606 let r = DateFunc.invoke(&[float(2_460_384.5)]).unwrap();
1608 assert_text(&r, "2024-03-15");
1609 }
1610
1611 #[test]
1612 fn test_null_input() {
1613 assert_eq!(DateFunc.invoke(&[null()]).unwrap(), SqliteValue::Null);
1614 }
1615
1616 #[test]
1617 fn test_invalid_input() {
1618 assert_eq!(
1619 DateFunc.invoke(&[text("not-a-date")]).unwrap(),
1620 SqliteValue::Null
1621 );
1622 }
1623
1624 #[test]
1625 fn test_negative_time_component_invalid() {
1626 let r = TimeFunc.invoke(&[text("-01:00")]).unwrap();
1627 assert_eq!(r, SqliteValue::Null);
1628 }
1629
1630 #[test]
1633 fn test_leap_year() {
1634 let r = DateFunc
1635 .invoke(&[text("2024-02-28"), text("+1 days")])
1636 .unwrap();
1637 assert_text(&r, "2024-02-29");
1638 }
1639
1640 #[test]
1641 fn test_non_leap_year() {
1642 let r = DateFunc
1643 .invoke(&[text("2023-02-28"), text("+1 days")])
1644 .unwrap();
1645 assert_text(&r, "2023-03-01");
1646 }
1647
1648 #[test]
1651 fn test_strftime_basic() {
1652 let r = StrftimeFunc
1653 .invoke(&[text("%Y-%m-%d"), text("2024-03-15")])
1654 .unwrap();
1655 assert_text(&r, "2024-03-15");
1656 }
1657
1658 #[test]
1659 fn test_strftime_time_specifiers() {
1660 let r = StrftimeFunc
1661 .invoke(&[text("%H:%M:%S"), text("2024-03-15 14:30:45")])
1662 .unwrap();
1663 assert_text(&r, "14:30:45");
1664 }
1665
1666 #[test]
1667 fn test_strftime_unix_seconds() {
1668 let r = StrftimeFunc
1669 .invoke(&[text("%s"), text("1970-01-01 00:00:00")])
1670 .unwrap();
1671 assert_text(&r, "0");
1672 }
1673
1674 #[test]
1675 fn test_strftime_day_of_year() {
1676 let r = StrftimeFunc
1677 .invoke(&[text("%j"), text("2024-03-15")])
1678 .unwrap();
1679 assert_text(&r, "075");
1681 }
1682
1683 #[test]
1684 fn test_strftime_day_of_week() {
1685 let r = StrftimeFunc
1687 .invoke(&[text("%w"), text("2024-03-15")])
1688 .unwrap();
1689 assert_text(&r, "5");
1690
1691 let r = StrftimeFunc
1692 .invoke(&[text("%u"), text("2024-03-15")])
1693 .unwrap();
1694 assert_text(&r, "5");
1695 }
1696
1697 #[test]
1698 fn test_strftime_12hour() {
1699 let r = StrftimeFunc
1700 .invoke(&[text("%I %p"), text("2024-03-15 14:30:00")])
1701 .unwrap();
1702 assert_text(&r, "02 PM");
1703
1704 let r = StrftimeFunc
1705 .invoke(&[text("%I %P"), text("2024-03-15 09:30:00")])
1706 .unwrap();
1707 assert_text(&r, "09 am");
1708 }
1709
1710 #[test]
1711 fn test_strftime_all_specifiers_presence() {
1712 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|%%";
1713 let r = StrftimeFunc
1714 .invoke(&[text(fmt), text("2024-03-15 14:30:45.123")])
1715 .unwrap();
1716
1717 let s = match r {
1718 SqliteValue::Text(v) => v,
1719 other => panic!("expected Text, got {other:?}"),
1720 };
1721 let parts: Vec<&str> = s.split('|').collect();
1722 assert_eq!(parts.len(), 25, "unexpected specifier output: {s}");
1723 assert_eq!(parts[0], "15"); assert_eq!(parts[1], "15"); assert_eq!(parts[2], "45.123"); assert_eq!(parts[3], "14"); assert_eq!(parts[4], "02"); assert_eq!(parts[5], "075"); assert!(
1730 parts[6].parse::<f64>().is_ok(),
1731 "expected numeric %J output, got {}",
1732 parts[6]
1733 );
1734 assert_eq!(parts[7], "14"); assert_eq!(parts[8], " 2"); assert_eq!(parts[9], "03"); assert_eq!(parts[10], "30"); assert_eq!(parts[11], "PM"); assert_eq!(parts[12], "pm"); assert_eq!(parts[13], "14:30"); assert!(
1742 parts[14].parse::<i64>().is_ok(),
1743 "expected numeric %s output, got {}",
1744 parts[14]
1745 );
1746 assert_eq!(parts[15], "45"); assert_eq!(parts[16], "14:30:45"); assert_eq!(parts[17], "5"); assert_eq!(parts[18], "5"); assert_eq!(parts[19], "11"); assert_eq!(parts[20], "2024"); assert_eq!(parts[21], "24"); assert_eq!(parts[22], "11"); assert_eq!(parts[23], "2024"); assert_eq!(parts[24], "%"); }
1757
1758 #[test]
1759 fn test_strftime_null() {
1760 assert_eq!(
1761 StrftimeFunc.invoke(&[null(), text("2024-01-01")]).unwrap(),
1762 SqliteValue::Null
1763 );
1764 assert_eq!(
1765 StrftimeFunc.invoke(&[text("%Y"), null()]).unwrap(),
1766 SqliteValue::Null
1767 );
1768 }
1769
1770 #[test]
1771 #[ignore = "perf-only benchmark"]
1772 fn perf_strftime_timestamp_rows() {
1773 use std::hint::black_box;
1774 use std::time::Instant;
1775
1776 const ROWS: usize = 200_000;
1777 const REPEATS: usize = 5;
1778 const FORMAT: &str = "%Y-%m-%d %H:%M:%S";
1779 const INPUT: &str = "2024-03-15 14:30:45";
1780
1781 let func = StrftimeFunc;
1782 let fmt = text(FORMAT);
1783 let input = text(INPUT);
1784 let mut best_ns = u128::MAX;
1785 let mut output_len = 0usize;
1786
1787 for _ in 0..REPEATS {
1788 let started = Instant::now();
1789 for _ in 0..ROWS {
1790 let result = black_box(
1791 func.invoke(black_box(&[fmt.clone(), input.clone()]))
1792 .expect("strftime benchmark invocation must succeed"),
1793 );
1794 output_len = match result {
1795 SqliteValue::Text(text) => text.len(),
1796 SqliteValue::Null
1797 | SqliteValue::Integer(_)
1798 | SqliteValue::Float(_)
1799 | SqliteValue::Blob(_) => 0,
1800 };
1801 }
1802 let elapsed_ns = started.elapsed().as_nanos();
1803 if elapsed_ns < best_ns {
1804 best_ns = elapsed_ns;
1805 }
1806 }
1807
1808 println!(
1809 "strftime_timestamp_rows rows={ROWS} repeats={REPEATS} best_ns={best_ns} output_len={output_len}"
1810 );
1811 }
1812
1813 #[test]
1816 fn test_timediff_basic() {
1817 let r = TimediffFunc
1818 .invoke(&[text("2024-03-15"), text("2024-03-10")])
1819 .unwrap();
1820 assert_text(&r, "+0000-00-05 00:00:00.000");
1821 }
1822
1823 #[test]
1824 fn test_timediff_negative() {
1825 let r = TimediffFunc
1826 .invoke(&[text("2024-03-10"), text("2024-03-15")])
1827 .unwrap();
1828 assert_text(&r, "-0000-00-05 00:00:00.000");
1829 }
1830
1831 #[test]
1832 fn test_timediff_year_boundary() {
1833 let r = TimediffFunc
1834 .invoke(&[text("2024-01-01 01:00:00"), text("2023-12-31 23:00:00")])
1835 .unwrap();
1836 assert_text(&r, "+0000-00-00 02:00:00.000");
1837 }
1838
1839 #[test]
1842 fn test_modifier_subsec() {
1843 let r = TimeFunc
1844 .invoke(&[text("2024-01-01 12:00:00.123"), text("subsec")])
1845 .unwrap();
1846 match &r {
1847 SqliteValue::Text(s) => assert!(
1848 s.contains('.'),
1849 "expected fractional seconds with subsec: {s}"
1850 ),
1851 other => panic!("expected Text, got {other:?}"),
1852 }
1853 }
1854
1855 #[test]
1858 fn test_register_datetime_builtins_all_present() {
1859 let mut reg = FunctionRegistry::new();
1860 register_datetime_builtins(&mut reg);
1861
1862 let expected = [
1863 "date",
1864 "time",
1865 "datetime",
1866 "julianday",
1867 "unixepoch",
1868 "strftime",
1869 "timediff",
1870 ];
1871
1872 for name in expected {
1873 assert!(
1874 reg.find_scalar(name, 1).is_some() || reg.find_scalar(name, 2).is_some(),
1875 "datetime function '{name}' not registered"
1876 );
1877 }
1878 }
1879
1880 #[test]
1883 fn test_modifier_year_overflow() {
1884 let huge = i64::MAX;
1887 let modifier = format!("+{huge} years");
1888 let r = DateFunc.invoke(&[text("2000-01-01"), text(&modifier)]);
1889 assert_eq!(r.unwrap(), SqliteValue::Null);
1892 }
1893
1894 #[test]
1895 fn test_jdn_roundtrip() {
1896 let dates = [
1898 (2024, 3, 15),
1899 (2000, 1, 1),
1900 (1970, 1, 1),
1901 (2024, 2, 29),
1902 (1900, 1, 1),
1903 (2099, 12, 31),
1904 ];
1905 for (y, m, d) in dates {
1906 let jdn = ymd_to_jdn(y, m, d);
1907 let (y2, m2, d2) = jdn_to_ymd(jdn);
1908 assert_eq!(
1909 (y, m, d),
1910 (y2, m2, d2),
1911 "roundtrip failed for {y}-{m}-{d} (JDN={jdn})"
1912 );
1913 }
1914 }
1915
1916 #[test]
1917 fn test_unix_epoch_roundtrip() {
1918 let jdn = ymd_to_jdn(1970, 1, 1);
1919 let unix = jdn_to_unix(jdn);
1920 assert_eq!(unix, 0, "Unix epoch should be 0");
1921
1922 let jdn2 = unix_to_jdn(0.0);
1923 assert!((jdn2 - UNIX_EPOCH_JDN).abs() < 1e-10, "roundtrip failed");
1924 }
1925}