rusty_promql_parser/lexer/
duration.rs

1//! Duration literal parser for PromQL.
2//!
3//! Durations represent time spans and are used in:
4//! - Range selectors: `metric[5m]`
5//! - Offset modifiers: `metric offset 1h`
6//! - Subqueries: `metric[30m:5m]`
7//!
8//! # Format
9//!
10//! Durations consist of one or more `<number><unit>` pairs:
11//!
12//! | Unit | Name        | Milliseconds    |
13//! |------|-------------|-----------------|
14//! | `ms` | millisecond | 1               |
15//! | `s`  | second      | 1,000           |
16//! | `m`  | minute      | 60,000          |
17//! | `h`  | hour        | 3,600,000       |
18//! | `d`  | day         | 86,400,000      |
19//! | `w`  | week        | 604,800,000     |
20//! | `y`  | year        | 31,536,000,000  |
21//!
22//! # Examples
23//!
24//! ```rust
25//! use rusty_promql_parser::lexer::duration::duration;
26//!
27//! // Simple durations
28//! let (_, dur) = duration("5m").unwrap();
29//! assert_eq!(dur.as_millis(), 300_000);
30//!
31//! // Compound durations
32//! let (_, dur) = duration("1h30m").unwrap();
33//! assert_eq!(dur.as_millis(), 5_400_000);
34//!
35//! // Milliseconds
36//! let (_, dur) = duration("100ms").unwrap();
37//! assert_eq!(dur.as_millis(), 100);
38//! ```
39
40use nom::{
41    IResult, Parser,
42    branch::alt,
43    bytes::complete::tag,
44    character::complete::digit1,
45    combinator::{map, map_res, opt},
46    multi::many1,
47    sequence::pair,
48};
49
50/// A duration value representing a time span in milliseconds.
51///
52/// Durations are used throughout PromQL for specifying time ranges,
53/// offsets, and subquery steps.
54///
55/// # Example
56///
57/// ```rust
58/// use rusty_promql_parser::lexer::duration::Duration;
59///
60/// let dur = Duration::from_secs(300);
61/// assert_eq!(dur.as_millis(), 300_000);
62/// assert_eq!(dur.to_string(), "5m");
63/// ```
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub struct Duration {
66    /// Duration in milliseconds (can be negative for negative offsets)
67    pub milliseconds: i64,
68}
69
70impl Duration {
71    /// Create a new duration from milliseconds
72    pub const fn from_millis(ms: i64) -> Self {
73        Self { milliseconds: ms }
74    }
75
76    /// Create a new duration from seconds
77    pub const fn from_secs(secs: i64) -> Self {
78        Self {
79            milliseconds: secs * 1000,
80        }
81    }
82
83    /// Get the duration in milliseconds
84    pub const fn as_millis(&self) -> i64 {
85        self.milliseconds
86    }
87
88    /// Get the duration in seconds (truncated)
89    pub const fn as_secs(&self) -> i64 {
90        self.milliseconds / 1000
91    }
92}
93
94impl std::fmt::Display for Duration {
95    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96        let mut ms = self.milliseconds;
97        if ms == 0 {
98            return write!(f, "0s");
99        }
100
101        let mut result = String::new();
102
103        // Handle negative durations
104        if ms < 0 {
105            result.push('-');
106            ms = -ms;
107        }
108
109        // Handle years
110        let years = ms / 31_536_000_000;
111        if years > 0 {
112            result.push_str(&format!("{}y", years));
113            ms %= 31_536_000_000;
114        }
115
116        // Handle weeks
117        let weeks = ms / 604_800_000;
118        if weeks > 0 {
119            result.push_str(&format!("{}w", weeks));
120            ms %= 604_800_000;
121        }
122
123        // Handle days
124        let days = ms / 86_400_000;
125        if days > 0 {
126            result.push_str(&format!("{}d", days));
127            ms %= 86_400_000;
128        }
129
130        // Handle hours
131        let hours = ms / 3_600_000;
132        if hours > 0 {
133            result.push_str(&format!("{}h", hours));
134            ms %= 3_600_000;
135        }
136
137        // Handle minutes
138        let minutes = ms / 60_000;
139        if minutes > 0 {
140            result.push_str(&format!("{}m", minutes));
141            ms %= 60_000;
142        }
143
144        // Handle seconds
145        let seconds = ms / 1000;
146        if seconds > 0 {
147            result.push_str(&format!("{}s", seconds));
148            ms %= 1000;
149        }
150
151        // Handle milliseconds
152        if ms > 0 {
153            result.push_str(&format!("{}ms", ms));
154        }
155
156        write!(f, "{}", result)
157    }
158}
159
160/// Duration unit with its millisecond multiplier.
161#[derive(Debug, Clone, Copy, PartialEq, Eq)]
162enum DurationUnit {
163    Millisecond, // ms - 1
164    Second,      // s  - 1,000
165    Minute,      // m  - 60,000
166    Hour,        // h  - 3,600,000
167    Day,         // d  - 86,400,000
168    Week,        // w  - 604,800,000
169    Year,        // y  - 31,536,000,000
170}
171
172impl DurationUnit {
173    /// Get the multiplier in milliseconds
174    const fn millis(&self) -> i64 {
175        match self {
176            DurationUnit::Millisecond => 1,
177            DurationUnit::Second => 1_000,
178            DurationUnit::Minute => 60_000,
179            DurationUnit::Hour => 3_600_000,
180            DurationUnit::Day => 86_400_000,
181            DurationUnit::Week => 604_800_000,
182            DurationUnit::Year => 31_536_000_000,
183        }
184    }
185}
186
187/// Compute the total duration in milliseconds from components, with overflow checking.
188fn compute_duration_millis(components: Vec<(i64, DurationUnit)>) -> Result<Duration, ()> {
189    let mut total_ms: i64 = 0;
190    for (value, unit) in components {
191        let component_ms = value.checked_mul(unit.millis()).ok_or(())?;
192        total_ms = total_ms.checked_add(component_ms).ok_or(())?;
193    }
194    Ok(Duration::from_millis(total_ms))
195}
196
197/// Parse a PromQL duration literal.
198///
199/// Returns the Duration with total milliseconds.
200/// Compound durations like "1h30m" are supported.
201/// Returns an error if the duration would overflow i64.
202pub fn duration(input: &str) -> IResult<&str, Duration> {
203    map_res(many1(duration_component), compute_duration_millis).parse(input)
204}
205
206/// Parse a single duration component: <number><unit>
207fn duration_component(input: &str) -> IResult<&str, (i64, DurationUnit)> {
208    pair(map_res(digit1, |s: &str| s.parse::<i64>()), duration_unit).parse(input)
209}
210
211/// Parse a duration unit
212fn duration_unit(input: &str) -> IResult<&str, DurationUnit> {
213    alt((
214        // "ms" must come before "m" to match correctly
215        map(tag("ms"), |_| DurationUnit::Millisecond),
216        map(tag("s"), |_| DurationUnit::Second),
217        map(tag("m"), |_| DurationUnit::Minute),
218        map(tag("h"), |_| DurationUnit::Hour),
219        map(tag("d"), |_| DurationUnit::Day),
220        map(tag("w"), |_| DurationUnit::Week),
221        map(tag("y"), |_| DurationUnit::Year),
222    ))
223    .parse(input)
224}
225
226/// Parse a duration that may be preceded by a sign (+/-).
227/// Used for offset modifiers which can be negative.
228pub fn signed_duration(input: &str) -> IResult<&str, Duration> {
229    map(
230        pair(opt(alt((tag("+"), tag("-")))), duration),
231        |(sign, dur)| {
232            if sign == Some("-") {
233                Duration::from_millis(-dur.milliseconds)
234            } else {
235                dur
236            }
237        },
238    )
239    .parse(input)
240}
241
242#[cfg(test)]
243mod tests {
244    use super::*;
245
246    /// Helper to test duration parsing
247    fn assert_duration(input: &str, expected_ms: i64) {
248        let result = duration(input);
249        match result {
250            Ok((remaining, dur)) => {
251                assert!(
252                    remaining.is_empty(),
253                    "Parser did not consume entire input '{}', remaining: '{}'",
254                    input,
255                    remaining
256                );
257                assert_eq!(
258                    dur.milliseconds, expected_ms,
259                    "For input '{}', expected {}ms, got {}ms",
260                    input, expected_ms, dur.milliseconds
261                );
262            }
263            Err(e) => panic!("Failed to parse '{}': {:?}", input, e),
264        }
265    }
266
267    /// Helper to test signed duration parsing
268    fn assert_signed_duration(input: &str, expected_ms: i64) {
269        let result = signed_duration(input);
270        match result {
271            Ok((remaining, dur)) => {
272                assert!(
273                    remaining.is_empty(),
274                    "Parser did not consume entire input '{}', remaining: '{}'",
275                    input,
276                    remaining
277                );
278                assert_eq!(
279                    dur.milliseconds, expected_ms,
280                    "For input '{}', expected {}ms, got {}ms",
281                    input, expected_ms, dur.milliseconds
282                );
283            }
284            Err(e) => panic!("Failed to parse '{}': {:?}", input, e),
285        }
286    }
287
288    // Simple durations
289    #[test]
290    fn test_milliseconds() {
291        assert_duration("1ms", 1);
292        assert_duration("100ms", 100);
293        assert_duration("1000ms", 1000);
294    }
295
296    #[test]
297    fn test_seconds() {
298        assert_duration("1s", 1_000);
299        assert_duration("5s", 5_000);
300        assert_duration("30s", 30_000);
301    }
302
303    #[test]
304    fn test_minutes() {
305        assert_duration("1m", 60_000);
306        assert_duration("5m", 300_000);
307        assert_duration("30m", 1_800_000);
308        assert_duration("123m", 7_380_000);
309    }
310
311    #[test]
312    fn test_hours() {
313        assert_duration("1h", 3_600_000);
314        assert_duration("5h", 18_000_000);
315        assert_duration("24h", 86_400_000);
316    }
317
318    #[test]
319    fn test_days() {
320        assert_duration("1d", 86_400_000);
321        assert_duration("5d", 432_000_000);
322    }
323
324    #[test]
325    fn test_weeks() {
326        assert_duration("1w", 604_800_000);
327        assert_duration("3w", 1_814_400_000);
328        assert_duration("5w", 3_024_000_000);
329    }
330
331    #[test]
332    fn test_years() {
333        assert_duration("1y", 31_536_000_000);
334        assert_duration("5y", 157_680_000_000);
335    }
336
337    // Compound durations
338    #[test]
339    fn test_compound_hour_minute() {
340        assert_duration("1h30m", 5_400_000);
341    }
342
343    #[test]
344    fn test_compound_minute_second() {
345        assert_duration("5m30s", 330_000);
346    }
347
348    #[test]
349    fn test_compound_second_millisecond() {
350        assert_duration("4s180ms", 4_180);
351        assert_duration("4s18ms", 4_018);
352        assert_duration("1m30ms", 60_030);
353    }
354
355    #[test]
356    fn test_compound_complex() {
357        assert_duration("1h30m15s", 5_415_000);
358        assert_duration("2d12h", 216_000_000);
359        assert_duration("5m10s", 310_000);
360    }
361
362    // Signed durations
363    #[test]
364    fn test_signed_positive() {
365        assert_signed_duration("+5m", 300_000);
366        assert_signed_duration("+1h30m", 5_400_000);
367    }
368
369    #[test]
370    fn test_signed_negative() {
371        assert_signed_duration("-5m", -300_000);
372        assert_signed_duration("-7m", -420_000);
373        assert_signed_duration("-1h30m", -5_400_000);
374    }
375
376    #[test]
377    fn test_signed_no_sign() {
378        // Without sign, should parse same as unsigned
379        assert_signed_duration("5m", 300_000);
380    }
381
382    // Display formatting
383    #[test]
384    fn test_duration_display() {
385        assert_eq!(Duration::from_millis(0).to_string(), "0s");
386        assert_eq!(Duration::from_millis(1).to_string(), "1ms");
387        assert_eq!(Duration::from_millis(1000).to_string(), "1s");
388        assert_eq!(Duration::from_millis(60_000).to_string(), "1m");
389        assert_eq!(Duration::from_millis(3_600_000).to_string(), "1h");
390        assert_eq!(Duration::from_millis(5_400_000).to_string(), "1h30m");
391        assert_eq!(Duration::from_millis(86_400_000).to_string(), "1d");
392        assert_eq!(Duration::from_millis(604_800_000).to_string(), "1w");
393        assert_eq!(Duration::from_millis(31_536_000_000).to_string(), "1y");
394    }
395
396    // Edge cases
397    #[test]
398    fn test_partial_parse() {
399        // Duration followed by other content
400        let (remaining, dur) = duration("5m30s offset").unwrap();
401        assert_eq!(dur.milliseconds, 330_000);
402        assert_eq!(remaining, " offset");
403    }
404
405    #[test]
406    fn test_invalid_unit() {
407        // Invalid unit should fail
408        assert!(duration("5x").is_err());
409        assert!(duration("5").is_err()); // Number without unit
410    }
411
412    #[test]
413    fn test_fail_found_with_fuzzing() {
414        assert!(duration("5555555555555555555m").is_err());
415    }
416}