Skip to main content

krabka_units/
parse.rs

1//! Parsing the human forms of a quantity: `512MiB`, `30s`, `10MiB/s`, `25%`.
2//!
3//! Operators write sizes and durations with units, and a config file that spells
4//! them out cannot be misread. Each parser takes a magnitude followed by a unit,
5//! with optional spaces between them. Each parser is case-insensitive, so
6//! `512MiB`, `512 mib`, and `512MIB` are the same value.
7//!
8//! A unit is required. `30` is not a duration, because whether it means 30
9//! seconds or 30 milliseconds is the question the type must settle. The one
10//! exception is `0`, which is unambiguous in any unit.
11//!
12//! Binary and decimal size prefixes are distinct, as in the JEDEC/IEC split:
13//! `KiB`/`MiB`/`GiB`/`TiB` step by 1024, `kB`/`MB`/`GB`/`TB` by 1000.
14//!
15//! The parsers do not accept exponent notation (`1e6`). A unit gives the scale.
16
17use crate::{
18    ByteRate, ByteSize, Frequency, Ratio, Time,
19    convert::{ByteRateExt, ByteSizeExt, FrequencyExt, RatioExt, TimeExt},
20    fraction,
21};
22
23/// Why a quantity string could not be read.
24#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
25pub enum ParseError {
26    /// The input was empty or all whitespace.
27    #[error("empty quantity")]
28    Empty,
29    /// The magnitude was there but the unit was missing.
30    #[error(
31        "missing unit in `{input}`: write the unit explicitly, as in `512MiB`, `30s`, `10MiB/s`"
32    )]
33    MissingUnit {
34        /// The offending input.
35        input: String,
36    },
37    /// The magnitude was not a decimal number.
38    #[error("invalid number `{number}` in `{input}`")]
39    InvalidNumber {
40        /// The offending input.
41        input: String,
42        /// The part that should have been a number.
43        number: String,
44    },
45    /// The unit is not one this dimension knows.
46    #[error("unknown {dimension} unit `{unit}` in `{input}`")]
47    UnknownUnit {
48        /// Which dimension was being parsed.
49        dimension: &'static str,
50        /// The unrecognised unit.
51        unit: String,
52        /// The offending input.
53        input: String,
54    },
55    /// The magnitude parsed but is infinite or `NaN`.
56    #[error("`{input}` is not a finite quantity")]
57    NotFinite {
58        /// The offending input.
59        input: String,
60    },
61    /// The quantity is outside the accepted sign range.
62    #[error("`{input}` must be {requirement}")]
63    OutOfRange {
64        /// The offending input.
65        input: String,
66        /// The accepted sign range.
67        requirement: &'static str,
68    },
69}
70
71/// The magnitude and the lowercased unit of `input`.
72///
73/// A unitless `0` gives an empty unit, which every dimension reads as zero.
74fn 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
117/// The byte scale named by a lowercased size unit.
118fn 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
133/// The seconds-per-unit named by a lowercased time unit, as a numerator over a
134/// denominator.
135///
136/// This function gives sub-second units as a division and not as a fractional
137/// multiplier, because `1e-3` is not exactly a thousandth in binary while
138/// `x / 1e3` is correctly rounded. The difference shows when the code renders the
139/// result back. `250 * 1e-9` is not the nearest `f64` to 250 nanoseconds, and
140/// prints as `0.00000025000000000000004s`.
141fn 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
154/// A byte count: `1024`&nbsp;→ error, `1024B`, `512MiB`, `1.5 GiB`, `2MB`.
155///
156/// # Errors
157///
158/// [`ParseError`] if the magnitude is not a finite decimal number, the unit is
159/// missing on a non-zero magnitude, or the unit is not a size.
160pub 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
170/// A time extent: `30s`, `500ms`, `7d`, `1.5h`, `250 us`.
171///
172/// # Errors
173///
174/// [`ParseError`] if the magnitude is not a finite decimal number, the unit is
175/// missing on a non-zero magnitude, or the unit is not a duration.
176pub 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
189/// A byte throughput: `10MiB/s`, `1048576B/s`, `64KiBps`, `5 MB / sec`.
190///
191/// The rate is a size unit over a time unit. You can also write `/s` as the
192/// suffix `ps`. The time unit can be any duration, as in `1GiB/h`.
193///
194/// # Errors
195///
196/// [`ParseError`] if the magnitude is not a finite decimal number, the unit is
197/// missing on a non-zero magnitude, the first half of the compound unit is not a
198/// size, or the second half is not a duration.
199pub 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
225/// An event rate: `100/s`, `2.5Hz`, `60/min`.
226///
227/// # Errors
228///
229/// [`ParseError`] if the magnitude is not a finite decimal number, the unit is
230/// missing on a non-zero magnitude, or the unit is neither hertz nor a
231/// per-duration.
232pub 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
253/// A dimensionless fraction: `25%`, `0.25`, `1`.
254///
255/// This parser accepts a bare number, unlike the other dimensions, because a
256/// fraction's unit *is* "none". A `%` suffix scales by a hundredth.
257///
258/// # Errors
259///
260/// [`ParseError`] if the magnitude is not a finite decimal number, or the unit is
261/// anything other than `%`.
262pub 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
307/// A strictly positive time extent.
308///
309/// # Errors
310///
311/// [`ParseError`] if the value is not a finite, positive time.
312pub fn positive_time(input: &str) -> Result<Time, ParseError> {
313    let value = time(input)?;
314    require_sign(input, value, value.secs_f64(), false)
315}
316
317/// A non-negative time extent.
318///
319/// # Errors
320///
321/// [`ParseError`] if the value is not a finite, non-negative time.
322pub 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
327/// A strictly positive byte count.
328///
329/// # Errors
330///
331/// [`ParseError`] if the value is not a finite, positive byte count.
332pub 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
337/// A non-negative byte count.
338///
339/// # Errors
340///
341/// [`ParseError`] if the value is not a finite, non-negative byte count.
342pub 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
347/// A strictly positive byte throughput.
348///
349/// # Errors
350///
351/// [`ParseError`] if the value is not a finite, positive byte throughput.
352pub 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
357/// A non-negative byte throughput.
358///
359/// # Errors
360///
361/// [`ParseError`] if the value is not a finite, non-negative byte throughput.
362pub 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
367/// A strictly positive dimensionless ratio.
368///
369/// # Errors
370///
371/// [`ParseError`] if the value is not a finite, positive ratio.
372pub fn positive_ratio(input: &str) -> Result<Ratio, ParseError> {
373    let value = ratio(input)?;
374    require_sign(input, value, value.as_f64(), false)
375}
376
377/// A non-negative dimensionless ratio.
378///
379/// # Errors
380///
381/// [`ParseError`] if the value is not a finite, non-negative ratio.
382pub 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
387/// A dimensionless ratio in the inclusive range from zero through one.
388///
389/// # Errors
390///
391/// [`ParseError`] if the value is not finite or lies outside `0..=1`.
392pub 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}