use super::super::pair_span;
use crate::ast::{
DateTimeExpr, Duration, DurationUnit, Expr, NamedTime, RelativeTime, TimeDirection,
TimeReference, TimeUnit, Timeframe,
};
use crate::error::{Result, ShapeError};
use crate::parser::Rule;
use crate::parser::string_literals::parse_string_literal;
use pest::iterators::Pair;
pub fn parse_time_ref(pair: Pair<Rule>) -> Result<TimeReference> {
let inner = pair.into_inner().next().unwrap();
match inner.as_rule() {
Rule::quoted_time => Ok(TimeReference::Absolute(parse_string_literal(
inner.as_str(),
)?)),
Rule::named_time => {
let named = match inner.as_str() {
"today" => NamedTime::Today,
"yesterday" => NamedTime::Yesterday,
"now" => NamedTime::Now,
_ => {
return Err(ShapeError::ParseError {
message: format!("Unknown named time: {}", inner.as_str()),
location: None,
});
}
};
Ok(TimeReference::Named(named))
}
Rule::relative_time => {
let s = inner.as_str();
Ok(TimeReference::Relative(parse_relative_time(s)?))
}
_ => Err(ShapeError::ParseError {
message: format!("Unexpected time reference: {:?}", inner.as_rule()),
location: None,
}),
}
}
pub fn parse_relative_time(s: &str) -> Result<RelativeTime> {
let parts: Vec<&str> = s.split_whitespace().collect();
if parts.len() < 3 {
return Err(ShapeError::ParseError {
message: format!("Invalid relative time format: {}", s),
location: None,
});
}
let amount: i32 = parts[0].parse().map_err(|e| ShapeError::ParseError {
message: format!("Invalid integer in relative time: {}", e),
location: None,
})?;
let unit = match parts[1] {
"minute" | "minutes" => TimeUnit::Minutes,
"hour" | "hours" => TimeUnit::Hours,
"day" | "days" => TimeUnit::Days,
"week" | "weeks" => TimeUnit::Weeks,
"month" | "months" => TimeUnit::Months,
_ => {
return Err(ShapeError::ParseError {
message: format!("Unknown time unit: {}", parts[1]),
location: None,
});
}
};
let direction = match parts[2] {
"ago" => TimeDirection::Ago,
"future" | "ahead" => TimeDirection::Future,
_ => {
return Err(ShapeError::ParseError {
message: format!("Unknown time direction: {}", parts[2]),
location: None,
});
}
};
Ok(RelativeTime {
amount,
unit,
direction,
})
}
pub fn parse_temporal_nav(pair: Pair<Rule>) -> Result<Expr> {
let span = pair_span(&pair);
let inner = pair.into_inner().next().unwrap();
match inner.as_rule() {
Rule::back_nav | Rule::forward_nav => {
let is_back = inner.as_rule() == Rule::back_nav;
let nav_amount = inner.into_inner().next().unwrap();
let mut amount_inner = nav_amount.into_inner();
let num_pair = amount_inner.next().unwrap();
let value: f64 = num_pair
.as_str()
.parse()
.map_err(|e| ShapeError::ParseError {
message: format!("Invalid navigation amount: {}", e),
location: None,
})?;
let unit = if let Some(unit_pair) = amount_inner.next() {
match unit_pair.as_str() {
"sample" | "samples" | "record" | "records" => DurationUnit::Samples,
"minute" | "minutes" => DurationUnit::Minutes,
"hour" | "hours" => DurationUnit::Hours,
"day" | "days" => DurationUnit::Days,
"week" | "weeks" => DurationUnit::Weeks,
"month" | "months" => DurationUnit::Months,
_ => DurationUnit::Samples,
}
} else {
DurationUnit::Samples
};
let final_value = if is_back { -value } else { value };
Ok(Expr::Duration(
Duration {
value: final_value,
unit,
},
span,
))
}
_ => Err(ShapeError::ParseError {
message: format!(
"Expected back_nav or forward_nav, got {:?}",
inner.as_rule()
),
location: None,
}),
}
}
pub fn parse_timeframe_expr(pair: Pair<Rule>) -> Result<Expr> {
let span = pair_span(&pair);
let mut inner = pair.into_inner();
let timeframe_str = inner
.next()
.ok_or_else(|| ShapeError::ParseError {
message: "Expected timeframe in on() expression".to_string(),
location: None,
})?
.as_str();
let timeframe = Timeframe::parse(timeframe_str).ok_or_else(|| ShapeError::ParseError {
message: format!("Invalid timeframe: {}", timeframe_str),
location: None,
})?;
let expr_pair = inner.next().ok_or_else(|| ShapeError::ParseError {
message: "Expected expression in on() block".to_string(),
location: None,
})?;
let expr = crate::parser::expressions::parse_expression(expr_pair)?;
Ok(Expr::TimeframeContext {
timeframe,
expr: Box::new(expr),
span,
})
}
pub fn parse_datetime_expr(pair: Pair<Rule>) -> Result<DateTimeExpr> {
match pair.as_rule() {
Rule::datetime_expr => {
let inner = pair.into_inner().next().unwrap();
parse_datetime_expr(inner)
}
Rule::datetime_primary => {
let mut inner = pair.into_inner();
let expr_pair = inner.next().unwrap();
match expr_pair.as_rule() {
Rule::datetime_literal => {
let mut lit_inner = expr_pair.into_inner();
let string_pair = lit_inner.next().unwrap();
Ok(DateTimeExpr::Literal(parse_string_literal(
string_pair.as_str(),
)?))
}
Rule::named_time => {
let named = match expr_pair.as_str() {
"today" => NamedTime::Today,
"yesterday" => NamedTime::Yesterday,
"now" => NamedTime::Now,
_ => {
return Err(ShapeError::ParseError {
message: format!("Unknown named time: {}", expr_pair.as_str()),
location: None,
});
}
};
Ok(DateTimeExpr::Named(named))
}
_ => Err(ShapeError::ParseError {
message: format!("Unexpected datetime primary: {:?}", expr_pair.as_rule()),
location: None,
}),
}
}
Rule::datetime_arithmetic => {
let mut inner = pair.into_inner();
let base_pair = inner.next().unwrap();
let mut result = parse_datetime_expr(base_pair)?;
while let Some(op_pair) = inner.next() {
let op = op_pair.as_str();
if op != "+" && op != "-" {
return Err(ShapeError::ParseError {
message: format!("Invalid datetime arithmetic operator: {}", op),
location: None,
});
}
let duration_pair = inner.next().ok_or_else(|| ShapeError::ParseError {
message: "Datetime arithmetic missing duration".to_string(),
location: None,
})?;
let duration_expr = parse_duration(duration_pair)?;
let duration = match duration_expr {
Expr::Duration(duration, _) => duration,
_ => {
return Err(ShapeError::ParseError {
message: "Datetime arithmetic expects a duration".to_string(),
location: None,
});
}
};
result = DateTimeExpr::Arithmetic {
base: Box::new(result),
operator: op.to_string(),
duration,
};
}
Ok(result)
}
_ => Err(ShapeError::ParseError {
message: format!("Unexpected datetime expression: {:?}", pair.as_rule()),
location: None,
}),
}
}
pub fn parse_datetime_range(pair: Pair<Rule>) -> Result<(Expr, Option<Expr>)> {
let mut inner = pair.into_inner();
let first_pair = inner.next().unwrap();
let first_span = pair_span(&first_pair);
let first_datetime = parse_datetime_expr(first_pair)?;
if let Some(second_pair) = inner.next() {
let second_span = pair_span(&second_pair);
let second_datetime = parse_datetime_expr(second_pair)?;
Ok((
Expr::DateTime(first_datetime, first_span),
Some(Expr::DateTime(second_datetime, second_span)),
))
} else {
Ok((Expr::DateTime(first_datetime, first_span), None))
}
}
pub fn parse_duration(pair: Pair<Rule>) -> Result<Expr> {
let span = pair_span(&pair);
let duration_str = pair.as_str();
let mut components = Vec::new();
let mut current_number = String::new();
let mut chars = duration_str.chars().peekable();
while let Some(ch) = chars.next() {
if ch.is_numeric() || ch == '.' || (ch == '-' && current_number.is_empty()) {
current_number.push(ch);
} else {
if !current_number.is_empty() {
let value: f64 = current_number.parse().map_err(|e| ShapeError::ParseError {
message: format!("Invalid duration value: {}", e),
location: None,
})?;
let mut unit_str = String::new();
unit_str.push(ch);
while let Some(&next_ch) = chars.peek() {
if next_ch.is_alphabetic() {
unit_str.push(chars.next().unwrap());
} else {
break;
}
}
let unit = match unit_str.as_str() {
"s" | "seconds" => DurationUnit::Seconds,
"m" | "minutes" => DurationUnit::Minutes,
"h" | "hours" => DurationUnit::Hours,
"d" | "days" => DurationUnit::Days,
"w" | "weeks" => DurationUnit::Weeks,
"M" | "months" => DurationUnit::Months,
"y" | "years" => DurationUnit::Years,
"samples" => DurationUnit::Samples,
_ => {
return Err(ShapeError::ParseError {
message: format!("Unknown duration unit: {}", unit_str),
location: None,
});
}
};
components.push((value, unit));
current_number.clear();
}
}
}
if components.len() == 1 {
let (value, unit) = components.into_iter().next().unwrap();
return Ok(Expr::Duration(Duration { value, unit }, span));
}
let mut total_seconds = 0.0;
for (value, unit) in components {
let seconds = match unit {
DurationUnit::Seconds => value,
DurationUnit::Minutes => value * 60.0,
DurationUnit::Hours => value * 3600.0,
DurationUnit::Days => value * 86400.0,
DurationUnit::Weeks => value * 604800.0,
DurationUnit::Months => value * 2592000.0, DurationUnit::Years => value * 31536000.0, DurationUnit::Samples => {
return Err(ShapeError::ParseError {
message: "Cannot use 'samples' in compound duration".to_string(),
location: None,
});
}
};
total_seconds += seconds;
}
let (value, unit) = if total_seconds < 60.0 {
(total_seconds, DurationUnit::Seconds)
} else if total_seconds < 3600.0 {
(total_seconds / 60.0, DurationUnit::Minutes)
} else if total_seconds < 86400.0 {
(total_seconds / 3600.0, DurationUnit::Hours)
} else if total_seconds < 604800.0 {
(total_seconds / 86400.0, DurationUnit::Days)
} else if total_seconds < 2592000.0 {
(total_seconds / 604800.0, DurationUnit::Weeks)
} else if total_seconds < 31536000.0 {
(total_seconds / 2592000.0, DurationUnit::Months)
} else {
(total_seconds / 31536000.0, DurationUnit::Years)
};
Ok(Expr::Duration(Duration { value, unit }, span))
}
#[cfg(test)]
mod tests {
use crate::ast::{DateTimeExpr, DurationUnit, Expr};
fn parse_expr(code: &str) -> Expr {
let program = crate::parser::parse_program(code).expect("parse failed");
match &program.items[0] {
crate::ast::Item::Expression(expr, _) => expr.clone(),
crate::ast::Item::Statement(crate::ast::Statement::Expression(expr, _), _) => {
expr.clone()
}
other => panic!("expected expression statement, got {:?}", other),
}
}
#[test]
fn test_parse_datetime_literal_iso8601() {
let expr = parse_expr(r#"@"2024-06-15T14:30:00""#);
match expr {
Expr::DateTime(DateTimeExpr::Literal(s), _) => {
assert_eq!(s, "2024-06-15T14:30:00");
}
other => panic!("expected DateTime literal, got {:?}", other),
}
}
#[test]
fn test_parse_datetime_literal_date_only() {
let expr = parse_expr(r#"@"2024-01-15""#);
match expr {
Expr::DateTime(DateTimeExpr::Literal(s), _) => {
assert_eq!(s, "2024-01-15");
}
other => panic!("expected DateTime literal, got {:?}", other),
}
}
#[test]
fn test_parse_datetime_named_now() {
let expr = parse_expr("@now");
match expr {
Expr::DateTime(DateTimeExpr::Named(crate::ast::NamedTime::Now), _) => {}
other => panic!("expected DateTime Named(Now), got {:?}", other),
}
}
#[test]
fn test_parse_duration_days() {
let expr = parse_expr("3d");
match expr {
Expr::Duration(dur, _) => {
assert_eq!(dur.value, 3.0);
assert_eq!(dur.unit, DurationUnit::Days);
}
other => panic!("expected Duration, got {:?}", other),
}
}
#[test]
fn test_parse_duration_hours() {
let expr = parse_expr("2h");
match expr {
Expr::Duration(dur, _) => {
assert_eq!(dur.value, 2.0);
assert_eq!(dur.unit, DurationUnit::Hours);
}
other => panic!("expected Duration, got {:?}", other),
}
}
#[test]
fn test_parse_duration_minutes() {
let expr = parse_expr("30m");
match expr {
Expr::Duration(dur, _) => {
assert_eq!(dur.value, 30.0);
assert_eq!(dur.unit, DurationUnit::Minutes);
}
other => panic!("expected Duration, got {:?}", other),
}
}
#[test]
fn test_parse_duration_seconds() {
let expr = parse_expr("10s");
match expr {
Expr::Duration(dur, _) => {
assert_eq!(dur.value, 10.0);
assert_eq!(dur.unit, DurationUnit::Seconds);
}
other => panic!("expected Duration, got {:?}", other),
}
}
}