use std::borrow::Cow;
use chrono::{DateTime, NaiveDate, NaiveDateTime, Utc};
use crate::ast::Value;
use crate::error::{BqlError, BqlResult};
impl Value<'static> {
pub fn date(d: NaiveDate) -> Self {
Value::Date(Cow::Owned(d.format("%Y-%m-%d").to_string()))
}
pub fn date_time(dt: DateTime<Utc>) -> Self {
Value::DateTime(Cow::Owned(
dt.to_rfc3339_opts(chrono::SecondsFormat::AutoSi, true),
))
}
}
impl Value<'_> {
pub fn as_date(&self) -> BqlResult<NaiveDate> {
match self {
Value::Date(s) => NaiveDate::parse_from_str(s, "%Y-%m-%d")
.map_err(|e| BqlError::new(0, format!("invalid date `{s}`: {e}"))),
_ => Err(BqlError::new(0, "value is not a date")),
}
}
pub fn as_date_time(&self) -> BqlResult<DateTime<Utc>> {
match self {
Value::DateTime(s) => parse_date_time(s),
_ => Err(BqlError::new(0, "value is not a datetime")),
}
}
}
fn parse_date_time(s: &str) -> BqlResult<DateTime<Utc>> {
let normalized = normalize_separator(s);
if let Ok(dt) = DateTime::parse_from_rfc3339(&normalized) {
return Ok(dt.with_timezone(&Utc));
}
for fmt in [
"%Y-%m-%dT%H:%M:%S%.f",
"%Y-%m-%dT%H:%M:%S",
"%Y-%m-%dT%H:%M",
] {
if let Ok(naive) = NaiveDateTime::parse_from_str(&normalized, fmt) {
return Ok(DateTime::from_naive_utc_and_offset(naive, Utc));
}
}
Err(BqlError::new(0, format!("invalid datetime `{s}`")))
}
fn normalize_separator(s: &str) -> String {
let mut out = s.to_string();
if out.len() > 10
&& out.is_char_boundary(10)
&& out.is_char_boundary(11)
&& matches!(&out[10..11], "t" | " ")
{
out.replace_range(10..11, "T");
}
out
}