use std::ops::Range;
use easy_cast::Conv;
#[cfg_attr(feature = "proto", derive(prost::Message))]
#[cfg_attr(not(feature = "proto"), derive(Debug))]
#[derive(Copy, Clone, PartialEq, Eq, Hash)]
pub struct PyStyleIdxRange {
#[cfg_attr(feature = "proto", prost(int64, tag = "1"))]
pub start: i64,
#[cfg_attr(feature = "proto", prost(int64, tag = "2"))]
pub end: i64,
}
impl PyStyleIdxRange {
#[must_use]
pub fn new(start: i64, end: i64) -> Self {
Self { start, end }
}
#[must_use]
pub fn resolve_for<T>(self, slice: &[T]) -> Range<usize> {
let len = u64::try_from(slice.len()).unwrap_or(u64::MAX);
let norm = |i: i64| {
let abs = i.unsigned_abs();
if i < 0 {
len.checked_sub(abs)
} else {
Some(abs)
}
};
let start = norm(self.start).unwrap_or_default().clamp(0, len);
let end = norm(self.end).map_or_default(|n| n.saturating_add(1)).clamp(start, len);
usize::conv(start)..usize::conv(end)
}
pub fn resolve_for_split<T>(self, start: &[T], end: &[T]) -> SplitRanges {
if self.start >= 0 && self.end >= 0 {
let start_range = self.resolve_for(start);
let expected_unsigned_end =
usize::try_from(self.end).ok().and_then(|n| n.checked_add(1));
return SplitRanges {
truncated: self.start <= self.end
&& expected_unsigned_end.is_none_or(|end| end > start_range.end),
start: start_range,
end: 0..0,
};
}
if self.start < 0 && self.end < 0 {
let end_range = self.resolve_for(end);
let expected_unsigned_start = usize::try_from(self.start.unsigned_abs())
.ok()
.and_then(|n| end.len().checked_sub(n));
return SplitRanges {
truncated: self.start <= self.end
&& expected_unsigned_start.is_none_or(|start| start < end_range.start),
start: 0..0,
end: end_range,
};
}
if self.start < 0 {
return SplitRanges {
truncated: false,
start: 0..0,
end: 0..0,
};
}
SplitRanges {
truncated: true,
start: Self {
start: self.start,
end: -1,
}
.resolve_for(start),
end: Self {
start: 0,
end: self.end,
}
.resolve_for(end),
}
}
}
#[derive(Debug, Clone)]
pub struct SplitRanges {
pub start: Range<usize>,
pub end: Range<usize>,
pub truncated: bool,
}
#[cfg(test)]
mod tests {
use proptest::prelude::*;
use rstest::rstest;
use super::*;
#[rstest]
#[case::in_bounds(PyStyleIdxRange::new(1, 3), 1..4)]
#[case::single_line(PyStyleIdxRange::new(2, 2), 2..3)]
#[case::last_line_no_sentinel(PyStyleIdxRange::new(-1, -1), 4..5)]
#[case::whole_via_negative_end(PyStyleIdxRange::new(0, -1), 0..5)]
#[case::negative_from_end(PyStyleIdxRange::new(-2, -1), 3..5)]
#[case::more_than_available_clamps(PyStyleIdxRange::new(-100, -1), 0..5)]
#[case::clamped_past_end(PyStyleIdxRange::new(10, 20), 5..5)]
#[case::backwards_resolves_empty(PyStyleIdxRange::new(3, 1), 3..3)]
#[case::negative_backwards_resolves_empty(PyStyleIdxRange::new(-1, -5), 4..4)]
#[case::end_before_the_slice(PyStyleIdxRange::new(0, -100), 0..0)]
#[case::end_before_the_slice_from_within(PyStyleIdxRange::new(2, -100), 2..2)]
#[case::both_bounds_before_the_slice(PyStyleIdxRange::new(-100, -100), 0..0)]
fn resolve_for_is_always_sliceable(
#[case] range: PyStyleIdxRange,
#[case] expected: Range<usize>,
) {
let slice = [(); 5];
let resolved = range.resolve_for(&slice);
assert_eq!(resolved, expected);
let _ = &slice[resolved];
}
const START: [&str; 3] = ["a", "b", "c"];
const END: [&str; 3] = ["x", "y", "z"];
fn split(range: PyStyleIdxRange) -> (Vec<&'static str>, bool) {
let ranges = range.resolve_for_split(&START, &END);
let picked = [&START[ranges.start], &END[ranges.end]].concat();
(picked, ranges.truncated)
}
#[rstest]
#[case::first_line(PyStyleIdxRange::new(0, 0), vec!["a"], false)]
#[case::within_the_start(PyStyleIdxRange::new(1, 2), vec!["b", "c"], false)]
#[case::exactly_the_start(PyStyleIdxRange::new(0, 2), vec!["a", "b", "c"], false)]
#[case::just_past_the_start(PyStyleIdxRange::new(0, 3), vec!["a", "b", "c"], true)]
#[case::wholly_inside_the_gap(PyStyleIdxRange::new(5, 9), vec![], true)]
#[case::backwards_is_empty(PyStyleIdxRange::new(2, 1), vec![], false)]
#[case::last_line(PyStyleIdxRange::new(-1, -1), vec!["z"], false)]
#[case::exactly_the_end(PyStyleIdxRange::new(-3, -1), vec!["x", "y", "z"], false)]
#[case::just_past_the_end(PyStyleIdxRange::new(-4, -1), vec!["x", "y", "z"], true)]
#[case::wholly_inside_the_gap_from_behind(PyStyleIdxRange::new(-9, -5), vec![], true)]
#[case::negative_backwards_is_empty(PyStyleIdxRange::new(-1, -3), vec![], false)]
#[case::everything(PyStyleIdxRange::new(0, -1), vec!["a", "b", "c", "x", "y", "z"], true)]
#[case::across_the_gap(PyStyleIdxRange::new(1, -2), vec!["b", "c", "x", "y"], true)]
#[case::across_into_the_gap(PyStyleIdxRange::new(1, -9), vec!["b", "c"], true)]
#[case::from_the_end_back_to_the_start(PyStyleIdxRange::new(-2, 1), vec![], false)]
#[case::from_the_end_back_to_line_zero(PyStyleIdxRange::new(-2, 0), vec![], false)]
fn resolve_for_split_picks_from_both_halves(
#[case] range: PyStyleIdxRange,
#[case] expected: Vec<&str>,
#[case] truncated: bool,
) {
assert_eq!(split(range), (expected, truncated));
}
#[rstest]
#[case::everything(PyStyleIdxRange::new(0, -1))]
#[case::positive(PyStyleIdxRange::new(0, 5))]
#[case::negative(PyStyleIdxRange::new(-5, -1))]
#[case::spanning(PyStyleIdxRange::new(2, -2))]
#[case::extremes(PyStyleIdxRange::new(i64::MIN, i64::MAX))]
#[case::reversed_extremes(PyStyleIdxRange::new(i64::MAX, i64::MIN))]
fn resolve_for_split_empty_halves_never_panic(#[case] range: PyStyleIdxRange) {
let empty: [&str; 0] = [];
for (start, end) in [(&empty[..], &END[..]), (&START[..], &empty[..]), (&empty, &empty)] {
let ranges = range.resolve_for_split(start, end);
let _ = (&start[ranges.start], &end[ranges.end]);
}
}
#[rstest]
fn resolve_for_split_matches_resolve_for_when_nothing_is_missing() {
let empty: [&str; 0] = [];
for start in 0..4i64 {
for end in 0..4i64 {
let range = PyStyleIdxRange::new(start, end);
let ranges = range.resolve_for_split(&START, &empty);
assert_eq!(ranges.start, range.resolve_for(&START), "{start}..={end}");
assert!(ranges.end.is_empty(), "{start}..={end}");
}
}
}
proptest! {
#[test]
fn resolve_for_split_is_always_sliceable(
start_bound in -20i64..20,
end_bound in -20i64..20,
start_len in 0usize..6,
end_len in 0usize..6,
) {
let start = vec![(); start_len];
let end = vec![(); end_len];
let ranges = PyStyleIdxRange::new(start_bound, end_bound).resolve_for_split(&start, &end);
let _ = &start[ranges.start.clone()];
let _ = &end[ranges.end.clone()];
prop_assert!(ranges.start.end <= start_len);
prop_assert!(ranges.end.end <= end_len);
}
#[test]
fn resolve_for_split_reports_the_gap_when_a_bound_overshoots(
start_bound in -20i64..20,
end_bound in -20i64..20,
) {
let start = vec![(); 3];
let end = vec![(); 3];
let range = PyStyleIdxRange::new(start_bound, end_bound);
let ranges = range.resolve_for_split(&start, &end);
if start_bound >= 0 && end_bound >= 0 && start_bound <= end_bound {
prop_assert_eq!(ranges.truncated, end_bound + 1 > 3);
}
if start_bound < 0 && end_bound < 0 && start_bound <= end_bound {
prop_assert_eq!(ranges.truncated, start_bound < -3);
}
}
}
#[rstest]
#[case::everything(PyStyleIdxRange::new(0, -1), 0..0)]
#[case::last_line(PyStyleIdxRange::new(-1, -1), 0..0)]
#[case::positive(PyStyleIdxRange::new(0, 5), 0..0)]
fn resolve_for_empty_slice_never_panics(
#[case] range: PyStyleIdxRange,
#[case] expected: Range<usize>,
) {
let slice: [(); 0] = [];
let resolved = range.resolve_for(&slice);
assert_eq!(resolved, expected);
let _ = &slice[resolved];
}
}