mod unit;
use std::cmp::{PartialEq, PartialOrd};
use std::ops::Add;
use std::str::FromStr;
use thiserror::Error;
pub use self::unit::Unit;
const AMBIGOUS_RANGE_SEPARATORS: &[&str] = &["--"];
#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub enum RangeError {
#[error("Invalid range syntax: {0}")]
InvalidRangeSyntax(String),
#[error("Not a number: {0}")]
NotANumber(String),
#[error("Value and range separators cannot be the same")]
SeparatorsMustBeDifferent,
#[error("Start of the range cannot be bigger than the end: {0}")]
StartBiggerThanEnd(String),
#[error("Ambiguous separator: {0}")]
AmbiguousSeparator(String),
}
pub type RangeResult<T> = Result<T, RangeError>;
pub fn parse<T>(range_str: &str) -> RangeResult<Vec<T>>
where
T: FromStr + Add<Output = T> + PartialEq + PartialOrd + Unit + Copy,
{
parse_with(range_str, ",", "-")
}
pub fn parse_with<T>(
range_str: &str,
value_separator: &str,
range_separator: &str,
) -> RangeResult<Vec<T>>
where
T: FromStr + Add<Output = T> + PartialEq + PartialOrd + Unit + Copy,
{
if value_separator == range_separator {
return Err(RangeError::SeparatorsMustBeDifferent);
}
if AMBIGOUS_RANGE_SEPARATORS.contains(&range_separator) {
return Err(RangeError::AmbiguousSeparator(range_separator.to_string()));
}
let mut range = Vec::new();
for part in range_str.split(value_separator) {
parse_part(&mut range, part, range_separator)?;
}
Ok(range)
}
fn parse_part<T>(acc: &mut Vec<T>, part: &str, range_separator: &str) -> RangeResult<()>
where
T: FromStr + Add<Output = T> + PartialEq + PartialOrd + Unit + Copy,
{
if part.contains(range_separator) {
parse_value_range(acc, part, range_separator)?;
} else {
acc.push(parse_as_t(part)?);
}
Ok(())
}
fn parse_value_range<T>(acc: &mut Vec<T>, part: &str, range_separator: &str) -> RangeResult<()>
where
T: FromStr + Add<Output = T> + PartialEq + PartialOrd + Unit + Copy,
{
let parts: Vec<&str> = part.split(range_separator).collect();
let (start, end): (T, T) = match parts.len() {
2 if parts[0].is_empty() && range_separator == "-" => {
let end = format!("-{}", parts[1]);
let end: T = parse_as_t(&end)?;
acc.push(end);
return Ok(());
}
2 => {
let start = parts[0];
let end = parts[1];
let start: T = parse_as_t(start)?;
let end: T = parse_as_t(end)?;
(start, end)
}
3 if parts[0].is_empty() && range_separator == "-" => {
let start = format!("-{}", parts[1]);
let end = parts[2];
let start: T = parse_as_t(&start)?;
let end: T = parse_as_t(end)?;
(start, end)
}
3 => return Err(RangeError::StartBiggerThanEnd(part.to_string())),
4 if range_separator == "-" => {
let start = format!("-{}", parts[1]);
let end = format!("-{}", parts[3]);
let start: T = parse_as_t(&start)?;
let end: T = parse_as_t(&end)?;
(start, end)
}
_ => return Err(RangeError::InvalidRangeSyntax(part.to_string())),
};
if start > end {
return Err(RangeError::StartBiggerThanEnd(part.to_string()));
}
let mut x = start;
while x <= end {
acc.push(x);
x = x + T::unit();
}
Ok(())
}
fn parse_as_t<T>(part: &str) -> RangeResult<T>
where
T: FromStr + Add<Output = T> + PartialEq + PartialOrd + Unit + Copy,
{
part.trim()
.parse()
.map_err(|_| RangeError::NotANumber(part.to_string()))
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn should_parse_dashed_range_with_positive_numbers() {
let range: Vec<u64> = parse("1-3").unwrap();
assert_eq!(range, vec![1, 2, 3]);
}
#[test]
fn should_parse_dashed_range_with_mixed_numbers() {
let range: Vec<i32> = parse("-2-3").unwrap();
assert_eq!(range, vec![-2, -1, 0, 1, 2, 3]);
}
#[test]
fn should_parse_dashed_range_with_negative_numbers() {
let range: Vec<i32> = parse("-3--1").unwrap();
assert_eq!(range, vec![-3, -2, -1]);
}
#[test]
fn should_parse_range_with_floats() {
let range: Vec<f64> = parse("-1.0-3.0").unwrap();
assert_eq!(range, vec![-1.0, 0.0, 1.0, 2.0, 3.0]);
}
#[test]
fn should_parse_range_with_commas_with_positive_numbers() {
let range: Vec<u64> = parse("1,3,4").unwrap();
assert_eq!(range, vec![1, 3, 4]);
}
#[test]
fn should_parse_range_with_commas_with_mixed_numbers() {
let range: Vec<i32> = parse("-2,0,3,-1").unwrap();
assert_eq!(range, vec![-2, 0, 3, -1]);
}
#[test]
fn should_parse_mixed_range_with_positive_numbers() {
let range: Vec<u64> = parse("1,3-5,2").unwrap();
assert_eq!(range, vec![1, 3, 4, 5, 2]);
}
#[test]
fn should_parse_mixed_range_with_mixed_numbers() {
let range: Vec<i32> = parse("-2,0-3,-1,7").unwrap();
assert_eq!(range, vec![-2, 0, 1, 2, 3, -1, 7]);
}
#[test]
fn test_should_parse_with_whitespaces() {
let range: Vec<u64> = parse(" 1 , 3 - 5 , 2 ").unwrap();
assert_eq!(range, vec![1, 3, 4, 5, 2]);
}
#[test]
fn should_parse_mixed_range_with_mixed_numbers_with_custom_separators() {
let range: Vec<i32> = parse_with("-2;0..3;-1;7", ";", "..").unwrap();
assert_eq!(range, vec![-2, 0, 1, 2, 3, -1, 7]);
}
#[test]
fn test_should_not_allow_invalid_range() {
let range = parse::<i32>("1-3-5");
assert!(range.is_err());
}
#[test]
fn test_should_not_allow_invalid_range_with_custom_separators() {
let range = parse_with::<i32>("1-3-5", "-", "-");
assert!(range.is_err());
}
#[test]
fn test_should_not_allow_start_bigger_than_end() {
let range = parse::<i32>("3-1");
assert!(range.is_err());
}
#[test]
fn test_should_fail_with_custom_separator_in_place_of_minus() {
assert!(parse_with::<i32>("~1~3", "=", "~").is_err());
}
#[test]
fn test_should_not_allow_ambiguous_separator() {
assert!(parse_with::<i32>("1--3", "-", "--").is_err());
}
}