1mod unit;
62
63use std::cmp::{PartialEq, PartialOrd};
64use std::ops::Add;
65use std::str::FromStr;
66
67use thiserror::Error;
68
69pub use self::unit::Unit;
70
71const AMBIGOUS_RANGE_SEPARATORS: &[&str] = &["--"];
72
73#[derive(Debug, Error, Clone, PartialEq, Eq)]
75pub enum RangeError {
76 #[error("Invalid range syntax: {0}")]
77 InvalidRangeSyntax(String),
78 #[error("Not a number: {0}")]
79 NotANumber(String),
80 #[error("Value and range separators cannot be the same")]
81 SeparatorsMustBeDifferent,
82 #[error("Start of the range cannot be bigger than the end: {0}")]
83 StartBiggerThanEnd(String),
84 #[error("Ambiguous separator: {0}")]
85 AmbiguousSeparator(String),
86}
87
88pub type RangeResult<T> = Result<T, RangeError>;
90
91pub fn parse<T>(range_str: &str) -> RangeResult<Vec<T>>
114where
115 T: FromStr + Add<Output = T> + PartialEq + PartialOrd + Unit + Copy,
116{
117 parse_with(range_str, ",", "-")
118}
119
120pub fn parse_with<T>(
145 range_str: &str,
146 value_separator: &str,
147 range_separator: &str,
148) -> RangeResult<Vec<T>>
149where
150 T: FromStr + Add<Output = T> + PartialEq + PartialOrd + Unit + Copy,
151{
152 if value_separator == range_separator {
153 return Err(RangeError::SeparatorsMustBeDifferent);
154 }
155 if AMBIGOUS_RANGE_SEPARATORS.contains(&range_separator) {
156 return Err(RangeError::AmbiguousSeparator(range_separator.to_string()));
157 }
158
159 let mut range = Vec::new();
160
161 for part in range_str.split(value_separator) {
162 parse_part(&mut range, part, range_separator)?;
163 }
164
165 Ok(range)
166}
167
168fn parse_part<T>(acc: &mut Vec<T>, part: &str, range_separator: &str) -> RangeResult<()>
170where
171 T: FromStr + Add<Output = T> + PartialEq + PartialOrd + Unit + Copy,
172{
173 if part.contains(range_separator) {
174 parse_value_range(acc, part, range_separator)?;
175 } else {
176 acc.push(parse_as_t(part)?);
177 }
178 Ok(())
179}
180
181fn parse_value_range<T>(acc: &mut Vec<T>, part: &str, range_separator: &str) -> RangeResult<()>
186where
187 T: FromStr + Add<Output = T> + PartialEq + PartialOrd + Unit + Copy,
188{
189 let parts: Vec<&str> = part.split(range_separator).collect();
190
191 let (start, end): (T, T) = match parts.len() {
196 2 if parts[0].is_empty() && range_separator == "-" => {
197 let end = format!("-{}", parts[1]);
199 let end: T = parse_as_t(&end)?;
200 acc.push(end);
201 return Ok(());
202 }
203 2 => {
205 let start = parts[0];
206 let end = parts[1];
207 let start: T = parse_as_t(start)?;
208 let end: T = parse_as_t(end)?;
209 (start, end)
210 }
211 3 if parts[0].is_empty() && range_separator == "-" => {
214 let start = format!("-{}", parts[1]);
215 let end = parts[2];
216 let start: T = parse_as_t(&start)?;
217 let end: T = parse_as_t(end)?;
218 (start, end)
219 }
220 3 => return Err(RangeError::StartBiggerThanEnd(part.to_string())),
221 4 if range_separator == "-" => {
222 let start = format!("-{}", parts[1]);
223 let end = format!("-{}", parts[3]);
224 let start: T = parse_as_t(&start)?;
225 let end: T = parse_as_t(&end)?;
226 (start, end)
227 }
228 _ => return Err(RangeError::InvalidRangeSyntax(part.to_string())),
229 };
230
231 if start > end {
233 return Err(RangeError::StartBiggerThanEnd(part.to_string()));
234 }
235
236 let mut x = start;
237 while x <= end {
238 acc.push(x);
239 x = x + T::unit();
240 }
241
242 Ok(())
243}
244
245fn parse_as_t<T>(part: &str) -> RangeResult<T>
247where
248 T: FromStr + Add<Output = T> + PartialEq + PartialOrd + Unit + Copy,
249{
250 part.trim()
251 .parse()
252 .map_err(|_| RangeError::NotANumber(part.to_string()))
253}
254
255#[cfg(test)]
256mod tests {
257 use pretty_assertions::assert_eq;
258
259 use super::*;
260
261 #[test]
262 fn should_parse_dashed_range_with_positive_numbers() {
263 let range: Vec<u64> = parse("1-3").unwrap();
264 assert_eq!(range, vec![1, 2, 3]);
265 }
266
267 #[test]
268 fn should_parse_dashed_range_with_mixed_numbers() {
269 let range: Vec<i32> = parse("-2-3").unwrap();
270 assert_eq!(range, vec![-2, -1, 0, 1, 2, 3]);
271 }
272
273 #[test]
274 fn should_parse_dashed_range_with_negative_numbers() {
275 let range: Vec<i32> = parse("-3--1").unwrap();
276 assert_eq!(range, vec![-3, -2, -1]);
277 }
278
279 #[test]
280 fn should_parse_range_with_floats() {
281 let range: Vec<f64> = parse("-1.0-3.0").unwrap();
282 assert_eq!(range, vec![-1.0, 0.0, 1.0, 2.0, 3.0]);
283 }
284
285 #[test]
286 fn should_parse_range_with_commas_with_positive_numbers() {
287 let range: Vec<u64> = parse("1,3,4").unwrap();
288 assert_eq!(range, vec![1, 3, 4]);
289 }
290
291 #[test]
292 fn should_parse_range_with_commas_with_mixed_numbers() {
293 let range: Vec<i32> = parse("-2,0,3,-1").unwrap();
294 assert_eq!(range, vec![-2, 0, 3, -1]);
295 }
296
297 #[test]
298 fn should_parse_mixed_range_with_positive_numbers() {
299 let range: Vec<u64> = parse("1,3-5,2").unwrap();
300 assert_eq!(range, vec![1, 3, 4, 5, 2]);
301 }
302
303 #[test]
304 fn should_parse_mixed_range_with_mixed_numbers() {
305 let range: Vec<i32> = parse("-2,0-3,-1,7").unwrap();
306 assert_eq!(range, vec![-2, 0, 1, 2, 3, -1, 7]);
307 }
308
309 #[test]
310 fn test_should_parse_with_whitespaces() {
311 let range: Vec<u64> = parse(" 1 , 3 - 5 , 2 ").unwrap();
312 assert_eq!(range, vec![1, 3, 4, 5, 2]);
313 }
314
315 #[test]
316 fn should_parse_mixed_range_with_mixed_numbers_with_custom_separators() {
317 let range: Vec<i32> = parse_with("-2;0..3;-1;7", ";", "..").unwrap();
318 assert_eq!(range, vec![-2, 0, 1, 2, 3, -1, 7]);
319 }
320
321 #[test]
322 fn test_should_not_allow_invalid_range() {
323 let range = parse::<i32>("1-3-5");
324 assert!(range.is_err());
325 }
326
327 #[test]
328 fn test_should_not_allow_invalid_range_with_custom_separators() {
329 let range = parse_with::<i32>("1-3-5", "-", "-");
330 assert!(range.is_err());
331 }
332
333 #[test]
334 fn test_should_not_allow_start_bigger_than_end() {
335 let range = parse::<i32>("3-1");
336 assert!(range.is_err());
337 }
338
339 #[test]
340 fn test_should_fail_with_custom_separator_in_place_of_minus() {
341 assert!(parse_with::<i32>("~1~3", "=", "~").is_err());
342 }
343
344 #[test]
345 fn test_should_not_allow_ambiguous_separator() {
346 assert!(parse_with::<i32>("1--3", "-", "--").is_err());
347 }
348}