harn_vm/
duration_parse.rs1pub(crate) fn split_amount_unit(raw: &str) -> Option<(&str, String)> {
34 let trimmed = raw.trim();
35 if trimmed.is_empty() {
36 return None;
37 }
38 let split = trimmed
39 .find(|ch: char| !ch.is_ascii_digit())
40 .unwrap_or(trimmed.len());
41 if split == 0 {
42 return None; }
44 let (amount, unit) = trimmed.split_at(split);
45 Some((amount, unit.trim().to_ascii_lowercase()))
46}
47
48pub(crate) fn unit_to_millis(unit: &str) -> Option<u64> {
52 Some(match unit {
53 "ms" => 1,
54 "s" => 1_000,
55 "m" => 60_000,
56 "h" => 3_600_000,
57 "d" => 86_400_000,
58 "w" => 604_800_000,
59 _ => return None,
60 })
61}
62
63#[derive(Debug, Clone, PartialEq, Eq)]
68pub enum DurationParseError {
69 Empty,
71 NoDigits,
73 MissingUnit,
75 AmountOverflow,
77 UnknownUnit(String),
79 TooLarge,
81}
82
83pub fn parse_millis(raw: &str) -> Result<u64, DurationParseError> {
88 let Some((digits, unit)) = split_amount_unit(raw) else {
89 return Err(if raw.trim().is_empty() {
90 DurationParseError::Empty
91 } else {
92 DurationParseError::NoDigits
93 });
94 };
95 if unit.is_empty() {
96 return Err(DurationParseError::MissingUnit);
97 }
98 let amount: u64 = digits
99 .parse()
100 .map_err(|_| DurationParseError::AmountOverflow)?;
101 let multiplier = unit_to_millis(&unit).ok_or(DurationParseError::UnknownUnit(unit))?;
102 amount
103 .checked_mul(multiplier)
104 .ok_or(DurationParseError::TooLarge)
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110
111 #[test]
112 fn a_unit_suffix_is_required() {
113 assert_eq!(parse_millis("30"), Err(DurationParseError::MissingUnit));
116 }
117
118 #[test]
119 fn every_unit_through_weeks_is_accepted() {
120 assert_eq!(parse_millis("200ms"), Ok(200));
121 assert_eq!(parse_millis("5s"), Ok(5_000));
122 assert_eq!(parse_millis("5m"), Ok(300_000));
123 assert_eq!(parse_millis("2h"), Ok(7_200_000));
124 assert_eq!(parse_millis("7d"), Ok(7 * 86_400_000));
125 assert_eq!(parse_millis("2w"), Ok(2 * 604_800_000));
126 }
127
128 #[test]
129 fn overflow_is_reported_never_clamped() {
130 assert_eq!(
133 parse_millis("99999999999999999h"),
134 Err(DurationParseError::TooLarge)
135 );
136 assert_eq!(
137 parse_millis("99999999999999999999999s"),
138 Err(DurationParseError::AmountOverflow)
139 );
140 }
141
142 #[test]
143 fn blank_digitless_and_unknown_units_are_distinguishable() {
144 assert_eq!(parse_millis(" "), Err(DurationParseError::Empty));
145 assert_eq!(parse_millis("abc"), Err(DurationParseError::NoDigits));
146 assert_eq!(parse_millis("-5s"), Err(DurationParseError::NoDigits));
147 assert_eq!(
148 parse_millis("5y"),
149 Err(DurationParseError::UnknownUnit("y".to_string()))
150 );
151 }
152
153 #[test]
154 fn units_are_case_insensitive_and_tolerate_internal_space() {
155 assert_eq!(parse_millis("5 M"), Ok(300_000));
156 assert_eq!(parse_millis("1H"), Ok(3_600_000));
157 }
158
159 #[test]
160 fn splits_number_and_unit() {
161 assert_eq!(split_amount_unit("5m"), Some(("5", "m".to_string())));
162 assert_eq!(
163 split_amount_unit(" 200 ms "),
164 Some(("200", "ms".to_string()))
165 );
166 assert_eq!(split_amount_unit("30"), Some(("30", String::new())));
167 assert_eq!(split_amount_unit("1H"), Some(("1", "h".to_string())));
168 }
169
170 #[test]
171 fn rejects_blank_and_unitless_prefix() {
172 assert_eq!(split_amount_unit(""), None);
173 assert_eq!(split_amount_unit(" "), None);
174 assert_eq!(split_amount_unit("abc"), None);
175 }
176
177 #[test]
178 fn unit_table_is_canonical() {
179 assert_eq!(unit_to_millis("ms"), Some(1));
180 assert_eq!(unit_to_millis("s"), Some(1_000));
181 assert_eq!(unit_to_millis("m"), Some(60_000));
182 assert_eq!(unit_to_millis("h"), Some(3_600_000));
183 assert_eq!(unit_to_millis("d"), Some(86_400_000));
184 assert_eq!(unit_to_millis("w"), Some(604_800_000));
185 assert_eq!(unit_to_millis(""), None);
186 assert_eq!(unit_to_millis("y"), None);
187 }
188}