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 ymd_to_jdn(y: i64, m: i64, d: i64) -> f64 {
45 let (y, m) = if m <= 2 { (y - 1, m + 12) } else { (y, m) };
46 let a = y / 100;
47 let b = 2 - a + a / 4;
48 (365.25 * (y + 4716) as f64).floor() + (30.6001 * (m + 1) as f64).floor() + d as f64 + b as f64
49 - 1524.5
50}
51
52fn jdn_to_ymd(jdn: f64) -> (i64, i64, i64) {
54 let z = (jdn + 0.5).floor() as i64;
55 let a = if z < 2_299_161 {
56 z
57 } else {
58 let alpha = ((z as f64 - 1_867_216.25) / 36524.25).floor() as i64;
59 z + 1 + alpha - alpha / 4
60 };
61 let b = a + 1524;
62 let c = ((b as f64 - 122.1) / 365.25).floor() as i64;
63 let d = (365.25 * c as f64).floor() as i64;
64 let e = ((b - d) as f64 / 30.6001).floor() as i64;
65
66 let day = b - d - (30.6001 * e as f64).floor() as i64;
67 let month = if e < 14 { e - 1 } else { e - 13 };
68 let year = if month > 2 { c - 4716 } else { c - 4715 };
69 (year, month, day)
70}
71
72fn jdn_to_hms(jdn: f64) -> (i64, i64, i64, f64) {
74 let frac = jdn + 0.5 - (jdn + 0.5).floor();
75 let total_ms = (frac * 86_400_000.0).round() as i64;
77 let h = total_ms / 3_600_000;
78 let rem = total_ms % 3_600_000;
79 let m = rem / 60_000;
80 let rem = rem % 60_000;
81 let s = rem / 1000;
82 let ms_frac = (rem % 1000) as f64 / 1000.0;
83 (h, m, s, ms_frac)
84}
85
86fn ymdhms_to_jdn(y: i64, mo: i64, d: i64, h: i64, mi: i64, s: i64, frac: f64) -> f64 {
88 ymd_to_jdn(y, mo, d) + (h as f64 * 3600.0 + mi as f64 * 60.0 + s as f64 + frac) / 86400.0
89}
90
91const UNIX_EPOCH_JDN: f64 = 2_440_587.5;
93const AUTO_JDN_MAX: f64 = 5_373_484.499_999;
95const AUTO_UNIX_MIN: f64 = -210_866_760_000.0;
97const AUTO_UNIX_MAX: f64 = 253_402_300_799.0;
98
99fn jdn_to_unix(jdn: f64) -> i64 {
100 ((jdn - UNIX_EPOCH_JDN) * 86400.0).round() as i64
101}
102
103fn unix_to_jdn(ts: f64) -> f64 {
104 ts / 86400.0 + UNIX_EPOCH_JDN
105}
106
107fn is_leap_year(y: i64) -> bool {
108 (y % 4 == 0 && y % 100 != 0) || y % 400 == 0
109}
110
111fn days_in_month(y: i64, m: i64) -> i64 {
112 match m {
113 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
114 4 | 6 | 9 | 11 => 30,
115 2 => {
116 if is_leap_year(y) {
117 29
118 } else {
119 28
120 }
121 }
122 _ => 30,
123 }
124}
125
126fn day_of_year(y: i64, m: i64, d: i64) -> i64 {
127 let mut doy = d;
128 for mo in 1..m {
129 doy += days_in_month(y, mo);
130 }
131 doy
132}
133
134fn parse_timestring(s: &str) -> Option<f64> {
138 let s = s.trim();
139
140 if s.eq_ignore_ascii_case("now") {
143 return Some(ymd_to_jdn(2000, 1, 1));
145 }
146
147 if let Ok(jdn) = s.parse::<f64>() {
149 if jdn >= 0.0 {
150 return Some(jdn);
151 }
152 }
153
154 parse_iso8601(s)
156}
157
158fn parse_iso8601(s: &str) -> Option<f64> {
159 let bytes = s.as_bytes();
167 let len = bytes.len();
168
169 if len >= 10 && bytes[4] == b'-' && bytes[7] == b'-' {
171 let y = s[0..4].parse::<i64>().ok()?;
172 let m = s[5..7].parse::<i64>().ok()?;
173 let d = s[8..10].parse::<i64>().ok()?;
174
175 if m < 1 || m > 12 || d < 1 || d > days_in_month(y, m) {
176 return None;
177 }
178
179 if len == 10 {
180 return Some(ymd_to_jdn(y, m, d));
181 }
182
183 if len > 10 && (bytes[10] == b' ' || bytes[10] == b'T') {
185 let time_part = &s[11..];
186 let (h, mi, sec, frac) = parse_time_part(time_part)?;
187 return Some(ymdhms_to_jdn(y, m, d, h, mi, sec, frac));
188 }
189 return None;
190 }
191
192 if len >= 5 && bytes[2] == b':' {
194 let (h, mi, sec, frac) = parse_time_part(s)?;
195 return Some(ymdhms_to_jdn(2000, 1, 1, h, mi, sec, frac));
196 }
197
198 None
199}
200
201fn parse_time_part(s: &str) -> Option<(i64, i64, i64, f64)> {
203 let parts: Vec<&str> = s.splitn(3, ':').collect();
204 if parts.len() < 2 {
205 return None;
206 }
207 let h = parts[0].parse::<i64>().ok()?;
208 let mi = parts[1].parse::<i64>().ok()?;
209 if !(0..=23).contains(&h) || !(0..=59).contains(&mi) {
210 return None;
211 }
212
213 if parts.len() == 2 {
214 return Some((h, mi, 0, 0.0));
215 }
216
217 let sec_str = parts[2];
219 if let Some(dot_pos) = sec_str.find('.') {
220 let sec = sec_str[..dot_pos].parse::<i64>().ok()?;
221 let frac_str = &sec_str[dot_pos..]; let frac = frac_str.parse::<f64>().ok()?;
223 if !(0..=59).contains(&sec) {
224 return None;
225 }
226 Some((h, mi, sec, frac))
227 } else {
228 let sec = sec_str.parse::<i64>().ok()?;
229 if !(0..=59).contains(&sec) {
230 return None;
231 }
232 Some((h, mi, sec, 0.0))
233 }
234}
235
236fn apply_modifier(jdn: f64, modifier: &str) -> Option<f64> {
240 let m = modifier.trim().to_ascii_lowercase();
241
242 if m == "start of month" {
244 let (y, mo, _d) = jdn_to_ymd(jdn);
245 return Some(ymd_to_jdn(y, mo, 1));
246 }
247 if m == "start of year" {
248 let (y, _mo, _d) = jdn_to_ymd(jdn);
249 return Some(ymd_to_jdn(y, 1, 1));
250 }
251 if m == "start of day" {
252 let (y, mo, d) = jdn_to_ymd(jdn);
253 return Some(ymd_to_jdn(y, mo, d));
254 }
255
256 if m == "unixepoch" {
258 return Some(unix_to_jdn(jdn));
259 }
260
261 if m == "julianday" {
264 return Some(jdn);
265 }
266
267 if m == "auto" {
272 if (0.0..=AUTO_JDN_MAX).contains(&jdn) {
273 return Some(jdn);
274 }
275 if (AUTO_UNIX_MIN..=AUTO_UNIX_MAX).contains(&jdn) {
276 return Some(unix_to_jdn(jdn));
277 }
278 return None;
279 }
280
281 if m == "localtime" || m == "utc" {
284 return Some(jdn);
285 }
286
287 if m == "subsec" || m == "subsecond" {
290 return Some(jdn);
291 }
292
293 if let Some(rest) = m.strip_prefix("weekday ") {
295 let wd = rest.trim().parse::<i64>().ok()?;
296 if !(0..=6).contains(&wd) {
297 return None;
298 }
299 let current_jdn_int = (jdn + 0.5).floor() as i64;
301 let current_wd = (current_jdn_int + 1) % 7; let mut diff = wd - current_wd;
303 if diff <= 0 {
304 diff += 7;
305 }
306 return Some(jdn + diff as f64);
308 }
309
310 parse_arithmetic_modifier(&m).map(|delta| jdn + delta)
312}
313
314fn parse_arithmetic_modifier(m: &str) -> Option<f64> {
316 let (sign, rest) = if let Some(r) = m.strip_prefix('+') {
317 (1.0, r.trim())
318 } else if let Some(r) = m.strip_prefix('-') {
319 (-1.0, r.trim())
320 } else {
321 return None;
322 };
323
324 let mut parts = rest.splitn(2, ' ');
325 let num_str = parts.next()?;
326 let unit = parts.next()?.trim();
327
328 let num = num_str.parse::<f64>().ok()?;
329 let delta = num * sign;
330
331 match unit.trim_end_matches('s') {
332 "day" => Some(delta),
333 "hour" => Some(delta / 24.0),
334 "minute" => Some(delta / 1440.0),
335 "second" => Some(delta / 86400.0),
336 "month" => Some(apply_month_delta(delta)),
337 "year" => Some(apply_month_delta(delta * 12.0)),
338 _ => None,
339 }
340}
341
342fn apply_month_delta(months: f64) -> f64 {
347 months * 30.436875
349}
350
351fn apply_modifiers(jdn: f64, modifiers: &[String]) -> Option<(f64, bool)> {
353 let mut j = jdn;
354 let mut subsec = false;
355 for m in modifiers {
356 let m_lower = m.trim().to_ascii_lowercase();
357 if m_lower == "subsec" || m_lower == "subsecond" {
358 subsec = true;
359 }
360 if is_month_year_modifier(&m_lower) {
362 j = apply_month_year_exact(j, &m_lower)?;
363 } else {
364 j = apply_modifier(j, m)?;
365 }
366 }
367 Some((j, subsec))
368}
369
370fn is_month_year_modifier(m: &str) -> bool {
371 (m.contains("month") || m.contains("year")) && (m.starts_with('+') || m.starts_with('-'))
372}
373
374fn apply_month_year_exact(jdn: f64, m: &str) -> Option<f64> {
376 let (sign, rest) = if let Some(r) = m.strip_prefix('+') {
377 (1_i64, r.trim())
378 } else if let Some(r) = m.strip_prefix('-') {
379 (-1_i64, r.trim())
380 } else {
381 return None;
382 };
383
384 let mut parts = rest.splitn(2, ' ');
385 let num_str = parts.next()?;
386 let unit = parts.next()?.trim();
387 let num = num_str.parse::<i64>().ok()?;
388
389 let (y, mo, d) = jdn_to_ymd(jdn);
390 let (h, mi, s, frac) = jdn_to_hms(jdn);
391
392 let total_months = match unit.trim_end_matches('s') {
393 "month" => num.checked_mul(sign)?,
394 "year" => num.checked_mul(sign)?.checked_mul(12)?,
395 _ => return None,
396 };
397
398 let current_months = y.checked_mul(12)?.checked_add(mo - 1)?;
400 let new_total = current_months.checked_add(total_months)?;
401
402 let new_y = new_total.div_euclid(12);
403 let new_mo = new_total.rem_euclid(12) + 1;
404 let new_d = d.min(days_in_month(new_y, new_mo));
405
406 Some(ymdhms_to_jdn(new_y, new_mo, new_d, h, mi, s, frac))
407}
408
409fn format_date(jdn: f64) -> String {
412 let (y, m, d) = jdn_to_ymd(jdn);
413 format!("{y:04}-{m:02}-{d:02}")
414}
415
416fn format_time(jdn: f64, subsec: bool) -> String {
417 let (h, m, s, frac) = jdn_to_hms(jdn);
418 if subsec && frac > 1e-9 {
419 format!("{h:02}:{m:02}:{s:02}.{:03}", (frac * 1000.0).round() as i64)
420 } else {
421 format!("{h:02}:{m:02}:{s:02}")
422 }
423}
424
425fn format_datetime(jdn: f64, subsec: bool) -> String {
426 format!("{} {}", format_date(jdn), format_time(jdn, subsec))
427}
428
429fn format_strftime(fmt: &str, jdn: f64) -> String {
431 let (y, mo, d) = jdn_to_ymd(jdn);
432 let (h, mi, s, frac) = jdn_to_hms(jdn);
433 let doy = day_of_year(y, mo, d);
434 let jdn_int = (jdn + 0.5).floor() as i64;
436 let dow = (jdn_int + 1) % 7; let mut result = String::new();
439 let chars: Vec<char> = fmt.chars().collect();
440 let mut i = 0;
441
442 while i < chars.len() {
443 if chars[i] == '%' && i + 1 < chars.len() {
444 i += 1;
445 match chars[i] {
446 'd' => result.push_str(&format!("{d:02}")),
447 'e' => result.push_str(&format!("{d:>2}")), 'f' => {
449 let total = s as f64 + frac;
451 result.push_str(&format!("{total:06.3}"));
452 }
453 'H' => result.push_str(&format!("{h:02}")),
454 'I' => {
455 let h12 = if h == 0 {
457 12
458 } else if h > 12 {
459 h - 12
460 } else {
461 h
462 };
463 result.push_str(&format!("{h12:02}"));
464 }
465 'j' => result.push_str(&format!("{doy:03}")),
466 'J' => result.push_str(&format!("{jdn:.6}")),
467 'k' => {
468 result.push_str(&format!("{h:>2}"));
470 }
471 'l' => {
472 let h12 = if h == 0 {
474 12
475 } else if h > 12 {
476 h - 12
477 } else {
478 h
479 };
480 result.push_str(&format!("{h12:>2}"));
481 }
482 'm' => result.push_str(&format!("{mo:02}")),
483 'M' => result.push_str(&format!("{mi:02}")),
484 'p' => {
485 result.push_str(if h < 12 { "AM" } else { "PM" });
486 }
487 'P' => {
488 result.push_str(if h < 12 { "am" } else { "pm" });
489 }
490 'R' => result.push_str(&format!("{h:02}:{mi:02}")),
491 's' => {
492 let unix = jdn_to_unix(jdn);
493 result.push_str(&unix.to_string());
494 }
495 'S' => result.push_str(&format!("{s:02}")),
496 'T' => result.push_str(&format!("{h:02}:{mi:02}:{s:02}")),
497 'u' => {
498 let u = if dow == 0 { 7 } else { dow };
500 result.push_str(&u.to_string());
501 }
502 'w' => result.push_str(&dow.to_string()),
503 'W' => {
504 let w = (doy + 6 - ((dow + 6) % 7)) / 7;
506 result.push_str(&format!("{w:02}"));
507 }
508 'Y' => result.push_str(&format!("{y:04}")),
509 'G' | 'g' | 'V' => {
510 let (iso_y, iso_w) = iso_week(y, mo, d);
512 match chars[i] {
513 'G' => result.push_str(&format!("{iso_y:04}")),
514 'g' => result.push_str(&format!("{:02}", iso_y % 100)),
515 'V' => result.push_str(&format!("{iso_w:02}")),
516 _ => unreachable!(),
517 }
518 }
519 '%' => result.push('%'),
520 other => {
521 result.push('%');
522 result.push(other);
523 }
524 }
525 } else {
526 result.push(chars[i]);
527 }
528 i += 1;
529 }
530
531 result
532}
533
534fn iso_week(y: i64, m: i64, d: i64) -> (i64, i64) {
536 let jdn = ymd_to_jdn(y, m, d);
537 let jdn_int = (jdn + 0.5).floor() as i64;
538 let dow = (jdn_int + 1) % 7;
540 let iso_dow = if dow == 0 { 7 } else { dow };
541
542 let thu_jdn = jdn_int + (4 - iso_dow);
544 let (thu_y, _, _) = jdn_to_ymd(thu_jdn as f64);
545
546 let jan4_jdn = (ymd_to_jdn(thu_y, 1, 4) + 0.5).floor() as i64;
548 let jan4_dow = (jan4_jdn + 1) % 7;
549 let jan4_iso_dow = if jan4_dow == 0 { 7 } else { jan4_dow };
550 let week1_start = jan4_jdn - (jan4_iso_dow - 1);
551
552 let week = (thu_jdn - week1_start) / 7 + 1;
553 (thu_y, week)
554}
555
556fn timediff_impl(jdn1: f64, jdn2: f64) -> String {
559 let (sign, start_jdn, end_jdn) = if jdn1 >= jdn2 {
560 ('+', jdn2, jdn1)
561 } else {
562 ('-', jdn1, jdn2)
563 };
564
565 let (start_y, start_mo, start_d) = jdn_to_ymd(start_jdn);
566 let (start_h, start_mi, start_s, start_frac) = jdn_to_hms(start_jdn);
567 let mut start_ms = (start_frac * 1000.0).round() as i64;
568 if start_ms >= 1000 {
569 start_ms = 999;
570 }
571
572 let (end_y, end_mo, end_d) = jdn_to_ymd(end_jdn);
573 let (end_h, end_mi, end_s, end_frac) = jdn_to_hms(end_jdn);
574 let mut end_ms = (end_frac * 1000.0).round() as i64;
575 if end_ms >= 1000 {
576 end_ms = 999;
577 }
578
579 let mut years = end_y - start_y;
580 let mut months = end_mo - start_mo;
581 let mut days = end_d - start_d;
582 let mut hours = end_h - start_h;
583 let mut minutes = end_mi - start_mi;
584 let mut seconds = end_s - start_s;
585 let mut millis = end_ms - start_ms;
586
587 if millis < 0 {
588 millis += 1000;
589 seconds -= 1;
590 }
591 if seconds < 0 {
592 seconds += 60;
593 minutes -= 1;
594 }
595 if minutes < 0 {
596 minutes += 60;
597 hours -= 1;
598 }
599 if hours < 0 {
600 hours += 24;
601 days -= 1;
602 }
603 if days < 0 {
604 months -= 1;
605 let (borrow_y, borrow_mo) = if end_mo == 1 {
606 (end_y - 1, 12)
607 } else {
608 (end_y, end_mo - 1)
609 };
610 days += days_in_month(borrow_y, borrow_mo);
611 }
612 if months < 0 {
613 months += 12;
614 years -= 1;
615 }
616
617 format!(
618 "{sign}{years:04}-{months:02}-{days:02} {hours:02}:{minutes:02}:{seconds:02}.{millis:03}"
619 )
620}
621
622fn parse_args(args: &[SqliteValue]) -> Option<(f64, bool)> {
626 if args.is_empty() || args[0].is_null() {
627 return None;
628 }
629
630 let input = match &args[0] {
631 SqliteValue::Text(s) => parse_timestring(s)?,
632 SqliteValue::Integer(i) => *i as f64,
633 SqliteValue::Float(f) => *f,
634 _ => return None,
635 };
636
637 let modifiers: Vec<String> = args[1..]
638 .iter()
639 .filter_map(|a| if a.is_null() { None } else { Some(a.to_text()) })
640 .collect();
641
642 apply_modifiers(input, &modifiers)
643}
644
645pub struct DateFunc;
648
649impl ScalarFunction for DateFunc {
650 fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
651 match parse_args(args) {
652 Some((jdn, _)) => Ok(SqliteValue::Text(format_date(jdn))),
653 None => Ok(SqliteValue::Null),
654 }
655 }
656
657 fn num_args(&self) -> i32 {
658 -1
659 }
660
661 fn name(&self) -> &str {
662 "date"
663 }
664}
665
666pub struct TimeFunc;
669
670impl ScalarFunction for TimeFunc {
671 fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
672 match parse_args(args) {
673 Some((jdn, subsec)) => Ok(SqliteValue::Text(format_time(jdn, subsec))),
674 None => Ok(SqliteValue::Null),
675 }
676 }
677
678 fn num_args(&self) -> i32 {
679 -1
680 }
681
682 fn name(&self) -> &str {
683 "time"
684 }
685}
686
687pub struct DateTimeFunc;
690
691impl ScalarFunction for DateTimeFunc {
692 fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
693 match parse_args(args) {
694 Some((jdn, subsec)) => Ok(SqliteValue::Text(format_datetime(jdn, subsec))),
695 None => Ok(SqliteValue::Null),
696 }
697 }
698
699 fn num_args(&self) -> i32 {
700 -1
701 }
702
703 fn name(&self) -> &str {
704 "datetime"
705 }
706}
707
708pub struct JuliandayFunc;
711
712impl ScalarFunction for JuliandayFunc {
713 fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
714 match parse_args(args) {
715 Some((jdn, _)) => Ok(SqliteValue::Float(jdn)),
716 None => Ok(SqliteValue::Null),
717 }
718 }
719
720 fn num_args(&self) -> i32 {
721 -1
722 }
723
724 fn name(&self) -> &str {
725 "julianday"
726 }
727}
728
729pub struct UnixepochFunc;
732
733impl ScalarFunction for UnixepochFunc {
734 fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
735 match parse_args(args) {
736 Some((jdn, _)) => Ok(SqliteValue::Integer(jdn_to_unix(jdn))),
737 None => Ok(SqliteValue::Null),
738 }
739 }
740
741 fn num_args(&self) -> i32 {
742 -1
743 }
744
745 fn name(&self) -> &str {
746 "unixepoch"
747 }
748}
749
750pub struct StrftimeFunc;
753
754impl ScalarFunction for StrftimeFunc {
755 fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
756 if args.len() < 2 || args[0].is_null() || args[1].is_null() {
757 return Ok(SqliteValue::Null);
758 }
759 let fmt = args[0].to_text();
760 let rest = &args[1..];
761 match parse_args(rest) {
762 Some((jdn, _)) => Ok(SqliteValue::Text(format_strftime(&fmt, jdn))),
763 None => Ok(SqliteValue::Null),
764 }
765 }
766
767 fn num_args(&self) -> i32 {
768 -1
769 }
770
771 fn name(&self) -> &str {
772 "strftime"
773 }
774}
775
776pub struct TimediffFunc;
779
780impl ScalarFunction for TimediffFunc {
781 fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
782 if args.len() < 2 || args[0].is_null() || args[1].is_null() {
783 return Ok(SqliteValue::Null);
784 }
785
786 let jdn1 = match &args[0] {
787 SqliteValue::Text(s) => parse_timestring(s),
788 SqliteValue::Integer(i) => Some(*i as f64),
789 SqliteValue::Float(f) => Some(*f),
790 _ => None,
791 };
792 let jdn2 = match &args[1] {
793 SqliteValue::Text(s) => parse_timestring(s),
794 SqliteValue::Integer(i) => Some(*i as f64),
795 SqliteValue::Float(f) => Some(*f),
796 _ => None,
797 };
798
799 match (jdn1, jdn2) {
800 (Some(j1), Some(j2)) => Ok(SqliteValue::Text(timediff_impl(j1, j2))),
801 _ => Ok(SqliteValue::Null),
802 }
803 }
804
805 fn num_args(&self) -> i32 {
806 2
807 }
808
809 fn name(&self) -> &str {
810 "timediff"
811 }
812}
813
814pub fn register_datetime_builtins(registry: &mut FunctionRegistry) {
818 registry.register_scalar(DateFunc);
819 registry.register_scalar(TimeFunc);
820 registry.register_scalar(DateTimeFunc);
821 registry.register_scalar(JuliandayFunc);
822 registry.register_scalar(UnixepochFunc);
823 registry.register_scalar(StrftimeFunc);
824 registry.register_scalar(TimediffFunc);
825}
826
827#[cfg(test)]
830mod tests {
831 use super::*;
832
833 fn text(s: &str) -> SqliteValue {
834 SqliteValue::Text(s.to_owned())
835 }
836
837 fn int(v: i64) -> SqliteValue {
838 SqliteValue::Integer(v)
839 }
840
841 fn float(v: f64) -> SqliteValue {
842 SqliteValue::Float(v)
843 }
844
845 fn null() -> SqliteValue {
846 SqliteValue::Null
847 }
848
849 fn assert_text(result: &SqliteValue, expected: &str) {
850 match result {
851 SqliteValue::Text(s) => assert_eq!(s, expected, "text mismatch"),
852 other => panic!("expected Text(\"{expected}\"), got {other:?}"),
853 }
854 }
855
856 #[test]
859 fn test_date_basic() {
860 let r = DateFunc.invoke(&[text("2024-03-15 14:30:00")]).unwrap();
861 assert_text(&r, "2024-03-15");
862 }
863
864 #[test]
865 fn test_time_basic() {
866 let r = TimeFunc.invoke(&[text("2024-03-15 14:30:45")]).unwrap();
867 assert_text(&r, "14:30:45");
868 }
869
870 #[test]
871 fn test_datetime_basic() {
872 let r = DateTimeFunc.invoke(&[text("2024-03-15 14:30:00")]).unwrap();
873 assert_text(&r, "2024-03-15 14:30:00");
874 }
875
876 #[test]
877 fn test_julianday_basic() {
878 let r = JuliandayFunc.invoke(&[text("2024-03-15")]).unwrap();
879 match r {
880 SqliteValue::Float(jdn) => {
881 assert!((jdn - 2_460_384.5).abs() < 0.01, "unexpected JDN: {jdn}");
883 }
884 other => panic!("expected Float, got {other:?}"),
885 }
886 }
887
888 #[test]
889 fn test_unixepoch_basic() {
890 let r = UnixepochFunc
891 .invoke(&[text("1970-01-01 00:00:00")])
892 .unwrap();
893 assert_eq!(r, int(0));
894 }
895
896 #[test]
897 fn test_unixepoch_known_date() {
898 let r = UnixepochFunc
899 .invoke(&[text("2024-01-01 00:00:00")])
900 .unwrap();
901 assert_eq!(r, int(1_704_067_200));
903 }
904
905 #[test]
908 fn test_modifier_days() {
909 let r = DateFunc
910 .invoke(&[text("2024-01-15"), text("+10 days")])
911 .unwrap();
912 assert_text(&r, "2024-01-25");
913 }
914
915 #[test]
916 fn test_modifier_months() {
917 let r = DateFunc
919 .invoke(&[text("2024-01-31"), text("+1 months")])
920 .unwrap();
921 assert_text(&r, "2024-02-29");
922 }
923
924 #[test]
925 fn test_modifier_years() {
926 let r = DateFunc
928 .invoke(&[text("2024-02-29"), text("+1 years")])
929 .unwrap();
930 assert_text(&r, "2025-02-28");
931 }
932
933 #[test]
934 fn test_modifier_hours() {
935 let r = DateTimeFunc
936 .invoke(&[text("2024-01-01 23:00:00"), text("+2 hours")])
937 .unwrap();
938 assert_text(&r, "2024-01-02 01:00:00");
939 }
940
941 #[test]
942 fn test_modifier_start_of_month() {
943 let r = DateFunc
944 .invoke(&[text("2024-03-15"), text("start of month")])
945 .unwrap();
946 assert_text(&r, "2024-03-01");
947 }
948
949 #[test]
950 fn test_modifier_start_of_year() {
951 let r = DateFunc
952 .invoke(&[text("2024-06-15"), text("start of year")])
953 .unwrap();
954 assert_text(&r, "2024-01-01");
955 }
956
957 #[test]
958 fn test_modifier_start_of_day() {
959 let r = DateTimeFunc
960 .invoke(&[text("2024-03-15 14:30:00"), text("start of day")])
961 .unwrap();
962 assert_text(&r, "2024-03-15 00:00:00");
963 }
964
965 #[test]
966 fn test_modifier_unixepoch() {
967 let r = DateTimeFunc.invoke(&[int(0), text("unixepoch")]).unwrap();
968 assert_text(&r, "1970-01-01 00:00:00");
969 }
970
971 #[test]
972 fn test_modifier_weekday() {
973 let r = DateFunc
975 .invoke(&[text("2024-03-15"), text("weekday 0")])
976 .unwrap();
977 assert_text(&r, "2024-03-17");
978 }
979
980 #[test]
981 fn test_modifier_auto_unixepoch() {
982 let ts = int(1_710_531_045);
983 let r = DateTimeFunc.invoke(&[ts.clone(), text("auto")]).unwrap();
984 let expected = DateTimeFunc.invoke(&[ts, text("unixepoch")]).unwrap();
985 assert_eq!(
986 r, expected,
987 "auto and unixepoch should agree for unix-like values"
988 );
989 }
990
991 #[test]
992 fn test_modifier_auto_julian_day() {
993 let r = DateFunc
994 .invoke(&[float(2_460_384.5), text("auto")])
995 .unwrap();
996 assert_text(&r, "2024-03-15");
997 }
998
999 #[test]
1000 fn test_modifier_localtime_utc_roundtrip() {
1001 let r = DateTimeFunc
1002 .invoke(&[
1003 text("2024-03-15 14:30:45"),
1004 text("utc"),
1005 text("localtime"),
1006 text("utc"),
1007 ])
1008 .unwrap();
1009 assert_text(&r, "2024-03-15 14:30:45");
1010 }
1011
1012 #[test]
1013 fn test_modifier_auto_out_of_range_returns_null() {
1014 let r = DateTimeFunc.invoke(&[float(1.0e20), text("auto")]).unwrap();
1015 assert_eq!(r, SqliteValue::Null);
1016 }
1017
1018 #[test]
1019 fn test_modifier_order_matters() {
1020 let r1 = DateFunc
1022 .invoke(&[text("2024-03-15"), text("start of month"), text("+1 days")])
1023 .unwrap();
1024 assert_text(&r1, "2024-03-02");
1025
1026 let r2 = DateFunc
1028 .invoke(&[text("2024-03-15"), text("+1 days"), text("start of month")])
1029 .unwrap();
1030 assert_text(&r2, "2024-03-01");
1031 }
1032
1033 #[test]
1034 fn test_modifier_weekday_same_day_advances_next_week() {
1035 let r = DateFunc
1037 .invoke(&[text("2024-03-17"), text("weekday 0")])
1038 .unwrap();
1039 assert_text(&r, "2024-03-24");
1040 }
1041
1042 #[test]
1045 fn test_bare_time_defaults() {
1046 let r = DateFunc.invoke(&[text("12:30:00")]).unwrap();
1047 assert_text(&r, "2000-01-01");
1048 }
1049
1050 #[test]
1051 fn test_t_separator() {
1052 let r = DateTimeFunc.invoke(&[text("2024-03-15T14:30:00")]).unwrap();
1053 assert_text(&r, "2024-03-15 14:30:00");
1054 }
1055
1056 #[test]
1057 fn test_julian_day_input() {
1058 let r = DateFunc.invoke(&[float(2_460_384.5)]).unwrap();
1060 assert_text(&r, "2024-03-15");
1061 }
1062
1063 #[test]
1064 fn test_null_input() {
1065 assert_eq!(DateFunc.invoke(&[null()]).unwrap(), SqliteValue::Null);
1066 }
1067
1068 #[test]
1069 fn test_invalid_input() {
1070 assert_eq!(
1071 DateFunc.invoke(&[text("not-a-date")]).unwrap(),
1072 SqliteValue::Null
1073 );
1074 }
1075
1076 #[test]
1077 fn test_negative_time_component_invalid() {
1078 let r = TimeFunc.invoke(&[text("-01:00")]).unwrap();
1079 assert_eq!(r, SqliteValue::Null);
1080 }
1081
1082 #[test]
1085 fn test_leap_year() {
1086 let r = DateFunc
1087 .invoke(&[text("2024-02-28"), text("+1 days")])
1088 .unwrap();
1089 assert_text(&r, "2024-02-29");
1090 }
1091
1092 #[test]
1093 fn test_non_leap_year() {
1094 let r = DateFunc
1095 .invoke(&[text("2023-02-28"), text("+1 days")])
1096 .unwrap();
1097 assert_text(&r, "2023-03-01");
1098 }
1099
1100 #[test]
1103 fn test_strftime_basic() {
1104 let r = StrftimeFunc
1105 .invoke(&[text("%Y-%m-%d"), text("2024-03-15")])
1106 .unwrap();
1107 assert_text(&r, "2024-03-15");
1108 }
1109
1110 #[test]
1111 fn test_strftime_time_specifiers() {
1112 let r = StrftimeFunc
1113 .invoke(&[text("%H:%M:%S"), text("2024-03-15 14:30:45")])
1114 .unwrap();
1115 assert_text(&r, "14:30:45");
1116 }
1117
1118 #[test]
1119 fn test_strftime_unix_seconds() {
1120 let r = StrftimeFunc
1121 .invoke(&[text("%s"), text("1970-01-01 00:00:00")])
1122 .unwrap();
1123 assert_text(&r, "0");
1124 }
1125
1126 #[test]
1127 fn test_strftime_day_of_year() {
1128 let r = StrftimeFunc
1129 .invoke(&[text("%j"), text("2024-03-15")])
1130 .unwrap();
1131 assert_text(&r, "075");
1133 }
1134
1135 #[test]
1136 fn test_strftime_day_of_week() {
1137 let r = StrftimeFunc
1139 .invoke(&[text("%w"), text("2024-03-15")])
1140 .unwrap();
1141 assert_text(&r, "5");
1142
1143 let r = StrftimeFunc
1144 .invoke(&[text("%u"), text("2024-03-15")])
1145 .unwrap();
1146 assert_text(&r, "5");
1147 }
1148
1149 #[test]
1150 fn test_strftime_12hour() {
1151 let r = StrftimeFunc
1152 .invoke(&[text("%I %p"), text("2024-03-15 14:30:00")])
1153 .unwrap();
1154 assert_text(&r, "02 PM");
1155
1156 let r = StrftimeFunc
1157 .invoke(&[text("%I %P"), text("2024-03-15 09:30:00")])
1158 .unwrap();
1159 assert_text(&r, "09 am");
1160 }
1161
1162 #[test]
1163 fn test_strftime_all_specifiers_presence() {
1164 let fmt = "%d|%e|%f|%H|%I|%j|%J|%k|%l|%m|%M|%p|%P|%R|%s|%S|%T|%u|%w|%W|%G|%g|%V|%Y|%%";
1165 let r = StrftimeFunc
1166 .invoke(&[text(fmt), text("2024-03-15 14:30:45.123")])
1167 .unwrap();
1168
1169 let s = match r {
1170 SqliteValue::Text(v) => v,
1171 other => panic!("expected Text, got {other:?}"),
1172 };
1173 let parts: Vec<&str> = s.split('|').collect();
1174 assert_eq!(parts.len(), 25, "unexpected specifier output: {s}");
1175 assert_eq!(parts[0], "15"); 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!(
1182 parts[6].parse::<f64>().is_ok(),
1183 "expected numeric %J output, got {}",
1184 parts[6]
1185 );
1186 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!(
1194 parts[14].parse::<i64>().is_ok(),
1195 "expected numeric %s output, got {}",
1196 parts[14]
1197 );
1198 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], "%"); }
1209
1210 #[test]
1211 fn test_strftime_null() {
1212 assert_eq!(
1213 StrftimeFunc.invoke(&[null(), text("2024-01-01")]).unwrap(),
1214 SqliteValue::Null
1215 );
1216 assert_eq!(
1217 StrftimeFunc.invoke(&[text("%Y"), null()]).unwrap(),
1218 SqliteValue::Null
1219 );
1220 }
1221
1222 #[test]
1225 fn test_timediff_basic() {
1226 let r = TimediffFunc
1227 .invoke(&[text("2024-03-15"), text("2024-03-10")])
1228 .unwrap();
1229 assert_text(&r, "+0000-00-05 00:00:00.000");
1230 }
1231
1232 #[test]
1233 fn test_timediff_negative() {
1234 let r = TimediffFunc
1235 .invoke(&[text("2024-03-10"), text("2024-03-15")])
1236 .unwrap();
1237 assert_text(&r, "-0000-00-05 00:00:00.000");
1238 }
1239
1240 #[test]
1241 fn test_timediff_year_boundary() {
1242 let r = TimediffFunc
1243 .invoke(&[text("2024-01-01 01:00:00"), text("2023-12-31 23:00:00")])
1244 .unwrap();
1245 assert_text(&r, "+0000-00-00 02:00:00.000");
1246 }
1247
1248 #[test]
1251 fn test_modifier_subsec() {
1252 let r = TimeFunc
1253 .invoke(&[text("2024-01-01 12:00:00.123"), text("subsec")])
1254 .unwrap();
1255 match &r {
1256 SqliteValue::Text(s) => assert!(
1257 s.contains('.'),
1258 "expected fractional seconds with subsec: {s}"
1259 ),
1260 other => panic!("expected Text, got {other:?}"),
1261 }
1262 }
1263
1264 #[test]
1267 fn test_register_datetime_builtins_all_present() {
1268 let mut reg = FunctionRegistry::new();
1269 register_datetime_builtins(&mut reg);
1270
1271 let expected = [
1272 "date",
1273 "time",
1274 "datetime",
1275 "julianday",
1276 "unixepoch",
1277 "strftime",
1278 "timediff",
1279 ];
1280
1281 for name in expected {
1282 assert!(
1283 reg.find_scalar(name, 1).is_some() || reg.find_scalar(name, 2).is_some(),
1284 "datetime function '{name}' not registered"
1285 );
1286 }
1287 }
1288
1289 #[test]
1292 fn test_modifier_year_overflow() {
1293 let huge = i64::MAX;
1296 let modifier = format!("+{huge} years");
1297 let r = DateFunc.invoke(&[text("2000-01-01"), text(&modifier)]);
1298 assert_eq!(r.unwrap(), SqliteValue::Null);
1301 }
1302
1303 #[test]
1304 fn test_jdn_roundtrip() {
1305 let dates = [
1307 (2024, 3, 15),
1308 (2000, 1, 1),
1309 (1970, 1, 1),
1310 (2024, 2, 29),
1311 (1900, 1, 1),
1312 (2099, 12, 31),
1313 ];
1314 for (y, m, d) in dates {
1315 let jdn = ymd_to_jdn(y, m, d);
1316 let (y2, m2, d2) = jdn_to_ymd(jdn);
1317 assert_eq!(
1318 (y, m, d),
1319 (y2, m2, d2),
1320 "roundtrip failed for {y}-{m}-{d} (JDN={jdn})"
1321 );
1322 }
1323 }
1324
1325 #[test]
1326 fn test_unix_epoch_roundtrip() {
1327 let jdn = ymd_to_jdn(1970, 1, 1);
1328 let unix = jdn_to_unix(jdn);
1329 assert_eq!(unix, 0, "Unix epoch should be 0");
1330
1331 let jdn2 = unix_to_jdn(0.0);
1332 assert!((jdn2 - UNIX_EPOCH_JDN).abs() < 1e-10, "roundtrip failed");
1333 }
1334}