use thiserror::Error;
#[derive(Debug, Error)]
pub enum ParseIntegersError {
#[error("Could not parse integers: {0}")]
ParseId(String),
#[error("Unmatched range: {0}")]
UnmatchedRange(String),
#[error("Range too large: {0}")]
RangeTooLarge(String),
}
pub fn parse_integer_sequences(
integers: &str,
) -> std::result::Result<Vec<u64>, ParseIntegersError> {
integers
.split(',')
.map(|id| {
id.parse::<u64>()
.map_err(|_| ParseIntegersError::ParseId(id.into()))
})
.collect::<std::result::Result<Vec<_>, _>>()
}
pub fn parse_integer_ranges(
integer_ranges: &str,
) -> std::result::Result<Vec<u64>, ParseIntegersError> {
let ranges = integer_ranges.split('-').collect::<Vec<_>>();
if ranges.len() == 1 {
parse_integer_sequences(ranges[0])
} else if ranges.len() > 2 && ranges.len() % 2 == 1 {
Err(ParseIntegersError::UnmatchedRange(integer_ranges.into()))
} else {
let mut i = 0;
let mut ids = Vec::new();
while i < ranges.len() {
let mut start = parse_integer_sequences(ranges[i])?;
let mut start_id = start[start.len() - 1] + 1;
let mut end = parse_integer_sequences(ranges[i + 1])?;
let end_id = end[0];
if end_id - start_id > 100_000 {
return Err(ParseIntegersError::RangeTooLarge(format!(
"{}-{}",
start_id - 1,
end_id,
)));
}
ids.append(&mut start);
while start_id < end_id {
ids.push(start_id);
start_id += 1;
}
ids.append(&mut end);
i += 2;
}
Ok(ids)
}
}
#[cfg(test)]
mod tests {
use super::*;
mod parse_integer_sequences {
use super::*;
#[test_log::test]
fn parses_single_integer() {
let result = parse_integer_sequences("42").unwrap();
assert_eq!(result, vec![42]);
}
#[test_log::test]
fn parses_multiple_comma_separated_integers() {
let result = parse_integer_sequences("1,2,3,10").unwrap();
assert_eq!(result, vec![1, 2, 3, 10]);
}
#[test_log::test]
fn parses_large_integers() {
let result = parse_integer_sequences("1000000,2000000,3000000").unwrap();
assert_eq!(result, vec![1_000_000, 2_000_000, 3_000_000]);
}
#[test_log::test]
fn parses_zero() {
let result = parse_integer_sequences("0").unwrap();
assert_eq!(result, vec![0]);
}
#[test_log::test]
fn parses_max_u64() {
let max = u64::MAX.to_string();
let result = parse_integer_sequences(&max).unwrap();
assert_eq!(result, vec![u64::MAX]);
}
#[test_log::test]
fn returns_error_for_invalid_integer() {
let result = parse_integer_sequences("1,not_a_number,3");
assert!(result.is_err());
match result.unwrap_err() {
ParseIntegersError::ParseId(s) => assert_eq!(s, "not_a_number"),
_ => panic!("Expected ParseId error"),
}
}
#[test_log::test]
fn returns_error_for_negative_number() {
let result = parse_integer_sequences("1,-5,3");
assert!(result.is_err());
match result.unwrap_err() {
ParseIntegersError::ParseId(s) => assert_eq!(s, "-5"),
_ => panic!("Expected ParseId error"),
}
}
#[test_log::test]
fn returns_error_for_float() {
let result = parse_integer_sequences("1,2.5,3");
assert!(result.is_err());
match result.unwrap_err() {
ParseIntegersError::ParseId(s) => assert_eq!(s, "2.5"),
_ => panic!("Expected ParseId error"),
}
}
#[test_log::test]
fn returns_error_for_overflow() {
let result = parse_integer_sequences("18446744073709551616");
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
ParseIntegersError::ParseId(_)
));
}
}
mod parse_integer_ranges {
use super::*;
#[test_log::test]
fn parses_single_integer_no_range() {
let result = parse_integer_ranges("42").unwrap();
assert_eq!(result, vec![42]);
}
#[test_log::test]
fn parses_comma_separated_no_ranges() {
let result = parse_integer_ranges("1,5,10").unwrap();
assert_eq!(result, vec![1, 5, 10]);
}
#[test_log::test]
fn parses_simple_range() {
let result = parse_integer_ranges("1-5").unwrap();
assert_eq!(result, vec![1, 2, 3, 4, 5]);
}
#[test_log::test]
fn parses_range_with_single_gap() {
let result = parse_integer_ranges("10-12").unwrap();
assert_eq!(result, vec![10, 11, 12]);
}
#[test_log::test]
fn parses_range_with_no_gap() {
let result = parse_integer_ranges("5-6").unwrap();
assert_eq!(result, vec![5, 6]);
}
#[test_log::test]
fn parses_mixed_values_and_single_range() {
let result = parse_integer_ranges("1,2-5,10").unwrap();
assert_eq!(result, vec![1, 2, 3, 4, 5, 10]);
}
#[test_log::test]
fn parses_value_before_range() {
let result = parse_integer_ranges("1,5-7").unwrap();
assert_eq!(result, vec![1, 5, 6, 7]);
}
#[test_log::test]
fn parses_value_after_range() {
let result = parse_integer_ranges("1-3,10").unwrap();
assert_eq!(result, vec![1, 2, 3, 10]);
}
#[test_log::test]
fn parses_multiple_values_with_range() {
let result = parse_integer_ranges("1,2,5-7,10,11").unwrap();
assert_eq!(result, vec![1, 2, 5, 6, 7, 10, 11]);
}
#[test_log::test]
fn returns_error_for_comma_separated_ranges() {
let result = parse_integer_ranges("1-3,5-7");
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
ParseIntegersError::UnmatchedRange(_)
));
}
#[test_log::test]
fn parses_range_starting_at_zero() {
let result = parse_integer_ranges("0-2").unwrap();
assert_eq!(result, vec![0, 1, 2]);
}
#[test_log::test]
fn parses_zero_in_sequence() {
let result = parse_integer_ranges("0,5-7").unwrap();
assert_eq!(result, vec![0, 5, 6, 7]);
}
#[test_log::test]
fn parses_range_with_large_numbers() {
let result = parse_integer_ranges("1000000-1000003").unwrap();
assert_eq!(result, vec![1_000_000, 1_000_001, 1_000_002, 1_000_003]);
}
#[test_log::test]
fn returns_error_for_invalid_integer_in_range() {
let result = parse_integer_ranges("1-abc");
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
ParseIntegersError::ParseId(_)
));
}
#[test_log::test]
fn returns_error_for_invalid_start_value() {
let result = parse_integer_ranges("abc-5");
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
ParseIntegersError::ParseId(_)
));
}
#[test_log::test]
fn returns_error_for_negative_in_range() {
let result = parse_integer_ranges("1--5");
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
ParseIntegersError::UnmatchedRange(_)
));
}
#[test_log::test]
fn returns_error_for_three_hyphens_unmatched() {
let result = parse_integer_ranges("1-2-3");
assert!(result.is_err());
match result.unwrap_err() {
ParseIntegersError::UnmatchedRange(s) => assert_eq!(s, "1-2-3"),
_ => panic!("Expected UnmatchedRange error"),
}
}
#[test_log::test]
fn returns_error_for_five_hyphens_unmatched() {
let result = parse_integer_ranges("1-2-3-4-5");
assert!(result.is_err());
match result.unwrap_err() {
ParseIntegersError::UnmatchedRange(s) => assert_eq!(s, "1-2-3-4-5"),
_ => panic!("Expected UnmatchedRange error"),
}
}
#[test_log::test]
fn returns_error_for_four_segments() {
let result = parse_integer_ranges("1-2,3-4");
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
ParseIntegersError::UnmatchedRange(_)
));
}
#[test_log::test]
fn returns_error_for_range_exceeding_100000() {
let result = parse_integer_ranges("1-100003");
assert!(result.is_err());
match result.unwrap_err() {
ParseIntegersError::RangeTooLarge(s) => {
assert!(s.contains("100003") || s.contains("2-100003"));
}
_ => panic!("Expected RangeTooLarge error"),
}
}
#[test_log::test]
fn returns_error_for_large_range_in_sequence() {
let result = parse_integer_ranges("1,5-100010");
assert!(result.is_err());
match result.unwrap_err() {
ParseIntegersError::RangeTooLarge(_) => {}
_ => panic!("Expected RangeTooLarge error"),
}
}
#[test_log::test]
fn accepts_range_at_100000_limit() {
let result = parse_integer_ranges("1-100002");
assert!(result.is_ok());
let values = result.unwrap();
assert_eq!(values.len(), 100_002);
assert_eq!(values[0], 1);
assert_eq!(values[values.len() - 1], 100_002);
}
#[test_log::test]
#[should_panic(expected = "attempt to subtract with overflow")]
fn panics_on_range_with_same_start_and_end() {
let _result = parse_integer_ranges("5-5");
}
#[test_log::test]
fn parses_large_sequence_of_single_values() {
let input = (0..50).map(|n| n.to_string()).collect::<Vec<_>>().join(",");
let result = parse_integer_ranges(&input).unwrap();
assert_eq!(result.len(), 50);
assert_eq!(result[0], 0);
assert_eq!(result[49], 49);
}
#[test_log::test]
fn parses_complex_mixed_sequence() {
let result = parse_integer_ranges("1,3,5-7,10-12,20,25-27,30").unwrap();
assert_eq!(result, vec![1, 3, 5, 6, 7, 10, 12, 20, 25, 26, 27, 30]);
}
#[test_log::test]
fn preserves_order_of_values() {
let result = parse_integer_ranges("10,1-3,5").unwrap();
assert_eq!(result, vec![10, 1, 2, 3, 5]);
}
#[test_log::test]
fn returns_error_for_empty_string() {
let result = parse_integer_ranges("");
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
ParseIntegersError::ParseId(_)
));
}
#[test_log::test]
fn returns_error_for_whitespace_in_values() {
let result = parse_integer_ranges("1, 2, 3");
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
ParseIntegersError::ParseId(_)
));
}
#[test_log::test]
fn processes_even_hyphen_segments_as_consecutive_ranges() {
let result = parse_integer_ranges("1-2-3-4").unwrap();
assert_eq!(result, vec![1, 2, 3, 4]);
}
#[test_log::test]
#[should_panic(expected = "attempt to subtract with overflow")]
fn panics_on_reverse_range() {
let _result = parse_integer_ranges("5-4");
}
#[test_log::test]
fn parses_six_segments_as_three_consecutive_ranges() {
let result = parse_integer_ranges("1-2-3-4-5-6").unwrap();
assert_eq!(result, vec![1, 2, 3, 4, 5, 6]);
}
}
}
#[cfg(test)]
mod parse_integer_sequences_edge_cases {
use super::*;
#[test_log::test]
fn returns_error_for_empty_string() {
let result = parse_integer_sequences("");
assert!(result.is_err());
match result.unwrap_err() {
ParseIntegersError::ParseId(s) => assert_eq!(s, ""),
_ => panic!("Expected ParseId error"),
}
}
#[test_log::test]
fn returns_error_for_whitespace_around_values() {
let result = parse_integer_sequences(" 1,2,3");
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
ParseIntegersError::ParseId(_)
));
}
#[test_log::test]
fn returns_error_for_trailing_comma() {
let result = parse_integer_sequences("1,2,3,");
assert!(result.is_err());
match result.unwrap_err() {
ParseIntegersError::ParseId(s) => assert_eq!(s, ""),
_ => panic!("Expected ParseId error"),
}
}
#[test_log::test]
fn returns_error_for_leading_comma() {
let result = parse_integer_sequences(",1,2,3");
assert!(result.is_err());
match result.unwrap_err() {
ParseIntegersError::ParseId(s) => assert_eq!(s, ""),
_ => panic!("Expected ParseId error"),
}
}
#[test_log::test]
fn returns_error_for_double_comma() {
let result = parse_integer_sequences("1,,3");
assert!(result.is_err());
match result.unwrap_err() {
ParseIntegersError::ParseId(s) => assert_eq!(s, ""),
_ => panic!("Expected ParseId error"),
}
}
}