ferritin_core/data/
segmentation.rs1use std::ops::Range;
8
9#[derive(Clone, Debug, PartialEq)]
14pub struct Segmentation {
15 offsets: Vec<u32>, }
17
18impl Segmentation {
19 pub fn try_from_offsets(offsets: Vec<u32>) -> Result<Self, String> {
21 if offsets.is_empty() {
22 return Err("offsets must have at least one element".to_string());
23 }
24 if offsets[0] != 0 {
25 return Err(format!("offsets must start at 0; found {}", offsets[0]));
26 }
27 for window in offsets.windows(2) {
28 if window[0] > window[1] {
29 return Err(format!(
30 "offsets must be monotonically non-decreasing; found {} > {}",
31 window[0], window[1]
32 ));
33 }
34 }
35 Ok(Self { offsets })
36 }
37
38 pub fn from_offsets(offsets: Vec<u32>) -> Self {
43 Self::try_from_offsets(offsets).expect("invalid segmentation offsets")
44 }
45
46 pub fn from_change_points<K: PartialEq>(keys: impl Iterator<Item = K>) -> Self {
57 let mut offsets: Vec<u32> = vec![0];
58 let mut prev: Option<K> = None;
59 let mut idx: u32 = 0;
60
61 for key in keys {
62 match &prev {
63 None => {}
64 Some(p) if *p == key => {}
65 Some(_) => {
66 offsets.push(idx);
67 }
68 }
69 prev = Some(key);
70 idx += 1;
71 }
72 offsets.push(idx); Self { offsets }
74 }
75
76 pub fn offsets(&self) -> &[u32] {
78 &self.offsets
79 }
80
81 pub fn count(&self) -> usize {
83 self.offsets.len().saturating_sub(1)
84 }
85
86 pub fn element_count(&self) -> usize {
88 self.offsets.last().copied().unwrap_or(0) as usize
89 }
90
91 pub fn segment(&self, seg: usize) -> Range<usize> {
96 let start = self.offsets[seg] as usize;
97 let end = self.offsets[seg + 1] as usize;
98 start..end
99 }
100
101 pub fn segment_of(&self, elem: usize) -> usize {
108 assert!(
109 elem < self.element_count(),
110 "element {} is out of range (element_count={})",
111 elem,
112 self.element_count()
113 );
114 let elem_u32 = elem as u32;
115 let pos = self.offsets.partition_point(|&o| o <= elem_u32);
118 assert!(pos > 0, "element {} is out of range", elem);
119 pos - 1
120 }
121
122 pub fn iter(&self) -> impl Iterator<Item = Range<usize>> + '_ {
124 self.offsets
125 .windows(2)
126 .map(|w| (w[0] as usize)..(w[1] as usize))
127 }
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133
134 #[test]
135 fn test_segmentation_from_change_points() {
136 let chain_ids = ["A", "A", "A", "B", "B", "C"];
137 let seg = Segmentation::from_change_points(chain_ids.iter().copied());
138 assert_eq!(seg.count(), 3);
139 assert_eq!(seg.segment(0), 0..3);
140 assert_eq!(seg.segment(1), 3..5);
141 assert_eq!(seg.segment(2), 5..6);
142 }
143
144 #[test]
145 fn test_segmentation_from_offsets() {
146 let seg = Segmentation::from_offsets(vec![0, 3, 5, 6]);
147 assert_eq!(seg.count(), 3);
148 assert_eq!(seg.segment(0), 0..3);
149 assert_eq!(seg.segment(1), 3..5);
150 assert_eq!(seg.segment(2), 5..6);
151 }
152
153 #[test]
154 fn test_segmentation_segment_of() {
155 let seg = Segmentation::from_offsets(vec![0, 3, 5, 6]);
156 assert_eq!(seg.segment_of(0), 0);
158 assert_eq!(seg.segment_of(1), 0);
159 assert_eq!(seg.segment_of(2), 0);
160 assert_eq!(seg.segment_of(3), 1);
162 assert_eq!(seg.segment_of(4), 1);
163 assert_eq!(seg.segment_of(5), 2);
165 }
166
167 #[test]
168 #[should_panic(expected = "out of range")]
169 fn test_segmentation_segment_of_rejects_end_sentinel() {
170 Segmentation::from_offsets(vec![0, 3, 5]).segment_of(5);
171 }
172
173 #[test]
174 fn test_segmentation_round_trip() {
175 let chain_ids = ["A", "A", "A", "B", "B", "C"];
176 let seg = Segmentation::from_change_points(chain_ids.iter().copied());
177
178 let expected = [0usize, 0, 0, 1, 1, 2];
180 for (elem, &exp_seg) in expected.iter().enumerate() {
181 assert_eq!(
182 seg.segment_of(elem),
183 exp_seg,
184 "elem {} should be in segment {}",
185 elem,
186 exp_seg
187 );
188 }
189 }
190
191 #[test]
192 fn test_segmentation_single_segment() {
193 let keys = ["X", "X", "X", "X"];
194 let seg = Segmentation::from_change_points(keys.iter().copied());
195 assert_eq!(seg.count(), 1);
196 assert_eq!(seg.segment(0), 0..4);
197 for elem in 0..4 {
198 assert_eq!(seg.segment_of(elem), 0);
199 }
200 }
201
202 #[test]
203 fn test_segmentation_all_different() {
204 let keys = ["A", "B", "C", "D"];
205 let seg = Segmentation::from_change_points(keys.iter().copied());
206 assert_eq!(seg.count(), 4);
207 for i in 0..4 {
208 assert_eq!(seg.segment(i), i..(i + 1));
209 assert_eq!(seg.segment_of(i), i);
210 }
211 }
212
213 #[test]
214 fn test_segmentation_iter() {
215 let seg = Segmentation::from_offsets(vec![0, 3, 5, 6]);
216 let ranges: Vec<Range<usize>> = seg.iter().collect();
217 assert_eq!(ranges, vec![0..3, 3..5, 5..6]);
218 }
219
220 #[test]
221 #[should_panic(expected = "monotonically non-decreasing")]
222 fn test_segmentation_from_offsets_non_monotonic_panics() {
223 Segmentation::from_offsets(vec![0, 10, 5, 15]);
224 }
225
226 #[test]
227 fn test_segmentation_try_from_offsets_rejects_nonzero_start() {
228 assert_eq!(
229 Segmentation::try_from_offsets(vec![2, 5]).unwrap_err(),
230 "offsets must start at 0; found 2"
231 );
232 }
233}