Skip to main content

gam_runtime/
span.rs

1/// Select the span containing `value`, using `[left, right)` for every span
2/// except the final span, which is right-closed.
3pub 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    /// Documents the `span_index_for_breakpoints` helper convention only.
30    /// Specific design evaluators may override this for endpoint convention.
31    /// Anchored deviation runtimes apply a LEFT-bias at interior breakpoints so
32    /// span-local third derivatives are reported from the left span; their
33    /// cubic basis is C², so value, first derivative, and second derivative are
34    /// unaffected by that choice.
35    #[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        // 0.5 is in [0,1) → span 0
94        assert_eq!(span_index_for_breakpoints(&bp, 0.5, "t").unwrap(), 0);
95        // 1.5 is in [1,2) → span 1
96        assert_eq!(span_index_for_breakpoints(&bp, 1.5, "t").unwrap(), 1);
97        // 2.5 is in [2,3) → span 2
98        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}