Skip to main content

nmbrs_runtime/
timeval.rs

1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! Shared parser for user-facing time-valued settings.
5//!
6//! The convention for time-based settings a user sets (e.g. the CQL `timeout`
7//! op field): a value is EITHER
8//!
9//! - a **duration spec-string with a unit suffix** — `60s`, `500ms`,
10//!   `1h30m`, `2.5s` (units `ms`/`s`/`m`/`h`/`d`; compound and fractional
11//!   allowed), OR
12//! - a **bare number meaning fractional SECONDS** — `60` = 60s, `60.5` =
13//!   60.5s (→ 60500 ms), `0.25` = 250 ms.
14//!
15//! Distinct from [`crate::session::parse_duration`], which is integer-only
16//! (whole seconds/minutes/…) and used for the session shelf-life cap. This
17//! parser is the convention for millisecond-resolution op/connection timeouts.
18
19/// Parse a user time value to whole milliseconds.
20///
21/// A bare number is fractional seconds; a suffixed/compound string is a
22/// duration (`ms`/`s`/`m`/`h`/`d`). Returns an actionable error for a
23/// negative, non-finite, unit-less, or unknown-unit value.
24pub fn parse_time_ms(s: &str) -> Result<u64, String> {
25    let s = s.trim();
26    if s.is_empty() {
27        return Err("empty time value".into());
28    }
29    // Bare number → fractional seconds.
30    if let Ok(n) = s.parse::<f64>() {
31        if !n.is_finite() || n < 0.0 {
32            return Err(format!("time value {s:?}: must be a non-negative number"));
33        }
34        return Ok((n * 1000.0).round() as u64);
35    }
36    // Suffixed / compound duration (e.g. `1h30m`, `500ms`, `2.5s`).
37    let bytes = s.as_bytes();
38    let mut i = 0;
39    let mut total_ms: f64 = 0.0;
40    while i < bytes.len() {
41        let start = i;
42        while i < bytes.len() && (bytes[i].is_ascii_digit() || bytes[i] == b'.') {
43            i += 1;
44        }
45        if i == start {
46            return Err(format!(
47                "time value {s:?}: expected a number before the unit"
48            ));
49        }
50        let n: f64 = s[start..i]
51            .parse()
52            .map_err(|e| format!("time value {s:?}: bad number: {e}"))?;
53        if !n.is_finite() || n < 0.0 {
54            return Err(format!("time value {s:?}: must be non-negative"));
55        }
56        let unit_start = i;
57        while i < bytes.len() && bytes[i].is_ascii_alphabetic() {
58            i += 1;
59        }
60        let unit = &s[unit_start..i];
61        let mult = match unit {
62            "ms" => 1.0,
63            "s" => 1_000.0,
64            "m" => 60_000.0,
65            "h" => 3_600_000.0,
66            "d" => 86_400_000.0,
67            "" => {
68                return Err(format!(
69                    "time value {s:?}: missing unit (use ms/s/m/h/d, \
70                 or a bare number for seconds)"
71                ));
72            }
73            other => {
74                return Err(format!(
75                    "time value {s:?}: unknown unit {other:?} (use ms/s/m/h/d)"
76                ));
77            }
78        };
79        total_ms += n * mult;
80    }
81    Ok(total_ms.round() as u64)
82}
83
84#[cfg(test)]
85mod tests {
86    use super::parse_time_ms;
87
88    #[test]
89    fn suffix_forms() {
90        assert_eq!(parse_time_ms("60s").unwrap(), 60_000);
91        assert_eq!(parse_time_ms("500ms").unwrap(), 500);
92        assert_eq!(parse_time_ms("2.5s").unwrap(), 2_500);
93        assert_eq!(parse_time_ms("1h30m").unwrap(), 5_400_000);
94        assert_eq!(parse_time_ms("1d").unwrap(), 86_400_000);
95    }
96
97    #[test]
98    fn bare_number_is_fractional_seconds() {
99        assert_eq!(parse_time_ms("60").unwrap(), 60_000);
100        assert_eq!(parse_time_ms("60.5").unwrap(), 60_500);
101        assert_eq!(parse_time_ms("0.25").unwrap(), 250);
102        assert_eq!(parse_time_ms("0").unwrap(), 0);
103    }
104
105    #[test]
106    fn rejects_garbage() {
107        assert!(parse_time_ms("abc").is_err());
108        assert!(parse_time_ms("10x").is_err());
109        assert!(parse_time_ms("").is_err());
110        assert!(parse_time_ms("-5").is_err());
111        assert!(parse_time_ms("10 s foo").is_err());
112    }
113}