range_parser/
lib.rs

1//! # Range Parser
2//!
3//! range-parser is a simple Rust crate to parse range from text representation (e.g. `1-3,5-8`, `1,3,4`, `1-5`)
4//! into a Vector containing all the items for that range.
5//!
6//! ## Get started
7//!
8//! First add range-parser to your `Cargo.toml`:
9//!
10//! ```toml
11//! [dependencies]
12//! range-parser = "0.1.2"
13//! ```
14//!
15//! Then parse a range from a string:
16//!
17//! ```rust
18//! let range_str = "1-3,5-8";
19//! let range: Vec<u64> = range_parser::parse(range_str).unwrap();
20//! assert_eq!(&range, &[1, 2, 3, 5, 6, 7, 8]);
21//! ```
22//!
23//! ## Examples
24//!
25//! ### Parse a range with a dash
26//!
27//! ```rust
28//! let range: Vec<u64> = range_parser::parse("1-3").unwrap();
29//! assert_eq!(range, vec![1, 2, 3]);
30//! ```
31//!
32//! ### Parse a range with commas
33//!
34//! ```rust
35//! let range: Vec<u64> = range_parser::parse("1,3,4").unwrap();
36//! assert_eq!(range, vec![1, 3, 4]);
37//! ```
38//!
39//! ### Parse a mixed range
40//!
41//! ```rust
42//! let range: Vec<u64> = range_parser::parse("1,3-5,2").unwrap();
43//! assert_eq!(range, vec![1, 3, 4, 5, 2]);
44//! ```
45//!
46//! ### Parse a range with negative numbers
47//!
48//! ```rust
49//! let range: Vec<i32> = range_parser::parse("-8,-5--1,0-3,-1").unwrap();
50//! assert_eq!(range, vec![-8, -5, -4, -3, -2, -1, 0, 1, 2, 3, -1]);
51//! ```
52//!
53//! ### Parse a range with custom separators
54//!
55//! ```rust
56//! let range: Vec<i32> = range_parser::parse_with("-2;0..3;-1;7", ";", "..").unwrap();
57//! assert_eq!(range, vec![-2, 0, 1, 2, 3, -1, 7]);
58//! ```
59//!
60
61mod 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/// Parse error
74#[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
88/// Parse result
89pub type RangeResult<T> = Result<T, RangeError>;
90
91/// Parse a range string to a vector of any kind of number
92///
93/// The type T must implement the `FromStr`, `Add`, `PartialEq`, `PartialOrd`, `Unit` and `Copy` traits.
94///
95/// # Arguments
96/// - range_str: &str - the range string to parse
97///
98/// # Returns
99/// - Result<Vec<T>, RangeError> - the parsed range
100///
101/// # Example
102///
103/// ```rust
104/// let range: Vec<u64> = range_parser::parse::<u64>("0-3").unwrap();
105/// assert_eq!(range, vec![0, 1, 2, 3]);
106///
107/// let range: Vec<u64> = range_parser::parse::<u64>("0,1,2,3").unwrap();
108/// assert_eq!(range, vec![0, 1, 2, 3]);
109///
110/// let range: Vec<i32> = range_parser::parse::<i32>("0,3,5-8,-1").unwrap();
111/// assert_eq!(range, vec![0, 3, 5, 6, 7, 8, -1]);
112/// ```
113pub 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
120/// Parse a range string to a vector of any kind of numbers with custom separators
121///
122/// The type T must implement the `FromStr`, `Add`, `PartialEq`, `PartialOrd`, `Unit` and `Copy` traits.
123///
124/// # Arguments
125/// - range_str: &str - the range string to parse
126/// - value_separator: &str - the separator for single values
127/// - range_separator: &str - the separator for ranges
128///
129///
130/// # Returns
131/// - Result<Vec<T>, RangeError> - the parsed range
132///
133/// # Ambiguous separators
134///
135/// The range separator cannot be the same as the value separator, and it cannot be one of the following: `--`,
136/// because it's ambiguous since it couldn't resolve negative numbers.
137///
138/// # Example
139///
140/// ```rust
141/// let range: Vec<i32> = range_parser::parse_with::<i32>("0;3;5..8;-1", ";", "..").unwrap();
142/// assert_eq!(range, vec![0, 3, 5, 6, 7, 8, -1]);
143/// ```
144pub 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
168/// Parse a range part to a vector of T
169fn 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
181/// Parse value range to a vector of T
182///
183/// If the range is `1-3`, it will add 1, 2, 3 to the accumulator.
184/// If the range starts with `-`, but has not a number before it, it will consider it as a negative number.
185fn 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    // here it gets a bit tricky
192    // because for example we could have `-1-3` which is a valid range
193    // or `-5--3` which is also a valid range. So we need to find a way to tell what is dividing the range exactly
194    // so let's calculate the first part index
195    let (start, end): (T, T) = match parts.len() {
196        2 if parts[0].is_empty() && range_separator == "-" => {
197            // if the first part is empty, it means it's a negative number
198            let end = format!("-{}", parts[1]);
199            let end: T = parse_as_t(&end)?;
200            acc.push(end);
201            return Ok(());
202        }
203        // 2 positive numbers (or also negative if range_separator is not `-`)
204        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 is tricky, because it could be both `-1-2` or `1--3`, but the second case is invalid actually,
212        // because start cannot be greater than end
213        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 is bigger than end, it's an invalid range
232    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
245/// Parse a string to a T
246fn 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}