#![no_std]
#![forbid(unsafe_code)]
#![doc(html_root_url = "https://docs.rs/parse-numeric-range/0.1.0")]
extern crate alloc;
use alloc::vec::Vec;
#[cfg(doctest)]
#[doc = include_str!("../README.md")]
struct ReadmeDoctests;
#[must_use]
pub fn parse(input: &str) -> Vec<i64> {
let mut result = Vec::new();
for part in input.split(',') {
let entry = part.trim_matches(is_js_whitespace);
if is_integer(entry) {
if let Ok(value) = entry.parse::<i64>() {
result.push(value);
}
} else if let Some((lhs, inclusive, rhs)) = parse_range(entry) {
let increment: i64 = if lhs < rhs { 1 } else { -1 };
let end = if inclusive {
rhs.wrapping_add(increment)
} else {
rhs
};
let mut current = lhs;
while current != end {
result.push(current);
current = current.wrapping_add(increment);
}
}
}
result
}
fn is_integer(s: &str) -> bool {
let bytes = s.as_bytes();
let digits = if bytes.first() == Some(&b'-') {
&bytes[1..]
} else {
bytes
};
!digits.is_empty() && digits.iter().all(u8::is_ascii_digit)
}
fn take_integer(s: &str) -> Option<(&str, &str)> {
let bytes = s.as_bytes();
let mut end = usize::from(bytes.first() == Some(&b'-'));
let digits_start = end;
while end < bytes.len() && bytes[end].is_ascii_digit() {
end += 1;
}
if end == digits_start {
return None;
}
Some((&s[..end], &s[end..]))
}
fn take_separator(s: &str) -> Option<(bool, &str)> {
if let Some(rest) = s.strip_prefix('-') {
Some((true, rest))
} else if let Some(rest) = s.strip_prefix("...") {
Some((false, rest))
} else if let Some(rest) = s.strip_prefix("..") {
Some((true, rest))
} else if let Some(rest) = s.strip_prefix('\u{2025}') {
Some((true, rest))
} else if let Some(rest) = s.strip_prefix('\u{2026}') {
Some((false, rest))
} else if let Some(rest) = s.strip_prefix('\u{22EF}') {
Some((false, rest))
} else {
None
}
}
fn parse_range(s: &str) -> Option<(i64, bool, i64)> {
let (lhs, rest) = take_integer(s)?;
let (inclusive, rest) = take_separator(rest)?;
let (rhs, rest) = take_integer(rest)?;
if !rest.is_empty() {
return None;
}
Some((lhs.parse().ok()?, inclusive, rhs.parse().ok()?))
}
fn is_js_whitespace(c: char) -> bool {
matches!(
c,
'\u{0009}'
| '\u{000A}'
| '\u{000B}'
| '\u{000C}'
| '\u{000D}'
| '\u{0020}'
| '\u{00A0}'
| '\u{1680}'
| '\u{2000}'
..='\u{200A}'
| '\u{2028}'
| '\u{2029}'
| '\u{202F}'
| '\u{205F}'
| '\u{3000}'
| '\u{FEFF}'
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn numbers() {
assert_eq!(parse("1,2,3"), [1, 2, 3]);
assert_eq!(parse("0"), [0]);
assert_eq!(parse("-0"), [0]);
assert_eq!(parse("007"), [7]);
assert_eq!(parse(",,1,,2,,"), [1, 2]);
assert_eq!(parse("1,1"), [1, 1]); }
#[test]
fn ranges() {
assert_eq!(parse("1-5"), [1, 2, 3, 4, 5]);
assert_eq!(parse("5-1"), [5, 4, 3, 2, 1]);
assert_eq!(parse("3-3"), [3]);
assert_eq!(parse("0-0"), [0]);
assert_eq!(parse("10-8,1"), [10, 9, 8, 1]);
}
#[test]
fn negatives() {
assert_eq!(parse("-5--2"), [-5, -4, -3, -2]);
assert_eq!(parse("-3-2"), [-3, -2, -1, 0, 1, 2]);
}
#[test]
fn separators() {
assert_eq!(parse("1..5"), [1, 2, 3, 4, 5]); assert_eq!(parse("1...5"), [1, 2, 3, 4]); assert_eq!(parse("1\u{2025}5"), [1, 2, 3, 4, 5]); assert_eq!(parse("1\u{2026}5"), [1, 2, 3, 4]); assert_eq!(parse("1\u{22EF}5"), [1, 2, 3, 4]); }
#[test]
fn ignored() {
assert_eq!(parse("a-c"), [] as [i64; 0]);
assert_eq!(parse("2-"), [] as [i64; 0]);
assert_eq!(parse("1-2-3"), [] as [i64; 0]);
assert_eq!(parse("1.5"), [] as [i64; 0]);
assert_eq!(parse(" 3 - 5 "), [] as [i64; 0]); assert_eq!(parse(""), [] as [i64; 0]);
}
}