prange2 3.0.1

Parse numeric ranges for indexing
Documentation
//! Parse numeric ranges for indexing.
//!
//! Inclusive-inclusive 1-based integer ranges. Parsed from strings.
//!
//! # Examples
//!
//! ```ignore
//! "2"         => [2]
//! "1-5"       => [1, 2, 3, 4, 5]
//! "1-3,5-6"   => [1, 2, 3, 5, 6]
//! "-3"        => [1, 2, 3]
//! "1-"        => [1, 2, 3, ..]
//! "1-3,2-4,7" => [1, 2, 3, 2, 3, 4, 7]
//! ```

#[cfg(test)]
mod test;

use ::num_traits::{Bounded, Num, NumAssign};
use ::thiserror::Error;

pub fn parse<T>(value: &str) -> Result<RangeIterator<T>, ParseError<<T as Num>::FromStrRadixErr>>
where
    T: NumAssign + Clone + Bounded + PartialOrd,
{
    if value.is_empty() {
        return Ok(RangeIterator { ranges: Vec::new() });
    }

    let mut ranges = Vec::new();
    let mut token1 = String::new();
    let mut token2 = String::new();
    let mut first_token = true;
    for (n, c) in format!("{value},").chars().enumerate() {
        match c {
            ',' => {
                match (token1.is_empty(), token2.is_empty(), first_token) {
                    (true, true, true) => return Err(ParseError::EmptyRange(n)),
                    (true, true, false) => return Err(ParseError::InvalidRange(n)),
                    /* x-y */
                    (false, false, _) => {
                        let num1 = parse_num(n, &token1)?;
                        let num2 = parse_num(n, &token2)?;
                        ranges.push([num1, num2]);
                    }
                    /* x */
                    (false, true, true) => {
                        let num: T = parse_num(n, &token1)?;
                        ranges.push([num.clone(), num]);
                    }
                    /* x- */
                    (false, true, false) => {
                        let num = parse_num(n, &token1)?;
                        ranges.push([num, T::max_value()]);
                    }
                    /* -x */
                    (true, false, _) => {
                        let num = parse_num(n, &token2)?;
                        ranges.push([T::one(), num]);
                    }
                }
                token1.clear();
                token2.clear();
                first_token = true;
            }
            '-' => {
                if first_token {
                    first_token = false
                } else {
                    return Err(ParseError::InvalidRange(n));
                }
            }
            '0'..='9' => {
                if first_token {
                    token1.push(c)
                } else {
                    token2.push(c)
                }
            }
            c => return Err(ParseError::IllegalChar(n, c)),
        }
    }

    ranges.reverse();
    Ok(RangeIterator { ranges })
}

fn parse_num<T>(n: usize, num: &str) -> Result<T, ParseError<<T as Num>::FromStrRadixErr>>
where
    T: Num,
{
    match T::from_str_radix(num, 10) {
        Ok(number) => Ok(number),
        Err(error) => Err(ParseError::ParseNumber(n, num.to_string(), error)),
    }
}

pub struct RangeIterator<T> {
    ranges: Vec<[T; 2]>,
}

impl<T> Iterator for RangeIterator<T>
where
    T: NumAssign + Clone + PartialOrd,
{
    type Item = T;

    fn next(&mut self) -> Option<T> {
        match self.ranges.last_mut() {
            Some([ref mut start, end]) => {
                if start < end {
                    let result = start.clone();
                    *start += T::one();
                    Some(result)
                } else if start == end {
                    let [start, _] = self.ranges.pop().unwrap();
                    Some(start)
                } else {
                    self.ranges.pop();
                    self.next()
                }
            }
            None => None,
        }
    }
}

#[derive(Error, Debug)]
#[non_exhaustive]
pub enum ParseError<T> {
    #[error("Input contains an invalid character `{1}` at position {0}")]
    IllegalChar(usize, char),
    #[error("Input contains an empty range at position {0}")]
    EmptyRange(usize),
    #[error("Input contains an invalid range at position {0}")]
    InvalidRange(usize),
    #[error("Input contains an invalid number `{1}` at position {0}")]
    ParseNumber(usize, String, #[source] T),
}