Skip to main content

shape_ast/parser/expressions/
temporal.rs

1//! Temporal expression parsing
2//!
3//! This module handles parsing of time-related expressions:
4//! - Time references (@today, @yesterday, "2024-01-01")
5//! - DateTime expressions
6//! - Duration expressions (1h, 5m30s, 2 days)
7//! - Temporal navigation expressions
8//! - Relative time expressions
9
10use super::super::pair_span;
11use crate::ast::{
12    DateTimeExpr, Duration, DurationUnit, Expr, NamedTime, RelativeTime, TimeDirection,
13    TimeReference, TimeUnit, Timeframe,
14};
15use crate::error::{Result, ShapeError};
16use crate::parser::Rule;
17use crate::parser::string_literals::parse_string_literal;
18use pest::iterators::Pair;
19
20/// Parse a time reference
21pub fn parse_time_ref(pair: Pair<Rule>) -> Result<TimeReference> {
22    let inner = pair.into_inner().next().unwrap();
23
24    match inner.as_rule() {
25        Rule::quoted_time => Ok(TimeReference::Absolute(parse_string_literal(
26            inner.as_str(),
27        )?)),
28        Rule::named_time => {
29            let named = match inner.as_str() {
30                "today" => NamedTime::Today,
31                "yesterday" => NamedTime::Yesterday,
32                "now" => NamedTime::Now,
33                _ => {
34                    return Err(ShapeError::ParseError {
35                        message: format!("Unknown named time: {}", inner.as_str()),
36                        location: None,
37                    });
38                }
39            };
40            Ok(TimeReference::Named(named))
41        }
42        Rule::relative_time => {
43            // For now, store as string and parse later
44            let s = inner.as_str();
45            Ok(TimeReference::Relative(parse_relative_time(s)?))
46        }
47        _ => Err(ShapeError::ParseError {
48            message: format!("Unexpected time reference: {:?}", inner.as_rule()),
49            location: None,
50        }),
51    }
52}
53
54/// Parse relative time expression
55pub fn parse_relative_time(s: &str) -> Result<RelativeTime> {
56    // Simple parsing for now - this would be improved
57    // Expected format: "1 week ago" or similar
58    let parts: Vec<&str> = s.split_whitespace().collect();
59    if parts.len() < 3 {
60        return Err(ShapeError::ParseError {
61            message: format!("Invalid relative time format: {}", s),
62            location: None,
63        });
64    }
65
66    let amount: i32 = parts[0].parse().map_err(|e| ShapeError::ParseError {
67        message: format!("Invalid integer in relative time: {}", e),
68        location: None,
69    })?;
70    let unit = match parts[1] {
71        "minute" | "minutes" => TimeUnit::Minutes,
72        "hour" | "hours" => TimeUnit::Hours,
73        "day" | "days" => TimeUnit::Days,
74        "week" | "weeks" => TimeUnit::Weeks,
75        "month" | "months" => TimeUnit::Months,
76        _ => {
77            return Err(ShapeError::ParseError {
78                message: format!("Unknown time unit: {}", parts[1]),
79                location: None,
80            });
81        }
82    };
83
84    let direction = match parts[2] {
85        "ago" => TimeDirection::Ago,
86        "future" | "ahead" => TimeDirection::Future,
87        _ => {
88            return Err(ShapeError::ParseError {
89                message: format!("Unknown time direction: {}", parts[2]),
90                location: None,
91            });
92        }
93    };
94
95    Ok(RelativeTime {
96        amount,
97        unit,
98        direction,
99    })
100}
101
102/// Parse temporal navigation expression
103/// Handles back(n) and forward(n) expressions
104pub fn parse_temporal_nav(pair: Pair<Rule>) -> Result<Expr> {
105    let span = pair_span(&pair);
106    let inner = pair.into_inner().next().unwrap();
107
108    match inner.as_rule() {
109        Rule::back_nav | Rule::forward_nav => {
110            let is_back = inner.as_rule() == Rule::back_nav;
111            let nav_amount = inner.into_inner().next().unwrap();
112            let mut amount_inner = nav_amount.into_inner();
113
114            // Parse the number
115            let num_pair = amount_inner.next().unwrap();
116            let value: f64 = num_pair
117                .as_str()
118                .parse()
119                .map_err(|e| ShapeError::ParseError {
120                    message: format!("Invalid navigation amount: {}", e),
121                    location: None,
122                })?;
123
124            // Parse optional time unit (defaults to samples)
125            let unit = if let Some(unit_pair) = amount_inner.next() {
126                match unit_pair.as_str() {
127                    "sample" | "samples" | "record" | "records" => DurationUnit::Samples,
128                    "minute" | "minutes" => DurationUnit::Minutes,
129                    "hour" | "hours" => DurationUnit::Hours,
130                    "day" | "days" => DurationUnit::Days,
131                    "week" | "weeks" => DurationUnit::Weeks,
132                    "month" | "months" => DurationUnit::Months,
133                    _ => DurationUnit::Samples,
134                }
135            } else {
136                DurationUnit::Samples
137            };
138
139            // For back navigation, negate the value
140            let final_value = if is_back { -value } else { value };
141
142            Ok(Expr::Duration(
143                Duration {
144                    value: final_value,
145                    unit,
146                },
147                span,
148            ))
149        }
150        _ => Err(ShapeError::ParseError {
151            message: format!(
152                "Expected back_nav or forward_nav, got {:?}",
153                inner.as_rule()
154            ),
155            location: None,
156        }),
157    }
158}
159
160/// Parse timeframe expression
161pub fn parse_timeframe_expr(pair: Pair<Rule>) -> Result<Expr> {
162    let span = pair_span(&pair);
163    let mut inner = pair.into_inner();
164
165    // Parse the timeframe
166    let timeframe_str = inner
167        .next()
168        .ok_or_else(|| ShapeError::ParseError {
169            message: "Expected timeframe in on() expression".to_string(),
170            location: None,
171        })?
172        .as_str();
173
174    let timeframe = Timeframe::parse(timeframe_str).ok_or_else(|| ShapeError::ParseError {
175        message: format!("Invalid timeframe: {}", timeframe_str),
176        location: None,
177    })?;
178
179    // Parse the expression
180    let expr_pair = inner.next().ok_or_else(|| ShapeError::ParseError {
181        message: "Expected expression in on() block".to_string(),
182        location: None,
183    })?;
184
185    let expr = crate::parser::expressions::parse_expression(expr_pair)?;
186
187    Ok(Expr::TimeframeContext {
188        timeframe,
189        expr: Box::new(expr),
190        span,
191    })
192}
193
194/// Parse datetime expression
195pub fn parse_datetime_expr(pair: Pair<Rule>) -> Result<DateTimeExpr> {
196    match pair.as_rule() {
197        Rule::datetime_expr => {
198            // Delegate to inner rule
199            let inner = pair.into_inner().next().unwrap();
200            parse_datetime_expr(inner)
201        }
202        Rule::datetime_primary => {
203            let mut inner = pair.into_inner();
204            let expr_pair = inner.next().unwrap();
205
206            match expr_pair.as_rule() {
207                Rule::datetime_literal => {
208                    let mut lit_inner = expr_pair.into_inner();
209                    let string_pair = lit_inner.next().unwrap();
210                    Ok(DateTimeExpr::Literal(parse_string_literal(
211                        string_pair.as_str(),
212                    )?))
213                }
214                Rule::named_time => {
215                    let named = match expr_pair.as_str() {
216                        "today" => NamedTime::Today,
217                        "yesterday" => NamedTime::Yesterday,
218                        "now" => NamedTime::Now,
219                        _ => {
220                            return Err(ShapeError::ParseError {
221                                message: format!("Unknown named time: {}", expr_pair.as_str()),
222                                location: None,
223                            });
224                        }
225                    };
226                    Ok(DateTimeExpr::Named(named))
227                }
228                _ => Err(ShapeError::ParseError {
229                    message: format!("Unexpected datetime primary: {:?}", expr_pair.as_rule()),
230                    location: None,
231                }),
232            }
233        }
234        Rule::datetime_arithmetic => {
235            let mut inner = pair.into_inner();
236            let base_pair = inner.next().unwrap();
237            let mut result = parse_datetime_expr(base_pair)?;
238
239            while let Some(op_pair) = inner.next() {
240                let op = op_pair.as_str();
241                if op != "+" && op != "-" {
242                    return Err(ShapeError::ParseError {
243                        message: format!("Invalid datetime arithmetic operator: {}", op),
244                        location: None,
245                    });
246                }
247
248                let duration_pair = inner.next().ok_or_else(|| ShapeError::ParseError {
249                    message: "Datetime arithmetic missing duration".to_string(),
250                    location: None,
251                })?;
252                let duration_expr = parse_duration(duration_pair)?;
253                let duration = match duration_expr {
254                    Expr::Duration(duration, _) => duration,
255                    _ => {
256                        return Err(ShapeError::ParseError {
257                            message: "Datetime arithmetic expects a duration".to_string(),
258                            location: None,
259                        });
260                    }
261                };
262
263                result = DateTimeExpr::Arithmetic {
264                    base: Box::new(result),
265                    operator: op.to_string(),
266                    duration,
267                };
268            }
269
270            Ok(result)
271        }
272        _ => Err(ShapeError::ParseError {
273            message: format!("Unexpected datetime expression: {:?}", pair.as_rule()),
274            location: None,
275        }),
276    }
277}
278
279/// Parse datetime range
280pub fn parse_datetime_range(pair: Pair<Rule>) -> Result<(Expr, Option<Expr>)> {
281    // Parse datetime_range: datetime_expr ("to" datetime_expr)?
282    let mut inner = pair.into_inner();
283    let first_pair = inner.next().unwrap();
284    let first_span = pair_span(&first_pair);
285    let first_datetime = parse_datetime_expr(first_pair)?;
286
287    // Check if there's a "to" and second datetime
288    if let Some(second_pair) = inner.next() {
289        let second_span = pair_span(&second_pair);
290        let second_datetime = parse_datetime_expr(second_pair)?;
291        Ok((
292            Expr::DateTime(first_datetime, first_span),
293            Some(Expr::DateTime(second_datetime, second_span)),
294        ))
295    } else {
296        Ok((Expr::DateTime(first_datetime, first_span), None))
297    }
298}
299
300/// Parse duration expression
301pub fn parse_duration(pair: Pair<Rule>) -> Result<Expr> {
302    let span = pair_span(&pair);
303    // Since duration is now atomic, parse the string directly
304    let duration_str = pair.as_str();
305
306    // Check if it's a compound duration (contains multiple units)
307    let mut components = Vec::new();
308    let mut current_number = String::new();
309    let mut chars = duration_str.chars().peekable();
310
311    while let Some(ch) = chars.next() {
312        if ch.is_numeric() || ch == '.' || (ch == '-' && current_number.is_empty()) {
313            current_number.push(ch);
314        } else {
315            // We've hit a unit character
316            if !current_number.is_empty() {
317                let value: f64 = current_number.parse().map_err(|e| ShapeError::ParseError {
318                    message: format!("Invalid duration value: {}", e),
319                    location: None,
320                })?;
321
322                // Collect the unit string
323                let mut unit_str = String::new();
324                unit_str.push(ch);
325
326                // For long unit names like "minutes", "hours", etc.
327                while let Some(&next_ch) = chars.peek() {
328                    if next_ch.is_alphabetic() {
329                        unit_str.push(chars.next().unwrap());
330                    } else {
331                        break;
332                    }
333                }
334
335                let unit = match unit_str.as_str() {
336                    "s" | "seconds" => DurationUnit::Seconds,
337                    "m" | "minutes" => DurationUnit::Minutes,
338                    "h" | "hours" => DurationUnit::Hours,
339                    "d" | "days" => DurationUnit::Days,
340                    "w" | "weeks" => DurationUnit::Weeks,
341                    "M" | "months" => DurationUnit::Months,
342                    "y" | "years" => DurationUnit::Years,
343                    "samples" => DurationUnit::Samples,
344                    _ => {
345                        return Err(ShapeError::ParseError {
346                            message: format!("Unknown duration unit: {}", unit_str),
347                            location: None,
348                        });
349                    }
350                };
351
352                components.push((value, unit));
353                current_number.clear();
354            }
355        }
356    }
357
358    // If there's only one component, return it directly
359    if components.len() == 1 {
360        let (value, unit) = components.into_iter().next().unwrap();
361        return Ok(Expr::Duration(Duration { value, unit }, span));
362    }
363
364    // For compound durations, convert to seconds and find appropriate unit
365    let mut total_seconds = 0.0;
366    for (value, unit) in components {
367        let seconds = match unit {
368            DurationUnit::Seconds => value,
369            DurationUnit::Minutes => value * 60.0,
370            DurationUnit::Hours => value * 3600.0,
371            DurationUnit::Days => value * 86400.0,
372            DurationUnit::Weeks => value * 604800.0,
373            DurationUnit::Months => value * 2592000.0, // Approximate: 30 days
374            DurationUnit::Years => value * 31536000.0, // Approximate: 365 days
375            DurationUnit::Samples => {
376                return Err(ShapeError::ParseError {
377                    message: "Cannot use 'samples' in compound duration".to_string(),
378                    location: None,
379                });
380            }
381        };
382        total_seconds += seconds;
383    }
384
385    // Convert back to the most appropriate unit
386    let (value, unit) = if total_seconds < 60.0 {
387        (total_seconds, DurationUnit::Seconds)
388    } else if total_seconds < 3600.0 {
389        (total_seconds / 60.0, DurationUnit::Minutes)
390    } else if total_seconds < 86400.0 {
391        (total_seconds / 3600.0, DurationUnit::Hours)
392    } else if total_seconds < 604800.0 {
393        (total_seconds / 86400.0, DurationUnit::Days)
394    } else if total_seconds < 2592000.0 {
395        (total_seconds / 604800.0, DurationUnit::Weeks)
396    } else if total_seconds < 31536000.0 {
397        (total_seconds / 2592000.0, DurationUnit::Months)
398    } else {
399        (total_seconds / 31536000.0, DurationUnit::Years)
400    };
401
402    Ok(Expr::Duration(Duration { value, unit }, span))
403}
404
405#[cfg(test)]
406mod tests {
407    use crate::ast::{DateTimeExpr, DurationUnit, Expr};
408
409    fn parse_expr(code: &str) -> Expr {
410        let program = crate::parser::parse_program(code).expect("parse failed");
411        // The last expression-statement's expr
412        match &program.items[0] {
413            crate::ast::Item::Expression(expr, _) => expr.clone(),
414            crate::ast::Item::Statement(crate::ast::Statement::Expression(expr, _), _) => {
415                expr.clone()
416            }
417            other => panic!("expected expression statement, got {:?}", other),
418        }
419    }
420
421    #[test]
422    fn test_parse_datetime_literal_iso8601() {
423        let expr = parse_expr(r#"@"2024-06-15T14:30:00""#);
424        match expr {
425            Expr::DateTime(DateTimeExpr::Literal(s), _) => {
426                assert_eq!(s, "2024-06-15T14:30:00");
427            }
428            other => panic!("expected DateTime literal, got {:?}", other),
429        }
430    }
431
432    #[test]
433    fn test_parse_datetime_literal_date_only() {
434        let expr = parse_expr(r#"@"2024-01-15""#);
435        match expr {
436            Expr::DateTime(DateTimeExpr::Literal(s), _) => {
437                assert_eq!(s, "2024-01-15");
438            }
439            other => panic!("expected DateTime literal, got {:?}", other),
440        }
441    }
442
443    #[test]
444    fn test_parse_datetime_named_now() {
445        let expr = parse_expr("@now");
446        match expr {
447            Expr::DateTime(DateTimeExpr::Named(crate::ast::NamedTime::Now), _) => {}
448            other => panic!("expected DateTime Named(Now), got {:?}", other),
449        }
450    }
451
452    #[test]
453    fn test_parse_duration_days() {
454        let expr = parse_expr("3d");
455        match expr {
456            Expr::Duration(dur, _) => {
457                assert_eq!(dur.value, 3.0);
458                assert_eq!(dur.unit, DurationUnit::Days);
459            }
460            other => panic!("expected Duration, got {:?}", other),
461        }
462    }
463
464    #[test]
465    fn test_parse_duration_hours() {
466        let expr = parse_expr("2h");
467        match expr {
468            Expr::Duration(dur, _) => {
469                assert_eq!(dur.value, 2.0);
470                assert_eq!(dur.unit, DurationUnit::Hours);
471            }
472            other => panic!("expected Duration, got {:?}", other),
473        }
474    }
475
476    #[test]
477    fn test_parse_duration_minutes() {
478        let expr = parse_expr("30m");
479        match expr {
480            Expr::Duration(dur, _) => {
481                assert_eq!(dur.value, 30.0);
482                assert_eq!(dur.unit, DurationUnit::Minutes);
483            }
484            other => panic!("expected Duration, got {:?}", other),
485        }
486    }
487
488    #[test]
489    fn test_parse_duration_seconds() {
490        let expr = parse_expr("10s");
491        match expr {
492            Expr::Duration(dur, _) => {
493                assert_eq!(dur.value, 10.0);
494                assert_eq!(dur.unit, DurationUnit::Seconds);
495            }
496            other => panic!("expected Duration, got {:?}", other),
497        }
498    }
499}