1use anyhow::{Context, Result, ensure};
3use std::error::Error;
4use std::fmt::Display;
5use std::ops::RangeInclusive;
6use std::str::FromStr;
7
8pub 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
21pub 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
42pub 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("..")] #[case("-1..10")] #[case("0..101")] #[case("2..1")] fn parse_range_error(#[case] input: &str) {
145 parse_range(input, 0..=100).unwrap_err();
146 }
147}