Skip to main content

muse2/input/
range.rs

1//! Provides a helper for parsing range-type parameters from input files.
2use anyhow::{Context, Result, ensure};
3use std::error::Error;
4use std::fmt::Display;
5use std::ops::RangeInclusive;
6use std::str::FromStr;
7
8/// Parse a range from an input string, using values in `limits` as defaults.
9///
10/// Start and end values must be a type that is parseable from a string. Ranges are inclusive.
11/// Whitespace is trimmed from start and end values before parsing.
12///
13/// Valid ranges:
14///
15/// - Range of values (e.g. 1990..2000)
16/// - Range with no upper limit (e.g. 1990..)
17/// - Range with no lower limit (e.g. ..2000)
18pub fn parse_range<T>(s: &str, limits: RangeInclusive<T>) -> Result<RangeInclusive<T>>
19where
20    T: FromStr + Copy + PartialOrd + Display,
21    <T as FromStr>::Err: Error + Sync + Send + 'static,
22{
23    let (start, end) = s.split_once("..").context(
24        "Range must be in the form [start]..[end] (where [start] and [end] can be empty)",
25    )?;
26    parse_range_parts(start, end, limits.clone(), *limits.start(), *limits.end())
27}
28
29/// Parse parts of a range from input strings.
30///
31/// Start and end values must be a type that is parseable from a string. Ranges are inclusive.
32/// Whitespace is trimmed from start and end values before parsing.
33///
34/// If start or end values are empty, the values in `defaults` will be used.
35///
36/// # Panics
37///
38/// Panics if `limits` has a start after its end or `default_lower` is greater than
39/// `default_upper`.
40pub fn parse_range_parts<T>(
41    start: &str,
42    end: &str,
43    limits: RangeInclusive<T>,
44    default_lower: T,
45    default_upper: T,
46) -> Result<RangeInclusive<T>>
47where
48    T: FromStr + Copy + PartialOrd + Display,
49    <T as FromStr>::Err: Error + Sync + Send + 'static,
50{
51    assert!(
52        limits.start() <= limits.end(),
53        "Start of limits must be before end"
54    );
55    assert!(
56        default_lower <= default_upper,
57        "default_lower must be less than default_upper"
58    );
59
60    let start = start.trim();
61    let end = end.trim();
62    ensure!(
63        !start.is_empty() || !end.is_empty(),
64        "Start and end of range cannot both be omitted"
65    );
66
67    let value1 = if start.is_empty() {
68        default_lower
69    } else {
70        start.parse()?
71    };
72    let value2 = if end.is_empty() {
73        default_upper
74    } else {
75        end.parse()?
76    };
77
78    ensure!(
79        value1 <= value2,
80        "Start value must be less than or equal to end value"
81    );
82    ensure!(
83        value1 >= *limits.start(),
84        "Start value must be >= {}",
85        limits.start()
86    );
87    ensure!(
88        value2 <= *limits.end(),
89        "End value must be <= {}",
90        limits.end()
91    );
92
93    Ok(value1..=value2)
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99    use rstest::rstest;
100
101    #[rstest]
102    #[case("1..2", 1..=2)]
103    #[case("1..1", 1..=1)]
104    #[case("..2", 0..=2)]
105    #[case("1..", 1..=100)]
106    fn parse_range_ok(#[case] input: &str, #[case] expected: RangeInclusive<i32>) {
107        assert_eq!(parse_range(input, 0..=100).unwrap(), expected);
108    }
109
110    #[rstest]
111    #[case("..")] // can't omit start and end
112    #[case("-1..10")] // start out of range
113    #[case("0..101")] // end out of range
114    #[case("2..1")] // start greater than end
115    fn parse_range_error(#[case] input: &str) {
116        parse_range(input, 0..=100).unwrap_err();
117    }
118}