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 fsqlite_error::Result;
35use fsqlite_types::SqliteValue;
36
37use crate::{FunctionRegistry, ScalarFunction};
38
39fn parse_tz_offset(s: &str) -> Option<i64> {
47 let s = s.trim();
48 if s.len() < 5 {
49 return None;
50 }
51 let sign: i64 = if s.starts_with('-') { -1 } else { 1 };
52 let digits = if s.starts_with('+') || s.starts_with('-') {
53 &s[1..]
54 } else {
55 s
56 };
57 if digits.len() < 4 {
58 return None;
59 }
60 let hours: i64 = digits[..2].parse().ok()?;
61 let minutes: i64 = digits[2..4].parse().ok()?;
62 Some(sign * (hours * 3600 + minutes * 60))
63}
64
65fn compute_utc_offset() -> i64 {
67 if let Ok(tz) = std::env::var("TZ") {
69 let tz_upper = tz.trim().to_uppercase();
70 if tz_upper == "UTC"
71 || tz_upper == "GMT"
72 || tz_upper == "UTC0"
73 || tz_upper.starts_with("UTC-0")
74 || tz_upper.starts_with("UTC+0")
75 {
76 return 0;
77 }
78 }
79 let output = std::process::Command::new("date").arg("+%z").output();
80 match output {
81 Ok(out) if out.status.success() => {
82 let s = String::from_utf8_lossy(&out.stdout);
83 parse_tz_offset(s.trim()).unwrap_or(0)
84 }
85 _ => 0,
86 }
87}
88
89fn utc_offset_seconds() -> i64 {
91 use std::sync::OnceLock;
92 static OFFSET: OnceLock<i64> = OnceLock::new();
93 *OFFSET.get_or_init(compute_utc_offset)
94}
95
96fn ymd_to_jdn(y: i64, m: i64, d: i64) -> f64 {
102 let (y, m) = if m <= 2 {
103 (y.saturating_sub(1), m.saturating_add(12))
104 } else {
105 (y, m)
106 };
107 let a = y / 100;
108 let b = 2_i64.saturating_sub(a).saturating_add(a / 4);
109 (365.25 * y.saturating_add(4716) as f64).floor()
110 + (30.6001 * m.saturating_add(1) as f64).floor()
111 + d as f64
112 + b as f64
113 - 1524.5
114}
115
116fn jdn_to_ymd(jdn: f64) -> (i64, i64, i64) {
123 let z = (jdn + 0.5).floor() as i64;
124 let a = if z < 2_299_161 {
125 z
126 } else {
127 let alpha = ((z as f64 - 1_867_216.25) / 36524.25).floor() as i64;
128 z.saturating_add(1)
129 .saturating_add(alpha)
130 .saturating_sub(alpha / 4)
131 };
132 let b = a.saturating_add(1524);
133 let c = ((b as f64 - 122.1) / 365.25).floor() as i64;
134 let d = (365.25 * c as f64).floor() as i64;
135 let e = ((b.saturating_sub(d)) as f64 / 30.6001).floor() as i64;
136
137 let day = b
138 .saturating_sub(d)
139 .saturating_sub((30.6001 * e as f64).floor() as i64);
140 let month = if e < 14 {
141 e.saturating_sub(1)
142 } else {
143 e.saturating_sub(13)
144 };
145 let year = if month > 2 {
146 c.saturating_sub(4716)
147 } else {
148 c.saturating_sub(4715)
149 };
150 (year, month, day)
151}
152
153fn jdn_to_hms(jdn: f64) -> (i64, i64, i64, f64) {
155 let frac = jdn + 0.5 - (jdn + 0.5).floor();
156 let total_ms = (frac * 86_400_000.0).round() as i64;
158 let h = total_ms / 3_600_000;
159 let rem = total_ms % 3_600_000;
160 let m = rem / 60_000;
161 let rem = rem % 60_000;
162 let s = rem / 1000;
163 let ms_frac = (rem % 1000) as f64 / 1000.0;
164 (h, m, s, ms_frac)
165}
166
167fn ymdhms_to_jdn(y: i64, mo: i64, d: i64, h: i64, mi: i64, s: i64, frac: f64) -> f64 {
169 ymd_to_jdn(y, mo, d) + (h as f64 * 3600.0 + mi as f64 * 60.0 + s as f64 + frac) / 86400.0
170}
171
172const UNIX_EPOCH_JDN: f64 = 2_440_587.5;
174const AUTO_JDN_MAX: f64 = 5_373_484.499_999;
176const AUTO_UNIX_MIN: f64 = -210_866_760_000.0;
178const AUTO_UNIX_MAX: f64 = 253_402_300_799.0;
179
180fn jdn_to_unix(jdn: f64) -> i64 {
181 ((jdn - UNIX_EPOCH_JDN) * 86400.0).round() as i64
182}
183
184fn unix_to_jdn(ts: f64) -> f64 {
185 ts / 86400.0 + UNIX_EPOCH_JDN
186}
187
188fn is_leap_year(y: i64) -> bool {
189 (y % 4 == 0 && y % 100 != 0) || y % 400 == 0
190}
191
192fn days_in_month(y: i64, m: i64) -> i64 {
193 match m {
194 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
195 4 | 6 | 9 | 11 => 30,
196 2 => {
197 if is_leap_year(y) {
198 29
199 } else {
200 28
201 }
202 }
203 _ => 30,
204 }
205}
206
207fn day_of_year(y: i64, m: i64, d: i64) -> i64 {
208 let mut doy = d;
209 for mo in 1..m {
210 doy = doy.saturating_add(days_in_month(y, mo));
211 }
212 doy
213}
214
215fn parse_timestring(s: &str) -> Option<f64> {
219 let s = s.trim();
220
221 if s.eq_ignore_ascii_case("now") {
224 return Some(ymd_to_jdn(2000, 1, 1));
226 }
227
228 if let Ok(jdn) = s.parse::<f64>() {
231 if jdn >= 0.0 && jdn.is_finite() {
232 return Some(jdn);
233 }
234 }
235
236 parse_iso8601(s)
238}
239
240fn parse_iso8601(s: &str) -> Option<f64> {
241 let bytes = s.as_bytes();
249 let len = bytes.len();
250
251 if len >= 10 && bytes[4] == b'-' && bytes[7] == b'-' {
253 let y = s[0..4].parse::<i64>().ok()?;
254 let m = s[5..7].parse::<i64>().ok()?;
255 let d = s[8..10].parse::<i64>().ok()?;
256
257 if m < 1 || m > 12 || d < 1 || d > 31 {
258 return None;
259 }
260
261 if len == 10 {
262 return Some(ymd_to_jdn(y, m, d));
263 }
264
265 if len > 10 && (bytes[10] == b' ' || bytes[10] == b'T') {
267 let time_part = &s[11..];
268 let (h, mi, sec, frac) = parse_time_part(time_part)?;
269 return Some(ymdhms_to_jdn(y, m, d, h, mi, sec, frac));
270 }
271 return None;
272 }
273
274 if len >= 5 && bytes[2] == b':' {
276 let (h, mi, sec, frac) = parse_time_part(s)?;
277 return Some(ymdhms_to_jdn(2000, 1, 1, h, mi, sec, frac));
278 }
279
280 None
281}
282
283fn parse_time_part(s: &str) -> Option<(i64, i64, i64, f64)> {
285 let parts: Vec<&str> = s.splitn(3, ':').collect();
286 if parts.len() < 2 {
287 return None;
288 }
289 let h = parts[0].parse::<i64>().ok()?;
290 let mi = parts[1].parse::<i64>().ok()?;
291 if !(0..=23).contains(&h) || !(0..=59).contains(&mi) {
292 return None;
293 }
294
295 if parts.len() == 2 {
296 return Some((h, mi, 0, 0.0));
297 }
298
299 let sec_str = parts[2];
301 if let Some(dot_pos) = sec_str.find('.') {
302 let sec = sec_str[..dot_pos].parse::<i64>().ok()?;
303 let frac_str = &sec_str[dot_pos..]; let frac = frac_str.parse::<f64>().ok()?;
305 if !(0..=59).contains(&sec) {
306 return None;
307 }
308 Some((h, mi, sec, frac))
309 } else {
310 let sec = sec_str.parse::<i64>().ok()?;
311 if !(0..=59).contains(&sec) {
312 return None;
313 }
314 Some((h, mi, sec, 0.0))
315 }
316}
317
318fn apply_modifier(jdn: f64, modifier: &str) -> Option<f64> {
322 let m = modifier.trim().to_ascii_lowercase();
323
324 if m == "start of month" {
326 let (y, mo, _d) = jdn_to_ymd(jdn);
327 return Some(ymd_to_jdn(y, mo, 1));
328 }
329 if m == "start of year" {
330 let (y, _mo, _d) = jdn_to_ymd(jdn);
331 return Some(ymd_to_jdn(y, 1, 1));
332 }
333 if m == "start of day" {
334 let (y, mo, d) = jdn_to_ymd(jdn);
335 return Some(ymd_to_jdn(y, mo, d));
336 }
337
338 if m == "unixepoch" {
340 return Some(unix_to_jdn(jdn));
341 }
342
343 if m == "julianday" {
346 return Some(jdn);
347 }
348
349 if m == "auto" {
354 if (0.0..=AUTO_JDN_MAX).contains(&jdn) {
355 return Some(jdn);
356 }
357 if (AUTO_UNIX_MIN..=AUTO_UNIX_MAX).contains(&jdn) {
358 return Some(unix_to_jdn(jdn));
359 }
360 return None;
361 }
362
363 if m == "localtime" {
365 let offset = utc_offset_seconds();
366 return Some(jdn + offset as f64 / 86400.0);
367 }
368 if m == "utc" {
370 let offset = utc_offset_seconds();
371 return Some(jdn - offset as f64 / 86400.0);
372 }
373
374 if m == "subsec" || m == "subsecond" {
377 return Some(jdn);
378 }
379
380 if let Some(rest) = m.strip_prefix("weekday ") {
382 let wd = rest.trim().parse::<i64>().ok()?;
383 if !(0..=6).contains(&wd) {
384 return None;
385 }
386 let current_jdn_int = (jdn + 0.5).floor() as i64;
388 let current_wd = (current_jdn_int + 1) % 7; let mut diff = wd - current_wd;
390 if diff < 0 {
391 diff += 7;
392 }
393 return Some(jdn + diff as f64);
395 }
396
397 parse_arithmetic_modifier(&m).map(|delta| jdn + delta)
399}
400
401fn parse_arithmetic_modifier(m: &str) -> Option<f64> {
403 let (sign, rest) = if let Some(r) = m.strip_prefix('+') {
404 (1.0, r.trim())
405 } else if let Some(r) = m.strip_prefix('-') {
406 (-1.0, r.trim())
407 } else {
408 return None;
409 };
410
411 let mut parts = rest.splitn(2, ' ');
412 let num_str = parts.next()?;
413 let unit = parts.next()?.trim();
414
415 let num = num_str.parse::<f64>().ok().filter(|f| f.is_finite())?;
417 let delta = num * sign;
418
419 match unit.trim_end_matches('s') {
420 "day" => Some(delta),
421 "hour" => Some(delta / 24.0),
422 "minute" => Some(delta / 1440.0),
423 "second" => Some(delta / 86400.0),
424 "month" => Some(apply_month_delta(delta)),
425 "year" => Some(apply_month_delta(delta * 12.0)),
426 _ => None,
427 }
428}
429
430fn apply_month_delta(months: f64) -> f64 {
435 months * 30.436875
437}
438
439fn apply_modifiers(jdn: f64, modifiers: &[String]) -> Option<(f64, bool)> {
441 let mut j = jdn;
442 let mut subsec = false;
443 for m in modifiers {
444 let m_lower = m.trim().to_ascii_lowercase();
445 if m_lower == "subsec" || m_lower == "subsecond" {
446 subsec = true;
447 }
448 if is_month_year_modifier(&m_lower) {
454 match apply_month_year_exact(j, &m_lower) {
455 Ok(new_jdn) => {
456 j = new_jdn?;
457 continue;
458 }
459 Err(()) => {
460 }
462 }
463 }
464 j = apply_modifier(j, m)?;
465 }
466 Some((j, subsec))
467}
468
469fn is_month_year_modifier(m: &str) -> bool {
470 (m.contains("month") || m.contains("year")) && (m.starts_with('+') || m.starts_with('-'))
471}
472
473fn apply_month_year_exact(jdn: f64, m: &str) -> std::result::Result<Option<f64>, ()> {
478 let (sign, rest) = if let Some(r) = m.strip_prefix('+') {
479 (1_i64, r.trim())
480 } else if let Some(r) = m.strip_prefix('-') {
481 (-1_i64, r.trim())
482 } else {
483 return Err(());
484 };
485
486 let mut parts = rest.splitn(2, ' ');
487 let num_str = parts.next().ok_or(())?;
488 let unit = parts.next().ok_or(())?.trim();
489
490 let num = if let Ok(n) = num_str.parse::<i64>() {
492 n
493 } else if let Ok(f) = num_str.parse::<f64>() {
494 if f.fract() == 0.0 && f >= i64::MIN as f64 && f <= i64::MAX as f64 {
495 f as i64
496 } else {
497 return Err(());
498 }
499 } else {
500 return Err(());
501 };
502
503 let (y, mo, d) = jdn_to_ymd(jdn);
504 let (h, mi, s, frac) = jdn_to_hms(jdn);
505
506 let total_months = match unit.trim_end_matches('s') {
507 "month" => {
508 if let Some(val) = num.checked_mul(sign) {
509 val
510 } else {
511 return Ok(None);
512 }
513 }
514 "year" => {
515 if let Some(val) = num.checked_mul(sign).and_then(|v| v.checked_mul(12)) {
516 val
517 } else {
518 return Ok(None);
519 }
520 }
521 _ => return Err(()),
522 };
523
524 let current_months = if let Some(val) = y.checked_mul(12).and_then(|v| v.checked_add(mo - 1)) {
526 val
527 } else {
528 return Ok(None);
529 };
530 let new_total = if let Some(val) = current_months.checked_add(total_months) {
531 val
532 } else {
533 return Ok(None);
534 };
535
536 let new_y = new_total.div_euclid(12);
537 let new_mo = new_total.rem_euclid(12) + 1;
538 Ok(Some(ymdhms_to_jdn(new_y, new_mo, d, h, mi, s, frac)))
541}
542
543fn format_date(jdn: f64) -> String {
546 let (y, m, d) = jdn_to_ymd(jdn);
547 format!("{y:04}-{m:02}-{d:02}")
548}
549
550fn format_time(jdn: f64, subsec: bool) -> String {
551 let (h, m, s, frac) = jdn_to_hms(jdn);
552 if subsec && frac > 1e-9 {
553 format!("{h:02}:{m:02}:{s:02}.{:03}", (frac * 1000.0).round() as i64)
554 } else {
555 format!("{h:02}:{m:02}:{s:02}")
556 }
557}
558
559fn format_datetime(jdn: f64, subsec: bool) -> String {
560 format!("{} {}", format_date(jdn), format_time(jdn, subsec))
561}
562
563fn format_strftime(fmt: &str, jdn: f64) -> String {
565 let (y, mo, d) = jdn_to_ymd(jdn);
566 let (h, mi, s, frac) = jdn_to_hms(jdn);
567 let doy = day_of_year(y, mo, d);
568 let jdn_int = (jdn + 0.5).floor() as i64;
570 let dow = (jdn_int + 1) % 7; let mut result = String::new();
573 let chars: Vec<char> = fmt.chars().collect();
574 let mut i = 0;
575
576 while i < chars.len() {
577 if chars[i] == '%' && i + 1 < chars.len() {
578 i += 1;
579 match chars[i] {
580 'd' => result.push_str(&format!("{d:02}")),
581 'e' => result.push_str(&format!("{d:>2}")), 'f' => {
583 let total = s as f64 + frac;
585 result.push_str(&format!("{total:06.3}"));
586 }
587 'H' => result.push_str(&format!("{h:02}")),
588 'I' => {
589 let h12 = if h == 0 {
591 12
592 } else if h > 12 {
593 h - 12
594 } else {
595 h
596 };
597 result.push_str(&format!("{h12:02}"));
598 }
599 'j' => result.push_str(&format!("{doy:03}")),
600 'J' => {
601 let s = format!("{jdn:.15}");
603 let s = s.trim_end_matches('0');
604 let s = s.strip_suffix('.').unwrap_or(s);
605 result.push_str(s);
606 }
607 'k' => {
608 result.push_str(&format!("{h:>2}"));
610 }
611 'l' => {
612 let h12 = if h == 0 {
614 12
615 } else if h > 12 {
616 h - 12
617 } else {
618 h
619 };
620 result.push_str(&format!("{h12:>2}"));
621 }
622 'm' => result.push_str(&format!("{mo:02}")),
623 'M' => result.push_str(&format!("{mi:02}")),
624 'p' => {
625 result.push_str(if h < 12 { "AM" } else { "PM" });
626 }
627 'P' => {
628 result.push_str(if h < 12 { "am" } else { "pm" });
629 }
630 'R' => result.push_str(&format!("{h:02}:{mi:02}")),
631 's' => {
632 let unix = jdn_to_unix(jdn);
633 result.push_str(&unix.to_string());
634 }
635 'S' => result.push_str(&format!("{s:02}")),
636 'T' => result.push_str(&format!("{h:02}:{mi:02}:{s:02}")),
637 'u' => {
638 let u = if dow == 0 { 7 } else { dow };
640 result.push_str(&u.to_string());
641 }
642 'w' => result.push_str(&dow.to_string()),
643 'W' => {
644 let w = (doy + 6 - ((dow + 6) % 7)) / 7;
646 result.push_str(&format!("{w:02}"));
647 }
648 'Y' => result.push_str(&format!("{y:04}")),
649 'G' | 'g' | 'V' => {
650 let (iso_y, iso_w) = iso_week(y, mo, d);
652 match chars[i] {
653 'G' => result.push_str(&format!("{iso_y:04}")),
654 'g' => result.push_str(&format!("{:02}", iso_y % 100)),
655 'V' => result.push_str(&format!("{iso_w:02}")),
656 _ => unreachable!(),
657 }
658 }
659 '%' => result.push('%'),
660 other => {
661 result.push('%');
662 result.push(other);
663 }
664 }
665 } else {
666 result.push(chars[i]);
667 }
668 i += 1;
669 }
670
671 result
672}
673
674fn iso_week(y: i64, m: i64, d: i64) -> (i64, i64) {
676 let jdn = ymd_to_jdn(y, m, d);
677 let jdn_int = (jdn + 0.5).floor() as i64;
678 let dow = (jdn_int + 1) % 7;
680 let iso_dow = if dow == 0 { 7 } else { dow };
681
682 let thu_jdn = jdn_int + (4 - iso_dow);
684 let (thu_y, _, _) = jdn_to_ymd(thu_jdn as f64);
685
686 let jan4_jdn = (ymd_to_jdn(thu_y, 1, 4) + 0.5).floor() as i64;
688 let jan4_dow = (jan4_jdn + 1) % 7;
689 let jan4_iso_dow = if jan4_dow == 0 { 7 } else { jan4_dow };
690 let week1_start = jan4_jdn - (jan4_iso_dow - 1);
691
692 let week = (thu_jdn - week1_start) / 7 + 1;
693 (thu_y, week)
694}
695
696fn timediff_impl(jdn1: f64, jdn2: f64) -> String {
699 let (sign, start_jdn, end_jdn) = if jdn1 >= jdn2 {
700 ('+', jdn2, jdn1)
701 } else {
702 ('-', jdn1, jdn2)
703 };
704
705 let (start_y, start_mo, start_d) = jdn_to_ymd(start_jdn);
706 let (start_h, start_mi, mut start_s, start_frac) = jdn_to_hms(start_jdn);
707 let mut start_ms = (start_frac * 1000.0).round() as i64;
708 if start_ms >= 1000 {
709 start_ms = 0;
710 start_s += 1;
711 }
712
713 let (end_y, end_mo, end_d) = jdn_to_ymd(end_jdn);
714 let (end_h, end_mi, mut end_s, end_frac) = jdn_to_hms(end_jdn);
715 let mut end_ms = (end_frac * 1000.0).round() as i64;
716 if end_ms >= 1000 {
717 end_ms = 0;
718 end_s += 1;
719 }
720
721 let mut years = end_y - start_y;
722 let mut months = end_mo - start_mo;
723 let mut days = end_d - start_d;
724 let mut hours = end_h - start_h;
725 let mut minutes = end_mi - start_mi;
726 let mut seconds = end_s - start_s;
727 let mut millis = end_ms - start_ms;
728
729 if millis < 0 {
730 millis += 1000;
731 seconds -= 1;
732 }
733 if seconds < 0 {
734 seconds += 60;
735 minutes -= 1;
736 }
737 if minutes < 0 {
738 minutes += 60;
739 hours -= 1;
740 }
741 if hours < 0 {
742 hours += 24;
743 days -= 1;
744 }
745 if days < 0 {
746 months -= 1;
747 let (borrow_y, borrow_mo) = if end_mo == 1 {
748 (end_y - 1, 12)
749 } else {
750 (end_y, end_mo - 1)
751 };
752 days += days_in_month(borrow_y, borrow_mo);
753 }
754 if months < 0 {
755 months += 12;
756 years -= 1;
757 }
758
759 format!(
760 "{sign}{years:04}-{months:02}-{days:02} {hours:02}:{minutes:02}:{seconds:02}.{millis:03}"
761 )
762}
763
764fn parse_args(args: &[SqliteValue]) -> Option<(f64, bool)> {
768 if args.is_empty() || args[0].is_null() {
769 return None;
770 }
771
772 let input = match &args[0] {
773 SqliteValue::Text(s) => parse_timestring(s)?,
774 SqliteValue::Integer(i) => *i as f64,
775 SqliteValue::Float(f) => *f,
776 _ => return None,
777 };
778
779 let modifiers: Vec<String> = args[1..]
780 .iter()
781 .filter_map(|a| if a.is_null() { None } else { Some(a.to_text()) })
782 .collect();
783
784 apply_modifiers(input, &modifiers)
785}
786
787pub struct DateFunc;
790
791impl ScalarFunction for DateFunc {
792 fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
793 match parse_args(args) {
794 Some((jdn, _)) => Ok(SqliteValue::Text(format_date(jdn).into())),
795 None => Ok(SqliteValue::Null),
796 }
797 }
798
799 fn num_args(&self) -> i32 {
800 -1
801 }
802
803 fn name(&self) -> &str {
804 "date"
805 }
806}
807
808pub struct TimeFunc;
811
812impl ScalarFunction for TimeFunc {
813 fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
814 match parse_args(args) {
815 Some((jdn, subsec)) => Ok(SqliteValue::Text(format_time(jdn, subsec).into())),
816 None => Ok(SqliteValue::Null),
817 }
818 }
819
820 fn num_args(&self) -> i32 {
821 -1
822 }
823
824 fn name(&self) -> &str {
825 "time"
826 }
827}
828
829pub struct DateTimeFunc;
832
833impl ScalarFunction for DateTimeFunc {
834 fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
835 match parse_args(args) {
836 Some((jdn, subsec)) => Ok(SqliteValue::Text(format_datetime(jdn, subsec).into())),
837 None => Ok(SqliteValue::Null),
838 }
839 }
840
841 fn num_args(&self) -> i32 {
842 -1
843 }
844
845 fn name(&self) -> &str {
846 "datetime"
847 }
848}
849
850pub struct JuliandayFunc;
853
854impl ScalarFunction for JuliandayFunc {
855 fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
856 match parse_args(args) {
857 Some((jdn, _)) => Ok(SqliteValue::Float(jdn)),
858 None => Ok(SqliteValue::Null),
859 }
860 }
861
862 fn num_args(&self) -> i32 {
863 -1
864 }
865
866 fn name(&self) -> &str {
867 "julianday"
868 }
869}
870
871pub struct UnixepochFunc;
874
875impl ScalarFunction for UnixepochFunc {
876 fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
877 match parse_args(args) {
878 Some((jdn, _)) => Ok(SqliteValue::Integer(jdn_to_unix(jdn))),
879 None => Ok(SqliteValue::Null),
880 }
881 }
882
883 fn num_args(&self) -> i32 {
884 -1
885 }
886
887 fn name(&self) -> &str {
888 "unixepoch"
889 }
890}
891
892pub struct StrftimeFunc;
895
896impl ScalarFunction for StrftimeFunc {
897 fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
898 if args.len() < 2 || args[0].is_null() || args[1].is_null() {
899 return Ok(SqliteValue::Null);
900 }
901 let fmt = args[0].to_text();
902 let rest = &args[1..];
903 match parse_args(rest) {
904 Some((jdn, _)) => Ok(SqliteValue::Text(format_strftime(&fmt, jdn).into())),
905 None => Ok(SqliteValue::Null),
906 }
907 }
908
909 fn num_args(&self) -> i32 {
910 -1
911 }
912
913 fn name(&self) -> &str {
914 "strftime"
915 }
916}
917
918pub struct TimediffFunc;
921
922impl ScalarFunction for TimediffFunc {
923 fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
924 if args.len() < 2 || args[0].is_null() || args[1].is_null() {
925 return Ok(SqliteValue::Null);
926 }
927
928 let jdn1 = match &args[0] {
929 SqliteValue::Text(s) => parse_timestring(s),
930 SqliteValue::Integer(i) => Some(*i as f64),
931 SqliteValue::Float(f) => Some(*f),
932 _ => None,
933 };
934 let jdn2 = match &args[1] {
935 SqliteValue::Text(s) => parse_timestring(s),
936 SqliteValue::Integer(i) => Some(*i as f64),
937 SqliteValue::Float(f) => Some(*f),
938 _ => None,
939 };
940
941 match (jdn1, jdn2) {
942 (Some(j1), Some(j2)) => Ok(SqliteValue::Text(timediff_impl(j1, j2).into())),
943 _ => Ok(SqliteValue::Null),
944 }
945 }
946
947 fn num_args(&self) -> i32 {
948 2
949 }
950
951 fn name(&self) -> &str {
952 "timediff"
953 }
954}
955
956pub fn register_datetime_builtins(registry: &mut FunctionRegistry) {
960 registry.register_scalar(DateFunc);
961 registry.register_scalar(TimeFunc);
962 registry.register_scalar(DateTimeFunc);
963 registry.register_scalar(JuliandayFunc);
964 registry.register_scalar(UnixepochFunc);
965 registry.register_scalar(StrftimeFunc);
966 registry.register_scalar(TimediffFunc);
967}
968
969#[cfg(test)]
972mod tests {
973 use super::*;
974
975 fn text(s: &str) -> SqliteValue {
976 SqliteValue::Text(s.into())
977 }
978
979 fn int(v: i64) -> SqliteValue {
980 SqliteValue::Integer(v)
981 }
982
983 fn float(v: f64) -> SqliteValue {
984 SqliteValue::Float(v)
985 }
986
987 fn null() -> SqliteValue {
988 SqliteValue::Null
989 }
990
991 fn assert_text(result: &SqliteValue, expected: &str) {
992 match result {
993 SqliteValue::Text(s) => assert_eq!(s.as_ref(), expected, "text mismatch"),
994 other => panic!("expected Text(\"{expected}\"), got {other:?}"),
995 }
996 }
997
998 #[test]
1001 fn test_date_basic() {
1002 let r = DateFunc.invoke(&[text("2024-03-15 14:30:00")]).unwrap();
1003 assert_text(&r, "2024-03-15");
1004 }
1005
1006 #[test]
1007 fn test_time_basic() {
1008 let r = TimeFunc.invoke(&[text("2024-03-15 14:30:45")]).unwrap();
1009 assert_text(&r, "14:30:45");
1010 }
1011
1012 #[test]
1013 fn test_datetime_basic() {
1014 let r = DateTimeFunc.invoke(&[text("2024-03-15 14:30:00")]).unwrap();
1015 assert_text(&r, "2024-03-15 14:30:00");
1016 }
1017
1018 #[test]
1019 fn test_julianday_basic() {
1020 let r = JuliandayFunc.invoke(&[text("2024-03-15")]).unwrap();
1021 match r {
1022 SqliteValue::Float(jdn) => {
1023 assert!((jdn - 2_460_384.5).abs() < 0.01, "unexpected JDN: {jdn}");
1025 }
1026 other => panic!("expected Float, got {other:?}"),
1027 }
1028 }
1029
1030 #[test]
1031 fn test_unixepoch_basic() {
1032 let r = UnixepochFunc
1033 .invoke(&[text("1970-01-01 00:00:00")])
1034 .unwrap();
1035 assert_eq!(r, int(0));
1036 }
1037
1038 #[test]
1039 fn test_unixepoch_known_date() {
1040 let r = UnixepochFunc
1041 .invoke(&[text("2024-01-01 00:00:00")])
1042 .unwrap();
1043 assert_eq!(r, int(1_704_067_200));
1045 }
1046
1047 #[test]
1050 fn test_modifier_days() {
1051 let r = DateFunc
1052 .invoke(&[text("2024-01-15"), text("+10 days")])
1053 .unwrap();
1054 assert_text(&r, "2024-01-25");
1055 }
1056
1057 #[test]
1058 fn test_modifier_months() {
1059 let r = DateFunc
1062 .invoke(&[text("2024-01-31"), text("+1 months")])
1063 .unwrap();
1064 assert_text(&r, "2024-03-02");
1065 }
1066
1067 #[test]
1068 fn test_modifier_years() {
1069 let r = DateFunc
1072 .invoke(&[text("2024-02-29"), text("+1 years")])
1073 .unwrap();
1074 assert_text(&r, "2025-03-01");
1075 }
1076
1077 #[test]
1078 fn test_modifier_hours() {
1079 let r = DateTimeFunc
1080 .invoke(&[text("2024-01-01 23:00:00"), text("+2 hours")])
1081 .unwrap();
1082 assert_text(&r, "2024-01-02 01:00:00");
1083 }
1084
1085 #[test]
1086 fn test_modifier_start_of_month() {
1087 let r = DateFunc
1088 .invoke(&[text("2024-03-15"), text("start of month")])
1089 .unwrap();
1090 assert_text(&r, "2024-03-01");
1091 }
1092
1093 #[test]
1094 fn test_modifier_start_of_year() {
1095 let r = DateFunc
1096 .invoke(&[text("2024-06-15"), text("start of year")])
1097 .unwrap();
1098 assert_text(&r, "2024-01-01");
1099 }
1100
1101 #[test]
1102 fn test_modifier_start_of_day() {
1103 let r = DateTimeFunc
1104 .invoke(&[text("2024-03-15 14:30:00"), text("start of day")])
1105 .unwrap();
1106 assert_text(&r, "2024-03-15 00:00:00");
1107 }
1108
1109 #[test]
1110 fn test_modifier_unixepoch() {
1111 let r = DateTimeFunc.invoke(&[int(0), text("unixepoch")]).unwrap();
1112 assert_text(&r, "1970-01-01 00:00:00");
1113 }
1114
1115 #[test]
1116 fn test_modifier_weekday() {
1117 let r = DateFunc
1119 .invoke(&[text("2024-03-15"), text("weekday 0")])
1120 .unwrap();
1121 assert_text(&r, "2024-03-17");
1122 }
1123
1124 #[test]
1125 fn test_modifier_auto_unixepoch() {
1126 let ts = int(1_710_531_045);
1127 let r = DateTimeFunc.invoke(&[ts.clone(), text("auto")]).unwrap();
1128 let expected = DateTimeFunc.invoke(&[ts, text("unixepoch")]).unwrap();
1129 assert_eq!(
1130 r, expected,
1131 "auto and unixepoch should agree for unix-like values"
1132 );
1133 }
1134
1135 #[test]
1136 fn test_modifier_auto_julian_day() {
1137 let r = DateFunc
1138 .invoke(&[float(2_460_384.5), text("auto")])
1139 .unwrap();
1140 assert_text(&r, "2024-03-15");
1141 }
1142
1143 #[test]
1144 fn test_modifier_localtime_utc_roundtrip() {
1145 let r = DateTimeFunc
1147 .invoke(&[text("2024-03-15 14:30:45"), text("localtime"), text("utc")])
1148 .unwrap();
1149 assert_text(&r, "2024-03-15 14:30:45");
1150 }
1151
1152 #[test]
1153 fn test_modifier_localtime_shifts_value() {
1154 let offset = utc_offset_seconds();
1156 if offset != 0 {
1157 let r = DateTimeFunc
1158 .invoke(&[text("2024-03-15 12:00:00"), text("localtime")])
1159 .unwrap();
1160 let shifted = match &r {
1162 SqliteValue::Text(s) => s.clone(),
1163 _ => panic!("expected text"),
1164 };
1165 assert_ne!(&*shifted, "2024-03-15 12:00:00");
1166 }
1167 }
1168
1169 #[test]
1170 fn test_modifier_auto_out_of_range_returns_null() {
1171 let r = DateTimeFunc.invoke(&[float(1.0e20), text("auto")]).unwrap();
1172 assert_eq!(r, SqliteValue::Null);
1173 }
1174
1175 #[test]
1176 fn test_modifier_order_matters() {
1177 let r1 = DateFunc
1179 .invoke(&[text("2024-03-15"), text("start of month"), text("+1 days")])
1180 .unwrap();
1181 assert_text(&r1, "2024-03-02");
1182
1183 let r2 = DateFunc
1185 .invoke(&[text("2024-03-15"), text("+1 days"), text("start of month")])
1186 .unwrap();
1187 assert_text(&r2, "2024-03-01");
1188 }
1189
1190 #[test]
1191 fn test_modifier_weekday_same_day_is_noop() {
1192 let r = DateFunc
1194 .invoke(&[text("2024-03-17"), text("weekday 0")])
1195 .unwrap();
1196 assert_text(&r, "2024-03-17");
1197 }
1198
1199 #[test]
1202 fn test_bare_time_defaults() {
1203 let r = DateFunc.invoke(&[text("12:30:00")]).unwrap();
1204 assert_text(&r, "2000-01-01");
1205 }
1206
1207 #[test]
1208 fn test_t_separator() {
1209 let r = DateTimeFunc.invoke(&[text("2024-03-15T14:30:00")]).unwrap();
1210 assert_text(&r, "2024-03-15 14:30:00");
1211 }
1212
1213 #[test]
1214 fn test_julian_day_input() {
1215 let r = DateFunc.invoke(&[float(2_460_384.5)]).unwrap();
1217 assert_text(&r, "2024-03-15");
1218 }
1219
1220 #[test]
1221 fn test_null_input() {
1222 assert_eq!(DateFunc.invoke(&[null()]).unwrap(), SqliteValue::Null);
1223 }
1224
1225 #[test]
1226 fn test_invalid_input() {
1227 assert_eq!(
1228 DateFunc.invoke(&[text("not-a-date")]).unwrap(),
1229 SqliteValue::Null
1230 );
1231 }
1232
1233 #[test]
1234 fn test_negative_time_component_invalid() {
1235 let r = TimeFunc.invoke(&[text("-01:00")]).unwrap();
1236 assert_eq!(r, SqliteValue::Null);
1237 }
1238
1239 #[test]
1242 fn test_leap_year() {
1243 let r = DateFunc
1244 .invoke(&[text("2024-02-28"), text("+1 days")])
1245 .unwrap();
1246 assert_text(&r, "2024-02-29");
1247 }
1248
1249 #[test]
1250 fn test_non_leap_year() {
1251 let r = DateFunc
1252 .invoke(&[text("2023-02-28"), text("+1 days")])
1253 .unwrap();
1254 assert_text(&r, "2023-03-01");
1255 }
1256
1257 #[test]
1260 fn test_strftime_basic() {
1261 let r = StrftimeFunc
1262 .invoke(&[text("%Y-%m-%d"), text("2024-03-15")])
1263 .unwrap();
1264 assert_text(&r, "2024-03-15");
1265 }
1266
1267 #[test]
1268 fn test_strftime_time_specifiers() {
1269 let r = StrftimeFunc
1270 .invoke(&[text("%H:%M:%S"), text("2024-03-15 14:30:45")])
1271 .unwrap();
1272 assert_text(&r, "14:30:45");
1273 }
1274
1275 #[test]
1276 fn test_strftime_unix_seconds() {
1277 let r = StrftimeFunc
1278 .invoke(&[text("%s"), text("1970-01-01 00:00:00")])
1279 .unwrap();
1280 assert_text(&r, "0");
1281 }
1282
1283 #[test]
1284 fn test_strftime_day_of_year() {
1285 let r = StrftimeFunc
1286 .invoke(&[text("%j"), text("2024-03-15")])
1287 .unwrap();
1288 assert_text(&r, "075");
1290 }
1291
1292 #[test]
1293 fn test_strftime_day_of_week() {
1294 let r = StrftimeFunc
1296 .invoke(&[text("%w"), text("2024-03-15")])
1297 .unwrap();
1298 assert_text(&r, "5");
1299
1300 let r = StrftimeFunc
1301 .invoke(&[text("%u"), text("2024-03-15")])
1302 .unwrap();
1303 assert_text(&r, "5");
1304 }
1305
1306 #[test]
1307 fn test_strftime_12hour() {
1308 let r = StrftimeFunc
1309 .invoke(&[text("%I %p"), text("2024-03-15 14:30:00")])
1310 .unwrap();
1311 assert_text(&r, "02 PM");
1312
1313 let r = StrftimeFunc
1314 .invoke(&[text("%I %P"), text("2024-03-15 09:30:00")])
1315 .unwrap();
1316 assert_text(&r, "09 am");
1317 }
1318
1319 #[test]
1320 fn test_strftime_all_specifiers_presence() {
1321 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|%%";
1322 let r = StrftimeFunc
1323 .invoke(&[text(fmt), text("2024-03-15 14:30:45.123")])
1324 .unwrap();
1325
1326 let s = match r {
1327 SqliteValue::Text(v) => v,
1328 other => panic!("expected Text, got {other:?}"),
1329 };
1330 let parts: Vec<&str> = s.split('|').collect();
1331 assert_eq!(parts.len(), 25, "unexpected specifier output: {s}");
1332 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!(
1339 parts[6].parse::<f64>().is_ok(),
1340 "expected numeric %J output, got {}",
1341 parts[6]
1342 );
1343 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!(
1351 parts[14].parse::<i64>().is_ok(),
1352 "expected numeric %s output, got {}",
1353 parts[14]
1354 );
1355 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], "%"); }
1366
1367 #[test]
1368 fn test_strftime_null() {
1369 assert_eq!(
1370 StrftimeFunc.invoke(&[null(), text("2024-01-01")]).unwrap(),
1371 SqliteValue::Null
1372 );
1373 assert_eq!(
1374 StrftimeFunc.invoke(&[text("%Y"), null()]).unwrap(),
1375 SqliteValue::Null
1376 );
1377 }
1378
1379 #[test]
1382 fn test_timediff_basic() {
1383 let r = TimediffFunc
1384 .invoke(&[text("2024-03-15"), text("2024-03-10")])
1385 .unwrap();
1386 assert_text(&r, "+0000-00-05 00:00:00.000");
1387 }
1388
1389 #[test]
1390 fn test_timediff_negative() {
1391 let r = TimediffFunc
1392 .invoke(&[text("2024-03-10"), text("2024-03-15")])
1393 .unwrap();
1394 assert_text(&r, "-0000-00-05 00:00:00.000");
1395 }
1396
1397 #[test]
1398 fn test_timediff_year_boundary() {
1399 let r = TimediffFunc
1400 .invoke(&[text("2024-01-01 01:00:00"), text("2023-12-31 23:00:00")])
1401 .unwrap();
1402 assert_text(&r, "+0000-00-00 02:00:00.000");
1403 }
1404
1405 #[test]
1408 fn test_modifier_subsec() {
1409 let r = TimeFunc
1410 .invoke(&[text("2024-01-01 12:00:00.123"), text("subsec")])
1411 .unwrap();
1412 match &r {
1413 SqliteValue::Text(s) => assert!(
1414 s.contains('.'),
1415 "expected fractional seconds with subsec: {s}"
1416 ),
1417 other => panic!("expected Text, got {other:?}"),
1418 }
1419 }
1420
1421 #[test]
1424 fn test_register_datetime_builtins_all_present() {
1425 let mut reg = FunctionRegistry::new();
1426 register_datetime_builtins(&mut reg);
1427
1428 let expected = [
1429 "date",
1430 "time",
1431 "datetime",
1432 "julianday",
1433 "unixepoch",
1434 "strftime",
1435 "timediff",
1436 ];
1437
1438 for name in expected {
1439 assert!(
1440 reg.find_scalar(name, 1).is_some() || reg.find_scalar(name, 2).is_some(),
1441 "datetime function '{name}' not registered"
1442 );
1443 }
1444 }
1445
1446 #[test]
1449 fn test_modifier_year_overflow() {
1450 let huge = i64::MAX;
1453 let modifier = format!("+{huge} years");
1454 let r = DateFunc.invoke(&[text("2000-01-01"), text(&modifier)]);
1455 assert_eq!(r.unwrap(), SqliteValue::Null);
1458 }
1459
1460 #[test]
1461 fn test_jdn_roundtrip() {
1462 let dates = [
1464 (2024, 3, 15),
1465 (2000, 1, 1),
1466 (1970, 1, 1),
1467 (2024, 2, 29),
1468 (1900, 1, 1),
1469 (2099, 12, 31),
1470 ];
1471 for (y, m, d) in dates {
1472 let jdn = ymd_to_jdn(y, m, d);
1473 let (y2, m2, d2) = jdn_to_ymd(jdn);
1474 assert_eq!(
1475 (y, m, d),
1476 (y2, m2, d2),
1477 "roundtrip failed for {y}-{m}-{d} (JDN={jdn})"
1478 );
1479 }
1480 }
1481
1482 #[test]
1483 fn test_unix_epoch_roundtrip() {
1484 let jdn = ymd_to_jdn(1970, 1, 1);
1485 let unix = jdn_to_unix(jdn);
1486 assert_eq!(unix, 0, "Unix epoch should be 0");
1487
1488 let jdn2 = unix_to_jdn(0.0);
1489 assert!((jdn2 - UNIX_EPOCH_JDN).abs() < 1e-10, "roundtrip failed");
1490 }
1491}