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
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;
137 let a = if z < 2_299_161 {
138 z
139 } else {
140 let alpha = ((z as f64 - 1_867_216.25) / 36524.25).floor() as i64;
141 z.saturating_add(1)
142 .saturating_add(alpha)
143 .saturating_sub(alpha / 4)
144 };
145 let b = a.saturating_add(1524);
146 let c = ((b as f64 - 122.1) / 365.25).floor() as i64;
147 let d = (365.25 * c as f64).floor() as i64;
148 let e = ((b.saturating_sub(d)) as f64 / 30.6001).floor() as i64;
149
150 let day = b
151 .saturating_sub(d)
152 .saturating_sub((30.6001 * e as f64).floor() as i64);
153 let month = if e < 14 {
154 e.saturating_sub(1)
155 } else {
156 e.saturating_sub(13)
157 };
158 let year = if month > 2 {
159 c.saturating_sub(4716)
160 } else {
161 c.saturating_sub(4715)
162 };
163 (year, month, day)
164}
165
166fn jdn_to_hms(jdn: f64) -> (i64, i64, i64, f64) {
168 let frac = jdn + 0.5 - (jdn + 0.5).floor();
169 let total_ms = (frac * 86_400_000.0).round() as i64;
171 let h = total_ms / 3_600_000;
172 let rem = total_ms % 3_600_000;
173 let m = rem / 60_000;
174 let rem = rem % 60_000;
175 let s = rem / 1000;
176 let ms_frac = (rem % 1000) as f64 / 1000.0;
177 (h, m, s, ms_frac)
178}
179
180fn ymdhms_to_jdn(y: i64, mo: i64, d: i64, h: i64, mi: i64, s: i64, frac: f64) -> f64 {
182 ymd_to_jdn(y, mo, d) + (h as f64 * 3600.0 + mi as f64 * 60.0 + s as f64 + frac) / 86400.0
183}
184
185const UNIX_EPOCH_JDN: f64 = 2_440_587.5;
187const AUTO_JDN_MAX: f64 = 5_373_484.499_999;
189const AUTO_UNIX_MIN: f64 = -210_866_760_000.0;
191const AUTO_UNIX_MAX: f64 = 253_402_300_799.0;
192
193fn jdn_to_unix(jdn: f64) -> i64 {
194 ((jdn - UNIX_EPOCH_JDN) * 86400.0).round() as i64
195}
196
197fn unix_to_jdn(ts: f64) -> f64 {
198 ts / 86400.0 + UNIX_EPOCH_JDN
199}
200
201fn is_leap_year(y: i64) -> bool {
202 (y % 4 == 0 && y % 100 != 0) || y % 400 == 0
203}
204
205fn days_in_month(y: i64, m: i64) -> i64 {
206 match m {
207 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
208 4 | 6 | 9 | 11 => 30,
209 2 => {
210 if is_leap_year(y) {
211 29
212 } else {
213 28
214 }
215 }
216 _ => 30,
217 }
218}
219
220fn day_of_year(y: i64, m: i64, d: i64) -> i64 {
221 let mut doy = d;
222 for mo in 1..m {
223 doy = doy.saturating_add(days_in_month(y, mo));
224 }
225 doy
226}
227
228fn parse_timestring(s: &str) -> Option<f64> {
232 let s = s.trim();
233
234 if s.eq_ignore_ascii_case("now") {
240 use std::time::{SystemTime, UNIX_EPOCH};
241 let secs = SystemTime::now()
242 .duration_since(UNIX_EPOCH)
243 .unwrap_or_default()
244 .as_secs_f64();
245 return Some(2_440_587.5 + secs / 86_400.0);
247 }
248
249 if let Ok(jdn) = s.parse::<f64>() {
252 if jdn >= 0.0 && jdn.is_finite() {
253 return Some(jdn);
254 }
255 }
256
257 parse_iso8601(s)
259}
260
261fn parse_iso8601(s: &str) -> Option<f64> {
262 let bytes = s.as_bytes();
270 let len = bytes.len();
271
272 if len >= 10 && bytes[4] == b'-' && bytes[7] == b'-' {
274 let y = s[0..4].parse::<i64>().ok()?;
275 let m = s[5..7].parse::<i64>().ok()?;
276 let d = s[8..10].parse::<i64>().ok()?;
277
278 if m < 1 || m > 12 || d < 1 || d > 31 {
279 return None;
280 }
281
282 if len == 10 {
283 return Some(ymd_to_jdn(y, m, d));
284 }
285
286 if len > 10 && (bytes[10] == b' ' || bytes[10] == b'T') {
288 let time_part = &s[11..];
289 let (h, mi, sec, frac, tz_offset_min) = parse_time_part_with_tz(time_part)?;
290 let jdn = ymdhms_to_jdn(y, m, d, h, mi, sec, frac);
291 return Some(jdn - (tz_offset_min as f64) / 1440.0);
294 }
295 return None;
296 }
297
298 if len >= 5 && bytes[2] == b':' {
300 let (h, mi, sec, frac, tz_offset_min) = parse_time_part_with_tz(s)?;
301 let jdn = ymdhms_to_jdn(2000, 1, 1, h, mi, sec, frac);
302 return Some(jdn - (tz_offset_min as f64) / 1440.0);
303 }
304
305 None
306}
307
308fn split_tz_suffix(s: &str) -> Option<(&str, i64)> {
316 if let Some(stripped) = s.strip_suffix('Z').or_else(|| s.strip_suffix('z')) {
318 return Some((stripped, 0));
319 }
320
321 let bytes = s.as_bytes();
330 for width in [6usize, 5, 3] {
332 if bytes.len() < width + 1 {
333 continue;
334 }
335 let split_at = bytes.len() - width;
336 let sign_byte = bytes[split_at];
337 if sign_byte != b'+' && sign_byte != b'-' {
338 continue;
339 }
340 let tz_part = &s[split_at..];
341 if let Some(offset) = parse_tz_offset(tz_part) {
342 return Some((&s[..split_at], offset));
343 }
344 }
345
346 Some((s, 0))
348}
349
350fn parse_tz_offset(tz: &str) -> Option<i64> {
353 let bytes = tz.as_bytes();
354 if bytes.is_empty() {
355 return None;
356 }
357 let sign: i64 = match bytes[0] {
358 b'+' => 1,
359 b'-' => -1,
360 _ => return None,
361 };
362 let rest = &tz[1..];
363 let (hours, minutes) = match rest.len() {
364 5 if rest.as_bytes()[2] == b':' => (
366 rest[0..2].parse::<i64>().ok()?,
367 rest[3..5].parse::<i64>().ok()?,
368 ),
369 4 => (
371 rest[0..2].parse::<i64>().ok()?,
372 rest[2..4].parse::<i64>().ok()?,
373 ),
374 2 => (rest.parse::<i64>().ok()?, 0),
376 _ => return None,
377 };
378 if !(0..=23).contains(&hours) || !(0..=59).contains(&minutes) {
379 return None;
380 }
381 Some(sign * (hours * 60 + minutes))
382}
383
384fn parse_time_part_with_tz(s: &str) -> Option<(i64, i64, i64, f64, i64)> {
387 let (time_only, tz_offset_min) = split_tz_suffix(s)?;
388 let (h, mi, sec, frac) = parse_time_part(time_only)?;
389 Some((h, mi, sec, frac, tz_offset_min))
390}
391
392fn parse_time_part(s: &str) -> Option<(i64, i64, i64, f64)> {
394 let [h_tens, h_ones, b':', mi_tens, mi_ones, rest @ ..] = s.as_bytes() else {
395 return None;
396 };
397 let h = parse_two_ascii_digits(*h_tens, *h_ones)?;
402 let mi = parse_two_ascii_digits(*mi_tens, *mi_ones)?;
403 if !(0..=23).contains(&h) || !(0..=59).contains(&mi) {
404 return None;
405 }
406
407 match rest {
412 [] => Some((h, mi, 0, 0.0)),
413 [b':', sec_tens, sec_ones] => {
414 let sec = parse_two_ascii_digits(*sec_tens, *sec_ones)?;
415 if !(0..=59).contains(&sec) {
416 return None;
417 }
418 Some((h, mi, sec, 0.0))
419 }
420 [b':', sec_tens, sec_ones, b'.', ..] => {
421 let sec = parse_two_ascii_digits(*sec_tens, *sec_ones)?;
422 if !(0..=59).contains(&sec) {
423 return None;
424 }
425 let frac = s.get(8..)?.parse::<f64>().ok()?;
426 Some((h, mi, sec, frac))
427 }
428 _ => None,
429 }
430}
431
432#[inline]
433fn parse_two_ascii_digits(tens: u8, ones: u8) -> Option<i64> {
434 if tens.is_ascii_digit() && ones.is_ascii_digit() {
435 Some(i64::from((tens - b'0') * 10 + (ones - b'0')))
436 } else {
437 None
438 }
439}
440
441fn apply_modifier(jdn: f64, modifier: &str) -> Option<f64> {
445 let m = modifier.trim().to_ascii_lowercase();
446
447 if m == "start of month" {
449 let (y, mo, _d) = jdn_to_ymd(jdn);
450 return Some(ymd_to_jdn(y, mo, 1));
451 }
452 if m == "start of year" {
453 let (y, _mo, _d) = jdn_to_ymd(jdn);
454 return Some(ymd_to_jdn(y, 1, 1));
455 }
456 if m == "start of day" {
457 let (y, mo, d) = jdn_to_ymd(jdn);
458 return Some(ymd_to_jdn(y, mo, d));
459 }
460
461 if m == "unixepoch" {
463 return Some(unix_to_jdn(jdn));
464 }
465
466 if m == "julianday" {
469 return Some(jdn);
470 }
471
472 if m == "auto" {
477 if (0.0..=AUTO_JDN_MAX).contains(&jdn) {
478 return Some(jdn);
479 }
480 if (AUTO_UNIX_MIN..=AUTO_UNIX_MAX).contains(&jdn) {
481 return Some(unix_to_jdn(jdn));
482 }
483 return None;
484 }
485
486 if m == "localtime" {
489 let offset = utc_offset_for_utc_jdn(jdn);
490 return Some(jdn + offset as f64 / 86400.0);
491 }
492 if m == "utc" {
495 let offset = utc_offset_for_local_jdn(jdn);
496 return Some(jdn - offset as f64 / 86400.0);
497 }
498
499 if m == "subsec" || m == "subsecond" {
502 return Some(jdn);
503 }
504
505 if let Some(rest) = m.strip_prefix("weekday ") {
507 let wd = rest.trim().parse::<i64>().ok()?;
508 if !(0..=6).contains(&wd) {
509 return None;
510 }
511 let current_jdn_int = (jdn + 0.5).floor() as i64;
513 let current_wd = (current_jdn_int + 1) % 7; let mut diff = wd - current_wd;
515 if diff < 0 {
516 diff += 7;
517 }
518 return Some(jdn + diff as f64);
520 }
521
522 parse_arithmetic_modifier(&m).map(|delta| jdn + delta)
524}
525
526fn parse_arithmetic_modifier(m: &str) -> Option<f64> {
528 let (sign, rest) = if let Some(r) = m.strip_prefix('+') {
529 (1.0, r.trim())
530 } else {
531 let r = m.strip_prefix('-')?;
532 (-1.0, r.trim())
533 };
534
535 let mut parts = rest.splitn(2, ' ');
536 let num_str = parts.next()?;
537 let unit = parts.next()?.trim();
538
539 let num = num_str.parse::<f64>().ok().filter(|f| f.is_finite())?;
541 let delta = num * sign;
542
543 match unit.trim_end_matches('s') {
544 "day" => Some(delta),
545 "hour" => Some(delta / 24.0),
546 "minute" => Some(delta / 1440.0),
547 "second" => Some(delta / 86400.0),
548 "month" => Some(apply_month_delta(delta)),
549 "year" => Some(apply_month_delta(delta * 12.0)),
550 _ => None,
551 }
552}
553
554fn apply_month_delta(months: f64) -> f64 {
559 months * 30.436875
561}
562
563fn apply_modifiers(jdn: f64, modifiers: &[String]) -> Option<(f64, bool)> {
565 let mut j = jdn;
566 let mut subsec = false;
567 for m in modifiers {
568 let m_lower = m.trim().to_ascii_lowercase();
569 if m_lower == "subsec" || m_lower == "subsecond" {
570 subsec = true;
571 }
572 if is_month_year_modifier(&m_lower) {
578 match apply_month_year_exact(j, &m_lower) {
579 Ok(new_jdn) => {
580 j = new_jdn?;
581 continue;
582 }
583 Err(()) => {
584 }
586 }
587 }
588 j = apply_modifier(j, m)?;
589 }
590 Some((j, subsec))
591}
592
593fn is_month_year_modifier(m: &str) -> bool {
594 (m.contains("month") || m.contains("year")) && (m.starts_with('+') || m.starts_with('-'))
595}
596
597fn apply_month_year_exact(jdn: f64, m: &str) -> std::result::Result<Option<f64>, ()> {
602 let (sign, rest) = if let Some(r) = m.strip_prefix('+') {
603 (1_i64, r.trim())
604 } else if let Some(r) = m.strip_prefix('-') {
605 (-1_i64, r.trim())
606 } else {
607 return Err(());
608 };
609
610 let mut parts = rest.splitn(2, ' ');
611 let num_str = parts.next().ok_or(())?;
612 let unit = parts.next().ok_or(())?.trim();
613
614 let num = if let Ok(n) = num_str.parse::<i64>() {
616 n
617 } else if let Ok(f) = num_str.parse::<f64>() {
618 if f.fract() == 0.0 && f >= i64::MIN as f64 && f <= i64::MAX as f64 {
619 f as i64
620 } else {
621 return Err(());
622 }
623 } else {
624 return Err(());
625 };
626
627 let (y, mo, d) = jdn_to_ymd(jdn);
628 let (h, mi, s, frac) = jdn_to_hms(jdn);
629
630 let total_months = match unit.trim_end_matches('s') {
631 "month" => {
632 if let Some(val) = num.checked_mul(sign) {
633 val
634 } else {
635 return Ok(None);
636 }
637 }
638 "year" => {
639 if let Some(val) = num.checked_mul(sign).and_then(|v| v.checked_mul(12)) {
640 val
641 } else {
642 return Ok(None);
643 }
644 }
645 _ => return Err(()),
646 };
647
648 let current_months = if let Some(val) = y.checked_mul(12).and_then(|v| v.checked_add(mo - 1)) {
650 val
651 } else {
652 return Ok(None);
653 };
654 let new_total = if let Some(val) = current_months.checked_add(total_months) {
655 val
656 } else {
657 return Ok(None);
658 };
659
660 let new_y = new_total.div_euclid(12);
661 let new_mo = new_total.rem_euclid(12) + 1;
662 Ok(Some(ymdhms_to_jdn(new_y, new_mo, d, h, mi, s, frac)))
665}
666
667fn format_date(jdn: f64) -> String {
670 let (y, m, d) = jdn_to_ymd(jdn);
671 format!("{y:04}-{m:02}-{d:02}")
672}
673
674fn format_time(jdn: f64, subsec: bool) -> String {
675 let (h, m, s, frac) = jdn_to_hms(jdn);
676 if subsec && frac > 1e-9 {
677 format!("{h:02}:{m:02}:{s:02}.{:03}", (frac * 1000.0).round() as i64)
678 } else {
679 format!("{h:02}:{m:02}:{s:02}")
680 }
681}
682
683fn format_datetime(jdn: f64, subsec: bool) -> String {
684 format!("{} {}", format_date(jdn), format_time(jdn, subsec))
685}
686
687#[inline]
688fn push_format(result: &mut String, args: Arguments<'_>) {
689 let _ = result.write_fmt(args);
690}
691
692#[inline]
693fn push_zero_padded_2(result: &mut String, value: i64) {
694 if (0..=99).contains(&value) {
695 let value = value as u8;
696 result.push(char::from(b'0' + value / 10));
697 result.push(char::from(b'0' + value % 10));
698 } else {
699 push_format(result, format_args!("{value:02}"));
700 }
701}
702
703#[inline]
704fn push_space_padded_2(result: &mut String, value: i64) {
705 if (0..=99).contains(&value) {
706 let value = value as u8;
707 if value >= 10 {
708 result.push(char::from(b'0' + value / 10));
709 } else {
710 result.push(' ');
711 }
712 result.push(char::from(b'0' + value % 10));
713 } else {
714 push_format(result, format_args!("{value:>2}"));
715 }
716}
717
718#[inline]
719fn push_zero_padded_3(result: &mut String, value: i64) {
720 if (0..=999).contains(&value) {
721 let value = value as u16;
722 result.push(char::from(b'0' + (value / 100) as u8));
723 result.push(char::from(b'0' + ((value / 10) % 10) as u8));
724 result.push(char::from(b'0' + (value % 10) as u8));
725 } else {
726 push_format(result, format_args!("{value:03}"));
727 }
728}
729
730#[inline]
731fn push_zero_padded_4(result: &mut String, value: i64) {
732 if (0..=9999).contains(&value) {
733 let value = value as u16;
734 result.push(char::from(b'0' + (value / 1000) as u8));
735 result.push(char::from(b'0' + ((value / 100) % 10) as u8));
736 result.push(char::from(b'0' + ((value / 10) % 10) as u8));
737 result.push(char::from(b'0' + (value % 10) as u8));
738 } else {
739 push_format(result, format_args!("{value:04}"));
740 }
741}
742
743fn format_strftime(fmt: &str, jdn: f64) -> String {
745 let (y, mo, d) = jdn_to_ymd(jdn);
746 let (h, mi, s, frac) = jdn_to_hms(jdn);
747 let doy = day_of_year(y, mo, d);
748 let jdn_int = (jdn + 0.5).floor() as i64;
750 let dow = (jdn_int + 1) % 7; let mut result = String::with_capacity(fmt.len().saturating_add(8));
753 let bytes = fmt.as_bytes();
754 let mut i = 0;
755 let mut literal_start = 0;
756
757 while i < bytes.len() {
758 if bytes[i] != b'%' || i + 1 >= bytes.len() {
759 i += 1;
760 continue;
761 }
762
763 result.push_str(&fmt[literal_start..i]);
764
765 let spec_suffix = &fmt[i + 1..];
766 let Some(spec) = spec_suffix.chars().next() else {
767 break;
768 };
769 i += 1 + spec.len_utf8();
770 literal_start = i;
771
772 match spec {
773 'd' => push_zero_padded_2(&mut result, d),
774 'e' => push_space_padded_2(&mut result, d),
775 'f' => {
776 let total = s as f64 + frac;
778 push_format(&mut result, format_args!("{total:06.3}"));
779 }
780 'H' => push_zero_padded_2(&mut result, h),
781 'I' => {
782 let h12 = if h == 0 {
784 12
785 } else if h > 12 {
786 h - 12
787 } else {
788 h
789 };
790 push_zero_padded_2(&mut result, h12);
791 }
792 'j' => push_zero_padded_3(&mut result, doy),
793 'J' => {
794 push_format(&mut result, format_args!("{jdn:.15}"));
796 while result.as_bytes().last() == Some(&b'0') {
797 result.pop();
798 }
799 if result.as_bytes().last() == Some(&b'.') {
800 result.pop();
801 }
802 }
803 'k' => {
804 push_space_padded_2(&mut result, h);
806 }
807 'l' => {
808 let h12 = if h == 0 {
810 12
811 } else if h > 12 {
812 h - 12
813 } else {
814 h
815 };
816 push_space_padded_2(&mut result, h12);
817 }
818 'm' => push_zero_padded_2(&mut result, mo),
819 'M' => push_zero_padded_2(&mut result, mi),
820 'p' => {
821 result.push_str(if h < 12 { "AM" } else { "PM" });
822 }
823 'P' => {
824 result.push_str(if h < 12 { "am" } else { "pm" });
825 }
826 'R' => {
827 push_zero_padded_2(&mut result, h);
828 result.push(':');
829 push_zero_padded_2(&mut result, mi);
830 }
831 's' => {
832 let unix = jdn_to_unix(jdn);
833 push_format(&mut result, format_args!("{unix}"));
834 }
835 'S' => push_zero_padded_2(&mut result, s),
836 'T' => {
837 push_zero_padded_2(&mut result, h);
838 result.push(':');
839 push_zero_padded_2(&mut result, mi);
840 result.push(':');
841 push_zero_padded_2(&mut result, s);
842 }
843 'u' => {
844 let u = if dow == 0 { 7 } else { dow };
846 push_format(&mut result, format_args!("{u}"));
847 }
848 'w' => push_format(&mut result, format_args!("{dow}")),
849 'W' => {
850 let w = (doy + 6 - ((dow + 6) % 7)) / 7;
852 push_zero_padded_2(&mut result, w);
853 }
854 'Y' => push_zero_padded_4(&mut result, y),
855 'G' | 'g' | 'V' => {
856 let (iso_y, iso_w) = iso_week(y, mo, d);
858 match spec {
859 'G' => push_zero_padded_4(&mut result, iso_y),
860 'g' => push_zero_padded_2(&mut result, iso_y % 100),
861 'V' => push_zero_padded_2(&mut result, iso_w),
862 _ => unreachable!(),
863 }
864 }
865 '%' => result.push('%'),
866 other => {
867 result.push('%');
868 result.push(other);
869 }
870 }
871 }
872
873 if literal_start < fmt.len() {
874 result.push_str(&fmt[literal_start..]);
875 }
876
877 result
878}
879
880fn iso_week(y: i64, m: i64, d: i64) -> (i64, i64) {
882 let jdn = ymd_to_jdn(y, m, d);
883 let jdn_int = (jdn + 0.5).floor() as i64;
884 let dow = (jdn_int + 1) % 7;
886 let iso_dow = if dow == 0 { 7 } else { dow };
887
888 let thu_jdn = jdn_int + (4 - iso_dow);
890 let (thu_y, _, _) = jdn_to_ymd(thu_jdn as f64);
891
892 let jan4_jdn = (ymd_to_jdn(thu_y, 1, 4) + 0.5).floor() as i64;
894 let jan4_dow = (jan4_jdn + 1) % 7;
895 let jan4_iso_dow = if jan4_dow == 0 { 7 } else { jan4_dow };
896 let week1_start = jan4_jdn - (jan4_iso_dow - 1);
897
898 let week = (thu_jdn - week1_start) / 7 + 1;
899 (thu_y, week)
900}
901
902fn timediff_impl(jdn1: f64, jdn2: f64) -> String {
905 let (sign, start_jdn, end_jdn) = if jdn1 >= jdn2 {
906 ('+', jdn2, jdn1)
907 } else {
908 ('-', jdn1, jdn2)
909 };
910
911 let (start_y, start_mo, start_d) = jdn_to_ymd(start_jdn);
912 let (start_h, start_mi, mut start_s, start_frac) = jdn_to_hms(start_jdn);
913 let mut start_ms = (start_frac * 1000.0).round() as i64;
914 if start_ms >= 1000 {
915 start_ms = 0;
916 start_s += 1;
917 }
918
919 let (end_y, end_mo, end_d) = jdn_to_ymd(end_jdn);
920 let (end_h, end_mi, mut end_s, end_frac) = jdn_to_hms(end_jdn);
921 let mut end_ms = (end_frac * 1000.0).round() as i64;
922 if end_ms >= 1000 {
923 end_ms = 0;
924 end_s += 1;
925 }
926
927 let mut years = end_y - start_y;
928 let mut months = end_mo - start_mo;
929 let mut days = end_d - start_d;
930 let mut hours = end_h - start_h;
931 let mut minutes = end_mi - start_mi;
932 let mut seconds = end_s - start_s;
933 let mut millis = end_ms - start_ms;
934
935 if millis < 0 {
936 millis += 1000;
937 seconds -= 1;
938 }
939 if seconds < 0 {
940 seconds += 60;
941 minutes -= 1;
942 }
943 if minutes < 0 {
944 minutes += 60;
945 hours -= 1;
946 }
947 if hours < 0 {
948 hours += 24;
949 days -= 1;
950 }
951 if days < 0 {
952 months -= 1;
953 let (borrow_y, borrow_mo) = if end_mo == 1 {
954 (end_y - 1, 12)
955 } else {
956 (end_y, end_mo - 1)
957 };
958 days += days_in_month(borrow_y, borrow_mo);
959 }
960 if months < 0 {
961 months += 12;
962 years -= 1;
963 }
964
965 format!(
966 "{sign}{years:04}-{months:02}-{days:02} {hours:02}:{minutes:02}:{seconds:02}.{millis:03}"
967 )
968}
969
970fn parse_args(args: &[SqliteValue]) -> Option<(f64, bool)> {
974 if args.is_empty() || args[0].is_null() {
975 return None;
976 }
977
978 let input = match &args[0] {
979 SqliteValue::Text(s) => parse_timestring(s)?,
980 SqliteValue::Integer(i) => *i as f64,
981 SqliteValue::Float(f) => *f,
982 _ => return None,
983 };
984
985 if args[1..].iter().any(SqliteValue::is_null) {
988 return None;
989 }
990 let modifiers: Vec<String> = args[1..].iter().map(SqliteValue::to_text).collect();
991
992 apply_modifiers(input, &modifiers)
993}
994
995pub struct DateFunc;
998
999impl ScalarFunction for DateFunc {
1000 fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1001 match parse_args(args) {
1002 Some((jdn, _)) => Ok(SqliteValue::Text(format_date(jdn).into())),
1003 None => Ok(SqliteValue::Null),
1004 }
1005 }
1006
1007 fn num_args(&self) -> i32 {
1008 -1
1009 }
1010
1011 fn name(&self) -> &str {
1012 "date"
1013 }
1014}
1015
1016pub struct TimeFunc;
1019
1020impl ScalarFunction for TimeFunc {
1021 fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1022 match parse_args(args) {
1023 Some((jdn, subsec)) => Ok(SqliteValue::Text(format_time(jdn, subsec).into())),
1024 None => Ok(SqliteValue::Null),
1025 }
1026 }
1027
1028 fn num_args(&self) -> i32 {
1029 -1
1030 }
1031
1032 fn name(&self) -> &str {
1033 "time"
1034 }
1035}
1036
1037pub struct DateTimeFunc;
1040
1041impl ScalarFunction for DateTimeFunc {
1042 fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1043 match parse_args(args) {
1044 Some((jdn, subsec)) => Ok(SqliteValue::Text(format_datetime(jdn, subsec).into())),
1045 None => Ok(SqliteValue::Null),
1046 }
1047 }
1048
1049 fn num_args(&self) -> i32 {
1050 -1
1051 }
1052
1053 fn name(&self) -> &str {
1054 "datetime"
1055 }
1056}
1057
1058pub struct JuliandayFunc;
1061
1062impl ScalarFunction for JuliandayFunc {
1063 fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1064 match parse_args(args) {
1065 Some((jdn, _)) => Ok(SqliteValue::Float(jdn)),
1066 None => Ok(SqliteValue::Null),
1067 }
1068 }
1069
1070 fn num_args(&self) -> i32 {
1071 -1
1072 }
1073
1074 fn name(&self) -> &str {
1075 "julianday"
1076 }
1077}
1078
1079pub struct UnixepochFunc;
1082
1083impl ScalarFunction for UnixepochFunc {
1084 fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1085 match parse_args(args) {
1086 Some((jdn, _)) => Ok(SqliteValue::Integer(jdn_to_unix(jdn))),
1087 None => Ok(SqliteValue::Null),
1088 }
1089 }
1090
1091 fn num_args(&self) -> i32 {
1092 -1
1093 }
1094
1095 fn name(&self) -> &str {
1096 "unixepoch"
1097 }
1098}
1099
1100pub struct StrftimeFunc;
1103
1104impl ScalarFunction for StrftimeFunc {
1105 fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1106 if args.len() < 2 || args[0].is_null() || args[1].is_null() {
1107 return Ok(SqliteValue::Null);
1108 }
1109 let rest = &args[1..];
1110 match parse_args(rest) {
1111 Some((jdn, _)) => {
1112 let fmt = match args[0].as_text_str() {
1113 Some(text) => Cow::Borrowed(text),
1114 None => Cow::Owned(args[0].to_text()),
1115 };
1116 Ok(SqliteValue::Text(format_strftime(fmt.as_ref(), jdn).into()))
1117 }
1118 None => Ok(SqliteValue::Null),
1119 }
1120 }
1121
1122 fn num_args(&self) -> i32 {
1123 -1
1124 }
1125
1126 fn name(&self) -> &str {
1127 "strftime"
1128 }
1129}
1130
1131pub struct TimediffFunc;
1134
1135impl ScalarFunction for TimediffFunc {
1136 fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1137 if args.len() < 2 || args[0].is_null() || args[1].is_null() {
1138 return Ok(SqliteValue::Null);
1139 }
1140
1141 let jdn1 = match &args[0] {
1142 SqliteValue::Text(s) => parse_timestring(s),
1143 SqliteValue::Integer(i) => Some(*i as f64),
1144 SqliteValue::Float(f) => Some(*f),
1145 _ => None,
1146 };
1147 let jdn2 = match &args[1] {
1148 SqliteValue::Text(s) => parse_timestring(s),
1149 SqliteValue::Integer(i) => Some(*i as f64),
1150 SqliteValue::Float(f) => Some(*f),
1151 _ => None,
1152 };
1153
1154 match (jdn1, jdn2) {
1155 (Some(j1), Some(j2)) => Ok(SqliteValue::Text(timediff_impl(j1, j2).into())),
1156 _ => Ok(SqliteValue::Null),
1157 }
1158 }
1159
1160 fn num_args(&self) -> i32 {
1161 2
1162 }
1163
1164 fn name(&self) -> &str {
1165 "timediff"
1166 }
1167}
1168
1169pub fn register_datetime_builtins(registry: &mut FunctionRegistry) {
1173 registry.register_scalar(DateFunc);
1174 registry.register_scalar(TimeFunc);
1175 registry.register_scalar(DateTimeFunc);
1176 registry.register_scalar(JuliandayFunc);
1177 registry.register_scalar(UnixepochFunc);
1178 registry.register_scalar(StrftimeFunc);
1179 registry.register_scalar(TimediffFunc);
1180}
1181
1182#[cfg(test)]
1185mod tests {
1186 use super::*;
1187
1188 fn text(s: &str) -> SqliteValue {
1189 SqliteValue::Text(s.into())
1190 }
1191
1192 fn int(v: i64) -> SqliteValue {
1193 SqliteValue::Integer(v)
1194 }
1195
1196 fn float(v: f64) -> SqliteValue {
1197 SqliteValue::Float(v)
1198 }
1199
1200 fn null() -> SqliteValue {
1201 SqliteValue::Null
1202 }
1203
1204 fn assert_text(result: &SqliteValue, expected: &str) {
1205 match result {
1206 SqliteValue::Text(s) => assert_eq!(s.as_ref(), expected, "text mismatch"),
1207 other => panic!("expected Text(\"{expected}\"), got {other:?}"),
1208 }
1209 }
1210
1211 #[test]
1214 fn test_date_basic() {
1215 let r = DateFunc.invoke(&[text("2024-03-15 14:30:00")]).unwrap();
1216 assert_text(&r, "2024-03-15");
1217 }
1218
1219 #[test]
1220 fn test_time_basic() {
1221 let r = TimeFunc.invoke(&[text("2024-03-15 14:30:45")]).unwrap();
1222 assert_text(&r, "14:30:45");
1223 }
1224
1225 #[test]
1226 fn test_datetime_basic() {
1227 let r = DateTimeFunc.invoke(&[text("2024-03-15 14:30:00")]).unwrap();
1228 assert_text(&r, "2024-03-15 14:30:00");
1229 }
1230
1231 #[test]
1232 fn test_julianday_basic() {
1233 let r = JuliandayFunc.invoke(&[text("2024-03-15")]).unwrap();
1234 match r {
1235 SqliteValue::Float(jdn) => {
1236 assert!((jdn - 2_460_384.5).abs() < 0.01, "unexpected JDN: {jdn}");
1238 }
1239 other => panic!("expected Float, got {other:?}"),
1240 }
1241 }
1242
1243 fn julianday_float(input: &str) -> f64 {
1251 match JuliandayFunc.invoke(&[text(input)]).unwrap() {
1252 SqliteValue::Float(v) => v,
1253 other => panic!("expected Float, got {other:?} for input {input:?}"),
1254 }
1255 }
1256
1257 fn assert_jdn_close(actual: f64, expected: f64, ctx: &str) {
1258 assert!(
1260 (actual - expected).abs() < 1e-6,
1261 "JDN mismatch for {ctx}: got {actual}, expected {expected}"
1262 );
1263 }
1264
1265 #[test]
1266 fn test_julianday_rfc3339_z_suffix() {
1267 let naive = julianday_float("2026-04-07 16:00:00");
1269 assert_jdn_close(julianday_float("2026-04-07T16:00:00Z"), naive, "T...Z");
1270 assert_jdn_close(
1271 julianday_float("2026-04-07T16:00:00z"),
1272 naive,
1273 "lowercase z",
1274 );
1275 }
1276
1277 #[test]
1278 fn test_julianday_rfc3339_zero_offset() {
1279 let naive = julianday_float("2026-04-07 16:00:00");
1280 assert_jdn_close(
1281 julianday_float("2026-04-07T16:00:00+00:00"),
1282 naive,
1283 "+00:00",
1284 );
1285 assert_jdn_close(
1286 julianday_float("2026-04-07T16:00:00-00:00"),
1287 naive,
1288 "-00:00",
1289 );
1290 }
1291
1292 #[test]
1293 fn test_julianday_rfc3339_positive_offset() {
1294 let base = julianday_float("2026-04-07 16:00:00");
1296 let expected = base - 1.0 / 24.0;
1297 assert_jdn_close(
1298 julianday_float("2026-04-07T16:00:00+01:00"),
1299 expected,
1300 "+01:00",
1301 );
1302 }
1303
1304 #[test]
1305 fn test_julianday_rfc3339_negative_offset() {
1306 let base = julianday_float("2026-04-07 16:00:00");
1308 let expected = base + 5.0 / 24.0;
1309 assert_jdn_close(
1310 julianday_float("2026-04-07T16:00:00-05:00"),
1311 expected,
1312 "-05:00",
1313 );
1314 }
1315
1316 #[test]
1317 fn test_julianday_rfc3339_half_hour_offset() {
1318 let base = julianday_float("2026-04-07 16:00:00");
1320 let expected = base - 5.5 / 24.0;
1321 assert_jdn_close(
1322 julianday_float("2026-04-07T16:00:00+05:30"),
1323 expected,
1324 "+05:30",
1325 );
1326 }
1327
1328 #[test]
1329 fn test_julianday_rfc3339_compact_offsets() {
1330 let base = julianday_float("2026-04-07 16:00:00");
1332 assert_jdn_close(
1333 julianday_float("2026-04-07T16:00:00+0100"),
1334 base - 1.0 / 24.0,
1335 "+0100",
1336 );
1337 assert_jdn_close(
1338 julianday_float("2026-04-07T16:00:00-0530"),
1339 base + 5.5 / 24.0,
1340 "-0530",
1341 );
1342 assert_jdn_close(
1343 julianday_float("2026-04-07T16:00:00+09"),
1344 base - 9.0 / 24.0,
1345 "+09",
1346 );
1347 }
1348
1349 #[test]
1350 fn test_julianday_rfc3339_fractional_seconds_with_tz() {
1351 let base = julianday_float("2026-04-07 16:00:00.500");
1353 assert_jdn_close(
1354 julianday_float("2026-04-07T16:00:00.500Z"),
1355 base,
1356 "fractional + Z",
1357 );
1358 assert_jdn_close(
1359 julianday_float("2026-04-07T16:00:00.500+01:00"),
1360 base - 1.0 / 24.0,
1361 "fractional + +01:00",
1362 );
1363 }
1364
1365 #[test]
1366 fn test_date_and_time_rfc3339_round_trip() {
1367 assert_text(
1370 &DateFunc
1371 .invoke(&[text("2026-04-07T16:00:00+05:00")])
1372 .unwrap(),
1373 "2026-04-07",
1375 );
1376 assert_text(
1377 &TimeFunc
1378 .invoke(&[text("2026-04-07T16:00:00+05:00")])
1379 .unwrap(),
1380 "11:00:00",
1381 );
1382 assert_text(
1383 &DateTimeFunc
1384 .invoke(&[text("2026-04-07T16:00:00+05:00")])
1385 .unwrap(),
1386 "2026-04-07 11:00:00",
1387 );
1388 }
1389
1390 #[test]
1391 fn test_julianday_rfc3339_invalid_offsets_return_null() {
1392 for bad in &[
1394 "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", ] {
1399 let result = JuliandayFunc.invoke(&[text(bad)]).unwrap();
1400 assert_eq!(
1401 result,
1402 SqliteValue::Null,
1403 "expected NULL for malformed offset {bad:?}, got {result:?}"
1404 );
1405 }
1406 }
1407
1408 #[test]
1409 fn test_julianday_rejects_malformed_time_fields() {
1410 for bad in &[
1414 "+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", ] {
1426 let result = JuliandayFunc.invoke(&[text(bad)]).unwrap();
1427 assert_eq!(
1428 result,
1429 SqliteValue::Null,
1430 "expected NULL for signed time field {bad:?}, got {result:?}"
1431 );
1432 }
1433 }
1434
1435 #[test]
1436 fn test_unixepoch_basic() {
1437 let r = UnixepochFunc
1438 .invoke(&[text("1970-01-01 00:00:00")])
1439 .unwrap();
1440 assert_eq!(r, int(0));
1441 }
1442
1443 #[test]
1444 fn test_unixepoch_known_date() {
1445 let r = UnixepochFunc
1446 .invoke(&[text("2024-01-01 00:00:00")])
1447 .unwrap();
1448 assert_eq!(r, int(1_704_067_200));
1450 }
1451
1452 #[test]
1455 fn test_modifier_days() {
1456 let r = DateFunc
1457 .invoke(&[text("2024-01-15"), text("+10 days")])
1458 .unwrap();
1459 assert_text(&r, "2024-01-25");
1460 }
1461
1462 #[test]
1463 fn test_modifier_months() {
1464 let r = DateFunc
1467 .invoke(&[text("2024-01-31"), text("+1 months")])
1468 .unwrap();
1469 assert_text(&r, "2024-03-02");
1470 }
1471
1472 #[test]
1473 fn test_modifier_years() {
1474 let r = DateFunc
1477 .invoke(&[text("2024-02-29"), text("+1 years")])
1478 .unwrap();
1479 assert_text(&r, "2025-03-01");
1480 }
1481
1482 #[test]
1483 fn test_modifier_hours() {
1484 let r = DateTimeFunc
1485 .invoke(&[text("2024-01-01 23:00:00"), text("+2 hours")])
1486 .unwrap();
1487 assert_text(&r, "2024-01-02 01:00:00");
1488 }
1489
1490 #[test]
1491 fn test_modifier_start_of_month() {
1492 let r = DateFunc
1493 .invoke(&[text("2024-03-15"), text("start of month")])
1494 .unwrap();
1495 assert_text(&r, "2024-03-01");
1496 }
1497
1498 #[test]
1499 fn test_modifier_start_of_year() {
1500 let r = DateFunc
1501 .invoke(&[text("2024-06-15"), text("start of year")])
1502 .unwrap();
1503 assert_text(&r, "2024-01-01");
1504 }
1505
1506 #[test]
1507 fn test_modifier_start_of_day() {
1508 let r = DateTimeFunc
1509 .invoke(&[text("2024-03-15 14:30:00"), text("start of day")])
1510 .unwrap();
1511 assert_text(&r, "2024-03-15 00:00:00");
1512 }
1513
1514 #[test]
1515 fn test_modifier_unixepoch() {
1516 let r = DateTimeFunc.invoke(&[int(0), text("unixepoch")]).unwrap();
1517 assert_text(&r, "1970-01-01 00:00:00");
1518 }
1519
1520 #[test]
1521 fn test_modifier_weekday() {
1522 let r = DateFunc
1524 .invoke(&[text("2024-03-15"), text("weekday 0")])
1525 .unwrap();
1526 assert_text(&r, "2024-03-17");
1527 }
1528
1529 #[test]
1530 fn test_modifier_auto_unixepoch() {
1531 let ts = int(1_710_531_045);
1532 let r = DateTimeFunc.invoke(&[ts.clone(), text("auto")]).unwrap();
1533 let expected = DateTimeFunc.invoke(&[ts, text("unixepoch")]).unwrap();
1534 assert_eq!(
1535 r, expected,
1536 "auto and unixepoch should agree for unix-like values"
1537 );
1538 }
1539
1540 #[test]
1541 fn test_modifier_auto_julian_day() {
1542 let r = DateFunc
1543 .invoke(&[float(2_460_384.5), text("auto")])
1544 .unwrap();
1545 assert_text(&r, "2024-03-15");
1546 }
1547
1548 #[test]
1549 fn test_modifier_localtime_utc_roundtrip() {
1550 let r = DateTimeFunc
1552 .invoke(&[text("2024-03-15 14:30:45"), text("localtime"), text("utc")])
1553 .unwrap();
1554 assert_text(&r, "2024-03-15 14:30:45");
1555 }
1556
1557 #[test]
1558 fn test_modifier_localtime_shifts_value() {
1559 let offset = utc_offset_for_utc_jdn(ymdhms_to_jdn(2024, 3, 15, 12, 0, 0, 0.0));
1561 if offset != 0 {
1562 let r = DateTimeFunc
1563 .invoke(&[text("2024-03-15 12:00:00"), text("localtime")])
1564 .unwrap();
1565 let shifted = match &r {
1567 SqliteValue::Text(s) => s.clone(),
1568 _ => panic!("expected text"),
1569 };
1570 assert_ne!(&*shifted, "2024-03-15 12:00:00");
1571 }
1572 }
1573
1574 #[test]
1575 fn test_modifier_auto_out_of_range_returns_null() {
1576 let r = DateTimeFunc.invoke(&[float(1.0e20), text("auto")]).unwrap();
1577 assert_eq!(r, SqliteValue::Null);
1578 }
1579
1580 #[test]
1581 fn test_modifier_order_matters() {
1582 let r1 = DateFunc
1584 .invoke(&[text("2024-03-15"), text("start of month"), text("+1 days")])
1585 .unwrap();
1586 assert_text(&r1, "2024-03-02");
1587
1588 let r2 = DateFunc
1590 .invoke(&[text("2024-03-15"), text("+1 days"), text("start of month")])
1591 .unwrap();
1592 assert_text(&r2, "2024-03-01");
1593 }
1594
1595 #[test]
1596 fn test_modifier_weekday_same_day_is_noop() {
1597 let r = DateFunc
1599 .invoke(&[text("2024-03-17"), text("weekday 0")])
1600 .unwrap();
1601 assert_text(&r, "2024-03-17");
1602 }
1603
1604 #[test]
1607 fn test_bare_time_defaults() {
1608 let r = DateFunc.invoke(&[text("12:30:00")]).unwrap();
1609 assert_text(&r, "2000-01-01");
1610 }
1611
1612 #[test]
1613 fn test_t_separator() {
1614 let r = DateTimeFunc.invoke(&[text("2024-03-15T14:30:00")]).unwrap();
1615 assert_text(&r, "2024-03-15 14:30:00");
1616 }
1617
1618 #[test]
1619 fn test_julian_day_input() {
1620 let r = DateFunc.invoke(&[float(2_460_384.5)]).unwrap();
1622 assert_text(&r, "2024-03-15");
1623 }
1624
1625 #[test]
1626 fn test_null_input() {
1627 assert_eq!(DateFunc.invoke(&[null()]).unwrap(), SqliteValue::Null);
1628 }
1629
1630 #[test]
1631 fn test_invalid_input() {
1632 assert_eq!(
1633 DateFunc.invoke(&[text("not-a-date")]).unwrap(),
1634 SqliteValue::Null
1635 );
1636 }
1637
1638 #[test]
1639 fn test_negative_time_component_invalid() {
1640 let r = TimeFunc.invoke(&[text("-01:00")]).unwrap();
1641 assert_eq!(r, SqliteValue::Null);
1642 }
1643
1644 #[test]
1647 fn test_leap_year() {
1648 let r = DateFunc
1649 .invoke(&[text("2024-02-28"), text("+1 days")])
1650 .unwrap();
1651 assert_text(&r, "2024-02-29");
1652 }
1653
1654 #[test]
1655 fn test_non_leap_year() {
1656 let r = DateFunc
1657 .invoke(&[text("2023-02-28"), text("+1 days")])
1658 .unwrap();
1659 assert_text(&r, "2023-03-01");
1660 }
1661
1662 #[test]
1665 fn test_strftime_basic() {
1666 let r = StrftimeFunc
1667 .invoke(&[text("%Y-%m-%d"), text("2024-03-15")])
1668 .unwrap();
1669 assert_text(&r, "2024-03-15");
1670 }
1671
1672 #[test]
1673 fn test_strftime_time_specifiers() {
1674 let r = StrftimeFunc
1675 .invoke(&[text("%H:%M:%S"), text("2024-03-15 14:30:45")])
1676 .unwrap();
1677 assert_text(&r, "14:30:45");
1678 }
1679
1680 #[test]
1681 fn test_strftime_unix_seconds() {
1682 let r = StrftimeFunc
1683 .invoke(&[text("%s"), text("1970-01-01 00:00:00")])
1684 .unwrap();
1685 assert_text(&r, "0");
1686 }
1687
1688 #[test]
1689 fn test_strftime_day_of_year() {
1690 let r = StrftimeFunc
1691 .invoke(&[text("%j"), text("2024-03-15")])
1692 .unwrap();
1693 assert_text(&r, "075");
1695 }
1696
1697 #[test]
1698 fn test_strftime_day_of_week() {
1699 let r = StrftimeFunc
1701 .invoke(&[text("%w"), text("2024-03-15")])
1702 .unwrap();
1703 assert_text(&r, "5");
1704
1705 let r = StrftimeFunc
1706 .invoke(&[text("%u"), text("2024-03-15")])
1707 .unwrap();
1708 assert_text(&r, "5");
1709 }
1710
1711 #[test]
1712 fn test_strftime_12hour() {
1713 let r = StrftimeFunc
1714 .invoke(&[text("%I %p"), text("2024-03-15 14:30:00")])
1715 .unwrap();
1716 assert_text(&r, "02 PM");
1717
1718 let r = StrftimeFunc
1719 .invoke(&[text("%I %P"), text("2024-03-15 09:30:00")])
1720 .unwrap();
1721 assert_text(&r, "09 am");
1722 }
1723
1724 #[test]
1725 fn test_strftime_all_specifiers_presence() {
1726 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|%%";
1727 let r = StrftimeFunc
1728 .invoke(&[text(fmt), text("2024-03-15 14:30:45.123")])
1729 .unwrap();
1730
1731 let s = match r {
1732 SqliteValue::Text(v) => v,
1733 other => panic!("expected Text, got {other:?}"),
1734 };
1735 let parts: Vec<&str> = s.split('|').collect();
1736 assert_eq!(parts.len(), 25, "unexpected specifier output: {s}");
1737 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!(
1744 parts[6].parse::<f64>().is_ok(),
1745 "expected numeric %J output, got {}",
1746 parts[6]
1747 );
1748 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!(
1756 parts[14].parse::<i64>().is_ok(),
1757 "expected numeric %s output, got {}",
1758 parts[14]
1759 );
1760 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], "%"); }
1771
1772 #[test]
1773 fn test_strftime_null() {
1774 assert_eq!(
1775 StrftimeFunc.invoke(&[null(), text("2024-01-01")]).unwrap(),
1776 SqliteValue::Null
1777 );
1778 assert_eq!(
1779 StrftimeFunc.invoke(&[text("%Y"), null()]).unwrap(),
1780 SqliteValue::Null
1781 );
1782 }
1783
1784 #[test]
1785 #[ignore = "perf-only benchmark"]
1786 fn perf_strftime_timestamp_rows() {
1787 use std::hint::black_box;
1788 use std::time::Instant;
1789
1790 const ROWS: usize = 200_000;
1791 const REPEATS: usize = 5;
1792 const FORMAT: &str = "%Y-%m-%d %H:%M:%S";
1793 const INPUT: &str = "2024-03-15 14:30:45";
1794
1795 let func = StrftimeFunc;
1796 let fmt = text(FORMAT);
1797 let input = text(INPUT);
1798 let mut best_ns = u128::MAX;
1799 let mut output_len = 0usize;
1800
1801 for _ in 0..REPEATS {
1802 let started = Instant::now();
1803 for _ in 0..ROWS {
1804 let result = black_box(
1805 func.invoke(black_box(&[fmt.clone(), input.clone()]))
1806 .expect("strftime benchmark invocation must succeed"),
1807 );
1808 output_len = match result {
1809 SqliteValue::Text(text) => text.len(),
1810 SqliteValue::Null
1811 | SqliteValue::Integer(_)
1812 | SqliteValue::Float(_)
1813 | SqliteValue::Blob(_) => 0,
1814 };
1815 }
1816 let elapsed_ns = started.elapsed().as_nanos();
1817 if elapsed_ns < best_ns {
1818 best_ns = elapsed_ns;
1819 }
1820 }
1821
1822 println!(
1823 "strftime_timestamp_rows rows={ROWS} repeats={REPEATS} best_ns={best_ns} output_len={output_len}"
1824 );
1825 }
1826
1827 #[test]
1830 fn test_timediff_basic() {
1831 let r = TimediffFunc
1832 .invoke(&[text("2024-03-15"), text("2024-03-10")])
1833 .unwrap();
1834 assert_text(&r, "+0000-00-05 00:00:00.000");
1835 }
1836
1837 #[test]
1838 fn test_timediff_negative() {
1839 let r = TimediffFunc
1840 .invoke(&[text("2024-03-10"), text("2024-03-15")])
1841 .unwrap();
1842 assert_text(&r, "-0000-00-05 00:00:00.000");
1843 }
1844
1845 #[test]
1846 fn test_timediff_year_boundary() {
1847 let r = TimediffFunc
1848 .invoke(&[text("2024-01-01 01:00:00"), text("2023-12-31 23:00:00")])
1849 .unwrap();
1850 assert_text(&r, "+0000-00-00 02:00:00.000");
1851 }
1852
1853 #[test]
1856 fn test_modifier_subsec() {
1857 let r = TimeFunc
1858 .invoke(&[text("2024-01-01 12:00:00.123"), text("subsec")])
1859 .unwrap();
1860 match &r {
1861 SqliteValue::Text(s) => assert!(
1862 s.contains('.'),
1863 "expected fractional seconds with subsec: {s}"
1864 ),
1865 other => panic!("expected Text, got {other:?}"),
1866 }
1867 }
1868
1869 #[test]
1872 fn test_register_datetime_builtins_all_present() {
1873 let mut reg = FunctionRegistry::new();
1874 register_datetime_builtins(&mut reg);
1875
1876 let expected = [
1877 "date",
1878 "time",
1879 "datetime",
1880 "julianday",
1881 "unixepoch",
1882 "strftime",
1883 "timediff",
1884 ];
1885
1886 for name in expected {
1887 assert!(
1888 reg.find_scalar(name, 1).is_some() || reg.find_scalar(name, 2).is_some(),
1889 "datetime function '{name}' not registered"
1890 );
1891 }
1892 }
1893
1894 #[test]
1897 fn test_modifier_year_overflow() {
1898 let huge = i64::MAX;
1901 let modifier = format!("+{huge} years");
1902 let r = DateFunc.invoke(&[text("2000-01-01"), text(&modifier)]);
1903 assert_eq!(r.unwrap(), SqliteValue::Null);
1906 }
1907
1908 #[test]
1909 fn test_jdn_roundtrip() {
1910 let dates = [
1912 (2024, 3, 15),
1913 (2000, 1, 1),
1914 (1970, 1, 1),
1915 (2024, 2, 29),
1916 (1900, 1, 1),
1917 (2099, 12, 31),
1918 ];
1919 for (y, m, d) in dates {
1920 let jdn = ymd_to_jdn(y, m, d);
1921 let (y2, m2, d2) = jdn_to_ymd(jdn);
1922 assert_eq!(
1923 (y, m, d),
1924 (y2, m2, d2),
1925 "roundtrip failed for {y}-{m}-{d} (JDN={jdn})"
1926 );
1927 }
1928 }
1929
1930 #[test]
1931 fn test_unix_epoch_roundtrip() {
1932 let jdn = ymd_to_jdn(1970, 1, 1);
1933 let unix = jdn_to_unix(jdn);
1934 assert_eq!(unix, 0, "Unix epoch should be 0");
1935
1936 let jdn2 = unix_to_jdn(0.0);
1937 assert!((jdn2 - UNIX_EPOCH_JDN).abs() < 1e-10, "roundtrip failed");
1938 }
1939}