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/// Try to divide a string into two parts at the specified delimiter.
9///
10/// # Returns
11///
12/// - `None` if `delimiter` is not present
13/// - `Some` tuple of the two strings if it is
14pub fn partition<'a>(s: &'a str, delimiter: &str) -> Option<(&'a str, &'a str)> {
15    let idx = s.find(delimiter)?;
16
17    #[allow(clippy::string_slice)]
18    Some((&s[..idx], &s[idx + delimiter.len()..]))
19}
20
21/// Parse a range from an input string, using values in `limits` as defaults.
22///
23/// Start and end values must be a type that is parseable from a string. Ranges are inclusive.
24/// Whitespace is trimmed from start and end values before parsing.
25///
26/// Valid ranges:
27///
28/// - Range of values (e.g. 1990..2000)
29/// - Range with no upper limit (e.g. 1990..)
30/// - Range with no lower limit (e.g. ..2000)
31pub fn parse_range<T>(s: &str, limits: RangeInclusive<T>) -> Result<RangeInclusive<T>>
32where
33    T: FromStr + Copy + PartialOrd + Display,
34    <T as FromStr>::Err: Error + Sync + Send + 'static,
35{
36    let (start, end) = partition(s, "..").context(
37        "Range must be in the form [start]..[end] (where [start] and [end] can be empty)",
38    )?;
39    parse_range_parts(start, end, limits.clone(), *limits.start(), *limits.end())
40}
41
42/// Parse parts of a range from input strings.
43///
44/// Start and end values must be a type that is parseable from a string. Ranges are inclusive.
45/// Whitespace is trimmed from start and end values before parsing.
46///
47/// If start or end values are empty, the values in `defaults` will be used.
48///
49/// # Panics
50///
51/// Panics if `limits` has a start after its end or `default_lower` is greater than
52/// `default_upper`.
53pub fn parse_range_parts<T>(
54    start: &str,
55    end: &str,
56    limits: RangeInclusive<T>,
57    default_lower: T,
58    default_upper: T,
59) -> Result<RangeInclusive<T>>
60where
61    T: FromStr + Copy + PartialOrd + Display,
62    <T as FromStr>::Err: Error + Sync + Send + 'static,
63{
64    assert!(
65        limits.start() <= limits.end(),
66        "Start of limits must be before end"
67    );
68    assert!(
69        default_lower <= default_upper,
70        "default_lower must be less than default_upper"
71    );
72
73    let start = start.trim();
74    let end = end.trim();
75    ensure!(
76        !start.is_empty() || !end.is_empty(),
77        "Start and end of range cannot both be omitted"
78    );
79
80    let value1 = if start.is_empty() {
81        default_lower
82    } else {
83        start.parse()?
84    };
85    let value2 = if end.is_empty() {
86        default_upper
87    } else {
88        end.parse()?
89    };
90
91    ensure!(
92        value1 <= value2,
93        "Start value must be less than or equal to end value"
94    );
95    ensure!(
96        value1 >= *limits.start(),
97        "Start value must be >= {}",
98        limits.start()
99    );
100    ensure!(
101        value2 <= *limits.end(),
102        "End value must be <= {}",
103        limits.end()
104    );
105
106    Ok(value1..=value2)
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112    use rstest::rstest;
113
114    #[rstest]
115    #[case("1,2", ",", Some(("1","2")))]
116    #[case("hello world", " ", Some(("hello", "world")))]
117    #[case("a..b", "..", Some(("a","b")))]
118    #[case("a", "", Some(("", "a")))]
119    #[case("", "", Some(("", "")))]
120    #[case("a..b", "c", None)]
121    #[case("🙂😐😞", "😐", Some(("🙂", "😞")))]
122    fn partition_works(
123        #[case] input: &str,
124        #[case] delim: &str,
125        #[case] expected: Option<(&str, &str)>,
126    ) {
127        assert_eq!(partition(input, delim), expected);
128    }
129
130    #[rstest]
131    #[case("1..2", 1..=2)]
132    #[case("1..1", 1..=1)]
133    #[case("..2", 0..=2)]
134    #[case("1..", 1..=100)]
135    fn parse_range_ok(#[case] input: &str, #[case] expected: RangeInclusive<i32>) {
136        assert_eq!(parse_range(input, 0..=100).unwrap(), expected);
137    }
138
139    #[rstest]
140    #[case("..")] // can't omit start and end
141    #[case("-1..10")] // start out of range
142    #[case("0..101")] // end out of range
143    #[case("2..1")] // start greater than end
144    fn parse_range_error(#[case] input: &str) {
145        parse_range(input, 0..=100).unwrap_err();
146    }
147}