use anyhow::{Context, Result};
use itertools::Itertools;
use std::collections::VecDeque;
#[derive(Debug)]
pub struct NumberRangeError;
impl std::error::Error for NumberRangeError {}
impl std::fmt::Display for NumberRangeError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "Invalid number range")
}
}
#[derive(Debug)]
pub enum Number<T> {
Single(T),
Range(T, T, T),
}
impl<T: num::Zero + std::cmp::PartialOrd + Copy> Number<T> {
pub fn is_valid(&self) -> bool {
match self {
Number::Single(_) => true,
Number::Range(start, step, end) => {
((start <= end) && (step > &num::Zero::zero()))
|| ((start >= end) && (step < &num::Zero::zero()))
}
}
}
pub fn is_invalid(&self) -> bool {
!self.is_valid()
}
}
#[derive(Debug)]
pub struct NumberRangeOptions<T> {
pub group_sep: char,
pub whitespace: bool,
pub decimal_sep: char,
pub list_sep: char,
pub range_sep: char,
pub default_start: Option<T>,
pub default_end: Option<T>,
}
#[derive(Debug)]
pub struct NumberRange<'a, T> {
pub numbers: VecDeque<Number<T>>,
original_repr: Option<&'a str>,
pub options: NumberRangeOptions<T>,
}
impl<'a, T: std::fmt::Display + num::One + std::cmp::PartialEq> std::fmt::Display
for NumberRange<'a, T>
{
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let repr = self
.numbers
.iter()
.map(|n| match n {
Number::Single(v) => format!("{}", v),
Number::Range(s, i, e) => {
if i.is_one() {
format!("{}{}{}", s, self.options.range_sep, e)
} else {
format!("{}{}{}{1}{}", s, self.options.range_sep, i, e)
}
}
})
.join(&self.options.list_sep.to_string());
write!(f, "{}", repr)
}
}
impl<'a, T: Copy + std::ops::Add<Output = T> + std::cmp::PartialOrd + num::Zero> Iterator
for NumberRange<'a, T>
{
type Item = T;
fn next(&mut self) -> Option<T> {
if self.numbers.is_empty() {
return None;
}
match self.numbers[0] {
Number::Single(v) => {
self.numbers.pop_front();
Some(v)
}
Number::Range(start, step, end) => {
if self.numbers[0].is_valid() {
let next_step = Number::Range(start + step, step, end);
if next_step.is_valid() {
self.numbers[0] = next_step;
} else {
self.numbers.pop_front();
}
Some(start)
} else {
self.numbers.pop_front();
self.next()
}
}
}
}
}
impl<
T: std::str::FromStr
+ num::One
+ Copy
+ std::str::FromStr
+ std::cmp::PartialOrd
+ std::ops::Add<Output = T>,
> Default for NumberRangeOptions<T>
{
fn default() -> Self {
Self::new()
}
}
impl<T: std::str::FromStr + num::One + Copy + std::cmp::PartialOrd + std::ops::Add<Output = T>>
NumberRangeOptions<T>
{
pub fn new() -> Self {
Self {
list_sep: ',',
range_sep: ':',
decimal_sep: '.',
group_sep: '_',
whitespace: false,
default_start: None,
default_end: None,
}
}
pub fn with_group_sep(mut self, sep: char) -> Self {
self.group_sep = sep;
self
}
pub fn with_whitespace(mut self, flag: bool) -> Self {
self.whitespace = flag;
self
}
pub fn with_decimal_sep(mut self, sep: char) -> Self {
self.decimal_sep = sep;
self
}
pub fn with_list_sep(mut self, sep: char) -> Self {
self.list_sep = sep;
self
}
pub fn with_range_sep(mut self, sep: char) -> Self {
self.range_sep = sep;
self
}
pub fn with_default_start(mut self, def: T) -> Self {
self.default_start = Some(def);
self
}
pub fn with_default_end(mut self, def: T) -> Self {
self.default_end = Some(def);
self
}
pub fn parse<'a>(self, numstr: &'a str) -> Result<NumberRange<T>>
where
<T as std::str::FromStr>::Err: std::error::Error + Send + Sync + 'static,
{
let nr = NumberRange::from_options(self);
nr.parse_str(numstr)
}
}
impl<
'a,
T: num::One
+ std::str::FromStr
+ num::One
+ Copy
+ std::cmp::PartialOrd
+ std::ops::Add<Output = T>,
> Default for NumberRange<'a, T>
{
fn default() -> Self {
Self {
numbers: VecDeque::new(),
original_repr: None,
options: NumberRangeOptions::default(),
}
}
}
impl<
'a,
T: std::str::FromStr + num::One + Copy + std::cmp::PartialOrd + std::ops::Add<Output = T>,
> NumberRange<'a, T>
where
<T as std::str::FromStr>::Err: std::error::Error + Send + Sync + 'static,
{
pub fn from_options(options: NumberRangeOptions<T>) -> Self {
Self {
numbers: VecDeque::new(),
original_repr: None,
options,
}
}
pub fn original(&self) -> &str {
self.original_repr.unwrap_or("")
}
pub fn parse_str(mut self, numstr: &'a str) -> Result<Self> {
self.original_repr = Some(numstr);
self.parse()
}
pub fn from_vec<V>(self, nums: V, increment: Option<T>) -> Self
where
T: std::cmp::Ord,
V: IntoIterator<Item = T>,
{
let mut nums: Vec<T> = nums.into_iter().collect();
nums.sort();
self.from_vec_nosort(&nums, increment)
}
pub fn from_vec_nosort(mut self, nums: &[T], increment: Option<T>) -> Self
where
T: std::cmp::Ord,
{
self.original_repr = None;
let inc = increment.unwrap_or(num::one());
self.numbers.clear();
if nums.len() > 0 {
let mut first = &nums[0];
let mut prev = &nums[0];
let mut rng = false;
for current in &nums[1..] {
if current == prev {
continue;
}
if *current == (*prev + inc) {
if !rng {
rng = true;
first = prev;
}
} else {
if rng {
self.numbers.push_back(Number::Range(*first, inc, *prev));
} else {
self.numbers.push_back(Number::Single(*prev));
}
rng = false;
}
prev = current;
}
if rng {
self.numbers.push_back(Number::Range(*first, inc, *prev));
} else {
self.numbers.push_back(Number::Single(*prev));
}
}
self
}
fn sanitize_number(&self, num: &str) -> String {
let num = num.trim().replace(self.options.group_sep, "");
let num = if self.options.whitespace {
num.split_whitespace().join("")
} else {
num
};
num.replace(self.options.decimal_sep, ".")
}
fn parse_number(&self, num: &str, def: &Option<T>) -> Result<T> {
let s = self.sanitize_number(num);
if def.is_some() && s == "" {
return Ok(def.unwrap());
} else {
s.parse::<T>()
.with_context(|| format!("{} Not a Number", num))
}
}
pub fn parse(mut self) -> Result<Self> {
if let Some(numstr) = self.original_repr {
if self.sanitize_number(numstr) == "" {
self.numbers.clear();
return Ok(self);
}
let numbers: VecDeque<Number<T>> = numstr
.split(self.options.list_sep)
.map(|seq_str| -> Result<Number<T>> {
match seq_str.matches(self.options.range_sep).count() {
0 => self.parse_number(seq_str, &None).map(|v| Number::Single(v)),
1 => match seq_str.split_once(self.options.range_sep) {
Some((start, end)) => {
let start =
self.parse_number(start, &self.options.default_start)?;
let end = self.parse_number(end, &self.options.default_end)?;
Ok(Number::Range(start, num::One::one(), end))
}
None => panic!(
"Checked there is single range_separator, yet split to 2 failed."
),
},
2 => {
let nums: Vec<T> = seq_str
.splitn(3, self.options.range_sep)
.enumerate()
.map(|(i, s)| -> Result<T> {
self.parse_number(
s,
[
&self.options.default_start,
&Some(num::One::one()),
&self.options.default_end,
][i],
)
})
.collect::<Result<Vec<T>>>()?;
Ok(Number::Range(nums[0], nums[1], nums[2]))
}
_ => Err::<Number<_>, anyhow::Error>(NumberRangeError {}.into())
.with_context(|| {
format!(
"Too many range separators ({}) on {}",
self.options.range_sep, seq_str
)
}),
}
})
.collect::<Result<VecDeque<Number<T>>>>()?;
self.numbers = numbers;
Ok(self)
} else {
Err::<NumberRange<'_, _>, anyhow::Error>(NumberRangeError {}.into())
.with_context(|| "Nothing to Parse".to_string())
}
}
}
#[macro_export]
macro_rules! numrng {
($($l:tt)*) => {
$crate::NumberRangeOptions::new().with_whitespace(true).parse(stringify!($($l)*)).unwrap()
};
}
#[macro_export]
macro_rules! numvec {
($($l:tt)*) => {
$crate::numrng!($($l)*)
.collect()
};
}
#[cfg(test)]
use rstest::rstest;
#[cfg(test)]
mod tests {
use super::*;
#[rstest]
fn manual_build() {
let mut rng = NumberRange::<i64>::default();
rng.numbers.push_back(Number::Single(1));
rng.numbers.push_back(Number::Range(3, 2, 6));
rng.numbers.push_back(Number::Range(-4, 1, -2));
assert_eq!(format!("{}", rng), "1,3:2:6,-4:-2");
assert_eq!(rng.collect::<Vec<i64>>(), vec![1, 3, 5, -4, -3, -2]);
}
#[rstest]
fn options_build() {
let rng: NumberRange<usize> = NumberRangeOptions::<usize>::default()
.with_list_sep('*')
.with_range_sep('/')
.parse("1*3/5*9/2/15")
.expect("Parsing should be succesful");
assert_eq!(rng.collect::<Vec<usize>>(), vec![1, 3, 4, 5, 9, 11, 13, 15]);
}
#[rstest]
fn manual_build_then_modify() {
let mut rng = NumberRange::<i64>::default();
rng.numbers.push_back(Number::Single(1));
rng.numbers.push_back(Number::Range(3, 2, 6));
rng.numbers.push_back(Number::Range(-4, 1, -2));
let values = vec![1, 3, 5, -4, -3, -2];
assert_eq!(format!("{}", rng), "1,3:2:6,-4:-2");
for i in 0..4 {
assert_eq!(rng.next().expect("Should have next"), values[i]);
}
assert_eq!(format!("{}", rng), "-3:-2");
rng.numbers.push_back(Number::Single(1));
assert_eq!(format!("{}", rng), "-3:-2,1");
assert_eq!(rng.collect::<Vec<i64>>(), vec![-3, -2, 1]);
}
#[rstest]
fn options_build_then_modify() {
let mut rng: NumberRange<usize> = NumberRangeOptions::<usize>::default()
.with_list_sep(':')
.with_range_sep('-')
.parse("1:3-5:9")
.expect("Parsing should be succesful");
let values = vec![1, 3, 4, 5, 9];
for i in 0..4 {
assert_eq!(rng.next().expect("Should have next"), values[i]);
}
assert_eq!(format!("{}", rng), "9");
rng.numbers.push_back(Number::Range(11, 2, 15));
assert_eq!(format!("{}", rng), "9:11-2-15");
assert_eq!(rng.collect::<Vec<usize>>(), vec![9, 11, 13, 15]);
}
#[rstest]
#[case("200", vec![200])]
#[case("-200", vec![-200])]
#[case("1,4", vec![1, 4])]
#[case("1, -4", vec![1, -4])]
#[should_panic]
#[case("1.0, 2", vec![])]
#[case("1: 3", vec![1, 2, 3])]
#[case(" -1:10", vec![-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10])]
#[case("3 , 5:10", vec![3, 5, 6, 7, 8, 9, 10])]
fn comma_default_int(#[case] numstr: &str, #[case] numvec: Vec<i64>) {
assert_eq!(
NumberRange::<i64>::default()
.parse_str(numstr)
.unwrap()
.collect::<Vec<i64>>(),
numvec
);
}
#[rstest]
fn limits_test() {
let nr1: Vec<usize> = NumberRangeOptions::<usize>::new()
.with_range_sep('-')
.with_default_start(0)
.parse("-2")
.unwrap()
.collect();
assert_eq!(nr1, vec![0, 1, 2]);
let nr1: Vec<usize> = NumberRangeOptions::<usize>::new()
.with_range_sep('-')
.with_default_end(5)
.parse("2-")
.unwrap()
.collect();
assert_eq!(nr1, vec![2, 3, 4, 5]);
let nr1: Vec<usize> = NumberRangeOptions::<usize>::new()
.with_range_sep('-')
.with_default_start(0)
.with_default_end(5)
.parse("-")
.unwrap()
.collect();
assert_eq!(nr1, vec![0, 1, 2, 3, 4, 5]);
}
#[rstest]
#[case("200", vec![200])]
#[case("1,4", vec![1, 4])]
#[should_panic]
#[case("1,-4", vec![])]
#[should_panic]
#[case(",4", vec![])]
#[should_panic]
#[case("1,,4", vec![])]
#[should_panic]
#[case("1,4,", vec![])]
#[should_panic]
#[case("1,4.0", vec![])]
#[case("1:3", vec![1, 2, 3])]
#[case("1:10", vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10])]
fn default_usize(#[case] numstr: &str, #[case] numvec: Vec<usize>) {
assert_eq!(
NumberRange::<usize>::default()
.parse_str(numstr)
.unwrap()
.collect::<Vec<usize>>(),
numvec
);
}
#[rstest]
#[case("200", vec!["200"])]
#[case("1,4", vec!["1,4", "4"])]
#[case("1:4", vec!["1:4", "2:4", "3:4", "4:4"])]
#[case("10:-4:4", vec!["10:-4:4", "6:-4:4"])]
#[case("4:-1:1", vec!["4:-1:1", "3:-1:1", "2:-1:1", "1:-1:1"])]
#[case("1:4:10", vec!["1:4:10", "5:4:10", "9:4:10"])]
fn format_test_loop(#[case] numstr: &str, #[case] numvec: Vec<&str>) {
let mut rng: NumberRange<i64> = NumberRange::default().parse_str(numstr).unwrap();
for fmt_str in numvec {
assert_eq!(fmt_str, format!("{}", &rng));
rng.next();
}
assert!(rng.next().is_none());
}
#[rstest]
#[case("200", ',',vec![200])]
#[case("1,4", ',', vec![1, 4])]
#[case("1:4", ':', vec![1, 4])]
#[case("1:4:4", ':', vec![1, 4, 4])]
#[case("1/4", '/', vec![1, 4])]
#[should_panic]
#[case("1--4", '-', vec![])]
#[should_panic]
#[case("1,-4", ':', vec![])]
fn comma_test_sep_usize(#[case] numstr: &str, #[case] sep: char, #[case] numvec: Vec<usize>) {
assert_eq!(
NumberRangeOptions::<usize>::new()
.with_list_sep(sep)
.parse(numstr)
.unwrap()
.collect::<Vec<usize>>(),
numvec
);
}
#[rstest]
#[case("200", '-',vec![200])]
#[case("1-4", '-', vec![1,2,3,4])]
#[case("1:3:4", ':', vec![1, 4])]
#[should_panic]
#[case("4:-3:1", ':', vec![])]
#[should_panic]
#[case("1--4", '-', vec![])]
fn comma_test_range_usize(#[case] numstr: &str, #[case] sep: char, #[case] numvec: Vec<usize>) {
assert_eq!(
NumberRangeOptions::<usize>::new()
.with_range_sep(sep)
.parse(numstr)
.unwrap()
.collect::<Vec<usize>>(),
numvec
);
}
#[rstest]
#[case("200", '-',vec![200])]
#[case("1-4", '-', vec![1,2,3,4])]
#[case("1:3:4", ':', vec![1, 4])]
#[case("4:-3:1", ':', vec![4, 1])]
#[case("-4:1", ':', vec![-4, -3, -2, -1, 0, 1])]
#[case("1:-4", ':', vec![])]
fn comma_test_range_i64(#[case] numstr: &str, #[case] sep: char, #[case] numvec: Vec<i64>) {
assert_eq!(
NumberRangeOptions::<i64>::new()
.with_range_sep(sep)
.parse(numstr)
.unwrap()
.collect::<Vec<i64>>(),
numvec
);
}
#[rstest]
#[case("200", '-',vec![200.0])]
#[case("1-4", '-', vec![1.0,2.0,3.0,4.0])]
#[case("1:3:4", ':', vec![1.0, 4.0])]
#[case("1:.5:4", ':', vec![1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0])]
#[case("4:-3:1", ':', vec![4.0, 1.0])]
#[case("-4:1", ':', vec![-4.0, -3.0, -2.0, -1.0, 0.0, 1.0])]
#[case("1:-4", ':', vec![])]
fn comma_test_range_f64(#[case] numstr: &str, #[case] sep: char, #[case] numvec: Vec<f64>) {
assert_eq!(
NumberRangeOptions::<f64>::new()
.with_range_sep(sep)
.parse(numstr)
.unwrap()
.collect::<Vec<f64>>(),
numvec
);
}
#[rstest]
fn comma_test_empty_range() {
assert_eq!(
NumberRange::default()
.parse_str("")
.unwrap()
.collect::<Vec<f64>>(),
vec![]
);
let rng = NumberRange::default().parse_str("1:10").unwrap();
assert_eq!(rng.parse_str("").unwrap().collect::<Vec<f64>>(), vec![]);
}
#[rstest]
#[case([1,2,3], None, "1:3")]
#[case(vec![1,2,3], None, "1:3")]
#[case([1,3,5,7], None, "1,3,5,7")]
#[case([1,3,5,7,10], Some(2), "1:2:7,10")]
fn rng_from_vec(
#[case] inp: impl IntoIterator<Item = i64>,
#[case] inc: Option<i64>,
#[case] s: &str,
) {
assert_eq!(format!("{}", NumberRange::default().from_vec(inp, inc)), s);
}
}