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::{SmallText, SqliteValue};
41
42use crate::{FunctionRegistry, ScalarFunction};
43
44#[cfg(not(target_arch = "wasm32"))]
57fn utc_offset_for_local_datetime(y: i32, mo: u32, d: u32, h: u32, mi: u32, s: u32) -> i64 {
58 use chrono::{Local, NaiveDate, NaiveDateTime, NaiveTime, TimeZone};
59 let date = NaiveDate::from_ymd_opt(y, mo, d).unwrap_or_default();
60 let time = NaiveTime::from_hms_opt(h, mi, s).unwrap_or_default();
61 let naive = NaiveDateTime::new(date, time);
62 match Local.from_local_datetime(&naive).earliest() {
63 Some(dt) => dt.offset().local_minus_utc() as i64,
64 None => 0, }
66}
67
68#[cfg(target_arch = "wasm32")]
69fn utc_offset_for_local_datetime(_y: i32, _mo: u32, _d: u32, _h: u32, _mi: u32, _s: u32) -> i64 {
70 0
71}
72
73#[cfg(not(target_arch = "wasm32"))]
80fn utc_offset_for_utc_datetime(y: i32, mo: u32, d: u32, h: u32, mi: u32, s: u32) -> i64 {
81 use chrono::{Local, NaiveDate, NaiveDateTime, NaiveTime, TimeZone, Utc};
82 let date = NaiveDate::from_ymd_opt(y, mo, d).unwrap_or_default();
83 let time = NaiveTime::from_hms_opt(h, mi, s).unwrap_or_default();
84 let naive = NaiveDateTime::new(date, time);
85 let utc_dt = Utc.from_utc_datetime(&naive);
86 let local_dt = utc_dt.with_timezone(&Local);
87 local_dt.offset().local_minus_utc() as i64
88}
89
90#[cfg(target_arch = "wasm32")]
91fn utc_offset_for_utc_datetime(_y: i32, _mo: u32, _d: u32, _h: u32, _mi: u32, _s: u32) -> i64 {
92 0
93}
94
95fn utc_offset_for_utc_jdn(jdn: f64) -> i64 {
97 let (y, mo, d) = jdn_to_ymd(jdn);
98 let (h, mi, s, _frac) = jdn_to_hms(jdn);
99 utc_offset_for_utc_datetime(y as i32, mo as u32, d as u32, h as u32, mi as u32, s as u32)
100}
101
102fn utc_offset_for_local_jdn(jdn: f64) -> i64 {
104 let (y, mo, d) = jdn_to_ymd(jdn);
105 let (h, mi, s, _frac) = jdn_to_hms(jdn);
106 utc_offset_for_local_datetime(y as i32, mo as u32, d as u32, h as u32, mi as u32, s as u32)
107}
108
109fn ymd_to_jdn(y: i64, m: i64, d: i64) -> f64 {
115 let (y, m) = if m <= 2 {
116 (y.saturating_sub(1), m.saturating_add(12))
117 } else {
118 (y, m)
119 };
120 let a = y / 100;
121 let b = 2_i64.saturating_sub(a).saturating_add(a / 4);
122 (365.25 * y.saturating_add(4716) as f64).floor()
123 + (30.6001 * m.saturating_add(1) as f64).floor()
124 + d as f64
125 + b as f64
126 - 1524.5
127}
128
129fn jdn_to_ymd(jdn: f64) -> (i64, i64, i64) {
136 let z = (jdn + 0.5).floor() as i64;
142 let alpha = ((z as f64 - 1_867_216.25) / 36524.25) as i64;
143 let a = z
144 .saturating_add(1)
145 .saturating_add(alpha)
146 .saturating_sub(alpha / 4);
147 let b = a.saturating_add(1524);
148 let c = ((b as f64 - 122.1) / 365.25) as i64;
149 let d = (365.25 * c as f64) as i64;
150 let e = ((b.saturating_sub(d)) as f64 / 30.6001) as i64;
151
152 let day = b
153 .saturating_sub(d)
154 .saturating_sub((30.6001 * e as f64) as i64);
155 let month = if e < 14 {
156 e.saturating_sub(1)
157 } else {
158 e.saturating_sub(13)
159 };
160 let year = if month > 2 {
161 c.saturating_sub(4716)
162 } else {
163 c.saturating_sub(4715)
164 };
165 (year, month, day)
166}
167
168fn jdn_to_hms(jdn: f64) -> (i64, i64, i64, f64) {
170 let frac = jdn + 0.5 - (jdn + 0.5).floor();
171 let total_ms = (frac * 86_400_000.0).round() as i64;
173 let h = total_ms / 3_600_000;
174 let rem = total_ms % 3_600_000;
175 let m = rem / 60_000;
176 let rem = rem % 60_000;
177 let s = rem / 1000;
178 let ms_frac = (rem % 1000) as f64 / 1000.0;
179 (h, m, s, ms_frac)
180}
181
182fn ymdhms_to_jdn(y: i64, mo: i64, d: i64, h: i64, mi: i64, s: i64, frac: f64) -> f64 {
184 ymd_to_jdn(y, mo, d) + (h as f64 * 3600.0 + mi as f64 * 60.0 + s as f64 + frac) / 86400.0
185}
186
187const UNIX_EPOCH_JDN: f64 = 2_440_587.5;
189const AUTO_JDN_MAX: f64 = 5_373_484.499_999;
191const AUTO_UNIX_MIN: f64 = -210_866_760_000.0;
193const AUTO_UNIX_MAX: f64 = 253_402_300_799.0;
194
195fn jdn_to_unix(jdn: f64) -> i64 {
196 ((jdn - UNIX_EPOCH_JDN) * 86400.0).round() as i64
197}
198
199fn unix_to_jdn(ts: f64) -> f64 {
200 ts / 86400.0 + UNIX_EPOCH_JDN
201}
202
203fn is_leap_year(y: i64) -> bool {
204 (y % 4 == 0 && y % 100 != 0) || y % 400 == 0
205}
206
207fn days_in_month(y: i64, m: i64) -> i64 {
208 match m {
209 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
210 4 | 6 | 9 | 11 => 30,
211 2 => {
212 if is_leap_year(y) {
213 29
214 } else {
215 28
216 }
217 }
218 _ => 30,
219 }
220}
221
222fn day_of_year(y: i64, m: i64, d: i64) -> i64 {
223 let mut doy = d;
224 for mo in 1..m {
225 doy = doy.saturating_add(days_in_month(y, mo));
226 }
227 doy
228}
229
230fn parse_timestring(s: &str) -> Option<f64> {
234 let s = s.trim();
235
236 if s.eq_ignore_ascii_case("now") {
242 use std::time::{SystemTime, UNIX_EPOCH};
243 let secs = SystemTime::now()
244 .duration_since(UNIX_EPOCH)
245 .unwrap_or_default()
246 .as_secs_f64();
247 return Some(2_440_587.5 + secs / 86_400.0);
249 }
250
251 if let Ok(jdn) = s.parse::<f64>() {
254 if jdn >= 0.0 && jdn.is_finite() {
255 return Some(jdn);
256 }
257 }
258
259 parse_iso8601(s)
261}
262
263fn parse_iso8601(s: &str) -> Option<f64> {
264 let bytes = s.as_bytes();
272 let len = bytes.len();
273
274 if len >= 10 && bytes[4] == b'-' && bytes[7] == b'-' {
276 let y = s[0..4].parse::<i64>().ok()?;
277 let m = s[5..7].parse::<i64>().ok()?;
278 let d = s[8..10].parse::<i64>().ok()?;
279
280 if m < 1 || m > 12 || d < 1 || d > 31 {
281 return None;
282 }
283
284 if len == 10 {
285 return Some(ymd_to_jdn(y, m, d));
286 }
287
288 if len > 10 && (bytes[10] == b' ' || bytes[10] == b'T') {
290 let time_part = &s[11..];
291 let (h, mi, sec, frac, tz_offset_min) = parse_time_part_with_tz(time_part)?;
292 let jdn = ymdhms_to_jdn(y, m, d, h, mi, sec, frac);
293 return Some(jdn - (tz_offset_min as f64) / 1440.0);
296 }
297 return None;
298 }
299
300 if len >= 5 && bytes[2] == b':' {
302 let (h, mi, sec, frac, tz_offset_min) = parse_time_part_with_tz(s)?;
303 let jdn = ymdhms_to_jdn(2000, 1, 1, h, mi, sec, frac);
304 return Some(jdn - (tz_offset_min as f64) / 1440.0);
305 }
306
307 None
308}
309
310fn split_tz_suffix(s: &str) -> Option<(&str, i64)> {
318 if let Some(stripped) = s.strip_suffix('Z').or_else(|| s.strip_suffix('z')) {
320 return Some((stripped, 0));
321 }
322
323 let bytes = s.as_bytes();
332 for width in [6usize, 5, 3] {
334 if bytes.len() < width + 1 {
335 continue;
336 }
337 let split_at = bytes.len() - width;
338 let sign_byte = bytes[split_at];
339 if sign_byte != b'+' && sign_byte != b'-' {
340 continue;
341 }
342 let tz_part = &s[split_at..];
343 if let Some(offset) = parse_tz_offset(tz_part) {
344 return Some((&s[..split_at], offset));
345 }
346 }
347
348 Some((s, 0))
350}
351
352fn parse_tz_offset(tz: &str) -> Option<i64> {
355 let bytes = tz.as_bytes();
356 if bytes.is_empty() {
357 return None;
358 }
359 let sign: i64 = match bytes[0] {
360 b'+' => 1,
361 b'-' => -1,
362 _ => return None,
363 };
364 let rest = &tz[1..];
365 let (hours, minutes) = match rest.len() {
366 5 if rest.as_bytes()[2] == b':' => (
368 rest[0..2].parse::<i64>().ok()?,
369 rest[3..5].parse::<i64>().ok()?,
370 ),
371 4 => (
373 rest[0..2].parse::<i64>().ok()?,
374 rest[2..4].parse::<i64>().ok()?,
375 ),
376 2 => (rest.parse::<i64>().ok()?, 0),
378 _ => return None,
379 };
380 if !(0..=23).contains(&hours) || !(0..=59).contains(&minutes) {
381 return None;
382 }
383 Some(sign * (hours * 60 + minutes))
384}
385
386fn parse_time_part_with_tz(s: &str) -> Option<(i64, i64, i64, f64, i64)> {
389 let (time_only, tz_offset_min) = split_tz_suffix(s)?;
390 let (h, mi, sec, frac) = parse_time_part(time_only)?;
391 Some((h, mi, sec, frac, tz_offset_min))
392}
393
394fn parse_time_part(s: &str) -> Option<(i64, i64, i64, f64)> {
396 let [h_tens, h_ones, b':', mi_tens, mi_ones, rest @ ..] = s.as_bytes() else {
397 return None;
398 };
399 let h = parse_two_ascii_digits(*h_tens, *h_ones)?;
404 let mi = parse_two_ascii_digits(*mi_tens, *mi_ones)?;
405 if !(0..=23).contains(&h) || !(0..=59).contains(&mi) {
406 return None;
407 }
408
409 match rest {
414 [] => Some((h, mi, 0, 0.0)),
415 [b':', sec_tens, sec_ones] => {
416 let sec = parse_two_ascii_digits(*sec_tens, *sec_ones)?;
417 if !(0..=59).contains(&sec) {
418 return None;
419 }
420 Some((h, mi, sec, 0.0))
421 }
422 [b':', sec_tens, sec_ones, b'.', ..] => {
423 let sec = parse_two_ascii_digits(*sec_tens, *sec_ones)?;
424 if !(0..=59).contains(&sec) {
425 return None;
426 }
427 let frac = s.get(8..)?.parse::<f64>().ok()?;
428 Some((h, mi, sec, frac))
429 }
430 _ => None,
431 }
432}
433
434#[inline]
435fn parse_two_ascii_digits(tens: u8, ones: u8) -> Option<i64> {
436 if tens.is_ascii_digit() && ones.is_ascii_digit() {
437 Some(i64::from((tens - b'0') * 10 + (ones - b'0')))
438 } else {
439 None
440 }
441}
442
443fn apply_modifier(jdn: f64, modifier: &str) -> Option<f64> {
447 let m = modifier.trim().to_ascii_lowercase();
448
449 if m == "start of month" {
451 let (y, mo, _d) = jdn_to_ymd(jdn);
452 return Some(ymd_to_jdn(y, mo, 1));
453 }
454 if m == "start of year" {
455 let (y, _mo, _d) = jdn_to_ymd(jdn);
456 return Some(ymd_to_jdn(y, 1, 1));
457 }
458 if m == "start of day" {
459 let (y, mo, d) = jdn_to_ymd(jdn);
460 return Some(ymd_to_jdn(y, mo, d));
461 }
462
463 if m == "unixepoch" {
465 return Some(unix_to_jdn(jdn));
466 }
467
468 if m == "julianday" {
471 return Some(jdn);
472 }
473
474 if m == "auto" {
479 if (0.0..=AUTO_JDN_MAX).contains(&jdn) {
480 return Some(jdn);
481 }
482 if (AUTO_UNIX_MIN..=AUTO_UNIX_MAX).contains(&jdn) {
483 return Some(unix_to_jdn(jdn));
484 }
485 return None;
486 }
487
488 if m == "localtime" {
491 let offset = utc_offset_for_utc_jdn(jdn);
492 return Some(jdn + offset as f64 / 86400.0);
493 }
494 if m == "utc" {
497 let offset = utc_offset_for_local_jdn(jdn);
498 return Some(jdn - offset as f64 / 86400.0);
499 }
500
501 if m == "subsec" || m == "subsecond" {
504 return Some(jdn);
505 }
506
507 if let Some(rest) = m.strip_prefix("weekday ") {
509 let wd = rest.trim().parse::<i64>().ok()?;
510 if !(0..=6).contains(&wd) {
511 return None;
512 }
513 let current_jdn_int = (jdn + 0.5).floor() as i64;
515 let current_wd = (current_jdn_int + 1) % 7; let mut diff = wd - current_wd;
517 if diff < 0 {
518 diff += 7;
519 }
520 return Some(jdn + diff as f64);
522 }
523
524 parse_arithmetic_modifier(&m).map(|delta| jdn + delta)
526}
527
528fn parse_arithmetic_modifier(m: &str) -> Option<f64> {
530 let (sign, rest) = if let Some(r) = m.strip_prefix('+') {
531 (1.0, r.trim())
532 } else {
533 let r = m.strip_prefix('-')?;
534 (-1.0, r.trim())
535 };
536
537 let mut parts = rest.splitn(2, ' ');
538 let num_str = parts.next()?;
539 let unit = parts.next()?.trim();
540
541 let num = num_str.parse::<f64>().ok().filter(|f| f.is_finite())?;
543 let delta = num * sign;
544
545 match unit.trim_end_matches('s') {
546 "day" => Some(delta),
547 "hour" => Some(delta / 24.0),
548 "minute" => Some(delta / 1440.0),
549 "second" => Some(delta / 86400.0),
550 "month" => Some(apply_month_delta(delta)),
551 "year" => Some(apply_month_delta(delta * 12.0)),
552 _ => None,
553 }
554}
555
556fn apply_month_delta(months: f64) -> f64 {
561 months * 30.436875
563}
564
565fn compute_floor(y: i64, m: i64, d: i64) -> i64 {
569 if d <= 28 {
570 0
571 } else if ((1_i64 << m) & 0x15aa) != 0 {
572 0
574 } else if m != 2 {
575 i64::from(d == 31)
577 } else if y % 4 != 0 || (y % 100 == 0 && y % 400 != 0) {
578 d - 28 } else {
580 d - 29 }
582}
583
584fn apply_modifiers(jdn: f64, modifiers: &[String]) -> Option<(f64, bool)> {
586 let mut j = jdn;
587 let mut subsec = false;
588 let mut n_floor: i64 = 0;
591 for m in modifiers {
592 let m_lower = m.trim().to_ascii_lowercase();
593 if m_lower == "subsec" || m_lower == "subsecond" {
594 subsec = true;
595 continue;
596 }
597 if m_lower == "ceiling" {
602 n_floor = 0;
603 continue;
604 }
605 if m_lower == "floor" {
606 j -= n_floor as f64;
607 n_floor = 0;
608 continue;
609 }
610 if is_month_year_modifier(&m_lower) {
616 match apply_month_year_exact(j, &m_lower) {
617 Ok(Some((new_jdn, nf))) => {
618 j = new_jdn;
619 n_floor = nf;
620 continue;
621 }
622 Ok(None) => return None,
623 Err(()) => {
624 }
626 }
627 }
628 n_floor = 0;
630 j = apply_modifier(j, m)?;
631 }
632 Some((j, subsec))
633}
634
635fn is_month_year_modifier(m: &str) -> bool {
636 (m.contains("month") || m.contains("year")) && (m.starts_with('+') || m.starts_with('-'))
637}
638
639fn apply_month_year_exact(jdn: f64, m: &str) -> std::result::Result<Option<(f64, i64)>, ()> {
645 let (sign, rest) = if let Some(r) = m.strip_prefix('+') {
646 (1_i64, r.trim())
647 } else if let Some(r) = m.strip_prefix('-') {
648 (-1_i64, r.trim())
649 } else {
650 return Err(());
651 };
652
653 let mut parts = rest.splitn(2, ' ');
654 let num_str = parts.next().ok_or(())?;
655 let unit = parts.next().ok_or(())?.trim();
656
657 let num = if let Ok(n) = num_str.parse::<i64>() {
659 n
660 } else if let Ok(f) = num_str.parse::<f64>() {
661 if f.fract() == 0.0 && f >= i64::MIN as f64 && f <= i64::MAX as f64 {
662 f as i64
663 } else {
664 return Err(());
665 }
666 } else {
667 return Err(());
668 };
669
670 let (y, mo, d) = jdn_to_ymd(jdn);
671 let (h, mi, s, frac) = jdn_to_hms(jdn);
672
673 let total_months = match unit.trim_end_matches('s') {
674 "month" => {
675 if let Some(val) = num.checked_mul(sign) {
676 val
677 } else {
678 return Ok(None);
679 }
680 }
681 "year" => {
682 if let Some(val) = num.checked_mul(sign).and_then(|v| v.checked_mul(12)) {
683 val
684 } else {
685 return Ok(None);
686 }
687 }
688 _ => return Err(()),
689 };
690
691 let current_months = if let Some(val) = y.checked_mul(12).and_then(|v| v.checked_add(mo - 1)) {
693 val
694 } else {
695 return Ok(None);
696 };
697 let new_total = if let Some(val) = current_months.checked_add(total_months) {
698 val
699 } else {
700 return Ok(None);
701 };
702
703 let new_y = new_total.div_euclid(12);
704 let new_mo = new_total.rem_euclid(12) + 1;
705 let n_floor = compute_floor(new_y, new_mo, d);
710 Ok(Some((
711 ymdhms_to_jdn(new_y, new_mo, d, h, mi, s, frac),
712 n_floor,
713 )))
714}
715
716struct StackStr {
722 buf: [u8; 48],
723 len: usize,
724}
725
726impl StackStr {
727 fn new() -> Self {
728 Self {
729 buf: [0; 48],
730 len: 0,
731 }
732 }
733
734 fn as_str(&self) -> &str {
735 core::str::from_utf8(&self.buf[..self.len]).unwrap_or("")
737 }
738}
739
740impl core::fmt::Write for StackStr {
741 fn write_str(&mut self, s: &str) -> core::fmt::Result {
742 let end = self.len + s.len();
743 if end > self.buf.len() {
744 return Err(core::fmt::Error);
745 }
746 self.buf[self.len..end].copy_from_slice(s.as_bytes());
747 self.len = end;
748 Ok(())
749 }
750}
751
752fn build_small_text(write: impl Fn(&mut dyn core::fmt::Write) -> core::fmt::Result) -> SmallText {
759 let mut buf = StackStr::new();
760 if write(&mut buf).is_ok() {
761 SmallText::new(buf.as_str())
762 } else {
763 let mut heap = String::new();
764 let _ = write(&mut heap);
765 SmallText::from_string(heap)
766 }
767}
768
769fn format_date(jdn: f64) -> SmallText {
770 let (y, m, d) = jdn_to_ymd(jdn);
771 build_small_text(move |w| write!(w, "{y:04}-{m:02}-{d:02}"))
772}
773
774fn format_time(jdn: f64, subsec: bool) -> SmallText {
775 let (h, m, s, frac) = jdn_to_hms(jdn);
776 if subsec && frac > 1e-9 {
777 let ms = (frac * 1000.0).round() as i64;
778 build_small_text(move |w| write!(w, "{h:02}:{m:02}:{s:02}.{ms:03}"))
779 } else {
780 build_small_text(move |w| write!(w, "{h:02}:{m:02}:{s:02}"))
781 }
782}
783
784fn format_datetime(jdn: f64, subsec: bool) -> SmallText {
785 let (y, mo, d) = jdn_to_ymd(jdn);
786 let (h, mi, s, frac) = jdn_to_hms(jdn);
787 if subsec && frac > 1e-9 {
788 let ms = (frac * 1000.0).round() as i64;
789 build_small_text(move |w| write!(w, "{y:04}-{mo:02}-{d:02} {h:02}:{mi:02}:{s:02}.{ms:03}"))
790 } else {
791 build_small_text(move |w| write!(w, "{y:04}-{mo:02}-{d:02} {h:02}:{mi:02}:{s:02}"))
792 }
793}
794
795#[inline]
796fn push_format(result: &mut String, args: Arguments<'_>) {
797 let _ = result.write_fmt(args);
798}
799
800#[inline]
801fn push_zero_padded_2(result: &mut String, value: i64) {
802 if (0..=99).contains(&value) {
803 let value = value as u8;
804 result.push(char::from(b'0' + value / 10));
805 result.push(char::from(b'0' + value % 10));
806 } else {
807 push_format(result, format_args!("{value:02}"));
808 }
809}
810
811#[inline]
812fn push_space_padded_2(result: &mut String, value: i64) {
813 if (0..=99).contains(&value) {
814 let value = value as u8;
815 if value >= 10 {
816 result.push(char::from(b'0' + value / 10));
817 } else {
818 result.push(' ');
819 }
820 result.push(char::from(b'0' + value % 10));
821 } else {
822 push_format(result, format_args!("{value:>2}"));
823 }
824}
825
826#[inline]
827fn push_zero_padded_3(result: &mut String, value: i64) {
828 if (0..=999).contains(&value) {
829 let value = value as u16;
830 result.push(char::from(b'0' + (value / 100) as u8));
831 result.push(char::from(b'0' + ((value / 10) % 10) as u8));
832 result.push(char::from(b'0' + (value % 10) as u8));
833 } else {
834 push_format(result, format_args!("{value:03}"));
835 }
836}
837
838#[inline]
839fn push_zero_padded_4(result: &mut String, value: i64) {
840 if (0..=9999).contains(&value) {
841 let value = value as u16;
842 result.push(char::from(b'0' + (value / 1000) as u8));
843 result.push(char::from(b'0' + ((value / 100) % 10) as u8));
844 result.push(char::from(b'0' + ((value / 10) % 10) as u8));
845 result.push(char::from(b'0' + (value % 10) as u8));
846 } else {
847 push_format(result, format_args!("{value:04}"));
848 }
849}
850
851fn format_strftime(fmt: &str, jdn: f64) -> String {
853 let (y, mo, d) = jdn_to_ymd(jdn);
854 let (h, mi, s, frac) = jdn_to_hms(jdn);
855 let doy = day_of_year(y, mo, d);
856 let jdn_int = (jdn + 0.5).floor() as i64;
858 let dow = (jdn_int + 1) % 7; let mut result = String::with_capacity(fmt.len().saturating_add(8));
861 let bytes = fmt.as_bytes();
862 let mut i = 0;
863 let mut literal_start = 0;
864
865 while i < bytes.len() {
866 if bytes[i] != b'%' || i + 1 >= bytes.len() {
867 i += 1;
868 continue;
869 }
870
871 result.push_str(&fmt[literal_start..i]);
872
873 let spec_suffix = &fmt[i + 1..];
874 let Some(spec) = spec_suffix.chars().next() else {
875 break;
876 };
877 i += 1 + spec.len_utf8();
878 literal_start = i;
879
880 match spec {
881 'd' => push_zero_padded_2(&mut result, d),
882 'e' => push_space_padded_2(&mut result, d),
883 'F' => {
884 push_zero_padded_4(&mut result, y);
886 result.push('-');
887 push_zero_padded_2(&mut result, mo);
888 result.push('-');
889 push_zero_padded_2(&mut result, d);
890 }
891 'f' => {
892 let total = s as f64 + frac;
894 push_format(&mut result, format_args!("{total:06.3}"));
895 }
896 'H' => push_zero_padded_2(&mut result, h),
897 'I' => {
898 let h12 = if h == 0 {
900 12
901 } else if h > 12 {
902 h - 12
903 } else {
904 h
905 };
906 push_zero_padded_2(&mut result, h12);
907 }
908 'j' => push_zero_padded_3(&mut result, doy),
909 'J' => {
910 push_format(&mut result, format_args!("{jdn:.15}"));
912 while result.as_bytes().last() == Some(&b'0') {
913 result.pop();
914 }
915 if result.as_bytes().last() == Some(&b'.') {
916 result.pop();
917 }
918 }
919 'k' => {
920 push_space_padded_2(&mut result, h);
922 }
923 'l' => {
924 let h12 = if h == 0 {
926 12
927 } else if h > 12 {
928 h - 12
929 } else {
930 h
931 };
932 push_space_padded_2(&mut result, h12);
933 }
934 'm' => push_zero_padded_2(&mut result, mo),
935 'M' => push_zero_padded_2(&mut result, mi),
936 'p' => {
937 result.push_str(if h < 12 { "AM" } else { "PM" });
938 }
939 'P' => {
940 result.push_str(if h < 12 { "am" } else { "pm" });
941 }
942 'R' => {
943 push_zero_padded_2(&mut result, h);
944 result.push(':');
945 push_zero_padded_2(&mut result, mi);
946 }
947 's' => {
948 let unix = jdn_to_unix(jdn);
949 push_format(&mut result, format_args!("{unix}"));
950 }
951 'S' => push_zero_padded_2(&mut result, s),
952 'T' => {
953 push_zero_padded_2(&mut result, h);
954 result.push(':');
955 push_zero_padded_2(&mut result, mi);
956 result.push(':');
957 push_zero_padded_2(&mut result, s);
958 }
959 'u' => {
960 let u = if dow == 0 { 7 } else { dow };
962 push_format(&mut result, format_args!("{u}"));
963 }
964 'w' => push_format(&mut result, format_args!("{dow}")),
965 'W' => {
966 let w = (doy + 6 - ((dow + 6) % 7)) / 7;
968 push_zero_padded_2(&mut result, w);
969 }
970 'Y' => push_zero_padded_4(&mut result, y),
971 'G' | 'g' | 'V' => {
972 let (iso_y, iso_w) = iso_week(y, mo, d);
974 match spec {
975 'G' => push_zero_padded_4(&mut result, iso_y),
976 'g' => push_zero_padded_2(&mut result, iso_y % 100),
977 'V' => push_zero_padded_2(&mut result, iso_w),
978 _ => unreachable!(),
979 }
980 }
981 '%' => result.push('%'),
982 other => {
983 result.push('%');
984 result.push(other);
985 }
986 }
987 }
988
989 if literal_start < fmt.len() {
990 result.push_str(&fmt[literal_start..]);
991 }
992
993 result
994}
995
996fn iso_week(y: i64, m: i64, d: i64) -> (i64, i64) {
998 let jdn = ymd_to_jdn(y, m, d);
999 let jdn_int = (jdn + 0.5).floor() as i64;
1000 let dow = (jdn_int + 1) % 7;
1002 let iso_dow = if dow == 0 { 7 } else { dow };
1003
1004 let thu_jdn = jdn_int + (4 - iso_dow);
1006 let (thu_y, _, _) = jdn_to_ymd(thu_jdn as f64);
1007
1008 let jan4_jdn = (ymd_to_jdn(thu_y, 1, 4) + 0.5).floor() as i64;
1010 let jan4_dow = (jan4_jdn + 1) % 7;
1011 let jan4_iso_dow = if jan4_dow == 0 { 7 } else { jan4_dow };
1012 let week1_start = jan4_jdn - (jan4_iso_dow - 1);
1013
1014 let week = (thu_jdn - week1_start) / 7 + 1;
1015 (thu_y, week)
1016}
1017
1018fn timediff_impl(jdn1: f64, jdn2: f64) -> String {
1021 let (sign, start_jdn, end_jdn) = if jdn1 >= jdn2 {
1022 ('+', jdn2, jdn1)
1023 } else {
1024 ('-', jdn1, jdn2)
1025 };
1026
1027 let (start_y, start_mo, start_d) = jdn_to_ymd(start_jdn);
1028 let (start_h, start_mi, mut start_s, start_frac) = jdn_to_hms(start_jdn);
1029 let mut start_ms = (start_frac * 1000.0).round() as i64;
1030 if start_ms >= 1000 {
1031 start_ms = 0;
1032 start_s += 1;
1033 }
1034
1035 let (end_y, end_mo, end_d) = jdn_to_ymd(end_jdn);
1036 let (end_h, end_mi, mut end_s, end_frac) = jdn_to_hms(end_jdn);
1037 let mut end_ms = (end_frac * 1000.0).round() as i64;
1038 if end_ms >= 1000 {
1039 end_ms = 0;
1040 end_s += 1;
1041 }
1042
1043 let mut years = end_y - start_y;
1044 let mut months = end_mo - start_mo;
1045 let mut days = end_d - start_d;
1046 let mut hours = end_h - start_h;
1047 let mut minutes = end_mi - start_mi;
1048 let mut seconds = end_s - start_s;
1049 let mut millis = end_ms - start_ms;
1050
1051 if millis < 0 {
1052 millis += 1000;
1053 seconds -= 1;
1054 }
1055 if seconds < 0 {
1056 seconds += 60;
1057 minutes -= 1;
1058 }
1059 if minutes < 0 {
1060 minutes += 60;
1061 hours -= 1;
1062 }
1063 if hours < 0 {
1064 hours += 24;
1065 days -= 1;
1066 }
1067 if days < 0 {
1068 months -= 1;
1069 let (borrow_y, borrow_mo) = if end_mo == 1 {
1070 (end_y - 1, 12)
1071 } else {
1072 (end_y, end_mo - 1)
1073 };
1074 days += days_in_month(borrow_y, borrow_mo);
1075 }
1076 if months < 0 {
1077 months += 12;
1078 years -= 1;
1079 }
1080
1081 format!(
1082 "{sign}{years:04}-{months:02}-{days:02} {hours:02}:{minutes:02}:{seconds:02}.{millis:03}"
1083 )
1084}
1085
1086fn parse_args(args: &[SqliteValue]) -> Option<(f64, bool)> {
1090 if args.is_empty() || args[0].is_null() {
1091 return None;
1092 }
1093
1094 let numeric_input = matches!(&args[0], SqliteValue::Integer(_) | SqliteValue::Float(_));
1095 let input = match &args[0] {
1096 SqliteValue::Text(s) => parse_timestring(s)?,
1097 SqliteValue::Integer(i) => *i as f64,
1098 SqliteValue::Float(f) => *f,
1099 _ => return None,
1100 };
1101
1102 if args[1..].iter().any(SqliteValue::is_null) {
1105 return None;
1106 }
1107 let modifiers: Vec<String> = args[1..].iter().map(SqliteValue::to_text).collect();
1108
1109 if numeric_input {
1114 let first = modifiers
1115 .first()
1116 .map(|modifier| modifier.trim().to_ascii_lowercase());
1117 let reinterprets_raw = matches!(first.as_deref(), Some("unixepoch" | "julianday" | "auto"));
1118 if !reinterprets_raw && !(0.0..=AUTO_JDN_MAX).contains(&input) {
1119 return None;
1120 }
1121 }
1122
1123 apply_modifiers(input, &modifiers)
1124}
1125
1126pub struct DateFunc;
1129
1130impl ScalarFunction for DateFunc {
1131 fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1132 match parse_args(args) {
1133 Some((jdn, _)) => Ok(SqliteValue::Text(format_date(jdn))),
1134 None => Ok(SqliteValue::Null),
1135 }
1136 }
1137
1138 fn num_args(&self) -> i32 {
1139 -1
1140 }
1141
1142 fn name(&self) -> &str {
1143 "date"
1144 }
1145}
1146
1147pub struct TimeFunc;
1150
1151impl ScalarFunction for TimeFunc {
1152 fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1153 match parse_args(args) {
1154 Some((jdn, subsec)) => Ok(SqliteValue::Text(format_time(jdn, subsec))),
1155 None => Ok(SqliteValue::Null),
1156 }
1157 }
1158
1159 fn num_args(&self) -> i32 {
1160 -1
1161 }
1162
1163 fn name(&self) -> &str {
1164 "time"
1165 }
1166}
1167
1168pub struct DateTimeFunc;
1171
1172impl ScalarFunction for DateTimeFunc {
1173 fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1174 match parse_args(args) {
1175 Some((jdn, subsec)) => Ok(SqliteValue::Text(format_datetime(jdn, subsec))),
1176 None => Ok(SqliteValue::Null),
1177 }
1178 }
1179
1180 fn num_args(&self) -> i32 {
1181 -1
1182 }
1183
1184 fn name(&self) -> &str {
1185 "datetime"
1186 }
1187}
1188
1189pub struct JuliandayFunc;
1192
1193impl ScalarFunction for JuliandayFunc {
1194 fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1195 match parse_args(args) {
1196 Some((jdn, _)) => Ok(SqliteValue::Float(jdn)),
1197 None => Ok(SqliteValue::Null),
1198 }
1199 }
1200
1201 fn num_args(&self) -> i32 {
1202 -1
1203 }
1204
1205 fn name(&self) -> &str {
1206 "julianday"
1207 }
1208}
1209
1210pub struct UnixepochFunc;
1213
1214impl ScalarFunction for UnixepochFunc {
1215 fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1216 match parse_args(args) {
1217 Some((jdn, true)) => {
1220 let secs = (jdn - UNIX_EPOCH_JDN) * 86400.0;
1221 let rounded = (secs * 1000.0).round() / 1000.0;
1222 Ok(SqliteValue::Float(rounded))
1223 }
1224 Some((jdn, false)) => Ok(SqliteValue::Integer(jdn_to_unix(jdn))),
1225 None => Ok(SqliteValue::Null),
1226 }
1227 }
1228
1229 fn num_args(&self) -> i32 {
1230 -1
1231 }
1232
1233 fn name(&self) -> &str {
1234 "unixepoch"
1235 }
1236}
1237
1238pub struct StrftimeFunc;
1241
1242impl ScalarFunction for StrftimeFunc {
1243 fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1244 if args.len() < 2 || args[0].is_null() || args[1].is_null() {
1245 return Ok(SqliteValue::Null);
1246 }
1247 let rest = &args[1..];
1248 match parse_args(rest) {
1249 Some((jdn, _)) => {
1250 let fmt = match args[0].as_text_str() {
1251 Some(text) => Cow::Borrowed(text),
1252 None => Cow::Owned(args[0].to_text()),
1253 };
1254 Ok(SqliteValue::Text(format_strftime(fmt.as_ref(), jdn).into()))
1255 }
1256 None => Ok(SqliteValue::Null),
1257 }
1258 }
1259
1260 fn num_args(&self) -> i32 {
1261 -1
1262 }
1263
1264 fn name(&self) -> &str {
1265 "strftime"
1266 }
1267}
1268
1269pub struct TimediffFunc;
1272
1273impl ScalarFunction for TimediffFunc {
1274 fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1275 if args.len() < 2 || args[0].is_null() || args[1].is_null() {
1276 return Ok(SqliteValue::Null);
1277 }
1278
1279 let jdn1 = match &args[0] {
1280 SqliteValue::Text(s) => parse_timestring(s),
1281 SqliteValue::Integer(i) => Some(*i as f64),
1282 SqliteValue::Float(f) => Some(*f),
1283 _ => None,
1284 };
1285 let jdn2 = match &args[1] {
1286 SqliteValue::Text(s) => parse_timestring(s),
1287 SqliteValue::Integer(i) => Some(*i as f64),
1288 SqliteValue::Float(f) => Some(*f),
1289 _ => None,
1290 };
1291
1292 match (jdn1, jdn2) {
1293 (Some(j1), Some(j2)) => Ok(SqliteValue::Text(timediff_impl(j1, j2).into())),
1294 _ => Ok(SqliteValue::Null),
1295 }
1296 }
1297
1298 fn num_args(&self) -> i32 {
1299 2
1300 }
1301
1302 fn name(&self) -> &str {
1303 "timediff"
1304 }
1305}
1306
1307pub fn register_datetime_builtins(registry: &mut FunctionRegistry) {
1311 registry.register_scalar(DateFunc);
1312 registry.register_scalar(TimeFunc);
1313 registry.register_scalar(DateTimeFunc);
1314 registry.register_scalar(JuliandayFunc);
1315 registry.register_scalar(UnixepochFunc);
1316 registry.register_scalar(StrftimeFunc);
1317 registry.register_scalar(TimediffFunc);
1318}
1319
1320#[cfg(test)]
1323mod tests {
1324 use super::*;
1325
1326 fn text(s: &str) -> SqliteValue {
1327 SqliteValue::Text(s.into())
1328 }
1329
1330 fn int(v: i64) -> SqliteValue {
1331 SqliteValue::Integer(v)
1332 }
1333
1334 fn float(v: f64) -> SqliteValue {
1335 SqliteValue::Float(v)
1336 }
1337
1338 fn null() -> SqliteValue {
1339 SqliteValue::Null
1340 }
1341
1342 fn assert_text(result: &SqliteValue, expected: &str) {
1343 match result {
1344 SqliteValue::Text(s) => assert_eq!(s.as_ref(), expected, "text mismatch"),
1345 other => panic!("expected Text(\"{expected}\"), got {other:?}"),
1346 }
1347 }
1348
1349 #[test]
1352 fn test_date_basic() {
1353 let r = DateFunc.invoke(&[text("2024-03-15 14:30:00")]).unwrap();
1354 assert_text(&r, "2024-03-15");
1355 }
1356
1357 #[test]
1358 fn test_time_basic() {
1359 let r = TimeFunc.invoke(&[text("2024-03-15 14:30:45")]).unwrap();
1360 assert_text(&r, "14:30:45");
1361 }
1362
1363 #[test]
1364 fn test_datetime_basic() {
1365 let r = DateTimeFunc.invoke(&[text("2024-03-15 14:30:00")]).unwrap();
1366 assert_text(&r, "2024-03-15 14:30:00");
1367 }
1368
1369 #[test]
1370 fn test_julianday_basic() {
1371 let r = JuliandayFunc.invoke(&[text("2024-03-15")]).unwrap();
1372 match r {
1373 SqliteValue::Float(jdn) => {
1374 assert!((jdn - 2_460_384.5).abs() < 0.01, "unexpected JDN: {jdn}");
1376 }
1377 other => panic!("expected Float, got {other:?}"),
1378 }
1379 }
1380
1381 fn julianday_float(input: &str) -> f64 {
1389 match JuliandayFunc.invoke(&[text(input)]).unwrap() {
1390 SqliteValue::Float(v) => v,
1391 other => panic!("expected Float, got {other:?} for input {input:?}"),
1392 }
1393 }
1394
1395 fn assert_jdn_close(actual: f64, expected: f64, ctx: &str) {
1396 assert!(
1398 (actual - expected).abs() < 1e-6,
1399 "JDN mismatch for {ctx}: got {actual}, expected {expected}"
1400 );
1401 }
1402
1403 #[test]
1404 fn test_julianday_rfc3339_z_suffix() {
1405 let naive = julianday_float("2026-04-07 16:00:00");
1407 assert_jdn_close(julianday_float("2026-04-07T16:00:00Z"), naive, "T...Z");
1408 assert_jdn_close(
1409 julianday_float("2026-04-07T16:00:00z"),
1410 naive,
1411 "lowercase z",
1412 );
1413 }
1414
1415 #[test]
1416 fn test_julianday_rfc3339_zero_offset() {
1417 let naive = julianday_float("2026-04-07 16:00:00");
1418 assert_jdn_close(
1419 julianday_float("2026-04-07T16:00:00+00:00"),
1420 naive,
1421 "+00:00",
1422 );
1423 assert_jdn_close(
1424 julianday_float("2026-04-07T16:00:00-00:00"),
1425 naive,
1426 "-00:00",
1427 );
1428 }
1429
1430 #[test]
1431 fn test_julianday_rfc3339_positive_offset() {
1432 let base = julianday_float("2026-04-07 16:00:00");
1434 let expected = base - 1.0 / 24.0;
1435 assert_jdn_close(
1436 julianday_float("2026-04-07T16:00:00+01:00"),
1437 expected,
1438 "+01:00",
1439 );
1440 }
1441
1442 #[test]
1443 fn test_julianday_rfc3339_negative_offset() {
1444 let base = julianday_float("2026-04-07 16:00:00");
1446 let expected = base + 5.0 / 24.0;
1447 assert_jdn_close(
1448 julianday_float("2026-04-07T16:00:00-05:00"),
1449 expected,
1450 "-05:00",
1451 );
1452 }
1453
1454 #[test]
1455 fn test_julianday_rfc3339_half_hour_offset() {
1456 let base = julianday_float("2026-04-07 16:00:00");
1458 let expected = base - 5.5 / 24.0;
1459 assert_jdn_close(
1460 julianday_float("2026-04-07T16:00:00+05:30"),
1461 expected,
1462 "+05:30",
1463 );
1464 }
1465
1466 #[test]
1467 fn test_julianday_rfc3339_compact_offsets() {
1468 let base = julianday_float("2026-04-07 16:00:00");
1470 assert_jdn_close(
1471 julianday_float("2026-04-07T16:00:00+0100"),
1472 base - 1.0 / 24.0,
1473 "+0100",
1474 );
1475 assert_jdn_close(
1476 julianday_float("2026-04-07T16:00:00-0530"),
1477 base + 5.5 / 24.0,
1478 "-0530",
1479 );
1480 assert_jdn_close(
1481 julianday_float("2026-04-07T16:00:00+09"),
1482 base - 9.0 / 24.0,
1483 "+09",
1484 );
1485 }
1486
1487 #[test]
1488 fn test_julianday_rfc3339_fractional_seconds_with_tz() {
1489 let base = julianday_float("2026-04-07 16:00:00.500");
1491 assert_jdn_close(
1492 julianday_float("2026-04-07T16:00:00.500Z"),
1493 base,
1494 "fractional + Z",
1495 );
1496 assert_jdn_close(
1497 julianday_float("2026-04-07T16:00:00.500+01:00"),
1498 base - 1.0 / 24.0,
1499 "fractional + +01:00",
1500 );
1501 }
1502
1503 #[test]
1504 fn test_date_and_time_rfc3339_round_trip() {
1505 assert_text(
1508 &DateFunc
1509 .invoke(&[text("2026-04-07T16:00:00+05:00")])
1510 .unwrap(),
1511 "2026-04-07",
1513 );
1514 assert_text(
1515 &TimeFunc
1516 .invoke(&[text("2026-04-07T16:00:00+05:00")])
1517 .unwrap(),
1518 "11:00:00",
1519 );
1520 assert_text(
1521 &DateTimeFunc
1522 .invoke(&[text("2026-04-07T16:00:00+05:00")])
1523 .unwrap(),
1524 "2026-04-07 11:00:00",
1525 );
1526 }
1527
1528 #[test]
1529 fn test_julianday_rfc3339_invalid_offsets_return_null() {
1530 for bad in &[
1532 "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", ] {
1537 let result = JuliandayFunc.invoke(&[text(bad)]).unwrap();
1538 assert_eq!(
1539 result,
1540 SqliteValue::Null,
1541 "expected NULL for malformed offset {bad:?}, got {result:?}"
1542 );
1543 }
1544 }
1545
1546 #[test]
1547 fn test_julianday_rejects_malformed_time_fields() {
1548 for bad in &[
1552 "+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", ] {
1564 let result = JuliandayFunc.invoke(&[text(bad)]).unwrap();
1565 assert_eq!(
1566 result,
1567 SqliteValue::Null,
1568 "expected NULL for signed time field {bad:?}, got {result:?}"
1569 );
1570 }
1571 }
1572
1573 #[test]
1574 fn test_unixepoch_basic() {
1575 let r = UnixepochFunc
1576 .invoke(&[text("1970-01-01 00:00:00")])
1577 .unwrap();
1578 assert_eq!(r, int(0));
1579 }
1580
1581 #[test]
1582 fn test_unixepoch_known_date() {
1583 let r = UnixepochFunc
1584 .invoke(&[text("2024-01-01 00:00:00")])
1585 .unwrap();
1586 assert_eq!(r, int(1_704_067_200));
1588 }
1589
1590 #[test]
1593 fn test_modifier_days() {
1594 let r = DateFunc
1595 .invoke(&[text("2024-01-15"), text("+10 days")])
1596 .unwrap();
1597 assert_text(&r, "2024-01-25");
1598 }
1599
1600 #[test]
1601 fn test_modifier_months() {
1602 let r = DateFunc
1605 .invoke(&[text("2024-01-31"), text("+1 months")])
1606 .unwrap();
1607 assert_text(&r, "2024-03-02");
1608 }
1609
1610 #[test]
1611 fn test_modifier_years() {
1612 let r = DateFunc
1615 .invoke(&[text("2024-02-29"), text("+1 years")])
1616 .unwrap();
1617 assert_text(&r, "2025-03-01");
1618 }
1619
1620 #[test]
1621 fn test_modifier_hours() {
1622 let r = DateTimeFunc
1623 .invoke(&[text("2024-01-01 23:00:00"), text("+2 hours")])
1624 .unwrap();
1625 assert_text(&r, "2024-01-02 01:00:00");
1626 }
1627
1628 #[test]
1629 fn test_modifier_start_of_month() {
1630 let r = DateFunc
1631 .invoke(&[text("2024-03-15"), text("start of month")])
1632 .unwrap();
1633 assert_text(&r, "2024-03-01");
1634 }
1635
1636 #[test]
1637 fn test_modifier_start_of_year() {
1638 let r = DateFunc
1639 .invoke(&[text("2024-06-15"), text("start of year")])
1640 .unwrap();
1641 assert_text(&r, "2024-01-01");
1642 }
1643
1644 #[test]
1645 fn test_modifier_start_of_day() {
1646 let r = DateTimeFunc
1647 .invoke(&[text("2024-03-15 14:30:00"), text("start of day")])
1648 .unwrap();
1649 assert_text(&r, "2024-03-15 00:00:00");
1650 }
1651
1652 #[test]
1653 fn test_modifier_unixepoch() {
1654 let r = DateTimeFunc.invoke(&[int(0), text("unixepoch")]).unwrap();
1655 assert_text(&r, "1970-01-01 00:00:00");
1656 }
1657
1658 #[test]
1659 fn test_modifier_weekday() {
1660 let r = DateFunc
1662 .invoke(&[text("2024-03-15"), text("weekday 0")])
1663 .unwrap();
1664 assert_text(&r, "2024-03-17");
1665 }
1666
1667 #[test]
1668 fn test_modifier_auto_unixepoch() {
1669 let ts = int(1_710_531_045);
1670 let r = DateTimeFunc.invoke(&[ts.clone(), text("auto")]).unwrap();
1671 let expected = DateTimeFunc.invoke(&[ts, text("unixepoch")]).unwrap();
1672 assert_eq!(
1673 r, expected,
1674 "auto and unixepoch should agree for unix-like values"
1675 );
1676 }
1677
1678 #[test]
1679 fn test_modifier_auto_julian_day() {
1680 let r = DateFunc
1681 .invoke(&[float(2_460_384.5), text("auto")])
1682 .unwrap();
1683 assert_text(&r, "2024-03-15");
1684 }
1685
1686 #[test]
1687 fn test_modifier_localtime_utc_roundtrip() {
1688 let r = DateTimeFunc
1690 .invoke(&[text("2024-03-15 14:30:45"), text("localtime"), text("utc")])
1691 .unwrap();
1692 assert_text(&r, "2024-03-15 14:30:45");
1693 }
1694
1695 #[test]
1696 fn test_modifier_localtime_shifts_value() {
1697 let offset = utc_offset_for_utc_jdn(ymdhms_to_jdn(2024, 3, 15, 12, 0, 0, 0.0));
1699 if offset != 0 {
1700 let r = DateTimeFunc
1701 .invoke(&[text("2024-03-15 12:00:00"), text("localtime")])
1702 .unwrap();
1703 let shifted = match &r {
1705 SqliteValue::Text(s) => s.clone(),
1706 _ => panic!("expected text"),
1707 };
1708 assert_ne!(&*shifted, "2024-03-15 12:00:00");
1709 }
1710 }
1711
1712 #[test]
1713 fn test_modifier_auto_out_of_range_returns_null() {
1714 let r = DateTimeFunc.invoke(&[float(1.0e20), text("auto")]).unwrap();
1715 assert_eq!(r, SqliteValue::Null);
1716 }
1717
1718 #[test]
1719 fn test_modifier_order_matters() {
1720 let r1 = DateFunc
1722 .invoke(&[text("2024-03-15"), text("start of month"), text("+1 days")])
1723 .unwrap();
1724 assert_text(&r1, "2024-03-02");
1725
1726 let r2 = DateFunc
1728 .invoke(&[text("2024-03-15"), text("+1 days"), text("start of month")])
1729 .unwrap();
1730 assert_text(&r2, "2024-03-01");
1731 }
1732
1733 #[test]
1734 fn test_modifier_weekday_same_day_is_noop() {
1735 let r = DateFunc
1737 .invoke(&[text("2024-03-17"), text("weekday 0")])
1738 .unwrap();
1739 assert_text(&r, "2024-03-17");
1740 }
1741
1742 #[test]
1745 fn test_bare_time_defaults() {
1746 let r = DateFunc.invoke(&[text("12:30:00")]).unwrap();
1747 assert_text(&r, "2000-01-01");
1748 }
1749
1750 #[test]
1751 fn test_t_separator() {
1752 let r = DateTimeFunc.invoke(&[text("2024-03-15T14:30:00")]).unwrap();
1753 assert_text(&r, "2024-03-15 14:30:00");
1754 }
1755
1756 #[test]
1757 fn test_julian_day_input() {
1758 let r = DateFunc.invoke(&[float(2_460_384.5)]).unwrap();
1760 assert_text(&r, "2024-03-15");
1761 }
1762
1763 #[test]
1764 fn test_null_input() {
1765 assert_eq!(DateFunc.invoke(&[null()]).unwrap(), SqliteValue::Null);
1766 }
1767
1768 #[test]
1769 fn test_invalid_input() {
1770 assert_eq!(
1771 DateFunc.invoke(&[text("not-a-date")]).unwrap(),
1772 SqliteValue::Null
1773 );
1774 }
1775
1776 #[test]
1777 fn test_negative_time_component_invalid() {
1778 let r = TimeFunc.invoke(&[text("-01:00")]).unwrap();
1779 assert_eq!(r, SqliteValue::Null);
1780 }
1781
1782 #[test]
1785 fn test_leap_year() {
1786 let r = DateFunc
1787 .invoke(&[text("2024-02-28"), text("+1 days")])
1788 .unwrap();
1789 assert_text(&r, "2024-02-29");
1790 }
1791
1792 #[test]
1793 fn test_non_leap_year() {
1794 let r = DateFunc
1795 .invoke(&[text("2023-02-28"), text("+1 days")])
1796 .unwrap();
1797 assert_text(&r, "2023-03-01");
1798 }
1799
1800 #[test]
1803 fn test_strftime_basic() {
1804 let r = StrftimeFunc
1805 .invoke(&[text("%Y-%m-%d"), text("2024-03-15")])
1806 .unwrap();
1807 assert_text(&r, "2024-03-15");
1808 }
1809
1810 #[test]
1811 fn test_strftime_time_specifiers() {
1812 let r = StrftimeFunc
1813 .invoke(&[text("%H:%M:%S"), text("2024-03-15 14:30:45")])
1814 .unwrap();
1815 assert_text(&r, "14:30:45");
1816 }
1817
1818 #[test]
1819 fn test_strftime_unix_seconds() {
1820 let r = StrftimeFunc
1821 .invoke(&[text("%s"), text("1970-01-01 00:00:00")])
1822 .unwrap();
1823 assert_text(&r, "0");
1824 }
1825
1826 #[test]
1827 fn test_strftime_day_of_year() {
1828 let r = StrftimeFunc
1829 .invoke(&[text("%j"), text("2024-03-15")])
1830 .unwrap();
1831 assert_text(&r, "075");
1833 }
1834
1835 #[test]
1836 fn test_strftime_day_of_week() {
1837 let r = StrftimeFunc
1839 .invoke(&[text("%w"), text("2024-03-15")])
1840 .unwrap();
1841 assert_text(&r, "5");
1842
1843 let r = StrftimeFunc
1844 .invoke(&[text("%u"), text("2024-03-15")])
1845 .unwrap();
1846 assert_text(&r, "5");
1847 }
1848
1849 #[test]
1850 fn test_strftime_12hour() {
1851 let r = StrftimeFunc
1852 .invoke(&[text("%I %p"), text("2024-03-15 14:30:00")])
1853 .unwrap();
1854 assert_text(&r, "02 PM");
1855
1856 let r = StrftimeFunc
1857 .invoke(&[text("%I %P"), text("2024-03-15 09:30:00")])
1858 .unwrap();
1859 assert_text(&r, "09 am");
1860 }
1861
1862 #[test]
1863 fn test_strftime_all_specifiers_presence() {
1864 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|%%";
1865 let r = StrftimeFunc
1866 .invoke(&[text(fmt), text("2024-03-15 14:30:45.123")])
1867 .unwrap();
1868
1869 let s = match r {
1870 SqliteValue::Text(v) => v,
1871 other => panic!("expected Text, got {other:?}"),
1872 };
1873 let parts: Vec<&str> = s.split('|').collect();
1874 assert_eq!(parts.len(), 25, "unexpected specifier output: {s}");
1875 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!(
1882 parts[6].parse::<f64>().is_ok(),
1883 "expected numeric %J output, got {}",
1884 parts[6]
1885 );
1886 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!(
1894 parts[14].parse::<i64>().is_ok(),
1895 "expected numeric %s output, got {}",
1896 parts[14]
1897 );
1898 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], "%"); }
1909
1910 #[test]
1911 fn test_strftime_null() {
1912 assert_eq!(
1913 StrftimeFunc.invoke(&[null(), text("2024-01-01")]).unwrap(),
1914 SqliteValue::Null
1915 );
1916 assert_eq!(
1917 StrftimeFunc.invoke(&[text("%Y"), null()]).unwrap(),
1918 SqliteValue::Null
1919 );
1920 }
1921
1922 #[test]
1923 #[ignore = "perf-only benchmark"]
1924 fn perf_strftime_timestamp_rows() {
1925 use std::hint::black_box;
1926 use std::time::Instant;
1927
1928 const ROWS: usize = 200_000;
1929 const REPEATS: usize = 5;
1930 const FORMAT: &str = "%Y-%m-%d %H:%M:%S";
1931 const INPUT: &str = "2024-03-15 14:30:45";
1932
1933 let func = StrftimeFunc;
1934 let fmt = text(FORMAT);
1935 let input = text(INPUT);
1936 let mut best_ns = u128::MAX;
1937 let mut output_len = 0usize;
1938
1939 for _ in 0..REPEATS {
1940 let started = Instant::now();
1941 for _ in 0..ROWS {
1942 let result = black_box(
1943 func.invoke(black_box(&[fmt.clone(), input.clone()]))
1944 .expect("strftime benchmark invocation must succeed"),
1945 );
1946 output_len = match result {
1947 SqliteValue::Text(text) => text.len(),
1948 SqliteValue::Null
1949 | SqliteValue::Integer(_)
1950 | SqliteValue::Float(_)
1951 | SqliteValue::Blob(_) => 0,
1952 };
1953 }
1954 let elapsed_ns = started.elapsed().as_nanos();
1955 if elapsed_ns < best_ns {
1956 best_ns = elapsed_ns;
1957 }
1958 }
1959
1960 println!(
1961 "strftime_timestamp_rows rows={ROWS} repeats={REPEATS} best_ns={best_ns} output_len={output_len}"
1962 );
1963 }
1964
1965 #[test]
1968 fn test_timediff_basic() {
1969 let r = TimediffFunc
1970 .invoke(&[text("2024-03-15"), text("2024-03-10")])
1971 .unwrap();
1972 assert_text(&r, "+0000-00-05 00:00:00.000");
1973 }
1974
1975 #[test]
1976 fn test_timediff_negative() {
1977 let r = TimediffFunc
1978 .invoke(&[text("2024-03-10"), text("2024-03-15")])
1979 .unwrap();
1980 assert_text(&r, "-0000-00-05 00:00:00.000");
1981 }
1982
1983 #[test]
1984 fn test_timediff_year_boundary() {
1985 let r = TimediffFunc
1986 .invoke(&[text("2024-01-01 01:00:00"), text("2023-12-31 23:00:00")])
1987 .unwrap();
1988 assert_text(&r, "+0000-00-00 02:00:00.000");
1989 }
1990
1991 #[test]
1994 fn test_modifier_subsec() {
1995 let r = TimeFunc
1996 .invoke(&[text("2024-01-01 12:00:00.123"), text("subsec")])
1997 .unwrap();
1998 match &r {
1999 SqliteValue::Text(s) => assert!(
2000 s.contains('.'),
2001 "expected fractional seconds with subsec: {s}"
2002 ),
2003 other => panic!("expected Text, got {other:?}"),
2004 }
2005 }
2006
2007 #[test]
2010 fn test_register_datetime_builtins_all_present() {
2011 let mut reg = FunctionRegistry::new();
2012 register_datetime_builtins(&mut reg);
2013
2014 let expected = [
2015 "date",
2016 "time",
2017 "datetime",
2018 "julianday",
2019 "unixepoch",
2020 "strftime",
2021 "timediff",
2022 ];
2023
2024 for name in expected {
2025 assert!(
2026 reg.find_scalar(name, 1).is_some() || reg.find_scalar(name, 2).is_some(),
2027 "datetime function '{name}' not registered"
2028 );
2029 }
2030 }
2031
2032 #[test]
2035 fn test_modifier_year_overflow() {
2036 let huge = i64::MAX;
2039 let modifier = format!("+{huge} years");
2040 let r = DateFunc.invoke(&[text("2000-01-01"), text(&modifier)]);
2041 assert_eq!(r.unwrap(), SqliteValue::Null);
2044 }
2045
2046 #[test]
2047 fn test_jdn_roundtrip() {
2048 let dates = [
2050 (2024, 3, 15),
2051 (2000, 1, 1),
2052 (1970, 1, 1),
2053 (2024, 2, 29),
2054 (1900, 1, 1),
2055 (2099, 12, 31),
2056 ];
2057 for (y, m, d) in dates {
2058 let jdn = ymd_to_jdn(y, m, d);
2059 let (y2, m2, d2) = jdn_to_ymd(jdn);
2060 assert_eq!(
2061 (y, m, d),
2062 (y2, m2, d2),
2063 "roundtrip failed for {y}-{m}-{d} (JDN={jdn})"
2064 );
2065 }
2066 }
2067
2068 #[test]
2069 fn test_unix_epoch_roundtrip() {
2070 let jdn = ymd_to_jdn(1970, 1, 1);
2071 let unix = jdn_to_unix(jdn);
2072 assert_eq!(unix, 0, "Unix epoch should be 0");
2073
2074 let jdn2 = unix_to_jdn(0.0);
2075 assert!((jdn2 - UNIX_EPOCH_JDN).abs() < 1e-10, "roundtrip failed");
2076 }
2077}