1use crate::error::{Error, Result};
23
24pub const PTS_MODULUS_33: u64 = 1 << 33;
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31#[non_exhaustive]
32pub enum SnapDirection {
33 Exact,
35 Before,
37 After,
40}
41
42impl SnapDirection {
43 pub fn name(&self) -> &'static str {
45 match self {
46 SnapDirection::Exact => "exact",
47 SnapDirection::Before => "before",
48 SnapDirection::After => "after",
49 }
50 }
51}
52broadcast_common::impl_spec_display!(SnapDirection);
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub struct ConditionedSplicePoint {
58 pub requested_pts: u64,
61 pub snapped_pts: u64,
63 pub delta_ticks: u64,
65 pub direction: SnapDirection,
67}
68
69impl ConditionedSplicePoint {
70 pub fn is_exact(&self) -> bool {
72 self.delta_ticks == 0
73 }
74}
75
76pub fn condition_splice_point(
92 requested_pts: u64,
93 candidates: &[u64],
94 max_delta_ticks: u64,
95) -> Result<ConditionedSplicePoint> {
96 let mut nearest: Option<(u64, u64)> = None; for &c in candidates {
98 let delta = c.abs_diff(requested_pts);
99 if nearest.is_none_or(|(best_c, best)| {
103 delta < best || (delta == best && c > requested_pts && best_c < requested_pts)
104 }) {
105 nearest = Some((c, delta));
106 }
107 }
108 let (snapped_pts, delta_ticks) = nearest.ok_or(Error::NoCandidates)?;
109 if delta_ticks > max_delta_ticks {
110 return Err(Error::NoAlignedBoundary {
111 requested_pts,
112 tolerance_ticks: max_delta_ticks,
113 nearest_delta_ticks: delta_ticks,
114 });
115 }
116 let direction = if snapped_pts == requested_pts {
117 SnapDirection::Exact
118 } else if snapped_pts < requested_pts {
119 SnapDirection::Before
120 } else {
121 SnapDirection::After
122 };
123 Ok(ConditionedSplicePoint {
124 requested_pts,
125 snapped_pts,
126 delta_ticks,
127 direction,
128 })
129}
130
131pub fn condition_splice_point_wrapping(
147 requested_pts: u64,
148 candidates: &[u64],
149 max_delta_ticks: u64,
150 modulus: u64,
151) -> Result<ConditionedSplicePoint> {
152 if modulus == 0 || requested_pts >= modulus {
153 return Err(Error::PtsOutOfRange {
154 pts: requested_pts,
155 modulus,
156 });
157 }
158 let m = u128::from(modulus);
159 let r = u128::from(requested_pts);
160 let mut nearest: Option<(u64, u64, bool)> = None;
162 for &c in candidates {
163 if c >= modulus {
164 return Err(Error::PtsOutOfRange { pts: c, modulus });
165 }
166 let cw = u128::from(c);
167 let forward = (cw + m - r) % m;
168 let backward = (r + m - cw) % m;
169 let (delta, is_forward) = if forward <= backward {
170 (forward, true)
171 } else {
172 (backward, false)
173 };
174 let delta = u64::try_from(delta).unwrap_or(u64::MAX);
176 if nearest.is_none_or(|(_, best, best_forward)| {
179 delta < best || (delta == best && is_forward && !best_forward)
180 }) {
181 nearest = Some((c, delta, is_forward));
182 }
183 }
184 let (snapped_pts, delta_ticks, is_forward) = nearest.ok_or(Error::NoCandidates)?;
185 if delta_ticks > max_delta_ticks {
186 return Err(Error::NoAlignedBoundary {
187 requested_pts,
188 tolerance_ticks: max_delta_ticks,
189 nearest_delta_ticks: delta_ticks,
190 });
191 }
192 let direction = if delta_ticks == 0 {
193 SnapDirection::Exact
194 } else if is_forward {
195 SnapDirection::After
196 } else {
197 SnapDirection::Before
198 };
199 Ok(ConditionedSplicePoint {
200 requested_pts,
201 snapped_pts,
202 delta_ticks,
203 direction,
204 })
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210
211 #[test]
212 fn snaps_to_nearest_candidate() {
213 let result = condition_splice_point(1_000, &[500, 1_050, 2_000], 100).unwrap();
214 assert_eq!(result.snapped_pts, 1_050);
215 assert_eq!(result.delta_ticks, 50);
216 assert_eq!(result.direction, SnapDirection::After);
217 assert!(!result.is_exact());
218 }
219
220 #[test]
221 fn exact_match_reports_zero_delta() {
222 let result = condition_splice_point(1_000, &[1_000], 0).unwrap();
223 assert_eq!(result.delta_ticks, 0);
224 assert_eq!(result.direction, SnapDirection::Exact);
225 assert!(result.is_exact());
226 }
227
228 #[test]
229 fn before_direction_when_candidate_precedes_request() {
230 let result = condition_splice_point(1_000, &[900], 200).unwrap();
231 assert_eq!(result.snapped_pts, 900);
232 assert_eq!(result.direction, SnapDirection::Before);
233 }
234
235 #[test]
236 fn rejects_a_candidate_outside_tolerance() {
237 let err = condition_splice_point(1_000, &[2_000], 500).unwrap_err();
238 match err {
239 Error::NoAlignedBoundary {
240 requested_pts,
241 tolerance_ticks,
242 nearest_delta_ticks,
243 } => {
244 assert_eq!(requested_pts, 1_000);
245 assert_eq!(tolerance_ticks, 500);
246 assert_eq!(nearest_delta_ticks, 1_000);
247 }
248 other => panic!("expected NoAlignedBoundary, got {other:?}"),
249 }
250 }
251
252 #[test]
253 fn rejects_empty_candidates() {
254 let err = condition_splice_point(1_000, &[], 500).unwrap_err();
255 assert!(matches!(err, Error::NoCandidates));
256 }
257
258 #[test]
262 fn wrapping_snaps_across_the_33_bit_boundary() {
263 let cue = PTS_MODULUS_33 - 100;
264 assert!(matches!(
265 condition_splice_point(cue, &[50], 1_000),
266 Err(Error::NoAlignedBoundary { .. })
267 ));
268 let r = condition_splice_point_wrapping(cue, &[50], 1_000, PTS_MODULUS_33).unwrap();
269 assert_eq!(r.snapped_pts, 50);
270 assert_eq!(r.delta_ticks, 150);
271 assert_eq!(r.direction, SnapDirection::After);
272 }
273
274 #[test]
275 fn wrapping_direction_before_across_the_wrap() {
276 let r = condition_splice_point_wrapping(50, &[PTS_MODULUS_33 - 100], 1_000, PTS_MODULUS_33)
277 .unwrap();
278 assert_eq!(r.delta_ticks, 150);
279 assert_eq!(r.direction, SnapDirection::Before);
280 }
281
282 #[test]
283 fn wrapping_picks_nearest_among_both_sides_of_the_wrap() {
284 let cue = PTS_MODULUS_33 - 100;
285 let r = condition_splice_point_wrapping(
286 cue,
287 &[PTS_MODULUS_33 - 500, 90, 4_000],
288 10_000,
289 PTS_MODULUS_33,
290 )
291 .unwrap();
292 assert_eq!(r.snapped_pts, 90);
293 assert_eq!(r.delta_ticks, 190);
294 let r = condition_splice_point_wrapping(
295 cue,
296 &[PTS_MODULUS_33 - 150, 90],
297 10_000,
298 PTS_MODULUS_33,
299 )
300 .unwrap();
301 assert_eq!(r.snapped_pts, PTS_MODULUS_33 - 150);
302 assert_eq!(r.delta_ticks, 50);
303 assert_eq!(r.direction, SnapDirection::Before);
304 }
305
306 #[test]
307 fn wrapping_exact_and_tolerance_and_tie() {
308 let r = condition_splice_point_wrapping(7, &[7], 0, PTS_MODULUS_33).unwrap();
309 assert_eq!(r.direction, SnapDirection::Exact);
310 assert!(r.is_exact());
311 let err = condition_splice_point_wrapping(PTS_MODULUS_33 - 100, &[50], 149, PTS_MODULUS_33)
312 .unwrap_err();
313 assert!(matches!(
314 err,
315 Error::NoAlignedBoundary {
316 nearest_delta_ticks: 150,
317 tolerance_ticks: 149,
318 ..
319 }
320 ));
321 let r = condition_splice_point_wrapping(0, &[5], 5, 10).unwrap();
322 assert_eq!((r.delta_ticks, r.direction), (5, SnapDirection::After));
323 }
324
325 #[test]
326 fn wrapping_rejects_out_of_range_inputs_and_empty_sets() {
327 assert!(matches!(
328 condition_splice_point_wrapping(PTS_MODULUS_33, &[1], 10, PTS_MODULUS_33),
329 Err(Error::PtsOutOfRange { .. })
330 ));
331 assert!(matches!(
332 condition_splice_point_wrapping(1, &[PTS_MODULUS_33], 10, PTS_MODULUS_33),
333 Err(Error::PtsOutOfRange { .. })
334 ));
335 assert!(matches!(
336 condition_splice_point_wrapping(0, &[0], 10, 0),
337 Err(Error::PtsOutOfRange { .. })
338 ));
339 assert!(matches!(
340 condition_splice_point_wrapping(1, &[], 10, PTS_MODULUS_33),
341 Err(Error::NoCandidates)
342 ));
343 let r = condition_splice_point_wrapping(u64::MAX - 2, &[1], u64::MAX, u64::MAX).unwrap();
345 assert_eq!(r.delta_ticks, 3);
346 }
347
348 #[test]
351 fn an_equidistant_tie_prefers_after_regardless_of_order() {
352 for candidates in [[900u64, 1_100], [1_100, 900]] {
353 let r = condition_splice_point(1_000, &candidates, 200).unwrap();
354 assert_eq!((r.snapped_pts, r.direction), (1_100, SnapDirection::After));
355 }
356 for candidates in [[900u64, 1_100], [1_100, 900]] {
357 let r =
358 condition_splice_point_wrapping(1_000, &candidates, 200, PTS_MODULUS_33).unwrap();
359 assert_eq!((r.snapped_pts, r.direction), (1_100, SnapDirection::After));
360 }
361 for candidates in [[PTS_MODULUS_33 - 100, 100], [100, PTS_MODULUS_33 - 100]] {
363 let r = condition_splice_point_wrapping(0, &candidates, 200, PTS_MODULUS_33).unwrap();
364 assert_eq!((r.snapped_pts, r.direction), (100, SnapDirection::After));
365 }
366 }
367
368 #[test]
369 fn direction_labels() {
370 assert_eq!(SnapDirection::Exact.name(), "exact");
371 assert_eq!(alloc::format!("{}", SnapDirection::After), "after");
372 }
373}