use chrono::{Datelike, NaiveDateTime, Timelike};
use vibesql_types::SqlValue;
use super::current::{naive_to_ijd_ms, resolve_time_value};
use crate::{errors::ExecutorError, evaluator::SchemaExprContext};
pub fn julianday(args: &[SqlValue], ctx: SchemaExprContext) -> Result<SqlValue, ExecutorError> {
if has_misplaced_base_modifier(args) {
return Ok(SqlValue::Null);
}
let (filtered, _subsec) = strip_subsec_modifiers(args);
let dt = match resolve_time_value(&filtered, "JULIANDAY", ctx)? {
Some(dt) => dt,
None => return Ok(SqlValue::Null),
};
Ok(SqlValue::Real(naive_to_ijd_ms(&dt) as f64 / 86_400_000.0))
}
fn has_misplaced_base_modifier(args: &[SqlValue]) -> bool {
args.iter().skip(2).any(|arg| {
if let SqlValue::Varchar(s) | SqlValue::Character(s) = arg {
let t = s.trim();
t.eq_ignore_ascii_case("unixepoch")
|| t.eq_ignore_ascii_case("julianday")
|| t.eq_ignore_ascii_case("auto")
} else {
false
}
})
}
fn strip_subsec_modifiers(args: &[SqlValue]) -> (Vec<SqlValue>, bool) {
let mut subsec = false;
let mut filtered: Vec<SqlValue> = Vec::with_capacity(args.len());
for (idx, arg) in args.iter().enumerate() {
if idx > 0 {
if let SqlValue::Varchar(s) | SqlValue::Character(s) = arg {
let t = s.trim();
if t.eq_ignore_ascii_case("subsec") || t.eq_ignore_ascii_case("subsecond") {
subsec = true;
continue;
}
}
}
filtered.push(arg.clone());
}
(filtered, subsec)
}
pub fn unixepoch(args: &[SqlValue], ctx: SchemaExprContext) -> Result<SqlValue, ExecutorError> {
if has_misplaced_base_modifier(args) {
return Ok(SqlValue::Null);
}
let (filtered, subsec) = strip_subsec_modifiers(args);
let dt = match resolve_time_value(&filtered, "UNIXEPOCH", ctx)? {
Some(dt) => dt,
None => return Ok(SqlValue::Null),
};
let unix_ms = dt.and_utc().timestamp_millis();
if subsec {
Ok(SqlValue::Real(unix_ms as f64 / 1000.0))
} else {
Ok(SqlValue::Integer(unix_ms.div_euclid(1000)))
}
}
pub fn timediff(args: &[SqlValue], ctx: SchemaExprContext) -> Result<SqlValue, ExecutorError> {
if args.len() != 2 {
return Err(ExecutorError::UnsupportedFeature(format!(
"TIMEDIFF requires exactly 2 arguments, got {}",
args.len()
)));
}
let d1 = match resolve_time_value(&args[0..1], "TIMEDIFF", ctx)? {
Some(dt) => dt,
None => return Ok(SqlValue::Null),
};
let d2 = if is_now(&args[0]) && is_now(&args[1]) {
d1
} else {
match resolve_time_value(&args[1..2], "TIMEDIFF", ctx)? {
Some(dt) => dt,
None => return Ok(SqlValue::Null),
}
};
match timediff_string(&d1, &d2) {
Some(s) => Ok(SqlValue::Varchar(s.into())),
None => Ok(SqlValue::Null),
}
}
fn is_now(value: &SqlValue) -> bool {
matches!(value, SqlValue::Varchar(s) | SqlValue::Character(s) if s.eq_ignore_ascii_case("now"))
}
fn time_of_day_ms(dt: &NaiveDateTime) -> i64 {
let time = dt.time();
time.num_seconds_from_midnight() as i64 * 1000 + (time.nanosecond() / 1_000_000).min(999) as i64
}
fn compose_unix_ms(year: i64, month: i64, day: u32, tod_ms: i64) -> Option<i64> {
debug_assert!((1..=12).contains(&month));
let year = i32::try_from(year).ok()?;
let base = chrono::NaiveDate::from_ymd_opt(year, month as u32, 1)?;
let base_ms = base.and_hms_opt(0, 0, 0)?.and_utc().timestamp_millis();
Some(base_ms + (day as i64 - 1) * 86_400_000 + tod_ms)
}
fn timediff_string(d1: &NaiveDateTime, d2: &NaiveDateTime) -> Option<String> {
let ms1 = d1.and_utc().timestamp_millis();
let ms2 = d2.and_utc().timestamp_millis();
let day = d2.day();
let tod = time_of_day_ms(d2);
let (sign, y, m, diff_ms) = if ms1 >= ms2 {
let mut y = d1.year() as i64 - d2.year() as i64;
let mut m = d1.month() as i64 - d2.month() as i64;
if m < 0 {
y -= 1;
m += 12;
}
let mut cy = d1.year() as i64;
let mut cm = d1.month() as i64;
let mut cur = compose_unix_ms(cy, cm, day, tod)?;
while ms1 < cur {
m -= 1;
if m < 0 {
m = 11;
y -= 1;
}
cm -= 1;
if cm < 1 {
cm = 12;
cy -= 1;
}
cur = compose_unix_ms(cy, cm, day, tod)?;
}
('+', y, m, ms1 - cur)
} else {
let mut y = d2.year() as i64 - d1.year() as i64;
let mut m = d2.month() as i64 - d1.month() as i64;
if m < 0 {
y -= 1;
m += 12;
}
let mut cy = d1.year() as i64;
let mut cm = d1.month() as i64;
let mut cur = compose_unix_ms(cy, cm, day, tod)?;
while ms1 > cur {
m -= 1;
if m < 0 {
m = 11;
y -= 1;
}
cm += 1;
if cm > 12 {
cm = 1;
cy += 1;
}
cur = compose_unix_ms(cy, cm, day, tod)?;
}
('-', y, m, cur - ms1)
};
let days = diff_ms / 86_400_000;
let rem = diff_ms % 86_400_000;
let hours = rem / 3_600_000;
let rem = rem % 3_600_000;
let minutes = rem / 60_000;
let rem = rem % 60_000;
let seconds = rem / 1000;
let millis = rem % 1000;
Some(format!(
"{}{:04}-{:02}-{:02} {:02}:{:02}:{:02}.{:03}",
sign, y, m, days, hours, minutes, seconds, millis
))
}
#[cfg(test)]
mod tests {
use super::super::current::datetime;
use super::*;
fn text(s: &str) -> SqlValue {
SqlValue::Varchar(s.into())
}
fn jd(args: &[SqlValue]) -> SqlValue {
julianday(args, SchemaExprContext::None).expect("julianday should not error")
}
fn ue(args: &[SqlValue]) -> SqlValue {
unixepoch(args, SchemaExprContext::None).expect("unixepoch should not error")
}
fn td(a: &str, b: &str) -> SqlValue {
timediff(&[text(a), text(b)], SchemaExprContext::None)
.expect("timediff should not error")
}
fn assert_real(value: SqlValue, expected: f64) {
match value {
SqlValue::Real(r) => {
assert!((r - expected).abs() < 1e-9, "expected {}, got {}", expected, r)
}
other => panic!("expected REAL {}, got {:?}", expected, other),
}
}
#[test]
fn test_julianday_basic() {
assert_real(jd(&[text("2000-01-01")]), 2451544.5);
assert_real(jd(&[text("1970-01-01")]), 2440587.5);
assert_real(jd(&[text("2024-01-01 12:00:00")]), 2460311.0);
}
#[test]
fn test_julianday_with_modifier() {
assert_real(jd(&[text("2024-01-01"), text("+1 day")]), 2460311.5);
}
#[test]
fn test_julianday_numeric_passthrough() {
assert_real(jd(&[SqlValue::Integer(2451545)]), 2451545.0);
assert_real(jd(&[SqlValue::Real(2451544.5)]), 2451544.5);
}
#[test]
fn test_julianday_fractional_seconds() {
assert_real(jd(&[text("2003-10-31 12:34:56.432")]), 2452944.0242642593);
}
#[test]
fn test_julianday_returns_real_type() {
assert!(matches!(jd(&[text("2024-01-01")]), SqlValue::Real(_)));
}
#[test]
fn test_julianday_null_and_invalid() {
assert_eq!(jd(&[SqlValue::Null]), SqlValue::Null);
assert_eq!(jd(&[text("not-a-date")]), SqlValue::Null);
assert_eq!(jd(&[text("2024-01-01"), text("bogus")]), SqlValue::Null);
}
#[test]
fn test_julianday_string_julian_day_input() {
assert_real(jd(&[text("2451545.0")]), 2451545.0);
}
#[test]
fn test_unixepoch_basic() {
assert_eq!(ue(&[text("2024-01-01")]), SqlValue::Integer(1704067200));
assert_eq!(ue(&[text("1970-01-01")]), SqlValue::Integer(0));
}
#[test]
fn test_unixepoch_floors_fractional_seconds() {
assert_eq!(ue(&[text("2024-01-01 00:00:00.500")]), SqlValue::Integer(1704067200));
assert_eq!(ue(&[text("1969-12-31 23:59:59.5")]), SqlValue::Integer(-1));
}
#[test]
fn test_unixepoch_subsec() {
assert_real(ue(&[text("2024-01-01 00:00:00.500"), text("subsec")]), 1704067200.5);
assert!(matches!(ue(&[text("2024-01-01"), text("subsec")]), SqlValue::Real(_)));
assert_real(ue(&[text("2024-01-01 00:00:00.250"), text("subsecond")]), 1704067200.25);
}
#[test]
fn test_unixepoch_round_trip() {
assert_eq!(
ue(&[SqlValue::Integer(1704067200), text("unixepoch")]),
SqlValue::Integer(1704067200)
);
}
#[test]
fn test_unixepoch_subsec_after_unixepoch_modifier() {
assert_eq!(
ue(&[SqlValue::Integer(1704067200), text("unixepoch"), text("subsec")]),
SqlValue::Real(1704067200.0)
);
}
#[test]
fn test_unixepoch_null_and_invalid() {
assert_eq!(ue(&[SqlValue::Null]), SqlValue::Null);
assert_eq!(ue(&[text("not-a-date")]), SqlValue::Null);
}
#[test]
fn test_unixepoch_modifier_after_subsec_is_null() {
assert_eq!(
ue(&[SqlValue::Integer(1704067200), text("subsec"), text("unixepoch")]),
SqlValue::Null
);
assert_eq!(
ue(&[SqlValue::Integer(1704067200), text("subsec"), text("subsec"), text("unixepoch")]),
SqlValue::Null
);
assert_eq!(
ue(&[SqlValue::Integer(1704067200), text("subsecond"), text("unixepoch")]),
SqlValue::Null
);
}
#[test]
fn test_julianday_modifier_after_subsec_is_null() {
assert_eq!(
jd(&[SqlValue::Integer(2451545), text("subsec"), text("julianday")]),
SqlValue::Null
);
assert_real(
jd(&[SqlValue::Integer(2451545), text("julianday"), text("subsec")]),
2451545.0,
);
}
#[test]
fn test_auto_modifier_after_subsec_is_null() {
assert_eq!(ue(&[SqlValue::Real(1.234), text("subsec"), text("auto")]), SqlValue::Null);
}
#[test]
fn test_subsec_remains_order_transparent_for_ordinary_modifiers() {
assert_real(
ue(&[text("2024-01-01 00:00:00.5"), text("subsec"), text("+1 day")]),
1704153600.5,
);
assert_real(
ue(&[text("2024-01-01 00:00:00.5"), text("+1 day"), text("subsec")]),
1704153600.5,
);
}
#[test]
fn test_misplaced_base_modifier_spelling_variants() {
assert_eq!(
ue(&[SqlValue::Integer(1704067200), text("SubSec"), text("UNIXEPOCH")]),
SqlValue::Null
);
assert_eq!(
ue(&[SqlValue::Integer(1704067200), text("subsec"), text(" unixepoch ")]),
SqlValue::Null
);
assert_eq!(ue(&[text("subsec")]), SqlValue::Null);
}
#[test]
fn test_timediff_basic_days() {
assert_eq!(td("2024-01-02", "2024-01-01"), text("+0000-00-01 00:00:00.000"));
assert_eq!(td("2024-01-01", "2024-01-02"), text("-0000-00-01 00:00:00.000"));
}
#[test]
fn test_timediff_whole_years_and_months() {
assert_eq!(td("2025-01-01", "2024-01-01"), text("+0001-00-00 00:00:00.000"));
assert_eq!(td("2024-03-31", "2024-01-31"), text("+0000-02-00 00:00:00.000"));
}
#[test]
fn test_timediff_month_borrowing() {
assert_eq!(td("2024-03-01", "2024-01-31"), text("+0000-00-30 00:00:00.000"));
assert_eq!(td("2024-04-30", "2024-05-31"), text("-0000-01-01 00:00:00.000"));
}
#[test]
fn test_timediff_leap_year() {
assert_eq!(td("2024-03-01", "2024-02-28"), text("+0000-00-02 00:00:00.000"));
assert_eq!(td("2023-03-01", "2023-02-28"), text("+0000-00-01 00:00:00.000"));
}
#[test]
fn test_timediff_across_year_boundary() {
assert_eq!(td("2023-02-15", "2022-12-31"), text("+0000-01-15 00:00:00.000"));
}
#[test]
fn test_timediff_with_time_and_millis() {
assert_eq!(
td("2024-02-28 12:30:45.250", "2023-12-31 23:45:50.500"),
text("+0000-01-27 12:44:54.750")
);
assert_eq!(
td("2024-01-01 00:00:00", "2024-01-01 00:00:00.001"),
text("-0000-00-00 00:00:00.001")
);
}
#[test]
fn test_timediff_now_now_is_zero() {
assert_eq!(td("now", "now"), text("+0000-00-00 00:00:00.000"));
}
#[test]
fn test_timediff_returns_text_type() {
assert!(matches!(td("2024-01-02", "2024-01-01"), SqlValue::Varchar(_)));
}
#[test]
fn test_timediff_null_and_invalid() {
assert_eq!(timediff(&[SqlValue::Null, text("2024-01-01")], SchemaExprContext::None).unwrap(), SqlValue::Null);
assert_eq!(timediff(&[text("2024-01-01"), SqlValue::Null], SchemaExprContext::None).unwrap(), SqlValue::Null);
assert_eq!(td("garbage", "2024-01-01"), SqlValue::Null);
assert_eq!(td("2024-01-01", "garbage"), SqlValue::Null);
}
#[test]
fn test_timediff_wrong_arg_count_errors() {
assert!(timediff(&[text("2024-01-01")], SchemaExprContext::None).is_err());
assert!(timediff(&[text("a"), text("b"), text("c")], SchemaExprContext::None).is_err());
}
#[test]
fn test_timediff_sqlite_timediff1_section3() {
assert_eq!(td("2000-03-02", "2000-01-31"), text("+0000-01-00 00:00:00.000"));
assert_eq!(td("2000-01-31", "2000-03-02"), text("-0000-01-02 00:00:00.000"));
assert_eq!(td("2000-03-02", "1999-01-31"), text("+0001-01-00 00:00:00.000"));
assert_eq!(td("1999-01-31", "2000-03-02"), text("-0001-01-02 00:00:00.000"));
}
fn datetime_text(args: &[SqlValue]) -> String {
match datetime(args, SchemaExprContext::None).expect("datetime should not error") {
SqlValue::Timestamp(ts) => ts.to_string(),
other => panic!("expected timestamp, got {:?}", other),
}
}
#[test]
fn test_timediff_invariant_grid() {
let dates = [
"2000-01-01 00:00:00",
"2000-02-29 13:00:00",
"2001-02-28 23:59:59",
"2001-03-31 15:15:00",
"2004-05-01 23:59:59",
"2008-01-01 01:59:00",
"2023-12-31 23:45:50.500",
"2024-01-31 00:00:00",
"2024-02-28 12:30:45.250",
"2024-03-01 00:00:00",
"2024-05-31 00:00:00",
];
for a in &dates {
for b in &dates {
let diff = match td(a, b) {
SqlValue::Varchar(s) => s.to_string(),
other => panic!("timediff({}, {}) returned {:?}", a, b, other),
};
let expected = datetime_text(&[text(a)]);
let actual = datetime_text(&[text(b), text(&diff)]);
assert_eq!(
actual, expected,
"datetime('{}', timediff('{}','{}') = '{}') should equal datetime('{}')",
b, a, b, diff, a
);
}
}
}
#[test]
fn test_strict_date_string_grammar() {
assert_eq!(jd(&[text("+2000-01-01")]), SqlValue::Null);
assert_eq!(jd(&[text("200-01-01")]), SqlValue::Null);
assert_eq!(jd(&[text("2000-1-01")]), SqlValue::Null);
assert_eq!(jd(&[text("2000-01-1")]), SqlValue::Null);
assert_eq!(jd(&[text("2001-01-01 12:59:60")]), SqlValue::Null);
}
#[test]
fn test_day_of_month_overflow_normalizes() {
assert_real(jd(&[text("2003-02-31")]), 2452701.5);
}
#[test]
fn test_t_separator_variants() {
assert_real(jd(&[text("2000-01-01T12:00:00")]), 2451545.0);
assert_real(jd(&[text("2000-01-01 T12:00:00")]), 2451545.0);
assert_real(jd(&[text("2000-01-01T 12:00:00")]), 2451545.0);
assert_real(jd(&[text("2000-01-01 T 12:00:00")]), 2451545.0);
}
#[test]
fn test_negative_year_input() {
assert_real(jd(&[text("-4713-11-24 12:00:00")]), 0.0);
assert_real(jd(&[text("-4713-11-24 13:40:48.864"), text("subsec")]), 0.07001);
}
#[test]
fn test_fractional_year_shift() {
assert_real(jd(&[SqlValue::Real(2454832.5), text("-1.5 years")]), 2454284.0);
assert_real(jd(&[SqlValue::Real(2454832.5), text("+1.5 years")]), 2455380.0);
}
#[test]
fn test_unixepoch_fractional_input() {
assert_real(ue(&[text("1970-01-01T00:00:00.1"), text("subsec")]), 0.1);
assert_real(ue(&[text("1970-01-01T00:00:00.2"), text("subsecond")]), 0.2);
assert_real(ue(&[SqlValue::Real(1.234), text("unixepoch"), text("subsec")]), 1.234);
}
fn datetime_or_null(args: &[SqlValue]) -> Option<String> {
match datetime(args, SchemaExprContext::None).expect("datetime should not error") {
SqlValue::Timestamp(ts) => Some(ts.to_string()),
SqlValue::Null => None,
other => panic!("expected timestamp or NULL, got {:?}", other),
}
}
#[test]
fn test_date_offset_modifier() {
let base = text("2000-01-01");
let case = |modifier: &str| datetime_or_null(&[base.clone(), text(modifier)]);
assert_eq!(case("+0001-02-03"), Some("2001-03-04 00:00:00".into()));
assert_eq!(case("+0001-02-03x"), None);
assert_eq!(case("+0001-11-03"), Some("2001-12-04 00:00:00".into()));
assert_eq!(case("+0001-12-03"), None); assert_eq!(case("+0001-02-30"), Some("2001-03-31 00:00:00".into()));
assert_eq!(case("+0001-02-31"), None); assert_eq!(case("+0001-02-03 0"), None);
assert_eq!(case("+0001-02-03 01"), None);
assert_eq!(case("+0001-02-03 01:"), None);
assert_eq!(case("+0001-02-03 01:0"), None);
assert_eq!(case("+0001-02-03 01:02"), Some("2001-03-04 01:02:00".into()));
assert_eq!(case("+0001-02-03 01:02:"), None);
assert_eq!(case("+0001-02-03 01:02:0"), None);
assert_eq!(case("+0001-02-03 01:02:03"), Some("2001-03-04 01:02:03".into()));
assert_eq!(case("+0001-02-03 01:02:03."), None);
assert_eq!(case("+0001-02-03 01:02:03.5"), Some("2001-03-04 01:02:03".into()));
assert_eq!(case("+0001-02-03 01:02:03.500x"), None);
assert_eq!(case("+0001-02-03 01:02:03.500 x"), None);
assert_eq!(
datetime_or_null(&[text("1998-11-10"), text("+0001-03-19 12:34:56")]),
Some("2000-02-29 12:34:56".into())
);
assert_eq!(
datetime_or_null(&[text("2001-03-31"), text("-0001-01-00 06:10")]),
Some("2000-03-01 17:50:00".into())
);
}
#[test]
fn test_datetime_julian_day_numeric_input() {
assert_eq!(datetime_text(&[SqlValue::Real(2451544.5)]), "2000-01-01 00:00:00");
assert_eq!(datetime_text(&[SqlValue::Real(2451545.0)]), "2000-01-01 12:00:00");
}
#[test]
fn test_datetime_julian_day_string_input() {
assert_eq!(datetime_text(&[text("2451545.0")]), "2000-01-01 12:00:00");
assert_eq!(datetime_text(&[text("2440587.5")]), "1970-01-01 00:00:00");
}
}