#![allow(
clippy::unnecessary_literal_bound,
clippy::too_many_lines,
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
clippy::cast_sign_loss,
clippy::cast_precision_loss,
clippy::items_after_statements,
clippy::match_same_arms,
clippy::float_cmp,
clippy::suboptimal_flops,
clippy::manual_let_else,
clippy::single_match_else,
clippy::unnecessary_wraps,
clippy::cognitive_complexity,
clippy::similar_names,
clippy::many_single_char_names,
clippy::unreadable_literal,
clippy::manual_range_contains,
clippy::range_plus_one,
clippy::format_push_string,
clippy::redundant_else
)]
use std::{
borrow::Cow,
fmt::{Arguments, Write as _},
};
use fsqlite_error::Result;
use fsqlite_types::SqliteValue;
use crate::{FunctionRegistry, ScalarFunction};
fn utc_offset_for_local_datetime(y: i32, mo: u32, d: u32, h: u32, mi: u32, s: u32) -> i64 {
use chrono::{Local, NaiveDate, NaiveDateTime, NaiveTime, TimeZone};
let date = NaiveDate::from_ymd_opt(y, mo, d).unwrap_or_default();
let time = NaiveTime::from_hms_opt(h, mi, s).unwrap_or_default();
let naive = NaiveDateTime::new(date, time);
match Local.from_local_datetime(&naive).earliest() {
Some(dt) => dt.offset().local_minus_utc() as i64,
None => 0, }
}
fn utc_offset_for_utc_datetime(y: i32, mo: u32, d: u32, h: u32, mi: u32, s: u32) -> i64 {
use chrono::{Local, NaiveDate, NaiveDateTime, NaiveTime, TimeZone, Utc};
let date = NaiveDate::from_ymd_opt(y, mo, d).unwrap_or_default();
let time = NaiveTime::from_hms_opt(h, mi, s).unwrap_or_default();
let naive = NaiveDateTime::new(date, time);
let utc_dt = Utc.from_utc_datetime(&naive);
let local_dt = utc_dt.with_timezone(&Local);
local_dt.offset().local_minus_utc() as i64
}
fn utc_offset_for_utc_jdn(jdn: f64) -> i64 {
let (y, mo, d) = jdn_to_ymd(jdn);
let (h, mi, s, _frac) = jdn_to_hms(jdn);
utc_offset_for_utc_datetime(y as i32, mo as u32, d as u32, h as u32, mi as u32, s as u32)
}
fn utc_offset_for_local_jdn(jdn: f64) -> i64 {
let (y, mo, d) = jdn_to_ymd(jdn);
let (h, mi, s, _frac) = jdn_to_hms(jdn);
utc_offset_for_local_datetime(y as i32, mo as u32, d as u32, h as u32, mi as u32, s as u32)
}
fn ymd_to_jdn(y: i64, m: i64, d: i64) -> f64 {
let (y, m) = if m <= 2 {
(y.saturating_sub(1), m.saturating_add(12))
} else {
(y, m)
};
let a = y / 100;
let b = 2_i64.saturating_sub(a).saturating_add(a / 4);
(365.25 * y.saturating_add(4716) as f64).floor()
+ (30.6001 * m.saturating_add(1) as f64).floor()
+ d as f64
+ b as f64
- 1524.5
}
fn jdn_to_ymd(jdn: f64) -> (i64, i64, i64) {
let z = (jdn + 0.5).floor() as i64;
let a = if z < 2_299_161 {
z
} else {
let alpha = ((z as f64 - 1_867_216.25) / 36524.25).floor() as i64;
z.saturating_add(1)
.saturating_add(alpha)
.saturating_sub(alpha / 4)
};
let b = a.saturating_add(1524);
let c = ((b as f64 - 122.1) / 365.25).floor() as i64;
let d = (365.25 * c as f64).floor() as i64;
let e = ((b.saturating_sub(d)) as f64 / 30.6001).floor() as i64;
let day = b
.saturating_sub(d)
.saturating_sub((30.6001 * e as f64).floor() as i64);
let month = if e < 14 {
e.saturating_sub(1)
} else {
e.saturating_sub(13)
};
let year = if month > 2 {
c.saturating_sub(4716)
} else {
c.saturating_sub(4715)
};
(year, month, day)
}
fn jdn_to_hms(jdn: f64) -> (i64, i64, i64, f64) {
let frac = jdn + 0.5 - (jdn + 0.5).floor();
let total_ms = (frac * 86_400_000.0).round() as i64;
let h = total_ms / 3_600_000;
let rem = total_ms % 3_600_000;
let m = rem / 60_000;
let rem = rem % 60_000;
let s = rem / 1000;
let ms_frac = (rem % 1000) as f64 / 1000.0;
(h, m, s, ms_frac)
}
fn ymdhms_to_jdn(y: i64, mo: i64, d: i64, h: i64, mi: i64, s: i64, frac: f64) -> f64 {
ymd_to_jdn(y, mo, d) + (h as f64 * 3600.0 + mi as f64 * 60.0 + s as f64 + frac) / 86400.0
}
const UNIX_EPOCH_JDN: f64 = 2_440_587.5;
const AUTO_JDN_MAX: f64 = 5_373_484.499_999;
const AUTO_UNIX_MIN: f64 = -210_866_760_000.0;
const AUTO_UNIX_MAX: f64 = 253_402_300_799.0;
fn jdn_to_unix(jdn: f64) -> i64 {
((jdn - UNIX_EPOCH_JDN) * 86400.0).round() as i64
}
fn unix_to_jdn(ts: f64) -> f64 {
ts / 86400.0 + UNIX_EPOCH_JDN
}
fn is_leap_year(y: i64) -> bool {
(y % 4 == 0 && y % 100 != 0) || y % 400 == 0
}
fn days_in_month(y: i64, m: i64) -> i64 {
match m {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
2 => {
if is_leap_year(y) {
29
} else {
28
}
}
_ => 30,
}
}
fn day_of_year(y: i64, m: i64, d: i64) -> i64 {
let mut doy = d;
for mo in 1..m {
doy = doy.saturating_add(days_in_month(y, mo));
}
doy
}
fn parse_timestring(s: &str) -> Option<f64> {
let s = s.trim();
if s.eq_ignore_ascii_case("now") {
use std::time::{SystemTime, UNIX_EPOCH};
let secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs_f64();
return Some(2_440_587.5 + secs / 86_400.0);
}
if let Ok(jdn) = s.parse::<f64>() {
if jdn >= 0.0 && jdn.is_finite() {
return Some(jdn);
}
}
parse_iso8601(s)
}
fn parse_iso8601(s: &str) -> Option<f64> {
let bytes = s.as_bytes();
let len = bytes.len();
if len >= 10 && bytes[4] == b'-' && bytes[7] == b'-' {
let y = s[0..4].parse::<i64>().ok()?;
let m = s[5..7].parse::<i64>().ok()?;
let d = s[8..10].parse::<i64>().ok()?;
if m < 1 || m > 12 || d < 1 || d > 31 {
return None;
}
if len == 10 {
return Some(ymd_to_jdn(y, m, d));
}
if len > 10 && (bytes[10] == b' ' || bytes[10] == b'T') {
let time_part = &s[11..];
let (h, mi, sec, frac, tz_offset_min) = parse_time_part_with_tz(time_part)?;
let jdn = ymdhms_to_jdn(y, m, d, h, mi, sec, frac);
return Some(jdn - (tz_offset_min as f64) / 1440.0);
}
return None;
}
if len >= 5 && bytes[2] == b':' {
let (h, mi, sec, frac, tz_offset_min) = parse_time_part_with_tz(s)?;
let jdn = ymdhms_to_jdn(2000, 1, 1, h, mi, sec, frac);
return Some(jdn - (tz_offset_min as f64) / 1440.0);
}
None
}
fn split_tz_suffix(s: &str) -> Option<(&str, i64)> {
if let Some(stripped) = s.strip_suffix('Z').or_else(|| s.strip_suffix('z')) {
return Some((stripped, 0));
}
let bytes = s.as_bytes();
for width in [6usize, 5, 3] {
if bytes.len() < width + 1 {
continue;
}
let split_at = bytes.len() - width;
let sign_byte = bytes[split_at];
if sign_byte != b'+' && sign_byte != b'-' {
continue;
}
let tz_part = &s[split_at..];
if let Some(offset) = parse_tz_offset(tz_part) {
return Some((&s[..split_at], offset));
}
}
Some((s, 0))
}
fn parse_tz_offset(tz: &str) -> Option<i64> {
let bytes = tz.as_bytes();
if bytes.is_empty() {
return None;
}
let sign: i64 = match bytes[0] {
b'+' => 1,
b'-' => -1,
_ => return None,
};
let rest = &tz[1..];
let (hours, minutes) = match rest.len() {
5 if rest.as_bytes()[2] == b':' => (
rest[0..2].parse::<i64>().ok()?,
rest[3..5].parse::<i64>().ok()?,
),
4 => (
rest[0..2].parse::<i64>().ok()?,
rest[2..4].parse::<i64>().ok()?,
),
2 => (rest.parse::<i64>().ok()?, 0),
_ => return None,
};
if !(0..=23).contains(&hours) || !(0..=59).contains(&minutes) {
return None;
}
Some(sign * (hours * 60 + minutes))
}
fn parse_time_part_with_tz(s: &str) -> Option<(i64, i64, i64, f64, i64)> {
let (time_only, tz_offset_min) = split_tz_suffix(s)?;
let (h, mi, sec, frac) = parse_time_part(time_only)?;
Some((h, mi, sec, frac, tz_offset_min))
}
fn parse_time_part(s: &str) -> Option<(i64, i64, i64, f64)> {
let [h_tens, h_ones, b':', mi_tens, mi_ones, rest @ ..] = s.as_bytes() else {
return None;
};
let h = parse_two_ascii_digits(*h_tens, *h_ones)?;
let mi = parse_two_ascii_digits(*mi_tens, *mi_ones)?;
if !(0..=23).contains(&h) || !(0..=59).contains(&mi) {
return None;
}
match rest {
[] => Some((h, mi, 0, 0.0)),
[b':', sec_tens, sec_ones] => {
let sec = parse_two_ascii_digits(*sec_tens, *sec_ones)?;
if !(0..=59).contains(&sec) {
return None;
}
Some((h, mi, sec, 0.0))
}
[b':', sec_tens, sec_ones, b'.', ..] => {
let sec = parse_two_ascii_digits(*sec_tens, *sec_ones)?;
if !(0..=59).contains(&sec) {
return None;
}
let frac = s.get(8..)?.parse::<f64>().ok()?;
Some((h, mi, sec, frac))
}
_ => None,
}
}
#[inline]
fn parse_two_ascii_digits(tens: u8, ones: u8) -> Option<i64> {
if tens.is_ascii_digit() && ones.is_ascii_digit() {
Some(i64::from((tens - b'0') * 10 + (ones - b'0')))
} else {
None
}
}
fn apply_modifier(jdn: f64, modifier: &str) -> Option<f64> {
let m = modifier.trim().to_ascii_lowercase();
if m == "start of month" {
let (y, mo, _d) = jdn_to_ymd(jdn);
return Some(ymd_to_jdn(y, mo, 1));
}
if m == "start of year" {
let (y, _mo, _d) = jdn_to_ymd(jdn);
return Some(ymd_to_jdn(y, 1, 1));
}
if m == "start of day" {
let (y, mo, d) = jdn_to_ymd(jdn);
return Some(ymd_to_jdn(y, mo, d));
}
if m == "unixepoch" {
return Some(unix_to_jdn(jdn));
}
if m == "julianday" {
return Some(jdn);
}
if m == "auto" {
if (0.0..=AUTO_JDN_MAX).contains(&jdn) {
return Some(jdn);
}
if (AUTO_UNIX_MIN..=AUTO_UNIX_MAX).contains(&jdn) {
return Some(unix_to_jdn(jdn));
}
return None;
}
if m == "localtime" {
let offset = utc_offset_for_utc_jdn(jdn);
return Some(jdn + offset as f64 / 86400.0);
}
if m == "utc" {
let offset = utc_offset_for_local_jdn(jdn);
return Some(jdn - offset as f64 / 86400.0);
}
if m == "subsec" || m == "subsecond" {
return Some(jdn);
}
if let Some(rest) = m.strip_prefix("weekday ") {
let wd = rest.trim().parse::<i64>().ok()?;
if !(0..=6).contains(&wd) {
return None;
}
let current_jdn_int = (jdn + 0.5).floor() as i64;
let current_wd = (current_jdn_int + 1) % 7; let mut diff = wd - current_wd;
if diff < 0 {
diff += 7;
}
return Some(jdn + diff as f64);
}
parse_arithmetic_modifier(&m).map(|delta| jdn + delta)
}
fn parse_arithmetic_modifier(m: &str) -> Option<f64> {
let (sign, rest) = if let Some(r) = m.strip_prefix('+') {
(1.0, r.trim())
} else {
let r = m.strip_prefix('-')?;
(-1.0, r.trim())
};
let mut parts = rest.splitn(2, ' ');
let num_str = parts.next()?;
let unit = parts.next()?.trim();
let num = num_str.parse::<f64>().ok().filter(|f| f.is_finite())?;
let delta = num * sign;
match unit.trim_end_matches('s') {
"day" => Some(delta),
"hour" => Some(delta / 24.0),
"minute" => Some(delta / 1440.0),
"second" => Some(delta / 86400.0),
"month" => Some(apply_month_delta(delta)),
"year" => Some(apply_month_delta(delta * 12.0)),
_ => None,
}
}
fn apply_month_delta(months: f64) -> f64 {
months * 30.436875
}
fn apply_modifiers(jdn: f64, modifiers: &[String]) -> Option<(f64, bool)> {
let mut j = jdn;
let mut subsec = false;
for m in modifiers {
let m_lower = m.trim().to_ascii_lowercase();
if m_lower == "subsec" || m_lower == "subsecond" {
subsec = true;
}
if is_month_year_modifier(&m_lower) {
match apply_month_year_exact(j, &m_lower) {
Ok(new_jdn) => {
j = new_jdn?;
continue;
}
Err(()) => {
}
}
}
j = apply_modifier(j, m)?;
}
Some((j, subsec))
}
fn is_month_year_modifier(m: &str) -> bool {
(m.contains("month") || m.contains("year")) && (m.starts_with('+') || m.starts_with('-'))
}
fn apply_month_year_exact(jdn: f64, m: &str) -> std::result::Result<Option<f64>, ()> {
let (sign, rest) = if let Some(r) = m.strip_prefix('+') {
(1_i64, r.trim())
} else if let Some(r) = m.strip_prefix('-') {
(-1_i64, r.trim())
} else {
return Err(());
};
let mut parts = rest.splitn(2, ' ');
let num_str = parts.next().ok_or(())?;
let unit = parts.next().ok_or(())?.trim();
let num = if let Ok(n) = num_str.parse::<i64>() {
n
} else if let Ok(f) = num_str.parse::<f64>() {
if f.fract() == 0.0 && f >= i64::MIN as f64 && f <= i64::MAX as f64 {
f as i64
} else {
return Err(());
}
} else {
return Err(());
};
let (y, mo, d) = jdn_to_ymd(jdn);
let (h, mi, s, frac) = jdn_to_hms(jdn);
let total_months = match unit.trim_end_matches('s') {
"month" => {
if let Some(val) = num.checked_mul(sign) {
val
} else {
return Ok(None);
}
}
"year" => {
if let Some(val) = num.checked_mul(sign).and_then(|v| v.checked_mul(12)) {
val
} else {
return Ok(None);
}
}
_ => return Err(()),
};
let current_months = if let Some(val) = y.checked_mul(12).and_then(|v| v.checked_add(mo - 1)) {
val
} else {
return Ok(None);
};
let new_total = if let Some(val) = current_months.checked_add(total_months) {
val
} else {
return Ok(None);
};
let new_y = new_total.div_euclid(12);
let new_mo = new_total.rem_euclid(12) + 1;
Ok(Some(ymdhms_to_jdn(new_y, new_mo, d, h, mi, s, frac)))
}
fn format_date(jdn: f64) -> String {
let (y, m, d) = jdn_to_ymd(jdn);
format!("{y:04}-{m:02}-{d:02}")
}
fn format_time(jdn: f64, subsec: bool) -> String {
let (h, m, s, frac) = jdn_to_hms(jdn);
if subsec && frac > 1e-9 {
format!("{h:02}:{m:02}:{s:02}.{:03}", (frac * 1000.0).round() as i64)
} else {
format!("{h:02}:{m:02}:{s:02}")
}
}
fn format_datetime(jdn: f64, subsec: bool) -> String {
format!("{} {}", format_date(jdn), format_time(jdn, subsec))
}
#[inline]
fn push_format(result: &mut String, args: Arguments<'_>) {
let _ = result.write_fmt(args);
}
#[inline]
fn push_zero_padded_2(result: &mut String, value: i64) {
if (0..=99).contains(&value) {
let value = value as u8;
result.push(char::from(b'0' + value / 10));
result.push(char::from(b'0' + value % 10));
} else {
push_format(result, format_args!("{value:02}"));
}
}
#[inline]
fn push_space_padded_2(result: &mut String, value: i64) {
if (0..=99).contains(&value) {
let value = value as u8;
if value >= 10 {
result.push(char::from(b'0' + value / 10));
} else {
result.push(' ');
}
result.push(char::from(b'0' + value % 10));
} else {
push_format(result, format_args!("{value:>2}"));
}
}
#[inline]
fn push_zero_padded_3(result: &mut String, value: i64) {
if (0..=999).contains(&value) {
let value = value as u16;
result.push(char::from(b'0' + (value / 100) as u8));
result.push(char::from(b'0' + ((value / 10) % 10) as u8));
result.push(char::from(b'0' + (value % 10) as u8));
} else {
push_format(result, format_args!("{value:03}"));
}
}
#[inline]
fn push_zero_padded_4(result: &mut String, value: i64) {
if (0..=9999).contains(&value) {
let value = value as u16;
result.push(char::from(b'0' + (value / 1000) as u8));
result.push(char::from(b'0' + ((value / 100) % 10) as u8));
result.push(char::from(b'0' + ((value / 10) % 10) as u8));
result.push(char::from(b'0' + (value % 10) as u8));
} else {
push_format(result, format_args!("{value:04}"));
}
}
fn format_strftime(fmt: &str, jdn: f64) -> String {
let (y, mo, d) = jdn_to_ymd(jdn);
let (h, mi, s, frac) = jdn_to_hms(jdn);
let doy = day_of_year(y, mo, d);
let jdn_int = (jdn + 0.5).floor() as i64;
let dow = (jdn_int + 1) % 7;
let mut result = String::with_capacity(fmt.len().saturating_add(8));
let bytes = fmt.as_bytes();
let mut i = 0;
let mut literal_start = 0;
while i < bytes.len() {
if bytes[i] != b'%' || i + 1 >= bytes.len() {
i += 1;
continue;
}
result.push_str(&fmt[literal_start..i]);
let spec_suffix = &fmt[i + 1..];
let Some(spec) = spec_suffix.chars().next() else {
break;
};
i += 1 + spec.len_utf8();
literal_start = i;
match spec {
'd' => push_zero_padded_2(&mut result, d),
'e' => push_space_padded_2(&mut result, d),
'f' => {
let total = s as f64 + frac;
push_format(&mut result, format_args!("{total:06.3}"));
}
'H' => push_zero_padded_2(&mut result, h),
'I' => {
let h12 = if h == 0 {
12
} else if h > 12 {
h - 12
} else {
h
};
push_zero_padded_2(&mut result, h12);
}
'j' => push_zero_padded_3(&mut result, doy),
'J' => {
push_format(&mut result, format_args!("{jdn:.15}"));
while result.as_bytes().last() == Some(&b'0') {
result.pop();
}
if result.as_bytes().last() == Some(&b'.') {
result.pop();
}
}
'k' => {
push_space_padded_2(&mut result, h);
}
'l' => {
let h12 = if h == 0 {
12
} else if h > 12 {
h - 12
} else {
h
};
push_space_padded_2(&mut result, h12);
}
'm' => push_zero_padded_2(&mut result, mo),
'M' => push_zero_padded_2(&mut result, mi),
'p' => {
result.push_str(if h < 12 { "AM" } else { "PM" });
}
'P' => {
result.push_str(if h < 12 { "am" } else { "pm" });
}
'R' => {
push_zero_padded_2(&mut result, h);
result.push(':');
push_zero_padded_2(&mut result, mi);
}
's' => {
let unix = jdn_to_unix(jdn);
push_format(&mut result, format_args!("{unix}"));
}
'S' => push_zero_padded_2(&mut result, s),
'T' => {
push_zero_padded_2(&mut result, h);
result.push(':');
push_zero_padded_2(&mut result, mi);
result.push(':');
push_zero_padded_2(&mut result, s);
}
'u' => {
let u = if dow == 0 { 7 } else { dow };
push_format(&mut result, format_args!("{u}"));
}
'w' => push_format(&mut result, format_args!("{dow}")),
'W' => {
let w = (doy + 6 - ((dow + 6) % 7)) / 7;
push_zero_padded_2(&mut result, w);
}
'Y' => push_zero_padded_4(&mut result, y),
'G' | 'g' | 'V' => {
let (iso_y, iso_w) = iso_week(y, mo, d);
match spec {
'G' => push_zero_padded_4(&mut result, iso_y),
'g' => push_zero_padded_2(&mut result, iso_y % 100),
'V' => push_zero_padded_2(&mut result, iso_w),
_ => unreachable!(),
}
}
'%' => result.push('%'),
other => {
result.push('%');
result.push(other);
}
}
}
if literal_start < fmt.len() {
result.push_str(&fmt[literal_start..]);
}
result
}
fn iso_week(y: i64, m: i64, d: i64) -> (i64, i64) {
let jdn = ymd_to_jdn(y, m, d);
let jdn_int = (jdn + 0.5).floor() as i64;
let dow = (jdn_int + 1) % 7;
let iso_dow = if dow == 0 { 7 } else { dow };
let thu_jdn = jdn_int + (4 - iso_dow);
let (thu_y, _, _) = jdn_to_ymd(thu_jdn as f64);
let jan4_jdn = (ymd_to_jdn(thu_y, 1, 4) + 0.5).floor() as i64;
let jan4_dow = (jan4_jdn + 1) % 7;
let jan4_iso_dow = if jan4_dow == 0 { 7 } else { jan4_dow };
let week1_start = jan4_jdn - (jan4_iso_dow - 1);
let week = (thu_jdn - week1_start) / 7 + 1;
(thu_y, week)
}
fn timediff_impl(jdn1: f64, jdn2: f64) -> String {
let (sign, start_jdn, end_jdn) = if jdn1 >= jdn2 {
('+', jdn2, jdn1)
} else {
('-', jdn1, jdn2)
};
let (start_y, start_mo, start_d) = jdn_to_ymd(start_jdn);
let (start_h, start_mi, mut start_s, start_frac) = jdn_to_hms(start_jdn);
let mut start_ms = (start_frac * 1000.0).round() as i64;
if start_ms >= 1000 {
start_ms = 0;
start_s += 1;
}
let (end_y, end_mo, end_d) = jdn_to_ymd(end_jdn);
let (end_h, end_mi, mut end_s, end_frac) = jdn_to_hms(end_jdn);
let mut end_ms = (end_frac * 1000.0).round() as i64;
if end_ms >= 1000 {
end_ms = 0;
end_s += 1;
}
let mut years = end_y - start_y;
let mut months = end_mo - start_mo;
let mut days = end_d - start_d;
let mut hours = end_h - start_h;
let mut minutes = end_mi - start_mi;
let mut seconds = end_s - start_s;
let mut millis = end_ms - start_ms;
if millis < 0 {
millis += 1000;
seconds -= 1;
}
if seconds < 0 {
seconds += 60;
minutes -= 1;
}
if minutes < 0 {
minutes += 60;
hours -= 1;
}
if hours < 0 {
hours += 24;
days -= 1;
}
if days < 0 {
months -= 1;
let (borrow_y, borrow_mo) = if end_mo == 1 {
(end_y - 1, 12)
} else {
(end_y, end_mo - 1)
};
days += days_in_month(borrow_y, borrow_mo);
}
if months < 0 {
months += 12;
years -= 1;
}
format!(
"{sign}{years:04}-{months:02}-{days:02} {hours:02}:{minutes:02}:{seconds:02}.{millis:03}"
)
}
fn parse_args(args: &[SqliteValue]) -> Option<(f64, bool)> {
if args.is_empty() || args[0].is_null() {
return None;
}
let input = match &args[0] {
SqliteValue::Text(s) => parse_timestring(s)?,
SqliteValue::Integer(i) => *i as f64,
SqliteValue::Float(f) => *f,
_ => return None,
};
if args[1..].iter().any(SqliteValue::is_null) {
return None;
}
let modifiers: Vec<String> = args[1..].iter().map(SqliteValue::to_text).collect();
apply_modifiers(input, &modifiers)
}
pub struct DateFunc;
impl ScalarFunction for DateFunc {
fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
match parse_args(args) {
Some((jdn, _)) => Ok(SqliteValue::Text(format_date(jdn).into())),
None => Ok(SqliteValue::Null),
}
}
fn num_args(&self) -> i32 {
-1
}
fn name(&self) -> &str {
"date"
}
}
pub struct TimeFunc;
impl ScalarFunction for TimeFunc {
fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
match parse_args(args) {
Some((jdn, subsec)) => Ok(SqliteValue::Text(format_time(jdn, subsec).into())),
None => Ok(SqliteValue::Null),
}
}
fn num_args(&self) -> i32 {
-1
}
fn name(&self) -> &str {
"time"
}
}
pub struct DateTimeFunc;
impl ScalarFunction for DateTimeFunc {
fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
match parse_args(args) {
Some((jdn, subsec)) => Ok(SqliteValue::Text(format_datetime(jdn, subsec).into())),
None => Ok(SqliteValue::Null),
}
}
fn num_args(&self) -> i32 {
-1
}
fn name(&self) -> &str {
"datetime"
}
}
pub struct JuliandayFunc;
impl ScalarFunction for JuliandayFunc {
fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
match parse_args(args) {
Some((jdn, _)) => Ok(SqliteValue::Float(jdn)),
None => Ok(SqliteValue::Null),
}
}
fn num_args(&self) -> i32 {
-1
}
fn name(&self) -> &str {
"julianday"
}
}
pub struct UnixepochFunc;
impl ScalarFunction for UnixepochFunc {
fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
match parse_args(args) {
Some((jdn, _)) => Ok(SqliteValue::Integer(jdn_to_unix(jdn))),
None => Ok(SqliteValue::Null),
}
}
fn num_args(&self) -> i32 {
-1
}
fn name(&self) -> &str {
"unixepoch"
}
}
pub struct StrftimeFunc;
impl ScalarFunction for StrftimeFunc {
fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
if args.len() < 2 || args[0].is_null() || args[1].is_null() {
return Ok(SqliteValue::Null);
}
let rest = &args[1..];
match parse_args(rest) {
Some((jdn, _)) => {
let fmt = match args[0].as_text_str() {
Some(text) => Cow::Borrowed(text),
None => Cow::Owned(args[0].to_text()),
};
Ok(SqliteValue::Text(format_strftime(fmt.as_ref(), jdn).into()))
}
None => Ok(SqliteValue::Null),
}
}
fn num_args(&self) -> i32 {
-1
}
fn name(&self) -> &str {
"strftime"
}
}
pub struct TimediffFunc;
impl ScalarFunction for TimediffFunc {
fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
if args.len() < 2 || args[0].is_null() || args[1].is_null() {
return Ok(SqliteValue::Null);
}
let jdn1 = match &args[0] {
SqliteValue::Text(s) => parse_timestring(s),
SqliteValue::Integer(i) => Some(*i as f64),
SqliteValue::Float(f) => Some(*f),
_ => None,
};
let jdn2 = match &args[1] {
SqliteValue::Text(s) => parse_timestring(s),
SqliteValue::Integer(i) => Some(*i as f64),
SqliteValue::Float(f) => Some(*f),
_ => None,
};
match (jdn1, jdn2) {
(Some(j1), Some(j2)) => Ok(SqliteValue::Text(timediff_impl(j1, j2).into())),
_ => Ok(SqliteValue::Null),
}
}
fn num_args(&self) -> i32 {
2
}
fn name(&self) -> &str {
"timediff"
}
}
pub fn register_datetime_builtins(registry: &mut FunctionRegistry) {
registry.register_scalar(DateFunc);
registry.register_scalar(TimeFunc);
registry.register_scalar(DateTimeFunc);
registry.register_scalar(JuliandayFunc);
registry.register_scalar(UnixepochFunc);
registry.register_scalar(StrftimeFunc);
registry.register_scalar(TimediffFunc);
}
#[cfg(test)]
mod tests {
use super::*;
fn text(s: &str) -> SqliteValue {
SqliteValue::Text(s.into())
}
fn int(v: i64) -> SqliteValue {
SqliteValue::Integer(v)
}
fn float(v: f64) -> SqliteValue {
SqliteValue::Float(v)
}
fn null() -> SqliteValue {
SqliteValue::Null
}
fn assert_text(result: &SqliteValue, expected: &str) {
match result {
SqliteValue::Text(s) => assert_eq!(s.as_ref(), expected, "text mismatch"),
other => panic!("expected Text(\"{expected}\"), got {other:?}"),
}
}
#[test]
fn test_date_basic() {
let r = DateFunc.invoke(&[text("2024-03-15 14:30:00")]).unwrap();
assert_text(&r, "2024-03-15");
}
#[test]
fn test_time_basic() {
let r = TimeFunc.invoke(&[text("2024-03-15 14:30:45")]).unwrap();
assert_text(&r, "14:30:45");
}
#[test]
fn test_datetime_basic() {
let r = DateTimeFunc.invoke(&[text("2024-03-15 14:30:00")]).unwrap();
assert_text(&r, "2024-03-15 14:30:00");
}
#[test]
fn test_julianday_basic() {
let r = JuliandayFunc.invoke(&[text("2024-03-15")]).unwrap();
match r {
SqliteValue::Float(jdn) => {
assert!((jdn - 2_460_384.5).abs() < 0.01, "unexpected JDN: {jdn}");
}
other => panic!("expected Float, got {other:?}"),
}
}
fn julianday_float(input: &str) -> f64 {
match JuliandayFunc.invoke(&[text(input)]).unwrap() {
SqliteValue::Float(v) => v,
other => panic!("expected Float, got {other:?} for input {input:?}"),
}
}
fn assert_jdn_close(actual: f64, expected: f64, ctx: &str) {
assert!(
(actual - expected).abs() < 1e-6,
"JDN mismatch for {ctx}: got {actual}, expected {expected}"
);
}
#[test]
fn test_julianday_rfc3339_z_suffix() {
let naive = julianday_float("2026-04-07 16:00:00");
assert_jdn_close(julianday_float("2026-04-07T16:00:00Z"), naive, "T...Z");
assert_jdn_close(
julianday_float("2026-04-07T16:00:00z"),
naive,
"lowercase z",
);
}
#[test]
fn test_julianday_rfc3339_zero_offset() {
let naive = julianday_float("2026-04-07 16:00:00");
assert_jdn_close(
julianday_float("2026-04-07T16:00:00+00:00"),
naive,
"+00:00",
);
assert_jdn_close(
julianday_float("2026-04-07T16:00:00-00:00"),
naive,
"-00:00",
);
}
#[test]
fn test_julianday_rfc3339_positive_offset() {
let base = julianday_float("2026-04-07 16:00:00");
let expected = base - 1.0 / 24.0;
assert_jdn_close(
julianday_float("2026-04-07T16:00:00+01:00"),
expected,
"+01:00",
);
}
#[test]
fn test_julianday_rfc3339_negative_offset() {
let base = julianday_float("2026-04-07 16:00:00");
let expected = base + 5.0 / 24.0;
assert_jdn_close(
julianday_float("2026-04-07T16:00:00-05:00"),
expected,
"-05:00",
);
}
#[test]
fn test_julianday_rfc3339_half_hour_offset() {
let base = julianday_float("2026-04-07 16:00:00");
let expected = base - 5.5 / 24.0;
assert_jdn_close(
julianday_float("2026-04-07T16:00:00+05:30"),
expected,
"+05:30",
);
}
#[test]
fn test_julianday_rfc3339_compact_offsets() {
let base = julianday_float("2026-04-07 16:00:00");
assert_jdn_close(
julianday_float("2026-04-07T16:00:00+0100"),
base - 1.0 / 24.0,
"+0100",
);
assert_jdn_close(
julianday_float("2026-04-07T16:00:00-0530"),
base + 5.5 / 24.0,
"-0530",
);
assert_jdn_close(
julianday_float("2026-04-07T16:00:00+09"),
base - 9.0 / 24.0,
"+09",
);
}
#[test]
fn test_julianday_rfc3339_fractional_seconds_with_tz() {
let base = julianday_float("2026-04-07 16:00:00.500");
assert_jdn_close(
julianday_float("2026-04-07T16:00:00.500Z"),
base,
"fractional + Z",
);
assert_jdn_close(
julianday_float("2026-04-07T16:00:00.500+01:00"),
base - 1.0 / 24.0,
"fractional + +01:00",
);
}
#[test]
fn test_date_and_time_rfc3339_round_trip() {
assert_text(
&DateFunc
.invoke(&[text("2026-04-07T16:00:00+05:00")])
.unwrap(),
"2026-04-07",
);
assert_text(
&TimeFunc
.invoke(&[text("2026-04-07T16:00:00+05:00")])
.unwrap(),
"11:00:00",
);
assert_text(
&DateTimeFunc
.invoke(&[text("2026-04-07T16:00:00+05:00")])
.unwrap(),
"2026-04-07 11:00:00",
);
}
#[test]
fn test_julianday_rfc3339_invalid_offsets_return_null() {
for bad in &[
"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", ] {
let result = JuliandayFunc.invoke(&[text(bad)]).unwrap();
assert_eq!(
result,
SqliteValue::Null,
"expected NULL for malformed offset {bad:?}, got {result:?}"
);
}
}
#[test]
fn test_julianday_rejects_malformed_time_fields() {
for bad in &[
"+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", ] {
let result = JuliandayFunc.invoke(&[text(bad)]).unwrap();
assert_eq!(
result,
SqliteValue::Null,
"expected NULL for signed time field {bad:?}, got {result:?}"
);
}
}
#[test]
fn test_unixepoch_basic() {
let r = UnixepochFunc
.invoke(&[text("1970-01-01 00:00:00")])
.unwrap();
assert_eq!(r, int(0));
}
#[test]
fn test_unixepoch_known_date() {
let r = UnixepochFunc
.invoke(&[text("2024-01-01 00:00:00")])
.unwrap();
assert_eq!(r, int(1_704_067_200));
}
#[test]
fn test_modifier_days() {
let r = DateFunc
.invoke(&[text("2024-01-15"), text("+10 days")])
.unwrap();
assert_text(&r, "2024-01-25");
}
#[test]
fn test_modifier_months() {
let r = DateFunc
.invoke(&[text("2024-01-31"), text("+1 months")])
.unwrap();
assert_text(&r, "2024-03-02");
}
#[test]
fn test_modifier_years() {
let r = DateFunc
.invoke(&[text("2024-02-29"), text("+1 years")])
.unwrap();
assert_text(&r, "2025-03-01");
}
#[test]
fn test_modifier_hours() {
let r = DateTimeFunc
.invoke(&[text("2024-01-01 23:00:00"), text("+2 hours")])
.unwrap();
assert_text(&r, "2024-01-02 01:00:00");
}
#[test]
fn test_modifier_start_of_month() {
let r = DateFunc
.invoke(&[text("2024-03-15"), text("start of month")])
.unwrap();
assert_text(&r, "2024-03-01");
}
#[test]
fn test_modifier_start_of_year() {
let r = DateFunc
.invoke(&[text("2024-06-15"), text("start of year")])
.unwrap();
assert_text(&r, "2024-01-01");
}
#[test]
fn test_modifier_start_of_day() {
let r = DateTimeFunc
.invoke(&[text("2024-03-15 14:30:00"), text("start of day")])
.unwrap();
assert_text(&r, "2024-03-15 00:00:00");
}
#[test]
fn test_modifier_unixepoch() {
let r = DateTimeFunc.invoke(&[int(0), text("unixepoch")]).unwrap();
assert_text(&r, "1970-01-01 00:00:00");
}
#[test]
fn test_modifier_weekday() {
let r = DateFunc
.invoke(&[text("2024-03-15"), text("weekday 0")])
.unwrap();
assert_text(&r, "2024-03-17");
}
#[test]
fn test_modifier_auto_unixepoch() {
let ts = int(1_710_531_045);
let r = DateTimeFunc.invoke(&[ts.clone(), text("auto")]).unwrap();
let expected = DateTimeFunc.invoke(&[ts, text("unixepoch")]).unwrap();
assert_eq!(
r, expected,
"auto and unixepoch should agree for unix-like values"
);
}
#[test]
fn test_modifier_auto_julian_day() {
let r = DateFunc
.invoke(&[float(2_460_384.5), text("auto")])
.unwrap();
assert_text(&r, "2024-03-15");
}
#[test]
fn test_modifier_localtime_utc_roundtrip() {
let r = DateTimeFunc
.invoke(&[text("2024-03-15 14:30:45"), text("localtime"), text("utc")])
.unwrap();
assert_text(&r, "2024-03-15 14:30:45");
}
#[test]
fn test_modifier_localtime_shifts_value() {
let offset = utc_offset_for_utc_jdn(ymdhms_to_jdn(2024, 3, 15, 12, 0, 0, 0.0));
if offset != 0 {
let r = DateTimeFunc
.invoke(&[text("2024-03-15 12:00:00"), text("localtime")])
.unwrap();
let shifted = match &r {
SqliteValue::Text(s) => s.clone(),
_ => panic!("expected text"),
};
assert_ne!(&*shifted, "2024-03-15 12:00:00");
}
}
#[test]
fn test_modifier_auto_out_of_range_returns_null() {
let r = DateTimeFunc.invoke(&[float(1.0e20), text("auto")]).unwrap();
assert_eq!(r, SqliteValue::Null);
}
#[test]
fn test_modifier_order_matters() {
let r1 = DateFunc
.invoke(&[text("2024-03-15"), text("start of month"), text("+1 days")])
.unwrap();
assert_text(&r1, "2024-03-02");
let r2 = DateFunc
.invoke(&[text("2024-03-15"), text("+1 days"), text("start of month")])
.unwrap();
assert_text(&r2, "2024-03-01");
}
#[test]
fn test_modifier_weekday_same_day_is_noop() {
let r = DateFunc
.invoke(&[text("2024-03-17"), text("weekday 0")])
.unwrap();
assert_text(&r, "2024-03-17");
}
#[test]
fn test_bare_time_defaults() {
let r = DateFunc.invoke(&[text("12:30:00")]).unwrap();
assert_text(&r, "2000-01-01");
}
#[test]
fn test_t_separator() {
let r = DateTimeFunc.invoke(&[text("2024-03-15T14:30:00")]).unwrap();
assert_text(&r, "2024-03-15 14:30:00");
}
#[test]
fn test_julian_day_input() {
let r = DateFunc.invoke(&[float(2_460_384.5)]).unwrap();
assert_text(&r, "2024-03-15");
}
#[test]
fn test_null_input() {
assert_eq!(DateFunc.invoke(&[null()]).unwrap(), SqliteValue::Null);
}
#[test]
fn test_invalid_input() {
assert_eq!(
DateFunc.invoke(&[text("not-a-date")]).unwrap(),
SqliteValue::Null
);
}
#[test]
fn test_negative_time_component_invalid() {
let r = TimeFunc.invoke(&[text("-01:00")]).unwrap();
assert_eq!(r, SqliteValue::Null);
}
#[test]
fn test_leap_year() {
let r = DateFunc
.invoke(&[text("2024-02-28"), text("+1 days")])
.unwrap();
assert_text(&r, "2024-02-29");
}
#[test]
fn test_non_leap_year() {
let r = DateFunc
.invoke(&[text("2023-02-28"), text("+1 days")])
.unwrap();
assert_text(&r, "2023-03-01");
}
#[test]
fn test_strftime_basic() {
let r = StrftimeFunc
.invoke(&[text("%Y-%m-%d"), text("2024-03-15")])
.unwrap();
assert_text(&r, "2024-03-15");
}
#[test]
fn test_strftime_time_specifiers() {
let r = StrftimeFunc
.invoke(&[text("%H:%M:%S"), text("2024-03-15 14:30:45")])
.unwrap();
assert_text(&r, "14:30:45");
}
#[test]
fn test_strftime_unix_seconds() {
let r = StrftimeFunc
.invoke(&[text("%s"), text("1970-01-01 00:00:00")])
.unwrap();
assert_text(&r, "0");
}
#[test]
fn test_strftime_day_of_year() {
let r = StrftimeFunc
.invoke(&[text("%j"), text("2024-03-15")])
.unwrap();
assert_text(&r, "075");
}
#[test]
fn test_strftime_day_of_week() {
let r = StrftimeFunc
.invoke(&[text("%w"), text("2024-03-15")])
.unwrap();
assert_text(&r, "5");
let r = StrftimeFunc
.invoke(&[text("%u"), text("2024-03-15")])
.unwrap();
assert_text(&r, "5");
}
#[test]
fn test_strftime_12hour() {
let r = StrftimeFunc
.invoke(&[text("%I %p"), text("2024-03-15 14:30:00")])
.unwrap();
assert_text(&r, "02 PM");
let r = StrftimeFunc
.invoke(&[text("%I %P"), text("2024-03-15 09:30:00")])
.unwrap();
assert_text(&r, "09 am");
}
#[test]
fn test_strftime_all_specifiers_presence() {
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|%%";
let r = StrftimeFunc
.invoke(&[text(fmt), text("2024-03-15 14:30:45.123")])
.unwrap();
let s = match r {
SqliteValue::Text(v) => v,
other => panic!("expected Text, got {other:?}"),
};
let parts: Vec<&str> = s.split('|').collect();
assert_eq!(parts.len(), 25, "unexpected specifier output: {s}");
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!(
parts[6].parse::<f64>().is_ok(),
"expected numeric %J output, got {}",
parts[6]
);
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!(
parts[14].parse::<i64>().is_ok(),
"expected numeric %s output, got {}",
parts[14]
);
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], "%"); }
#[test]
fn test_strftime_null() {
assert_eq!(
StrftimeFunc.invoke(&[null(), text("2024-01-01")]).unwrap(),
SqliteValue::Null
);
assert_eq!(
StrftimeFunc.invoke(&[text("%Y"), null()]).unwrap(),
SqliteValue::Null
);
}
#[test]
#[ignore = "perf-only benchmark"]
fn perf_strftime_timestamp_rows() {
use std::hint::black_box;
use std::time::Instant;
const ROWS: usize = 200_000;
const REPEATS: usize = 5;
const FORMAT: &str = "%Y-%m-%d %H:%M:%S";
const INPUT: &str = "2024-03-15 14:30:45";
let func = StrftimeFunc;
let fmt = text(FORMAT);
let input = text(INPUT);
let mut best_ns = u128::MAX;
let mut output_len = 0usize;
for _ in 0..REPEATS {
let started = Instant::now();
for _ in 0..ROWS {
let result = black_box(
func.invoke(black_box(&[fmt.clone(), input.clone()]))
.expect("strftime benchmark invocation must succeed"),
);
output_len = match result {
SqliteValue::Text(text) => text.len(),
SqliteValue::Null
| SqliteValue::Integer(_)
| SqliteValue::Float(_)
| SqliteValue::Blob(_) => 0,
};
}
let elapsed_ns = started.elapsed().as_nanos();
if elapsed_ns < best_ns {
best_ns = elapsed_ns;
}
}
println!(
"strftime_timestamp_rows rows={ROWS} repeats={REPEATS} best_ns={best_ns} output_len={output_len}"
);
}
#[test]
fn test_timediff_basic() {
let r = TimediffFunc
.invoke(&[text("2024-03-15"), text("2024-03-10")])
.unwrap();
assert_text(&r, "+0000-00-05 00:00:00.000");
}
#[test]
fn test_timediff_negative() {
let r = TimediffFunc
.invoke(&[text("2024-03-10"), text("2024-03-15")])
.unwrap();
assert_text(&r, "-0000-00-05 00:00:00.000");
}
#[test]
fn test_timediff_year_boundary() {
let r = TimediffFunc
.invoke(&[text("2024-01-01 01:00:00"), text("2023-12-31 23:00:00")])
.unwrap();
assert_text(&r, "+0000-00-00 02:00:00.000");
}
#[test]
fn test_modifier_subsec() {
let r = TimeFunc
.invoke(&[text("2024-01-01 12:00:00.123"), text("subsec")])
.unwrap();
match &r {
SqliteValue::Text(s) => assert!(
s.contains('.'),
"expected fractional seconds with subsec: {s}"
),
other => panic!("expected Text, got {other:?}"),
}
}
#[test]
fn test_register_datetime_builtins_all_present() {
let mut reg = FunctionRegistry::new();
register_datetime_builtins(&mut reg);
let expected = [
"date",
"time",
"datetime",
"julianday",
"unixepoch",
"strftime",
"timediff",
];
for name in expected {
assert!(
reg.find_scalar(name, 1).is_some() || reg.find_scalar(name, 2).is_some(),
"datetime function '{name}' not registered"
);
}
}
#[test]
fn test_modifier_year_overflow() {
let huge = i64::MAX;
let modifier = format!("+{huge} years");
let r = DateFunc.invoke(&[text("2000-01-01"), text(&modifier)]);
assert_eq!(r.unwrap(), SqliteValue::Null);
}
#[test]
fn test_jdn_roundtrip() {
let dates = [
(2024, 3, 15),
(2000, 1, 1),
(1970, 1, 1),
(2024, 2, 29),
(1900, 1, 1),
(2099, 12, 31),
];
for (y, m, d) in dates {
let jdn = ymd_to_jdn(y, m, d);
let (y2, m2, d2) = jdn_to_ymd(jdn);
assert_eq!(
(y, m, d),
(y2, m2, d2),
"roundtrip failed for {y}-{m}-{d} (JDN={jdn})"
);
}
}
#[test]
fn test_unix_epoch_roundtrip() {
let jdn = ymd_to_jdn(1970, 1, 1);
let unix = jdn_to_unix(jdn);
assert_eq!(unix, 0, "Unix epoch should be 0");
let jdn2 = unix_to_jdn(0.0);
assert!((jdn2 - UNIX_EPOCH_JDN).abs() < 1e-10, "roundtrip failed");
}
}