use chrono::{DateTime, Duration, Utc};
#[derive(Debug, Clone)]
pub struct ExpandedQuery {
pub cleaned_text: String,
pub date_filters: Vec<DateFilter>,
}
#[derive(Debug, Clone)]
pub struct DateFilter {
pub column: String,
pub operator: FilterOp,
}
#[derive(Debug, Clone)]
pub enum FilterOp {
After(DateTime<Utc>),
Before(DateTime<Utc>),
Between(DateTime<Utc>, DateTime<Utc>),
}
pub fn expand_query(query: &str, now: DateTime<Utc>) -> ExpandedQuery {
let lower = query.to_lowercase();
let mut cleaned = query.to_string();
let mut filters = Vec::new();
let patterns: &[(&str, Box<dyn Fn(DateTime<Utc>) -> FilterOp>)] = &[
(
"yesterday",
Box::new(|now| FilterOp::Between(now - Duration::days(2), now - Duration::days(1))),
),
(
"last week",
Box::new(|now| FilterOp::After(now - Duration::days(7))),
),
(
"last month",
Box::new(|now| FilterOp::After(now - Duration::days(30))),
),
(
"last year",
Box::new(|now| FilterOp::After(now - Duration::days(365))),
),
(
"today",
Box::new(|now| FilterOp::After(now - Duration::days(1))),
),
(
"this week",
Box::new(|now| FilterOp::After(now - Duration::days(7))),
),
(
"this month",
Box::new(|now| FilterOp::After(now - Duration::days(30))),
),
(
"recently",
Box::new(|now| FilterOp::After(now - Duration::days(7))),
),
];
for (pattern, make_filter) in patterns {
if lower.contains(pattern) {
filters.push(DateFilter {
column: "created_at".to_string(),
operator: make_filter(now),
});
if let Some(pos) = lower.find(pattern) {
cleaned = format!(
"{} {}",
cleaned[..pos].trim(),
cleaned[pos + pattern.len()..].trim()
)
.trim()
.to_string();
}
break; }
}
ExpandedQuery {
cleaned_text: cleaned,
date_filters: filters,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn no_temporal_expression() {
let now = Utc::now();
let result = expand_query("authentication error JWT", now);
assert_eq!(result.cleaned_text, "authentication error JWT");
assert!(result.date_filters.is_empty());
}
#[test]
fn last_week_expansion() {
let now = Utc::now();
let result = expand_query("errors from last week", now);
assert_eq!(result.cleaned_text, "errors from");
assert_eq!(result.date_filters.len(), 1);
assert!(matches!(
result.date_filters[0].operator,
FilterOp::After(_)
));
}
#[test]
fn yesterday_expansion() {
let now = Utc::now();
let result = expand_query("what happened yesterday", now);
assert!(result.date_filters.len() == 1);
assert!(matches!(
result.date_filters[0].operator,
FilterOp::Between(_, _)
));
}
#[test]
fn last_month_expansion() {
let now = Utc::now();
let result = expand_query("decisions last month", now);
assert_eq!(result.date_filters.len(), 1);
assert_eq!(result.date_filters[0].column, "created_at");
}
#[test]
fn cleaned_text_usable_for_fts() {
let now = Utc::now();
let result = expand_query("build errors from last week in production", now);
assert!(result.cleaned_text.contains("build errors"));
assert!(result.cleaned_text.contains("production"));
assert!(!result.cleaned_text.contains("last week"));
}
}