Skip to main content

citum_engine/values/
range.rs

1/*
2SPDX-License-Identifier: MIT OR Apache-2.0
3SPDX-FileCopyrightText: © 2023-2026 Bruce D'Arcus and Citum contributors
4*/
5
6//! Shared helpers for collapsing ordered consecutive numbering into spans.
7
8/// One collapsed segment from an ordered numeric sequence.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum ConsecutiveSegment {
11    /// A single standalone value.
12    Single(u32),
13    /// A consecutive range from `start` to `end`, inclusive.
14    Range {
15        /// The first value in the consecutive range.
16        start: u32,
17        /// The last value in the consecutive range.
18        end: u32,
19    },
20}
21
22/// Collapse an ordered sequence into standalone values and consecutive ranges.
23///
24/// Duplicate values are coalesced, and descending steps start a new segment.
25#[must_use]
26pub fn consecutive_segments(values: &[u32]) -> Vec<ConsecutiveSegment> {
27    let mut iter = values.iter();
28    let Some(&first) = iter.next() else {
29        return Vec::new();
30    };
31
32    let mut segments = Vec::new();
33    let mut start = first;
34    let mut prev = first;
35
36    for &value in iter {
37        if value == prev {
38            continue;
39        }
40
41        if value == prev + 1 {
42            prev = value;
43            continue;
44        }
45
46        push_segment(&mut segments, start, prev);
47        start = value;
48        prev = value;
49    }
50
51    push_segment(&mut segments, start, prev);
52    segments
53}
54
55fn push_segment(segments: &mut Vec<ConsecutiveSegment>, start: u32, end: u32) {
56    if start == end {
57        segments.push(ConsecutiveSegment::Single(start));
58    } else {
59        segments.push(ConsecutiveSegment::Range { start, end });
60    }
61}
62
63#[cfg(test)]
64#[allow(
65    clippy::unwrap_used,
66    clippy::expect_used,
67    clippy::panic,
68    clippy::indexing_slicing,
69    clippy::todo,
70    clippy::unimplemented,
71    clippy::unreachable,
72    clippy::get_unwrap,
73    reason = "Panicking is acceptable and often desired in tests."
74)]
75mod tests {
76    use super::*;
77
78    #[test]
79    fn test_consecutive_segments() {
80        for (input, expected) in [
81            (&[][..], vec![]),
82            (&[1][..], vec![ConsecutiveSegment::Single(1)]),
83            (
84                &[1, 2, 3][..],
85                vec![ConsecutiveSegment::Range { start: 1, end: 3 }],
86            ),
87            (
88                &[1, 3, 5][..],
89                vec![
90                    ConsecutiveSegment::Single(1),
91                    ConsecutiveSegment::Single(3),
92                    ConsecutiveSegment::Single(5),
93                ],
94            ),
95            (
96                &[1, 2, 4, 5, 6, 8][..],
97                vec![
98                    ConsecutiveSegment::Range { start: 1, end: 2 },
99                    ConsecutiveSegment::Range { start: 4, end: 6 },
100                    ConsecutiveSegment::Single(8),
101                ],
102            ),
103            (
104                &[1, 1, 2, 2, 3][..],
105                vec![ConsecutiveSegment::Range { start: 1, end: 3 }],
106            ),
107            (
108                &[3, 2, 1][..],
109                vec![
110                    ConsecutiveSegment::Single(3),
111                    ConsecutiveSegment::Single(2),
112                    ConsecutiveSegment::Single(1),
113                ],
114            ),
115        ] {
116            assert_eq!(consecutive_segments(input), expected);
117        }
118    }
119}