use chrono::{Datelike, Duration, Local, NaiveDateTime, Timelike, Weekday};
use vibesql_types::SqlValue;
use crate::{errors::ExecutorError, evaluator::SchemaExprContext};
pub(super) const JULIAN_EPOCH_OFFSET_MS: i64 = 210_866_760_000_000;
const MAX_IJD_MS: i64 = 464_269_060_799_999;
pub(super) fn naive_to_ijd_ms(dt: &NaiveDateTime) -> i64 {
dt.and_utc().timestamp_millis() + JULIAN_EPOCH_OFFSET_MS
}
pub fn current_date(args: &[SqlValue]) -> Result<SqlValue, ExecutorError> {
if !args.is_empty() {
return Err(ExecutorError::UnsupportedFeature(
"CURRENT_DATE takes no arguments".to_string(),
));
}
let now = Local::now();
use chrono::Datelike;
let date =
vibesql_types::Date::new(now.year(), now.month() as u8, now.day() as u8).map_err(|e| {
ExecutorError::UnsupportedFeature(format!("Failed to create current date: {}", e))
})?;
Ok(SqlValue::Date(date))
}
pub fn current_time(args: &[SqlValue]) -> Result<SqlValue, ExecutorError> {
let precision = if args.is_empty() {
None
} else if args.len() == 1 {
match &args[0] {
SqlValue::Integer(n) if *n >= 0 && *n <= 9 => Some(*n as u32),
SqlValue::Integer(n) => {
return Err(ExecutorError::UnsupportedFeature(format!(
"CURRENT_TIME precision must be 0-9, got {}",
n
)));
}
_ => {
return Err(ExecutorError::UnsupportedFeature(
"CURRENT_TIME precision must be an integer between 0 and 9".to_string(),
));
}
}
} else {
return Err(ExecutorError::UnsupportedFeature(
"CURRENT_TIME takes 0 or 1 arguments".to_string(),
));
};
let now = Local::now();
let time_naive = now.time();
let nanosecond = match precision {
None => 0,
Some(prec) => {
let nanos = time_naive.nanosecond();
let divisor = 10_u32.pow(9 - prec);
(nanos / divisor) * divisor
}
};
let time = vibesql_types::Time::new(
time_naive.hour() as u8,
time_naive.minute() as u8,
time_naive.second() as u8,
nanosecond,
)
.map_err(|e| {
ExecutorError::UnsupportedFeature(format!("Failed to create current time: {}", e))
})?;
Ok(SqlValue::Time(time))
}
pub fn current_timestamp(args: &[SqlValue]) -> Result<SqlValue, ExecutorError> {
let precision = if args.is_empty() {
None
} else if args.len() == 1 {
match &args[0] {
SqlValue::Integer(n) if *n >= 0 && *n <= 9 => Some(*n as u32),
SqlValue::Integer(n) => {
return Err(ExecutorError::UnsupportedFeature(format!(
"CURRENT_TIMESTAMP precision must be 0-9, got {}",
n
)));
}
_ => {
return Err(ExecutorError::UnsupportedFeature(
"CURRENT_TIMESTAMP precision must be an integer between 0 and 9".to_string(),
));
}
}
} else {
return Err(ExecutorError::UnsupportedFeature(
"CURRENT_TIMESTAMP takes 0 or 1 arguments".to_string(),
));
};
let now = Local::now();
use chrono::Datelike;
let time_naive = now.time();
let nanosecond = match precision {
None => 0,
Some(prec) => {
let nanos = time_naive.nanosecond();
let divisor = 10_u32.pow(9 - prec);
(nanos / divisor) * divisor
}
};
let date = vibesql_types::Date::new(now.year(), now.month() as u8, now.day() as u8)
.map_err(|e| ExecutorError::UnsupportedFeature(format!("Failed to create date: {}", e)))?;
let time = vibesql_types::Time::new(
time_naive.hour() as u8,
time_naive.minute() as u8,
time_naive.second() as u8,
nanosecond,
)
.map_err(|e| ExecutorError::UnsupportedFeature(format!("Failed to create time: {}", e)))?;
Ok(SqlValue::Timestamp(vibesql_types::Timestamp::new(date, time)))
}
#[allow(dead_code)]
fn format_time_with_precision(time: chrono::NaiveTime, precision: u32) -> String {
let base = time.format("%H:%M:%S").to_string();
if precision == 0 {
return base;
}
let nanos = time.nanosecond();
let divisor = 10_u32.pow(9 - precision);
let fractional = nanos / divisor;
format!("{}.{:0width$}", base, fractional, width = precision as usize)
}
pub fn datetime(args: &[SqlValue], ctx: SchemaExprContext) -> Result<SqlValue, ExecutorError> {
let dt = match resolve_time_value(args, "DATETIME", ctx)? {
Some(dt) => dt,
None => return Ok(SqlValue::Null),
};
naive_datetime_to_timestamp(dt)
}
#[derive(Clone, Copy)]
struct ResolvedDateTime {
dt: NaiveDateTime,
n_floor: i64,
}
impl ResolvedDateTime {
fn exact(dt: NaiveDateTime) -> Self {
ResolvedDateTime { dt, n_floor: 0 }
}
}
fn compute_floor(year: i32, month: u32, day: i64) -> i64 {
if day <= 28 || matches!(month, 1 | 3 | 5 | 7 | 8 | 10 | 12) {
0
} else if month != 2 {
i64::from(day == 31)
} else if year % 4 != 0 || (year % 100 == 0 && year % 400 != 0) {
day - 28
} else {
day - 29
}
}
pub(super) fn resolve_time_value(
args: &[SqlValue],
func_name: &str,
ctx: SchemaExprContext,
) -> Result<Option<NaiveDateTime>, ExecutorError> {
if args.is_empty() {
if let Some(err) = ctx.non_deterministic_error(func_name) {
return Err(err);
}
return Ok(Some(Local::now().naive_local()));
}
let first_modifier = match args.get(1) {
Some(SqlValue::Varchar(s)) | Some(SqlValue::Character(s)) => Some(s.as_str()),
_ => None,
};
let base_result = match first_modifier {
Some(m) if m.eq_ignore_ascii_case("unixepoch") => {
parse_base_datetime_for_unixepoch(&args[0], func_name)?
}
Some(m) if m.eq_ignore_ascii_case("auto") => {
parse_base_datetime_auto(&args[0], func_name, ctx)?
}
Some(m) if m.eq_ignore_ascii_case("julianday") => parse_base_datetime_julianday(&args[0]),
_ => parse_base_datetime(&args[0], func_name, ctx)?,
};
let mut rdt = match base_result {
Some(rdt) => rdt,
None => return Ok(None),
};
for (idx, modifier_arg) in args.iter().enumerate().skip(1) {
let modifier_str = match modifier_arg {
SqlValue::Varchar(s) | SqlValue::Character(s) => s.as_str(),
SqlValue::Null => return Ok(None),
_ => {
return Err(ExecutorError::UnsupportedFeature(format!(
"{} modifier must be a string, got {:?}",
func_name, modifier_arg
)))
}
};
if modifier_str.eq_ignore_ascii_case("unixepoch")
|| modifier_str.eq_ignore_ascii_case("auto")
|| modifier_str.eq_ignore_ascii_case("julianday")
{
if idx != 1 {
return Ok(None);
}
continue;
}
let trimmed = modifier_str.trim();
if trimmed.eq_ignore_ascii_case("localtime") || trimmed.eq_ignore_ascii_case("utc") {
if let Some(err) = ctx.non_deterministic_error(func_name) {
return Err(err);
}
}
match apply_datetime_modifier(rdt, modifier_str) {
Some(new_rdt) => rdt = new_rdt,
None => return Ok(None), }
}
if !(0..=MAX_IJD_MS).contains(&naive_to_ijd_ms(&rdt.dt)) {
return Ok(None);
}
Ok(Some(rdt.dt))
}
fn parse_base_datetime(
value: &SqlValue,
func_name: &str,
ctx: SchemaExprContext,
) -> Result<Option<ResolvedDateTime>, ExecutorError> {
match value {
SqlValue::Null => Ok(None),
SqlValue::Varchar(s) | SqlValue::Character(s) => {
if s.eq_ignore_ascii_case("now") {
if let Some(err) = ctx.non_deterministic_error(func_name) {
return Err(err);
}
let now = Local::now();
Ok(Some(ResolvedDateTime::exact(now.naive_local())))
} else {
Ok(parse_datetime_string_to_naive(s))
}
}
SqlValue::Date(d) => {
let naive_date = chrono::NaiveDate::from_ymd_opt(d.year, d.month as u32, d.day as u32);
Ok(naive_date.map(|date| ResolvedDateTime::exact(date.and_hms_opt(0, 0, 0).unwrap())))
}
SqlValue::Timestamp(ts) => {
let naive_date = chrono::NaiveDate::from_ymd_opt(
ts.date.year,
ts.date.month as u32,
ts.date.day as u32,
);
let naive_time = chrono::NaiveTime::from_hms_opt(
ts.time.hour as u32,
ts.time.minute as u32,
ts.time.second as u32,
);
match (naive_date, naive_time) {
(Some(date), Some(time)) => {
Ok(Some(ResolvedDateTime::exact(NaiveDateTime::new(date, time))))
}
_ => Ok(None),
}
}
SqlValue::Integer(n) => Ok(julian_day_to_naive(*n as f64).map(ResolvedDateTime::exact)),
SqlValue::Bigint(n) => Ok(julian_day_to_naive(*n as f64).map(ResolvedDateTime::exact)),
SqlValue::Smallint(n) => Ok(julian_day_to_naive(*n as f64).map(ResolvedDateTime::exact)),
SqlValue::Float(n) => Ok(julian_day_to_naive(*n as f64).map(ResolvedDateTime::exact)),
SqlValue::Double(n) | SqlValue::Real(n) | SqlValue::Numeric(n) => {
Ok(julian_day_to_naive(*n).map(ResolvedDateTime::exact))
}
_ => Err(ExecutorError::UnsupportedFeature(format!(
"{} requires string, date, timestamp, or numeric argument, got {:?}",
func_name, value
))),
}
}
fn parse_base_datetime_for_unixepoch(
value: &SqlValue,
func_name: &str,
) -> Result<Option<ResolvedDateTime>, ExecutorError> {
let dt = match value {
SqlValue::Null => None,
SqlValue::Varchar(s) | SqlValue::Character(s) => {
if let Ok(n) = s.trim().parse::<i64>() {
unix_epoch_to_datetime(n)
} else if let Ok(n) = s.trim().parse::<f64>() {
unix_epoch_seconds_f64_to_datetime(n)
} else {
None
}
}
SqlValue::Date(_) | SqlValue::Timestamp(_) => {
None
}
SqlValue::Integer(n) => unix_epoch_to_datetime(*n as i64),
SqlValue::Bigint(n) => unix_epoch_to_datetime(*n),
SqlValue::Smallint(n) => unix_epoch_to_datetime(*n as i64),
SqlValue::Float(n) => unix_epoch_seconds_f64_to_datetime(*n as f64),
SqlValue::Double(n) | SqlValue::Real(n) | SqlValue::Numeric(n) => {
unix_epoch_seconds_f64_to_datetime(*n)
}
_ => {
return Err(ExecutorError::UnsupportedFeature(format!(
"{} requires string, date, timestamp, or numeric argument, got {:?}",
func_name, value
)))
}
};
Ok(dt.map(ResolvedDateTime::exact))
}
fn numeric_time_value(value: &SqlValue) -> Option<f64> {
match value {
SqlValue::Integer(n) => Some(*n as f64),
SqlValue::Bigint(n) => Some(*n as f64),
SqlValue::Smallint(n) => Some(*n as f64),
SqlValue::Float(n) => Some(*n as f64),
SqlValue::Double(n) | SqlValue::Real(n) | SqlValue::Numeric(n) => Some(*n),
SqlValue::Varchar(s) | SqlValue::Character(s) => s.trim().parse::<f64>().ok(),
_ => None,
}
}
fn parse_base_datetime_auto(
value: &SqlValue,
func_name: &str,
ctx: SchemaExprContext,
) -> Result<Option<ResolvedDateTime>, ExecutorError> {
if matches!(value, SqlValue::Null) {
return Ok(None);
}
match numeric_time_value(value) {
Some(n) if (0.0..5_373_484.5).contains(&n) => {
Ok(julian_day_to_naive(n).map(ResolvedDateTime::exact))
}
Some(n) if (-210_866_760_000.0..=253_402_300_799.0).contains(&n) => {
Ok(unix_epoch_seconds_f64_to_datetime(n).map(ResolvedDateTime::exact))
}
Some(_) => Ok(None), None => parse_base_datetime(value, func_name, ctx), }
}
fn parse_base_datetime_julianday(value: &SqlValue) -> Option<ResolvedDateTime> {
match numeric_time_value(value) {
Some(n) if (0.0..5_373_484.5).contains(&n) => {
julian_day_to_naive(n).map(ResolvedDateTime::exact)
}
_ => None,
}
}
fn apply_datetime_modifier(rdt: ResolvedDateTime, modifier: &str) -> Option<ResolvedDateTime> {
let modifier = modifier.trim();
let lower = modifier.to_lowercase();
let dt = rdt.dt;
if lower == "floor" {
return Some(ResolvedDateTime::exact(dt - Duration::days(rdt.n_floor)));
}
if lower == "ceiling" {
return Some(ResolvedDateTime::exact(dt));
}
if lower.starts_with("start of ") {
let unit = &lower[9..].trim();
let new_dt = match *unit {
"month" => {
let date = chrono::NaiveDate::from_ymd_opt(dt.year(), dt.month(), 1)?;
date.and_hms_opt(0, 0, 0)?
}
"year" => {
let date = chrono::NaiveDate::from_ymd_opt(dt.year(), 1, 1)?;
date.and_hms_opt(0, 0, 0)?
}
"day" => dt.date().and_hms_opt(0, 0, 0)?,
_ => return None, };
return Some(ResolvedDateTime { dt: new_dt, n_floor: rdt.n_floor });
}
if lower == "start of" {
return None;
}
if lower.starts_with("weekday ") {
let rest = &modifier[8..].trim();
if rest.len() == 1 {
if let Ok(weekday_num) = rest.parse::<u32>() {
if weekday_num <= 6 {
return apply_weekday_modifier(dt, weekday_num)
.map(|new_dt| ResolvedDateTime { dt: new_dt, n_floor: rdt.n_floor });
}
}
}
return None; }
if lower == "localtime" || lower == "utc" {
return Some(rdt);
}
if let Some(result) = try_apply_date_offset_modifier(dt, modifier) {
return result;
}
if let Some(result) = try_apply_hh_mm_modifier(dt, modifier) {
return result.map(|new_dt| ResolvedDateTime { dt: new_dt, n_floor: rdt.n_floor });
}
parse_and_apply_time_shift(rdt, modifier)
}
fn try_apply_hh_mm_modifier(dt: NaiveDateTime, modifier: &str) -> Option<Option<NaiveDateTime>> {
let (sign, rest) = match modifier.as_bytes().first() {
Some(b'+') => (1i64, &modifier[1..]),
Some(b'-') => (-1i64, &modifier[1..]),
Some(b) if b.is_ascii_digit() => (1i64, modifier),
_ => return None,
};
let b = rest.as_bytes();
if !(b.len() >= 5 && b[0].is_ascii_digit() && b[1].is_ascii_digit() && b[2] == b':') {
return None; }
Some(parse_hh_mm_ss_ms(rest).map(|ms| dt + Duration::milliseconds(sign * ms)))
}
fn try_apply_date_offset_modifier(
dt: NaiveDateTime,
modifier: &str,
) -> Option<Option<ResolvedDateTime>> {
let sign: i64 = match modifier.as_bytes().first() {
Some(b'+') => 1,
Some(b'-') => -1,
_ => return None,
};
let rest = &modifier[1..];
let year_digits = rest.bytes().take_while(|b| b.is_ascii_digit()).count();
if !((year_digits == 4 || year_digits == 5) && rest.as_bytes().get(year_digits) == Some(&b'-'))
{
return None; }
Some(apply_date_offset_modifier(dt, sign, rest, year_digits))
}
fn apply_date_offset_modifier(
dt: NaiveDateTime,
sign: i64,
rest: &str,
year_digits: usize,
) -> Option<ResolvedDateTime> {
let years: i64 = rest[..year_digits].parse().ok()?;
let after = &rest[year_digits + 1..];
let ab = after.as_bytes();
if ab.len() < 5
|| !ab[0].is_ascii_digit()
|| !ab[1].is_ascii_digit()
|| ab[2] != b'-'
|| !ab[3].is_ascii_digit()
|| !ab[4].is_ascii_digit()
{
return None;
}
let months: i64 = after[..2].parse().ok()?;
let days: i64 = after[3..5].parse().ok()?;
if months >= 12 || days >= 31 {
return None;
}
let tail = &after[5..];
let time_offset_ms: i64 = if tail.is_empty() {
0
} else {
let mut chars = tail.chars();
if !chars.next().is_some_and(|c| c.is_ascii_whitespace()) {
return None;
}
parse_hh_mm_ss_ms(chars.as_str())?
};
let total_months =
dt.year() as i64 * 12 + (dt.month() as i64 - 1) + sign * (years * 12 + months);
let new_year = i32::try_from(total_months.div_euclid(12)).ok()?;
let new_month = total_months.rem_euclid(12) as u32 + 1;
let n_floor = compute_floor(new_year, new_month, dt.day() as i64);
let base = chrono::NaiveDate::from_ymd_opt(new_year, new_month, 1)?.and_time(dt.time())
+ Duration::days(dt.day() as i64 - 1);
Some(ResolvedDateTime {
dt: base + Duration::days(sign * days) + Duration::milliseconds(sign * time_offset_ms),
n_floor,
})
}
fn apply_weekday_modifier(dt: NaiveDateTime, weekday_num: u32) -> Option<NaiveDateTime> {
if weekday_num > 6 {
return None;
}
let current_weekday = dt.weekday();
let current_num = weekday_to_num(current_weekday);
let target_num = weekday_num;
let days_to_add = if current_num == target_num {
0
} else if target_num > current_num {
target_num - current_num
} else {
7 - current_num + target_num
};
Some(dt + Duration::days(days_to_add as i64))
}
fn weekday_to_num(wd: Weekday) -> u32 {
match wd {
Weekday::Sun => 0,
Weekday::Mon => 1,
Weekday::Tue => 2,
Weekday::Wed => 3,
Weekday::Thu => 4,
Weekday::Fri => 5,
Weekday::Sat => 6,
}
}
fn parse_and_apply_time_shift(rdt: ResolvedDateTime, modifier: &str) -> Option<ResolvedDateTime> {
let modifier = modifier.trim();
let dt = rdt.dt;
let (amount_str, unit) = split_amount_and_unit(modifier)?;
let amount: f64 = amount_str.parse().ok()?;
let unit_lower = unit.to_lowercase();
let unit_normalized = unit_lower.trim_end_matches('s');
let to_ms = |value: f64| -> i64 {
let rounder = if value < 0.0 { -0.5 } else { 0.5 };
(value + rounder) as i64
};
let duration_shift = |ms: i64| -> Option<ResolvedDateTime> {
Some(ResolvedDateTime { dt: dt + Duration::milliseconds(ms), n_floor: 0 })
};
match unit_normalized {
"day" => duration_shift(to_ms(amount * 86_400_000.0)),
"hour" => duration_shift(to_ms(amount * 3_600_000.0)),
"minute" => duration_shift(to_ms(amount * 60_000.0)),
"second" => duration_shift(to_ms(amount * 1000.0)),
"month" => {
let whole_months = amount.trunc() as i32;
let new_rdt = add_months(dt, whole_months)?;
Some(ResolvedDateTime {
dt: new_rdt.dt + Duration::milliseconds(to_ms(amount.fract() * 2_592_000_000.0)),
n_floor: new_rdt.n_floor,
})
}
"year" => {
let whole_years = amount.trunc() as i32;
let new_rdt = add_months(dt, whole_years * 12)?;
Some(ResolvedDateTime {
dt: new_rdt.dt + Duration::milliseconds(to_ms(amount.fract() * 31_536_000_000.0)),
n_floor: new_rdt.n_floor,
})
}
_ => None, }
}
fn split_amount_and_unit(s: &str) -> Option<(&str, &str)> {
let s = s.trim();
let mut unit_start = 0;
let mut found_digit = false;
for (i, c) in s.char_indices() {
if c.is_ascii_digit() || c == '.' || c == '+' || c == '-' {
found_digit = true;
unit_start = i + c.len_utf8();
} else if found_digit && (c == ' ' || c.is_alphabetic()) {
break;
}
}
if !found_digit || unit_start >= s.len() {
return None;
}
let amount = s[..unit_start].trim();
let unit = s[unit_start..].trim();
if amount.is_empty() || unit.is_empty() {
return None;
}
Some((amount, unit))
}
fn add_months(dt: NaiveDateTime, months: i32) -> Option<ResolvedDateTime> {
let total_months = dt.year() as i64 * 12 + dt.month() as i64 - 1 + months as i64;
let new_year = i32::try_from(total_months.div_euclid(12)).ok()?;
let new_month = total_months.rem_euclid(12) as u32 + 1;
let n_floor = compute_floor(new_year, new_month, dt.day() as i64);
let new_date = chrono::NaiveDate::from_ymd_opt(new_year, new_month, 1)?
+ Duration::days(dt.day() as i64 - 1);
Some(ResolvedDateTime { dt: NaiveDateTime::new(new_date, dt.time()), n_floor })
}
fn unix_epoch_to_datetime(timestamp: i64) -> Option<NaiveDateTime> {
chrono::DateTime::from_timestamp(timestamp, 0).map(|dt| dt.naive_utc())
}
fn unix_epoch_seconds_f64_to_datetime(seconds: f64) -> Option<NaiveDateTime> {
if !seconds.is_finite() {
return None;
}
let ijd_ms = (seconds * 1000.0 + JULIAN_EPOCH_OFFSET_MS as f64 + 0.5).floor() as i64;
if !(0..=MAX_IJD_MS).contains(&ijd_ms) {
return None;
}
chrono::DateTime::from_timestamp_millis(ijd_ms - JULIAN_EPOCH_OFFSET_MS)
.map(|dt| dt.naive_utc())
}
fn julian_day_to_naive(jd: f64) -> Option<NaiveDateTime> {
if !jd.is_finite() || jd < 0.0 {
return None;
}
let ijd_ms = (jd * 86_400_000.0 + 0.5).floor() as i64;
if ijd_ms > MAX_IJD_MS {
return None;
}
chrono::DateTime::from_timestamp_millis(ijd_ms - JULIAN_EPOCH_OFFSET_MS)
.map(|dt| dt.naive_utc())
}
fn naive_datetime_to_timestamp(dt: NaiveDateTime) -> Result<SqlValue, ExecutorError> {
let date = vibesql_types::Date::new(dt.year(), dt.month() as u8, dt.day() as u8)
.map_err(|e| ExecutorError::UnsupportedFeature(format!("Invalid date: {}", e)))?;
let time = vibesql_types::Time::new(dt.hour() as u8, dt.minute() as u8, dt.second() as u8, 0)
.map_err(|e| ExecutorError::UnsupportedFeature(format!("Invalid time: {}", e)))?;
Ok(SqlValue::Timestamp(vibesql_types::Timestamp::new(date, time)))
}
fn parse_datetime_string_to_naive(s: &str) -> Option<ResolvedDateTime> {
let s = s.trim();
if let Some(rdt) = parse_yyyy_mm_dd(s) {
return Some(rdt);
}
if let Some(time_ms) = parse_hh_mm_ss_tz_ms(s) {
let midnight = chrono::NaiveDate::from_ymd_opt(2000, 1, 1)?.and_hms_opt(0, 0, 0)?;
return Some(ResolvedDateTime::exact(midnight + Duration::milliseconds(time_ms)));
}
if let Ok(jd) = s.parse::<f64>() {
return julian_day_to_naive(jd).map(ResolvedDateTime::exact);
}
None
}
fn parse_yyyy_mm_dd(s: &str) -> Option<ResolvedDateTime> {
let (negative_year, rest) = match s.strip_prefix('-') {
Some(r) => (true, r),
None => (false, s),
};
let b = rest.as_bytes();
if b.len() < 10
|| !b[..4].iter().all(u8::is_ascii_digit)
|| b[4] != b'-'
|| !b[5].is_ascii_digit()
|| !b[6].is_ascii_digit()
|| b[7] != b'-'
|| !b[8].is_ascii_digit()
|| !b[9].is_ascii_digit()
{
return None;
}
let year: i32 = rest[..4].parse().ok()?;
let year = if negative_year { -year } else { year };
let month: u32 = rest[5..7].parse().ok()?;
let day: i64 = rest[8..10].parse().ok()?;
if !(1..=12).contains(&month) || !(1..=31).contains(&day) {
return None;
}
let n_floor = compute_floor(year, month, day);
let date = chrono::NaiveDate::from_ymd_opt(year, month, 1)? + Duration::days(day - 1);
let tail = rest[10..].trim_start_matches(|c: char| c.is_ascii_whitespace() || c == 'T');
if tail.is_empty() {
return Some(ResolvedDateTime { dt: date.and_hms_opt(0, 0, 0)?, n_floor });
}
let time_ms = parse_hh_mm_ss_tz_ms(tail)?;
Some(ResolvedDateTime {
dt: date.and_hms_opt(0, 0, 0)? + Duration::milliseconds(time_ms),
n_floor,
})
}
fn parse_hh_mm_ss_core(t: &str) -> Option<(i64, &str)> {
let b = t.as_bytes();
if b.len() < 5
|| !b[0].is_ascii_digit()
|| !b[1].is_ascii_digit()
|| b[2] != b':'
|| !b[3].is_ascii_digit()
|| !b[4].is_ascii_digit()
{
return None;
}
let hours: i64 = t[..2].parse().ok()?;
let minutes: i64 = t[3..5].parse().ok()?;
if hours > 24 || minutes > 59 {
return None;
}
let mut total_ms = (hours * 3600 + minutes * 60) * 1000;
let mut rest = &t[5..];
if let Some(sec_part) = rest.strip_prefix(':') {
let sb = sec_part.as_bytes();
if sb.len() < 2 || !sb[0].is_ascii_digit() || !sb[1].is_ascii_digit() {
return None;
}
let seconds: i64 = sec_part[..2].parse().ok()?;
if seconds > 59 {
return None;
}
total_ms += seconds * 1000;
rest = &sec_part[2..];
if let Some(frac_part) = rest.strip_prefix('.') {
let frac_digits = frac_part.bytes().take_while(|c| c.is_ascii_digit()).count();
if frac_digits == 0 {
return None;
}
let frac: f64 = format!("0.{}", &frac_part[..frac_digits]).parse().ok()?;
total_ms += ((frac.min(0.999)) * 1000.0 + 0.5).floor() as i64;
rest = &frac_part[frac_digits..];
}
}
Some((total_ms, rest))
}
fn parse_hh_mm_ss_ms(t: &str) -> Option<i64> {
let (total_ms, rest) = parse_hh_mm_ss_core(t)?;
if rest.chars().all(|c| c.is_ascii_whitespace()) {
Some(total_ms)
} else {
None
}
}
fn parse_hh_mm_ss_tz_ms(t: &str) -> Option<i64> {
let (total_ms, rest) = parse_hh_mm_ss_core(t)?;
let tz_minutes = parse_timezone_suffix(rest)?;
Some(total_ms - tz_minutes * 60_000)
}
fn parse_timezone_suffix(s: &str) -> Option<i64> {
let s = s.trim_start_matches(|c: char| c.is_ascii_whitespace());
if s.is_empty() {
return Some(0);
}
let (offset_minutes, rest) = match s.as_bytes()[0] {
b'Z' | b'z' => (0, &s[1..]),
sign @ (b'+' | b'-') => {
let b = s.as_bytes();
if b.len() < 6
|| !b[1].is_ascii_digit()
|| !b[2].is_ascii_digit()
|| b[3] != b':'
|| !b[4].is_ascii_digit()
|| !b[5].is_ascii_digit()
{
return None;
}
let hours: i64 = s[1..3].parse().ok()?;
let minutes: i64 = s[4..6].parse().ok()?;
if hours > 14 || minutes > 59 {
return None;
}
let total = hours * 60 + minutes;
(if sign == b'-' { -total } else { total }, &s[6..])
}
_ => return None,
};
if rest.chars().all(|c| c.is_ascii_whitespace()) {
Some(offset_minutes)
} else {
None
}
}