1use crate::{
18 ByteRate, ByteSize, Frequency, Ratio, Time,
19 convert::{ByteRateExt, ByteSizeExt, FrequencyExt, RatioExt, TimeExt},
20 fraction,
21};
22
23#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
25pub enum ParseError {
26 #[error("empty quantity")]
28 Empty,
29 #[error(
31 "missing unit in `{input}`: write the unit explicitly, as in `512MiB`, `30s`, `10MiB/s`"
32 )]
33 MissingUnit {
34 input: String,
36 },
37 #[error("invalid number `{number}` in `{input}`")]
39 InvalidNumber {
40 input: String,
42 number: String,
44 },
45 #[error("unknown {dimension} unit `{unit}` in `{input}`")]
47 UnknownUnit {
48 dimension: &'static str,
50 unit: String,
52 input: String,
54 },
55 #[error("`{input}` is not a finite quantity")]
57 NotFinite {
58 input: String,
60 },
61 #[error("`{input}` must be {requirement}")]
63 OutOfRange {
64 input: String,
66 requirement: &'static str,
68 },
69}
70
71fn split(input: &str) -> Result<(f64, String), ParseError> {
75 let trimmed = input.trim();
76 if trimmed.is_empty() {
77 return Err(ParseError::Empty);
78 }
79
80 let split_at = trimmed
81 .char_indices()
82 .position(|(index, ch)| {
83 !(ch.is_ascii_digit() || ch == '.' || (index == 0 && (ch == '-' || ch == '+')))
84 })
85 .unwrap_or(trimmed.len());
86 let (number, unit) = trimmed.split_at(split_at);
87 let number = number.trim();
88 let unit = unit.trim();
89
90 let magnitude: f64 = number.parse().map_err(|_| ParseError::InvalidNumber {
91 input: input.to_owned(),
92 number: number.to_owned(),
93 })?;
94 if !magnitude.is_finite() {
95 return Err(ParseError::NotFinite {
96 input: input.to_owned(),
97 });
98 }
99 if unit.is_empty() && magnitude != 0.0 {
100 return Err(ParseError::MissingUnit {
101 input: input.to_owned(),
102 });
103 }
104
105 Ok((magnitude, unit.to_lowercase()))
106}
107
108fn finite(input: &str, value: f64) -> Result<f64, ParseError> {
109 value
110 .is_finite()
111 .then_some(value)
112 .ok_or_else(|| ParseError::NotFinite {
113 input: input.to_owned(),
114 })
115}
116
117fn size_scale(unit: &str) -> Option<f64> {
119 match unit {
120 "" | "b" | "byte" | "bytes" => Some(1.0),
121 "kib" | "kibibyte" | "kibibytes" => Some(1024.0),
122 "mib" | "mebibyte" | "mebibytes" => Some(1_048_576.0),
123 "gib" | "gibibyte" | "gibibytes" => Some(1_073_741_824.0),
124 "tib" | "tebibyte" | "tebibytes" => Some(1_099_511_627_776.0),
125 "kb" | "kilobyte" | "kilobytes" => Some(1_000.0),
126 "mb" | "megabyte" | "megabytes" => Some(1_000_000.0),
127 "gb" | "gigabyte" | "gigabytes" => Some(1_000_000_000.0),
128 "tb" | "terabyte" | "terabytes" => Some(1_000_000_000_000.0),
129 _ => None,
130 }
131}
132
133fn time_scale(unit: &str) -> Option<(f64, f64)> {
142 match unit {
143 "ns" | "nanosecond" | "nanoseconds" => Some((1.0, 1e9)),
144 "us" | "µs" | "μs" | "microsecond" | "microseconds" => Some((1.0, 1e6)),
145 "ms" | "millisecond" | "milliseconds" => Some((1.0, 1e3)),
146 "" | "s" | "sec" | "secs" | "second" | "seconds" => Some((1.0, 1.0)),
147 "m" | "min" | "mins" | "minute" | "minutes" => Some((60.0, 1.0)),
148 "h" | "hr" | "hrs" | "hour" | "hours" => Some((3600.0, 1.0)),
149 "d" | "day" | "days" => Some((86_400.0, 1.0)),
150 _ => None,
151 }
152}
153
154pub fn byte_size(input: &str) -> Result<ByteSize, ParseError> {
161 let (magnitude, unit) = split(input)?;
162 let scale = size_scale(&unit).ok_or_else(|| ParseError::UnknownUnit {
163 dimension: "size",
164 unit,
165 input: input.to_owned(),
166 })?;
167 Ok(ByteSize::from_bytes_f64(finite(input, magnitude * scale)?))
168}
169
170pub fn time(input: &str) -> Result<Time, ParseError> {
177 let (magnitude, unit) = split(input)?;
178 let (numerator, denominator) = time_scale(&unit).ok_or_else(|| ParseError::UnknownUnit {
179 dimension: "time",
180 unit,
181 input: input.to_owned(),
182 })?;
183 Ok(Time::from_secs_f64(finite(
184 input,
185 magnitude * numerator / denominator,
186 )?))
187}
188
189pub fn byte_rate(input: &str) -> Result<ByteRate, ParseError> {
200 let (magnitude, unit) = split(input)?;
201 let unknown = || ParseError::UnknownUnit {
202 dimension: "rate",
203 unit: unit.clone(),
204 input: input.to_owned(),
205 };
206
207 let (size_unit, time_unit) = if let Some((size, time)) = unit.split_once('/') {
208 (size.trim(), time.trim())
209 } else if let Some(size) = unit.strip_suffix("ps") {
210 (size.trim(), "s")
211 } else if unit.is_empty() {
212 ("", "s")
213 } else {
214 return Err(unknown());
215 };
216
217 let size = size_scale(size_unit).ok_or_else(unknown)?;
218 let (numerator, denominator) = time_scale(time_unit).ok_or_else(unknown)?;
219 Ok(ByteRate::from_bytes_per_sec_f64(finite(
220 input,
221 magnitude * size * denominator / numerator,
222 )?))
223}
224
225pub fn frequency(input: &str) -> Result<Frequency, ParseError> {
233 let (magnitude, unit) = split(input)?;
234 let unknown = || ParseError::UnknownUnit {
235 dimension: "frequency",
236 unit: unit.clone(),
237 input: input.to_owned(),
238 };
239
240 let (numerator, denominator) = match unit.as_str() {
241 "" | "hz" | "hertz" => (1.0, 1.0),
242 _ => {
243 let time_unit = unit.strip_prefix('/').ok_or_else(unknown)?.trim();
244 time_scale(time_unit).ok_or_else(unknown)?
245 }
246 };
247 Ok(Frequency::from_per_sec(finite(
248 input,
249 magnitude * denominator / numerator,
250 )?))
251}
252
253pub fn ratio(input: &str) -> Result<Ratio, ParseError> {
263 let trimmed = input.trim();
264 let (body, scale) = trimmed
265 .strip_suffix('%')
266 .map_or((trimmed, 1.0), |body| (body.trim(), 0.01));
267 if body.is_empty() {
268 return Err(ParseError::Empty);
269 }
270 let magnitude: f64 = body.parse().map_err(|_| ParseError::InvalidNumber {
271 input: input.to_owned(),
272 number: body.to_owned(),
273 })?;
274 if !magnitude.is_finite() {
275 return Err(ParseError::NotFinite {
276 input: input.to_owned(),
277 });
278 }
279 Ok(fraction(finite(input, magnitude * scale)?))
280}
281
282fn require_sign<T>(
283 input: &str,
284 value: T,
285 magnitude: f64,
286 allow_zero: bool,
287) -> Result<T, ParseError> {
288 if !magnitude.is_finite() {
289 return Err(ParseError::NotFinite {
290 input: input.to_owned(),
291 });
292 }
293 if magnitude > 0.0 || (allow_zero && magnitude == 0.0) {
294 Ok(value)
295 } else {
296 Err(ParseError::OutOfRange {
297 input: input.to_owned(),
298 requirement: if allow_zero {
299 "non-negative"
300 } else {
301 "greater than zero"
302 },
303 })
304 }
305}
306
307pub fn positive_time(input: &str) -> Result<Time, ParseError> {
313 let value = time(input)?;
314 require_sign(input, value, value.secs_f64(), false)
315}
316
317pub fn non_negative_time(input: &str) -> Result<Time, ParseError> {
323 let value = time(input)?;
324 require_sign(input, value, value.secs_f64(), true)
325}
326
327pub fn positive_byte_size(input: &str) -> Result<ByteSize, ParseError> {
333 let value = byte_size(input)?;
334 require_sign(input, value, value.bytes_f64(), false)
335}
336
337pub fn non_negative_byte_size(input: &str) -> Result<ByteSize, ParseError> {
343 let value = byte_size(input)?;
344 require_sign(input, value, value.bytes_f64(), true)
345}
346
347pub fn positive_byte_rate(input: &str) -> Result<ByteRate, ParseError> {
353 let value = byte_rate(input)?;
354 require_sign(input, value, value.bytes_per_sec_f64(), false)
355}
356
357pub fn non_negative_byte_rate(input: &str) -> Result<ByteRate, ParseError> {
363 let value = byte_rate(input)?;
364 require_sign(input, value, value.bytes_per_sec_f64(), true)
365}
366
367pub fn positive_ratio(input: &str) -> Result<Ratio, ParseError> {
373 let value = ratio(input)?;
374 require_sign(input, value, value.as_f64(), false)
375}
376
377pub fn non_negative_ratio(input: &str) -> Result<Ratio, ParseError> {
383 let value = ratio(input)?;
384 require_sign(input, value, value.as_f64(), true)
385}
386
387pub fn unit_ratio(input: &str) -> Result<Ratio, ParseError> {
393 let value = non_negative_ratio(input)?;
394 if value <= Ratio::ONE {
395 Ok(value)
396 } else {
397 Err(ParseError::OutOfRange {
398 input: input.to_owned(),
399 requirement: "between 0% and 100%",
400 })
401 }
402}