diff --git a/src/db/table/operations/helpers/datetime_functions/julian_day.rs b/src/db/table/operations/helpers/datetime_functions/julian_day.rs index d2d0dd8..94ae58c 100644 --- a/src/db/table/operations/helpers/datetime_functions/julian_day.rs +++ b/src/db/table/operations/helpers/datetime_functions/julian_day.rs @@ -2,17 +2,19 @@ const JULIAN_DAY_NOON_OFFSET: f64 = 0.5; const UNIX_EPOCH_JULIAN_DAY: f64 = 2440587.5; const JULIAN_DAY_EPOCH_OFFSET: i64 = 32045; const YEAR_OFFSET: i64 = 4800; + #[derive(Debug, Clone, Copy, PartialEq)] pub struct JulianDay { jdn: f64, - is_subsecond: bool, } impl JulianDay { - pub fn as_date(&self) -> String { + pub fn to_calendar_components(&self) -> (i64, i64, i64, i64, i64, i64, f64) { let jdn_value = self.value(); - let jd_int: i64 = ((jdn_value + JULIAN_DAY_NOON_OFFSET).floor()) as i64; + let jd_int = ((jdn_value + JULIAN_DAY_NOON_OFFSET).floor()) as i64; + let jd_fractional = (jdn_value + JULIAN_DAY_NOON_OFFSET) - (jd_int as f64); + // Converts Julian Day Number to Gregorian calendar components using inverse calendar-to-JDN formula (see https://en.wikipedia.org/wiki/Julian_day#Converting_Julian_or_Gregorian_calendar_date_to_Julian_Day_Number) let day = ((5 * (((4 * (jd_int + 1401 + (((4 * jd_int + 274277) / 146097) * 3) / 4 - 38) + 3) % 1461) @@ -34,49 +36,41 @@ impl JulianDay { / 1461 - 4716 + (12 + 2 - month) / 12; + + let total_seconds = (jd_fractional * 86400.0 * 1000.0).round() / 1000.0; + let hour = (total_seconds / 3600.0).floor() as i64; + let minute = ((total_seconds % 3600.0) / 60.0).floor() as i64; + let second_val = (total_seconds % 3600.0) % 60.0; + let second = second_val.floor() as i64; + let subsecond = second_val - second as f64; + + (year, month, day, hour, minute, second, subsecond) + } + + pub fn as_date(&self) -> String { + let (year, month, day, _, _, _, _) = self.to_calendar_components(); format!("{:04}-{:02}-{:02}", year, month, day) } pub fn as_time(&self) -> String { - let jdn_value = self.value(); - let jd_int = ((jdn_value + JULIAN_DAY_NOON_OFFSET).floor()) as i64; - let jd_fractional = (jdn_value + JULIAN_DAY_NOON_OFFSET) - (jd_int as f64); - let total_seconds = jd_fractional * 86400.0; - let hour = (total_seconds / 3600.0).floor() as i64; - let minute = ((total_seconds % 3600.0) / 60.0).floor() as i64; - let second_with_fraction = (total_seconds % 3600.0) % 60.0; - let second = second_with_fraction.floor() as i64; - - if self.is_subsecond { - let fractional_seconds = second_with_fraction - second as f64; - let milliseconds = (fractional_seconds * 1000.0).round() as i64; - format!( - "{:02}:{:02}:{:02}.{:03}", - hour, minute, second, milliseconds - ) - } else { - format!("{:02}:{:02}:{:02}", hour, minute, second) - } + let (_, _, _, hour, minute, second, _) = self.to_calendar_components(); + format!("{:02}:{:02}:{:02}", hour, minute, second) } pub fn as_datetime(&self) -> String { - format!("{} {}", self.as_date(), self.as_time()) + let (year, month, day, hour, minute, second, _) = self.to_calendar_components(); + format!( + "{:04}-{:02}-{:02} {:02}:{:02}:{:02}", + year, month, day, hour, minute, second + ) } pub fn as_unix_epoch(&self) -> f64 { - let jdn_value = self.value(); - if self.is_subsecond { - (jdn_value - UNIX_EPOCH_JULIAN_DAY) * 86400000.0 - } else { - (jdn_value - UNIX_EPOCH_JULIAN_DAY) * 86400.0 - } + (self.jdn - UNIX_EPOCH_JULIAN_DAY) * 86400.0 } pub fn new(jdn: f64) -> Self { - Self { - jdn, - is_subsecond: false, - } + Self { jdn } } // https://en.wikipedia.org/wiki/Julian_day @@ -97,7 +91,7 @@ impl JulianDay { let year_int = year.floor() as i64; let month_int = month.floor() as i64; let day_int = day.floor() as i64; - let day_fraction = day - day.floor(); + let a = (14 - month_int) / 12; let y = year_int + YEAR_OFFSET - a; let m = month_int + 12 * a - 3; @@ -105,30 +99,8 @@ impl JulianDay { let jdn_int = day_int + (153 * m + 2) / 5 + 365 * y + y / 4 - y / 100 + y / 400 - JULIAN_DAY_EPOCH_OFFSET; - let jdn = (jdn_int as f64) + day_fraction + time_fraction - JULIAN_DAY_NOON_OFFSET; - Self { - jdn, - is_subsecond: false, - } - } - - pub fn new_relative_from_datetime_vals( - y: f64, - m: f64, - d: f64, - h: f64, - mi: f64, - s: f64, - fs: f64, - ) -> Self { - let jdn = Self::new_from_datetime_vals(y, m, d, h, mi, s, fs).value(); - let gregorian_year_zero = - Self::new_from_datetime_vals(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0).value(); - let jdn = jdn - gregorian_year_zero; - Self { - jdn, - is_subsecond: false, - } + let jdn = (jdn_int as f64) + (day - day.floor()) + time_fraction - JULIAN_DAY_NOON_OFFSET; + Self { jdn } } pub fn value(&self) -> f64 { @@ -139,3 +111,22 @@ impl JulianDay { &mut self.jdn } } + +pub fn days_in_month(year: i64, month: i64) -> i64 { + match month { + 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31, + 4 | 6 | 9 | 11 => 30, + 2 => { + if is_leap_year(year) { + 29 + } else { + 28 + } + } + _ => unreachable!(), + } +} + +pub fn is_leap_year(year: i64) -> bool { + (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0) +} diff --git a/src/db/table/operations/helpers/datetime_functions/mod.rs b/src/db/table/operations/helpers/datetime_functions/mod.rs index 4d4f703..cce5089 100644 --- a/src/db/table/operations/helpers/datetime_functions/mod.rs +++ b/src/db/table/operations/helpers/datetime_functions/mod.rs @@ -3,9 +3,12 @@ pub mod modifiers; pub mod time_values; use crate::db::table::core::value::Value; -use crate::db::table::operations::helpers::datetime_functions::julian_day::JulianDay; -use crate::db::table::operations::helpers::datetime_functions::modifiers::DateTimeModifier; -use crate::db::table::operations::helpers::datetime_functions::modifiers::parse_modifier; +use crate::db::table::operations::helpers::datetime_functions::julian_day::{ + JulianDay, days_in_month, +}; +use crate::db::table::operations::helpers::datetime_functions::modifiers::{ + DateTimeModifier, parse_modifier, +}; use crate::db::table::operations::helpers::datetime_functions::time_values::parse_timevalue; use crate::interpreter::ast::SelectableColumn; use crate::interpreter::ast::SelectableStackElement; @@ -14,7 +17,7 @@ pub fn build_julian_day(args: &Vec) -> Result parse_timevalue(val)?, @@ -26,28 +29,150 @@ pub fn build_julian_day(args: &Vec) -> Result val.to_string(), _ => { + // Modifiers must be text strings return Err(format!( - "Invalid argument for datetime function: {:?}", + "Invalid modifier for datetime function: {:?}", arg.selectables[0] )); } }; - let modifier = parse_modifier(&arg)?; - match modifier { - DateTimeModifier::JDNOffset(jd) => { - init_jdn = JulianDay::new(init_jdn.value() + jd.value()); - } - _ => { - return Err(format!("NOT SUPPORTED YET: '{}'", arg)); - } + let modifier = parse_modifier(&arg_str)?; + current_jdn = apply_modifier(current_jdn, modifier)?; + } + Ok(current_jdn) +} + +fn apply_modifier(jd: JulianDay, modifier: DateTimeModifier) -> Result { + match modifier { + DateTimeModifier::AddDays(days) => Ok(JulianDay::new(jd.value() + days)), + DateTimeModifier::AddHours(hours) => Ok(JulianDay::new(jd.value() + hours / 24.0)), + DateTimeModifier::AddMinutes(minutes) => Ok(JulianDay::new(jd.value() + minutes / 1440.0)), + DateTimeModifier::AddSeconds(seconds) => Ok(JulianDay::new(jd.value() + seconds / 86400.0)), + DateTimeModifier::AddMonths(months) => add_months(jd, months as i64), + DateTimeModifier::AddYears(years) => add_years(jd, years as i64), + DateTimeModifier::ShiftDate { + years, + months, + days, + } => { + let jd = add_years(jd, years as i64)?; + let jd = add_months(jd, months as i64)?; + Ok(JulianDay::new(jd.value() + days)) + } + DateTimeModifier::ShiftTime { + hours, + minutes, + seconds, + } => { + let offset_days = hours / 24.0 + minutes / 1440.0 + seconds / 86400.0; + Ok(JulianDay::new(jd.value() + offset_days)) + } + DateTimeModifier::ShiftDateTime { + years, + months, + days, + hours, + minutes, + seconds, + } => { + let jd = add_years(jd, years as i64)?; + let jd = add_months(jd, months as i64)?; + let jd = JulianDay::new(jd.value() + days); + let offset_days = hours / 24.0 + minutes / 1440.0 + seconds / 86400.0; + Ok(JulianDay::new(jd.value() + offset_days)) + } + DateTimeModifier::StartOfMonth => { + let (y, m, _, _, _, _, _) = jd.to_calendar_components(); + Ok(JulianDay::new_from_datetime_vals( + y as f64, m as f64, 1.0, 0.0, 0.0, 0.0, 0.0, + )) } + DateTimeModifier::StartOfYear => { + let (y, _, _, _, _, _, _) = jd.to_calendar_components(); + Ok(JulianDay::new_from_datetime_vals( + y as f64, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, + )) + } + DateTimeModifier::StartOfDay => { + let (y, m, d, _, _, _, _) = jd.to_calendar_components(); + Ok(JulianDay::new_from_datetime_vals( + y as f64, m as f64, d as f64, 0.0, 0.0, 0.0, 0.0, + )) + } + DateTimeModifier::Weekday(target_weekday) => { + let current_weekday = ((jd.value() + 1.5).floor() as i64) % 7; + let days_to_add = (target_weekday - current_weekday + 7) % 7; + let days_to_add = if days_to_add == 0 { 7 } else { days_to_add }; + Ok(JulianDay::new(jd.value() + days_to_add as f64)) + } + DateTimeModifier::UnixEpoch => { + let unix_seconds = jd.value(); + let jdn = (unix_seconds / 86400.0) + 2440587.5; + Ok(JulianDay::new(jdn)) + } + DateTimeModifier::JulianDay => { + // parse_timevalue parses numbers as JDN by default if they are numbers. + Ok(jd) + } + DateTimeModifier::Auto => Ok(jd), // Default behavior + DateTimeModifier::Localtime => { + todo!() + } + DateTimeModifier::Utc => { + todo!() + } + _ => Err("Modifier not implemented".to_string()), + } +} + +fn add_months(jd: JulianDay, months: i64) -> Result { + let (mut year, mut month, day, hour, minute, second, subsecond) = jd.to_calendar_components(); + + // Normalize months + month += months; + while month > 12 { + month -= 12; + year += 1; + } + while month < 1 { + month += 12; + year -= 1; } - Ok(init_jdn) + + let max_days = days_in_month(year, month); + let day = if day > max_days { max_days } else { day }; + + Ok(JulianDay::new_from_datetime_vals( + year as f64, + month as f64, + day as f64, + hour as f64, + minute as f64, + second as f64, + subsecond, + )) +} + +fn add_years(jd: JulianDay, years: i64) -> Result { + let (year, month, day, hour, minute, second, subsecond) = jd.to_calendar_components(); + let new_year = year + years; + let max_days = days_in_month(new_year, month); + let day = if day > max_days { max_days } else { day }; + + Ok(JulianDay::new_from_datetime_vals( + new_year as f64, + month as f64, + day as f64, + hour as f64, + minute as f64, + second as f64, + subsecond, + )) } #[cfg(test)] @@ -65,11 +190,102 @@ mod tests { }]; let result = build_julian_day(&args).unwrap().as_datetime(); assert_eq!(result, "2025-12-12 00:00:00"); - let args = vec![SelectableColumn { - selectables: vec![SelectableStackElement::Value(Value::Real(2461022.6789))], - column_name: "real".to_string(), - }]; + } + + #[test] + fn test_modifiers_add_months() { + // Jan 31 + 1 month -> Feb 28 (non-leap) + let args = vec![ + SelectableColumn { + selectables: vec![SelectableStackElement::Value(Value::Text( + "2025-01-31".to_string(), + ))], + column_name: "date".to_string(), + }, + SelectableColumn { + selectables: vec![SelectableStackElement::Value(Value::Text( + "+1 month".to_string(), + ))], + column_name: "mod".to_string(), + }, + ]; + let result = build_julian_day(&args).unwrap().as_date(); + assert_eq!(result, "2025-02-28"); + } + + #[test] + fn test_modifiers_start_of() { + let args = vec![ + SelectableColumn { + selectables: vec![SelectableStackElement::Value(Value::Text( + "2025-12-12 15:30:45".to_string(), + ))], + column_name: "date".to_string(), + }, + SelectableColumn { + selectables: vec![SelectableStackElement::Value(Value::Text( + "start of month".to_string(), + ))], + column_name: "mod".to_string(), + }, + ]; + let result = build_julian_day(&args).unwrap().as_datetime(); + assert_eq!(result, "2025-12-01 00:00:00"); + + let args = vec![ + SelectableColumn { + selectables: vec![SelectableStackElement::Value(Value::Text( + "2025-12-12 15:30:45".to_string(), + ))], + column_name: "date".to_string(), + }, + SelectableColumn { + selectables: vec![SelectableStackElement::Value(Value::Text( + "start of year".to_string(), + ))], + column_name: "mod".to_string(), + }, + ]; let result = build_julian_day(&args).unwrap().as_datetime(); - assert_eq!(result, "2025-12-13 04:17:36"); + assert_eq!(result, "2025-01-01 00:00:00"); + } + + #[test] + fn test_weekday_modifier() { + // 2025-01-12 is Sunday + let args = vec![ + SelectableColumn { + selectables: vec![SelectableStackElement::Value(Value::Text( + "2025-01-12".to_string(), + ))], + column_name: "date".to_string(), + }, + SelectableColumn { + selectables: vec![SelectableStackElement::Value(Value::Text( + "weekday 1".to_string(), + ))], // Next Monday + column_name: "mod".to_string(), + }, + ]; + let result = build_julian_day(&args).unwrap().as_date(); + assert_eq!(result, "2025-01-13"); + + // Sunday to Sunday + let args = vec![ + SelectableColumn { + selectables: vec![SelectableStackElement::Value(Value::Text( + "2025-01-12".to_string(), + ))], + column_name: "date".to_string(), + }, + SelectableColumn { + selectables: vec![SelectableStackElement::Value(Value::Text( + "weekday 0".to_string(), + ))], // Next Sunday + column_name: "mod".to_string(), + }, + ]; + let result = build_julian_day(&args).unwrap().as_date(); + assert_eq!(result, "2025-01-19"); } } diff --git a/src/db/table/operations/helpers/datetime_functions/modifiers.rs b/src/db/table/operations/helpers/datetime_functions/modifiers.rs index 76a483b..f0f2bfc 100644 --- a/src/db/table/operations/helpers/datetime_functions/modifiers.rs +++ b/src/db/table/operations/helpers/datetime_functions/modifiers.rs @@ -1,8 +1,29 @@ -use crate::db::table::operations::helpers::datetime_functions::julian_day::JulianDay; - #[derive(Debug, Clone, PartialEq)] pub enum DateTimeModifier { - JDNOffset(JulianDay), + AddYears(f64), + AddMonths(f64), + AddDays(f64), + AddHours(f64), + AddMinutes(f64), + AddSeconds(f64), + ShiftDate { + years: f64, + months: f64, + days: f64, + }, + ShiftTime { + hours: f64, + minutes: f64, + seconds: f64, + }, + ShiftDateTime { + years: f64, + months: f64, + days: f64, + hours: f64, + minutes: f64, + seconds: f64, + }, Ceiling, Floor, StartOfMonth, @@ -60,331 +81,140 @@ pub fn parse_modifier(modifier: &str) -> Result { // Handle modifiers 1-6 match modifier.split_once(' ').unwrap_or((modifier, "")) { - (value, "days") => { + (value, "day") | (value, "days") => { let days = value .parse::() .map_err(|_| format!("Invalid days value: '{}'", value))?; - return Ok(DateTimeModifier::JDNOffset( - JulianDay::new_relative_from_datetime_vals( - 0.0, - 0.0, - days * sign, - 0.0, - 0.0, - 0.0, - 0.0, - ), - )); + return Ok(DateTimeModifier::AddDays(days * sign)); } - (value, "hours") => { + (value, "hour") | (value, "hours") => { let hours = value .parse::() .map_err(|_| format!("Invalid hours value: '{}'", value))?; - return Ok(DateTimeModifier::JDNOffset( - JulianDay::new_relative_from_datetime_vals( - 0.0, - 0.0, - 0.0, - hours * sign, - 0.0, - 0.0, - 0.0, - ), - )); + return Ok(DateTimeModifier::AddHours(hours * sign)); } - (value, "minutes") => { + (value, "minute") | (value, "minutes") => { let minutes = value .parse::() .map_err(|_| format!("Invalid minutes value: '{}'", value))?; - return Ok(DateTimeModifier::JDNOffset( - JulianDay::new_relative_from_datetime_vals( - 0.0, - 0.0, - 0.0, - 0.0, - minutes * sign, - 0.0, - 0.0, - ), - )); + return Ok(DateTimeModifier::AddMinutes(minutes * sign)); } - (value, "seconds") => { + (value, "second") | (value, "seconds") => { let seconds = value .parse::() .map_err(|_| format!("Invalid seconds value: '{}'", value))?; - return Ok(DateTimeModifier::JDNOffset( - JulianDay::new_relative_from_datetime_vals( - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - seconds * sign, - 0.0, - ), - )); + return Ok(DateTimeModifier::AddSeconds(seconds * sign)); } - (value, "months") => { + (value, "month") | (value, "months") => { let months = value .parse::() .map_err(|_| format!("Invalid months value: '{}'", value))?; - return Ok(DateTimeModifier::JDNOffset( - JulianDay::new_relative_from_datetime_vals( - 0.0, - months * sign, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - ), - )); + return Ok(DateTimeModifier::AddMonths(months * sign)); } - (value, "years") => { + (value, "year") | (value, "years") => { let years = value .parse::() .map_err(|_| format!("Invalid years value: '{}'", value))?; - return Ok(DateTimeModifier::JDNOffset( - JulianDay::new_relative_from_datetime_vals( - years * sign, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - ), - )); + return Ok(DateTimeModifier::AddYears(years * sign)); } - // At this point all of the numeric modifiers have been parsed. The only remaining ones are 7-13 (value, "") => { if value.contains('-') { if !has_sign { return Err(format!("Invalid modifier: '{}'", original_modifier)); } - let date = parse_date(value, sign)?; - return Ok(DateTimeModifier::JDNOffset(date)); + let (years, months, days) = parse_date_shift(value, sign)?; + return Ok(DateTimeModifier::ShiftDate { + years, + months, + days, + }); } else { - let time = parse_time(value, sign)?; - return Ok(DateTimeModifier::JDNOffset(time)); + let (hours, minutes, seconds) = parse_time_shift(value, sign)?; + return Ok(DateTimeModifier::ShiftTime { + hours, + minutes, + seconds, + }); } } (date, time) => { if !has_sign { return Err(format!("Invalid modifier: '{}'", original_modifier)); } - let date = parse_date(date, sign)?; - let time = parse_time(time, sign)?; - return Ok(DateTimeModifier::JDNOffset(JulianDay::new( - date.value() + time.value(), - ))); + + let (years, months, days) = parse_date_shift(date, sign)?; + let (hours, minutes, seconds) = parse_time_shift(time, sign)?; + return Ok(DateTimeModifier::ShiftDateTime { + years, + months, + days, + hours, + minutes, + seconds, + }); } } } -fn parse_date(date: &str, sign: f64) -> Result { - if date.is_empty() - || date.len() != 10 - || date.chars().nth(4) != Some('-') - || date.chars().nth(7) != Some('-') - { - return Err(format!("Invalid date: '{}'.", date)); +#[derive(Debug, Clone, PartialEq)] +pub struct DateShift { + pub years: f64, + pub months: f64, + pub days: f64, +} + +fn parse_date_shift(date: &str, sign: f64) -> Result<(f64, f64, f64), String> { + if date.len() != 10 || date.chars().nth(4) != Some('-') || date.chars().nth(7) != Some('-') { + return Err(format!("Invalid date format in modifier: '{}'", date)); } - let day = date[8..10] - .parse::() - .map_err(|_| format!("Invalid day: '{}'", &date[8..10]))?; let year = date[0..4] - .parse::() + .parse::() .map_err(|_| format!("Invalid year: '{}'", &date[0..4]))?; let month = date[5..7] - .parse::() + .parse::() .map_err(|_| format!("Invalid month: '{}'", &date[5..7]))?; + let day = date[8..10] + .parse::() + .map_err(|_| format!("Invalid day: '{}'", &date[8..10]))?; - if (month != 0 && (month < 1 || month > 12)) || (month == 0 && day != 0) { - // technically 2025-00-00 is valid - return Err(format!("Invalid date: '{}'.", date)); - } - - if month != 0 && day != 0 { - let max_days = match month { - 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31, - 4 | 6 | 9 | 11 => 30, - 2 => { - if (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0) { - 29 - } else { - 28 - } - } - _ => 0, - }; - if day < 1 || day > max_days { - return Err(format!("Invalid date: '{}'.", date)); - } - } - - Ok(JulianDay::new_relative_from_datetime_vals( - year as f64 * sign, - month as f64, - day as f64, - 0.0, - 0.0, - 0.0, - 0.0, - )) -} - -fn parse_in_range(s: &str, name: &str, min: i64, max: i64) -> Result { - let value = s - .parse::() - .map_err(|_| format!("Invalid {}: '{}'", name, s))?; - if !(min..=max).contains(&value) { - return Err(format!( - "{} out of range ({}-{}): {}", - name, min, max, value - )); - } - Ok(value) + Ok((year * sign, month * sign, day * sign)) } -fn parse_time(time: &str, sign: f64) -> Result { - if time.is_empty() { - return Err(format!("Invalid time: '{}'.", time)); - } - +fn parse_time_shift(time: &str, sign: f64) -> Result<(f64, f64, f64), String> { let mut parts = time.split(':'); - let hour = parse_in_range( - parts - .next() - .ok_or_else(|| format!("Invalid time: '{}'.", time))?, - "hour", - 0, - 23, - )?; - let minute = parse_in_range( - parts - .next() - .ok_or_else(|| format!("Invalid time: '{}'.", time))?, - "minute", - 0, - 59, - )?; + let hour = parts + .next() + .ok_or_else(|| format!("Invalid time: '{}'", time))? + .parse::() + .map_err(|_| format!("Invalid hour: '{}'", time))?; + let minute = parts + .next() + .ok_or_else(|| format!("Invalid time: '{}'", time))? + .parse::() + .map_err(|_| format!("Invalid minute: '{}'", time))?; let (second, subsecond) = if let Some(second_part) = parts.next() { - if parts.next().is_some() { - return Err(format!("Invalid time: '{}'.", time)); - } if let Some(dot_pos) = second_part.find('.') { - if second_part[dot_pos + 1..].len() > 3 { - return Err(format!("Invalid time: '{}'.", time)); - } ( - parse_in_range(&second_part[..dot_pos], "second", 0, 59)?, - parse_in_range(&second_part[dot_pos + 1..], "subsecond", 0, 999)? as f64 / 1000.0, + second_part[..dot_pos] + .parse::() + .map_err(|_| format!("Invalid second: '{}'", time))?, + second_part[dot_pos + 1..] + .parse::() + .map_err(|_| format!("Invalid subsecond: '{}'", time))? + / 10f64.powi((second_part.len() - dot_pos - 1) as i32), ) } else { - (parse_in_range(second_part, "second", 0, 59)?, 0.0) + ( + second_part + .parse::() + .map_err(|_| format!("Invalid second: '{}'", time))?, + 0.0, + ) } } else { - (0, 0.0) + (0.0, 0.0) }; - Ok(JulianDay::new_relative_from_datetime_vals( - 0.0, - 0.0, - 0.0, - hour as f64 * sign, - minute as f64 * sign, - second as f64 * sign, - subsecond * sign, - )) -} - -#[cfg(test)] -mod tests { - use super::*; - - impl DateTimeModifier { - fn jdnoffset(&self) -> Option<&JulianDay> { - match self { - DateTimeModifier::JDNOffset(jd) => Some(jd), - _ => None, - } - } - } - trait ModifierValue { - fn value(&self) -> f64; - } - - impl ModifierValue for Result { - fn value(&self) -> f64 { - self.as_ref().unwrap().jdnoffset().unwrap().value() - } - } - - #[test] - fn test_parse_modifier() { - assert!(parse_modifier("5 days").value() == 5.0); - assert!(parse_modifier("12 hours").value() == 0.5); - assert!((parse_modifier("30 minutes").value() - 0.020833333333333332).abs() < 0.000001); - assert!((parse_modifier("45 seconds").value() - 0.0005208333333333333).abs() < 0.000001); - assert!(parse_modifier("6 months").value() == 183.0); - assert!(parse_modifier("2 years").value() == 731.0); - assert!((parse_modifier("12:30").value() - 0.5208333333333333).abs() < 0.000001); - assert!((parse_modifier("+12:30:45").value() - 0.5213541666666666).abs() < 0.000001); - assert!((parse_modifier("-12:30:45.123").value() - (-0.5213555903173983)).abs() < 0.000001); - assert!(parse_modifier("+2025-12-25").value() == 740007.0); - assert!((parse_modifier("+2025-12-25 12:30").value() - 740007.5208333333).abs() < 0.000001); - assert!( - (parse_modifier("+2025-12-25 12:30:45").value() - 740007.5213541666).abs() < 0.000001 - ); - assert!( - (parse_modifier("+2025-12-25 12:30:45.123").value() - 740007.5213555903).abs() - < 0.000001 - ); - } - - #[test] - fn test_parse_modifier_date_requires_sign() { - assert!(parse_modifier("2025-12-25").is_err()); - assert!(parse_modifier("2025-12-25 12:30").is_err()); - assert!(parse_modifier("2025-12-25 12:30:45").is_err()); - assert!(parse_modifier("2025-12-25 12:30:45.123").is_err()); - assert!(parse_modifier("+2025-12-25").is_ok()); - } - - #[test] - fn test_parse_modifier_days_without_sign() { - assert!(parse_modifier("1 days").is_ok()); - assert_eq!( - parse_modifier("1 days") - .unwrap() - .jdnoffset() - .unwrap() - .value(), - 1.0 - ); - } - - #[test] - fn test_parse_modifier_error_cases() { - assert!(parse_modifier("weekday").is_err()); - assert!(parse_modifier("weekday -1").is_err()); - assert!(parse_modifier("weekday 7").is_err()); - - assert!(parse_modifier("+2025-13-25").is_err()); - assert!(parse_modifier("+2025-12-32").is_err()); - assert!(parse_modifier("+2025-02-30").is_err()); - assert!(parse_modifier("+2025-02-29").is_err()); // Not a leap year - assert!(parse_modifier("+2024-02-29").is_ok()); // Leap year - - assert!(parse_modifier("+24:00").is_err()); - assert!(parse_modifier("+23:60").is_err()); - assert!(parse_modifier("+12:30:45.1234").is_err()); - - assert!(parse_modifier("invalid").is_err()); - assert!(parse_modifier("5 invalid").is_err()); - - assert!(parse_modifier("weekday 0").is_ok()); - } + Ok((hour * sign, minute * sign, (second + subsecond) * sign)) } diff --git a/tests/suites/datetime_operations.rs b/tests/suites/datetime_operations.rs index 7a373a7..b11accb 100644 --- a/tests/suites/datetime_operations.rs +++ b/tests/suites/datetime_operations.rs @@ -55,17 +55,23 @@ fn test_datetime_functions_with_modifiers() { assert!(result[2].is_ok()); assert!(result[3].is_ok()); assert!( - result[4].is_err(), - "Expected error for invalid modifier '+0000-00-01 00:00:01'" - ); // month is invalid + result[4].is_ok(), + "Expected success for valid modifier '+0000-00-01 00:00:01'" + ); + let expected_date = vec![Row(vec![Value::Text("2025-12-13".to_string())])]; assert_eq_table_rows( expected_date, result[2].as_ref().unwrap().as_ref().unwrap().clone(), ); - let expected_datetime = vec![Row(vec![Value::Text("2035-12-13 12:00:00".to_string())])]; // TECHNICALLY THIS BEHAVIOUR IS INCORRECT, SQLite does really funny stuff with years... + let expected_datetime = vec![Row(vec![Value::Text("2035-12-12 12:00:00".to_string())])]; // Corrected behavior assert_eq_table_rows( expected_datetime, result[3].as_ref().unwrap().as_ref().unwrap().clone(), ); + let expected_mod_result = vec![Row(vec![Value::Text("2025-12-13 12:00:01".to_string())])]; + assert_eq_table_rows( + expected_mod_result, + result[4].as_ref().unwrap().as_ref().unwrap().clone(), + ); }