1#![no_std]
31#![forbid(unsafe_code)]
32#![doc(html_root_url = "https://docs.rs/parse-numeric-range/0.1.0")]
33
34extern crate alloc;
35
36use alloc::vec::Vec;
37
38#[cfg(doctest)]
40#[doc = include_str!("../README.md")]
41struct ReadmeDoctests;
42
43#[must_use]
56pub fn parse(input: &str) -> Vec<i64> {
57 let mut result = Vec::new();
58
59 for part in input.split(',') {
60 let entry = part.trim_matches(is_js_whitespace);
61
62 if is_integer(entry) {
63 if let Ok(value) = entry.parse::<i64>() {
64 result.push(value);
65 }
66 } else if let Some((lhs, inclusive, rhs)) = parse_range(entry) {
67 let increment: i64 = if lhs < rhs { 1 } else { -1 };
68 let end = if inclusive {
70 rhs.wrapping_add(increment)
71 } else {
72 rhs
73 };
74 let mut current = lhs;
75 while current != end {
76 result.push(current);
77 current = current.wrapping_add(increment);
78 }
79 }
80 }
81
82 result
83}
84
85fn is_integer(s: &str) -> bool {
87 let bytes = s.as_bytes();
88 let digits = if bytes.first() == Some(&b'-') {
89 &bytes[1..]
90 } else {
91 bytes
92 };
93 !digits.is_empty() && digits.iter().all(u8::is_ascii_digit)
94}
95
96fn take_integer(s: &str) -> Option<(&str, &str)> {
98 let bytes = s.as_bytes();
99 let mut end = usize::from(bytes.first() == Some(&b'-'));
100 let digits_start = end;
101 while end < bytes.len() && bytes[end].is_ascii_digit() {
102 end += 1;
103 }
104 if end == digits_start {
105 return None;
106 }
107 Some((&s[..end], &s[end..]))
108}
109
110fn take_separator(s: &str) -> Option<(bool, &str)> {
115 if let Some(rest) = s.strip_prefix('-') {
116 Some((true, rest))
117 } else if let Some(rest) = s.strip_prefix("...") {
118 Some((false, rest))
119 } else if let Some(rest) = s.strip_prefix("..") {
120 Some((true, rest))
121 } else if let Some(rest) = s.strip_prefix('\u{2025}') {
122 Some((true, rest))
123 } else if let Some(rest) = s.strip_prefix('\u{2026}') {
124 Some((false, rest))
125 } else if let Some(rest) = s.strip_prefix('\u{22EF}') {
126 Some((false, rest))
127 } else {
128 None
129 }
130}
131
132fn parse_range(s: &str) -> Option<(i64, bool, i64)> {
134 let (lhs, rest) = take_integer(s)?;
135 let (inclusive, rest) = take_separator(rest)?;
136 let (rhs, rest) = take_integer(rest)?;
137 if !rest.is_empty() {
138 return None;
139 }
140 Some((lhs.parse().ok()?, inclusive, rhs.parse().ok()?))
141}
142
143fn is_js_whitespace(c: char) -> bool {
145 matches!(
146 c,
147 '\u{0009}'
148 | '\u{000A}'
149 | '\u{000B}'
150 | '\u{000C}'
151 | '\u{000D}'
152 | '\u{0020}'
153 | '\u{00A0}'
154 | '\u{1680}'
155 | '\u{2000}'
156 ..='\u{200A}'
157 | '\u{2028}'
158 | '\u{2029}'
159 | '\u{202F}'
160 | '\u{205F}'
161 | '\u{3000}'
162 | '\u{FEFF}'
163 )
164}
165
166#[cfg(test)]
167mod tests {
168 use super::*;
169
170 #[test]
171 fn numbers() {
172 assert_eq!(parse("1,2,3"), [1, 2, 3]);
173 assert_eq!(parse("0"), [0]);
174 assert_eq!(parse("-0"), [0]);
175 assert_eq!(parse("007"), [7]);
176 assert_eq!(parse(",,1,,2,,"), [1, 2]);
177 assert_eq!(parse("1,1"), [1, 1]); }
179
180 #[test]
181 fn ranges() {
182 assert_eq!(parse("1-5"), [1, 2, 3, 4, 5]);
183 assert_eq!(parse("5-1"), [5, 4, 3, 2, 1]);
184 assert_eq!(parse("3-3"), [3]);
185 assert_eq!(parse("0-0"), [0]);
186 assert_eq!(parse("10-8,1"), [10, 9, 8, 1]);
187 }
188
189 #[test]
190 fn negatives() {
191 assert_eq!(parse("-5--2"), [-5, -4, -3, -2]);
192 assert_eq!(parse("-3-2"), [-3, -2, -1, 0, 1, 2]);
193 }
194
195 #[test]
196 fn separators() {
197 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]); }
203
204 #[test]
205 fn ignored() {
206 assert_eq!(parse("a-c"), [] as [i64; 0]);
207 assert_eq!(parse("2-"), [] as [i64; 0]);
208 assert_eq!(parse("1-2-3"), [] as [i64; 0]);
209 assert_eq!(parse("1.5"), [] as [i64; 0]);
210 assert_eq!(parse(" 3 - 5 "), [] as [i64; 0]); assert_eq!(parse(""), [] as [i64; 0]);
212 }
213}