1pub fn span_index_for_breakpoints(
4 breakpoints: &[f64],
5 value: f64,
6 label: &str,
7) -> Result<usize, String> {
8 if !value.is_finite() {
9 return Err(format!("{label} requires finite value, got {value}"));
10 }
11 if breakpoints.len() < 2 {
12 return Err(format!("{label} requires at least two breakpoints"));
13 }
14 let last_idx = breakpoints.len() - 1;
15 if value <= breakpoints[0] {
16 return Ok(0);
17 }
18 if value >= breakpoints[last_idx] {
19 return Ok(last_idx - 1);
20 }
21 let insertion_idx = breakpoints.partition_point(|point| *point <= value);
22 Ok((insertion_idx - 1).min(last_idx - 1))
23}
24
25#[cfg(test)]
26mod tests {
27 use super::span_index_for_breakpoints;
28
29 #[test]
36 fn internal_breakpoints_use_right_hand_span() {
37 let breakpoints = [-1.5, -0.9, 0.4, 2.0];
38 assert_eq!(
39 span_index_for_breakpoints(&breakpoints, -1.5, "test span lookup").unwrap(),
40 0
41 );
42 assert_eq!(
43 span_index_for_breakpoints(&breakpoints, -0.9, "test span lookup").unwrap(),
44 1
45 );
46 assert_eq!(
47 span_index_for_breakpoints(&breakpoints, 0.4, "test span lookup").unwrap(),
48 2
49 );
50 assert_eq!(
51 span_index_for_breakpoints(&breakpoints, 2.0, "test span lookup").unwrap(),
52 2
53 );
54 }
55
56 #[test]
57 fn value_below_first_breakpoint_returns_span_zero() {
58 let bp = [0.0, 1.0, 2.0];
59 assert_eq!(span_index_for_breakpoints(&bp, -5.0, "t").unwrap(), 0);
60 }
61
62 #[test]
63 fn value_above_last_breakpoint_returns_last_span() {
64 let bp = [0.0, 1.0, 2.0];
65 assert_eq!(span_index_for_breakpoints(&bp, 99.0, "t").unwrap(), 1);
66 }
67
68 #[test]
69 fn two_breakpoints_only_one_span() {
70 let bp = [0.0, 1.0];
71 assert_eq!(span_index_for_breakpoints(&bp, 0.5, "t").unwrap(), 0);
72 assert_eq!(span_index_for_breakpoints(&bp, 0.0, "t").unwrap(), 0);
73 assert_eq!(span_index_for_breakpoints(&bp, 1.0, "t").unwrap(), 0);
74 }
75
76 #[test]
77 fn non_finite_value_returns_error() {
78 let bp = [0.0, 1.0, 2.0];
79 assert!(span_index_for_breakpoints(&bp, f64::NAN, "t").is_err());
80 assert!(span_index_for_breakpoints(&bp, f64::INFINITY, "t").is_err());
81 assert!(span_index_for_breakpoints(&bp, f64::NEG_INFINITY, "t").is_err());
82 }
83
84 #[test]
85 fn fewer_than_two_breakpoints_returns_error() {
86 assert!(span_index_for_breakpoints(&[], 0.5, "t").is_err());
87 assert!(span_index_for_breakpoints(&[1.0], 0.5, "t").is_err());
88 }
89
90 #[test]
91 fn interior_midpoint_selects_correct_span() {
92 let bp = [0.0, 1.0, 2.0, 3.0];
93 assert_eq!(span_index_for_breakpoints(&bp, 0.5, "t").unwrap(), 0);
95 assert_eq!(span_index_for_breakpoints(&bp, 1.5, "t").unwrap(), 1);
97 assert_eq!(span_index_for_breakpoints(&bp, 2.5, "t").unwrap(), 2);
99 }
100
101 #[test]
102 fn error_message_contains_label() {
103 let err = span_index_for_breakpoints(&[0.0], 0.5, "my_var").unwrap_err();
104 assert!(err.contains("my_var"), "error should mention label, got: {err}");
105 }
106}