rusty_promql_parser/lexer/
duration.rs1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub struct Duration {
66 pub milliseconds: i64,
68}
69
70impl Duration {
71 pub const fn from_millis(ms: i64) -> Self {
73 Self { milliseconds: ms }
74 }
75
76 pub const fn from_secs(secs: i64) -> Self {
78 Self {
79 milliseconds: secs * 1000,
80 }
81 }
82
83 pub const fn as_millis(&self) -> i64 {
85 self.milliseconds
86 }
87
88 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 if ms < 0 {
105 result.push('-');
106 ms = -ms;
107 }
108
109 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 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 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 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 let minutes = ms / 60_000;
139 if minutes > 0 {
140 result.push_str(&format!("{}m", minutes));
141 ms %= 60_000;
142 }
143
144 let seconds = ms / 1000;
146 if seconds > 0 {
147 result.push_str(&format!("{}s", seconds));
148 ms %= 1000;
149 }
150
151 if ms > 0 {
153 result.push_str(&format!("{}ms", ms));
154 }
155
156 write!(f, "{}", result)
157 }
158}
159
160#[derive(Debug, Clone, Copy, PartialEq, Eq)]
162enum DurationUnit {
163 Millisecond, Second, Minute, Hour, Day, Week, Year, }
171
172impl DurationUnit {
173 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
187fn 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
197pub fn duration(input: &str) -> IResult<&str, Duration> {
203 map_res(many1(duration_component), compute_duration_millis).parse(input)
204}
205
206fn duration_component(input: &str) -> IResult<&str, (i64, DurationUnit)> {
208 pair(map_res(digit1, |s: &str| s.parse::<i64>()), duration_unit).parse(input)
209}
210
211fn duration_unit(input: &str) -> IResult<&str, DurationUnit> {
213 alt((
214 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
226pub 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 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 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 #[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 #[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 #[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 assert_signed_duration("5m", 300_000);
380 }
381
382 #[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 #[test]
398 fn test_partial_parse() {
399 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 assert!(duration("5x").is_err());
409 assert!(duration("5").is_err()); }
411
412 #[test]
413 fn test_fail_found_with_fuzzing() {
414 assert!(duration("5555555555555555555m").is_err());
415 }
416}