Skip to main content

shape_ast/parser/
time.rs

1//! Time-related parsing module
2
3use crate::error::{Result, ShapeError};
4use crate::parser::pair_location;
5use crate::parser::string_literals::parse_string_literal;
6use pest::iterators::Pair;
7
8use super::Rule;
9use crate::ast::{TimeReference, TimeUnit, TimeWindow};
10
11/// Parse a time window
12///
13/// Supports: `last N units`, `between start and end`, `window(...)`, `session "start" to "end"`
14pub fn parse_time_window(pair: Pair<Rule>) -> Result<TimeWindow> {
15    let pair_loc = pair_location(&pair);
16    let inner = pair
17        .into_inner()
18        .next()
19        .ok_or_else(|| ShapeError::ParseError {
20            message: "expected time window specification".to_string(),
21            location: Some(
22                pair_loc
23                    .clone()
24                    .with_hint("use 'last N bars', 'last N days', 'between start and end', etc."),
25            ),
26        })?;
27
28    match inner.as_rule() {
29        Rule::last_window => parse_last_window(inner),
30        Rule::between_window => parse_between_window(inner),
31        Rule::window_range => parse_window_range(inner),
32        Rule::session_window => parse_session_window(inner),
33        _ => Err(ShapeError::ParseError {
34            message: format!("unexpected time window type: {:?}", inner.as_rule()),
35            location: Some(pair_location(&inner)),
36        }),
37    }
38}
39
40fn parse_last_window(pair: Pair<Rule>) -> Result<TimeWindow> {
41    let pair_loc = pair_location(&pair);
42    let mut inner = pair.into_inner();
43
44    let amount_pair = inner.next().ok_or_else(|| ShapeError::ParseError {
45        message: "expected amount in 'last' window".to_string(),
46        location: Some(
47            pair_loc
48                .clone()
49                .with_hint("specify an amount, e.g., 'last 100 bars'"),
50        ),
51    })?;
52
53    let amount: i32 = amount_pair
54        .as_str()
55        .parse()
56        .map_err(|e| ShapeError::ParseError {
57            message: format!("invalid number in time window: {}", e),
58            location: Some(
59                pair_location(&amount_pair).with_hint("amount must be a positive integer"),
60            ),
61        })?;
62
63    let unit_pair = inner.next().ok_or_else(|| ShapeError::ParseError {
64        message: "expected time unit after amount".to_string(),
65        location: Some(pair_loc.with_hint("add a time unit like 'bars', 'days', 'hours', 'weeks'")),
66    })?;
67    let unit = parse_time_unit(unit_pair)?;
68
69    Ok(TimeWindow::Last { amount, unit })
70}
71
72fn parse_between_window(pair: Pair<Rule>) -> Result<TimeWindow> {
73    let pair_loc = pair_location(&pair);
74    let mut inner = pair.into_inner();
75
76    let start_pair = inner.next().ok_or_else(|| ShapeError::ParseError {
77        message: "expected start time in 'between' window".to_string(),
78        location: Some(pair_loc.clone().with_hint(
79            "use 'between @yesterday and @today' or 'between \"2023-01-01\" and \"2023-12-31\"'",
80        )),
81    })?;
82    let start = parse_time_ref(start_pair)?;
83
84    let end_pair = inner.next().ok_or_else(|| ShapeError::ParseError {
85        message: "expected end time in 'between' window".to_string(),
86        location: Some(pair_loc.with_hint("add 'and <end_time>' after start time")),
87    })?;
88    let end = parse_time_ref(end_pair)?;
89
90    Ok(TimeWindow::Between { start, end })
91}
92
93fn parse_window_range(pair: Pair<Rule>) -> Result<TimeWindow> {
94    let pair_loc = pair_location(&pair);
95    let mut inner = pair.into_inner();
96
97    // Skip "window" keyword
98    inner.next();
99
100    let args = inner.next().ok_or_else(|| ShapeError::ParseError {
101        message: "expected window arguments".to_string(),
102        location: Some(
103            pair_loc
104                .clone()
105                .with_hint("use 'window(start, end)' with row indices or time references"),
106        ),
107    })?;
108
109    let args_loc = pair_location(&args);
110    let mut args_inner = args.into_inner();
111
112    let first = args_inner.next().ok_or_else(|| ShapeError::ParseError {
113        message: "expected first window argument".to_string(),
114        location: Some(args_loc.clone()),
115    })?;
116
117    match first.as_rule() {
118        Rule::number => {
119            let start: i32 = first.as_str().parse().map_err(|e| ShapeError::ParseError {
120                message: format!("invalid start index in window: {}", e),
121                location: Some(pair_location(&first)),
122            })?;
123
124            let end = args_inner
125                .next()
126                .map(|p| {
127                    p.as_str()
128                        .parse::<i32>()
129                        .map_err(|e| ShapeError::ParseError {
130                            message: format!("invalid end index in window: {}", e),
131                            location: Some(pair_location(&p)),
132                        })
133                })
134                .transpose()?;
135
136            Ok(TimeWindow::Window { start, end })
137        }
138        Rule::timeframe => Err(ShapeError::ParseError {
139            message: "timeframe windows not yet implemented".to_string(),
140            location: Some(
141                pair_location(&first).with_hint("use row indices or time references instead"),
142            ),
143        }),
144        Rule::time_ref => {
145            let start = parse_time_ref(first)?;
146            let end_pair = args_inner.next().ok_or_else(|| ShapeError::ParseError {
147                message: "expected end time reference in window".to_string(),
148                location: Some(
149                    args_loc.with_hint("provide two time references: window(@start, @end)"),
150                ),
151            })?;
152            let end = parse_time_ref(end_pair)?;
153            Ok(TimeWindow::Between { start, end })
154        }
155        _ => Err(ShapeError::ParseError {
156            message: format!("unexpected window argument type: {:?}", first.as_rule()),
157            location: Some(pair_location(&first)),
158        }),
159    }
160}
161
162fn parse_session_window(pair: Pair<Rule>) -> Result<TimeWindow> {
163    let pair_loc = pair_location(&pair);
164    let mut inner = pair.into_inner();
165
166    // Skip "session" keyword
167    inner.next();
168
169    let start_pair = inner.next().ok_or_else(|| ShapeError::ParseError {
170        message: "expected start time string in session window".to_string(),
171        location: Some(
172            pair_loc
173                .clone()
174                .with_hint("use 'session \"09:30\" to \"16:00\"'"),
175        ),
176    })?;
177    let start = parse_string_literal(start_pair.as_str())?;
178
179    let end_pair = inner.next().ok_or_else(|| ShapeError::ParseError {
180        message: "expected end time string in session window".to_string(),
181        location: Some(pair_loc.with_hint("add 'to \"HH:MM\"' after start time")),
182    })?;
183    let end = parse_string_literal(end_pair.as_str())?;
184
185    Ok(TimeWindow::Session { start, end })
186}
187
188/// Parse a time unit (samples, records, minutes, hours, days, weeks, months)
189pub fn parse_time_unit(pair: Pair<Rule>) -> Result<TimeUnit> {
190    let unit_str = pair.as_str();
191
192    match unit_str {
193        "sample" | "samples" | "record" | "records" => Ok(TimeUnit::Samples),
194        "minute" | "minutes" => Ok(TimeUnit::Minutes),
195        "hour" | "hours" => Ok(TimeUnit::Hours),
196        "day" | "days" => Ok(TimeUnit::Days),
197        "week" | "weeks" => Ok(TimeUnit::Weeks),
198        "month" | "months" => Ok(TimeUnit::Months),
199        _ => {
200            Err(ShapeError::ParseError {
201                message: format!("unknown time unit: '{}'", unit_str),
202                location: Some(pair_location(&pair).with_hint(
203                    "valid units: samples, records, minutes, hours, days, weeks, months",
204                )),
205            })
206        }
207    }
208}
209
210fn parse_time_ref(pair: Pair<Rule>) -> Result<TimeReference> {
211    crate::parser::expressions::temporal::parse_time_ref(pair)
212}