pub fn parse_range(spec: &str, total: usize) -> Result<Option<(usize, usize)>, String> {
if total == 0 {
return Ok(None);
}
let parse_one = |s: &str, what: &str| -> Result<usize, String> {
s.trim()
.parse::<usize>()
.map_err(|_| format!("invalid {what} in range '{spec}'"))
};
let (start1, end1): (usize, usize) = match spec.split_once(':') {
Some((a, b)) => {
let start = if a.trim().is_empty() {
1
} else {
parse_one(a, "start")?
};
let end = if b.trim().is_empty() {
total
} else {
parse_one(b, "end")?
};
(start, end)
}
None => {
let n = parse_one(spec, "line")?;
(n, n)
}
};
if start1 == 0 {
return Err(format!("range lines are 1-based; got 0 in '{spec}'"));
}
if start1 > total || end1 < start1 {
return Ok(None);
}
Ok(Some((
start1 - 1,
end1.min(total).saturating_sub(1).max(start1 - 1),
)))
}
pub fn expand_and_merge(hits: &[usize], ctx: usize, total: usize) -> Vec<usize> {
if hits.is_empty() || total == 0 {
return Vec::new();
}
let mut idx: Vec<usize> = Vec::new();
let last = total - 1;
for &h in hits {
let lo = h.saturating_sub(ctx);
let hi = (h + ctx).min(last);
for i in lo..=hi {
if idx.last() != Some(&i) {
idx.push(i);
}
}
}
idx.sort_unstable();
idx.dedup();
idx
}
pub fn segments(idx: &[usize]) -> Vec<(usize, usize)> {
let mut segs: Vec<(usize, usize)> = Vec::new();
for &i in idx {
match segs.last_mut() {
Some(seg) if i == seg.1 + 1 => seg.1 = i,
_ => segs.push((i, i)),
}
}
segs
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn range_parsing_handles_open_and_closed_forms() {
assert_eq!(parse_range("2:4", 10).unwrap(), Some((1, 3)));
assert_eq!(parse_range("3", 10).unwrap(), Some((2, 2)));
assert_eq!(parse_range("8:", 10).unwrap(), Some((7, 9)));
assert_eq!(parse_range(":3", 10).unwrap(), Some((0, 2)));
assert_eq!(parse_range("20:30", 10).unwrap(), None);
assert_eq!(parse_range("9:99", 10).unwrap(), Some((8, 9)));
assert!(parse_range("0:3", 10).is_err());
assert!(parse_range("x", 10).is_err());
}
#[test]
fn windows_merge_when_they_overlap() {
assert_eq!(expand_and_merge(&[2, 3], 1, 10), vec![1, 2, 3, 4]);
assert_eq!(expand_and_merge(&[1, 8], 0, 10), vec![1, 8]);
assert_eq!(expand_and_merge(&[0], 2, 3), vec![0, 1, 2]);
}
#[test]
fn segments_split_on_gaps() {
assert_eq!(segments(&[1, 2, 3]), vec![(1, 3)]);
assert_eq!(segments(&[1, 2, 5, 6]), vec![(1, 2), (5, 6)]);
assert_eq!(segments(&[]), vec![]);
}
}