use crate::{Error, Expr};
use jiff::{SignedDuration, Timestamp};
use std::cmp::Ordering;
const DAY: SignedDuration = SignedDuration::from_hours(24);
const SHYOFADAY: SignedDuration = DAY.checked_sub(SignedDuration::from_nanos(1)).unwrap();
fn parse_ts(s: &str) -> Result<Timestamp, Error> {
let stripped = s.replace(' ', "T");
let fromshort = match stripped.len() {
4 => format!("{stripped}-01-01T00:00:00Z"),
7 => format!("{stripped}-01T00:00:00Z"),
10 => format!("{stripped}T00:00:00Z"),
13 => format!("{stripped}:00:00Z"),
16 => format!("{stripped}:00Z"),
19 => format!("{stripped}Z"),
_ => stripped,
};
fromshort.parse().map_err(Error::ParseTimestamp)
}
fn parse_rendered_ts(expr: &Expr) -> Result<Timestamp, Error> {
parse_ts(&crate::parser::strip_quotes(&expr.to_text()?))
}
fn parse_bound(expr: &Expr, unbounded: Timestamp) -> Result<Timestamp, Error> {
let text = crate::parser::strip_quotes(&expr.to_text()?).into_owned();
if text == ".." {
Ok(unbounded)
} else {
parse_ts(&text)
}
}
pub(crate) fn canonical_timestamp(s: &str) -> String {
let has_time = s.contains('T') || s.contains(' ');
if !has_time || is_leap_second(s) {
return s.to_string();
}
parse_ts(s).map_or_else(|_| s.to_string(), |ts| ts.to_string())
}
fn is_leap_second(s: &str) -> bool {
s.split(':').nth(2).is_some_and(|seconds| {
seconds
.strip_prefix("60")
.is_some_and(|rest| !rest.starts_with(|c: char| c.is_ascii_digit()))
})
}
#[derive(Debug, Clone)]
pub struct DateRange {
pub start: Timestamp,
pub end: Timestamp,
}
impl TryFrom<Expr> for DateRange {
type Error = Error;
fn try_from(v: Expr) -> Result<DateRange, Error> {
match v {
Expr::Interval { interval } => {
let start = parse_bound(&interval[0], Timestamp::MIN)?;
let end = parse_bound(&interval[1], Timestamp::MAX)?;
Ok(DateRange { start, end })
}
Expr::Timestamp { timestamp } => {
let start: Timestamp = parse_rendered_ts(×tamp)?;
Ok(DateRange { start, end: start })
}
Expr::Date { date } => {
let start: Timestamp = parse_rendered_ts(&date)?;
let end: Timestamp = start + SHYOFADAY;
Ok(DateRange { start, end })
}
Expr::Literal(v) => {
let start: Timestamp = parse_ts(&v)?;
Ok(DateRange { start, end: start })
}
_ => Err(Error::ExprToDateRange(v)),
}
}
}
impl PartialEq for DateRange {
fn eq(&self, other: &Self) -> bool {
self.start == other.start && self.end == other.end
}
}
impl PartialOrd for DateRange {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
if self == other {
Some(Ordering::Equal)
} else if self.end < other.start {
Some(Ordering::Less)
} else if self.start > other.end {
Some(Ordering::Greater)
} else {
None
}
}
}
pub fn temporal_op(left_expr: Expr, right_expr: Expr, op: &str) -> Result<Expr, Error> {
let op = &crate::expr::canonical_op(op);
let invop = match op.as_str() {
"t_after" => "t_before",
"t_metBy" => "t_meets",
"t_overlappedBy" => "t_overlaps",
"t_startedBy" => "t_starts",
"t_contains" => "t_during",
"t_finishedBy" => "t_finishes",
_ => op,
};
let left = DateRange::try_from(left_expr)?;
let right = DateRange::try_from(right_expr)?;
let (left, right) = if invop == op {
(left, right)
} else {
(right, left)
};
let out = match invop {
"t_before" => left.end < right.start,
"t_meets" => left.end == right.start,
"t_overlaps" => left.start < right.start && right.start < left.end && left.end < right.end,
"t_starts" => left.start == right.start && left.end < right.end,
"t_during" => left.start > right.start && left.end < right.end,
"t_finishes" => left.start > right.start && left.end == right.end,
"t_equals" => left.start == right.start && left.end == right.end,
"t_disjoint" => !(left.start <= right.end && left.end >= right.start),
"t_intersects" => left.start <= right.end && left.end >= right.start,
_ => return Err(Error::OpNotImplemented("temporal")),
};
Ok(Expr::Bool(out))
}
#[cfg(test)]
mod tests {
use super::DateRange;
use crate::Expr;
use serde_json::json;
#[test]
fn timestamp_math() {
let expr: Expr = serde_json::from_value(json!({"date": "2020-02-18"})).unwrap();
let _: DateRange = expr.try_into().unwrap();
}
}