use super::{AxisLookup, KnotArray, assert_valid_knots, sealed};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct LinearAxis<const N: usize> {
knots: &'static [u16; N],
}
impl<const N: usize> LinearAxis<N> {
#[must_use]
pub const fn new(knots: &'static [u16; N]) -> Self {
assert_valid_knots(knots);
Self { knots }
}
#[must_use]
pub const fn knots(&self) -> &'static [u16; N] {
self.knots
}
}
impl<const N: usize> sealed::Sealed<N> for LinearAxis<N> {
#[inline(always)]
fn search_in_domain(&self, coordinate: u16) -> (usize, u32) {
debug_assert!(
self.knots[0] <= coordinate && coordinate <= self.knots[N - 1],
"the sealed search is only called on an in-domain coordinate"
);
let mut index = 0;
let mut comparisons = 0;
while index + 1 < N {
comparisons += 1;
if self.knots[index + 1] > coordinate {
break;
}
index += 1;
}
debug_assert!(
comparisons <= <Self as AxisLookup<N>>::MAX_SEARCH_COMPARISONS,
"the scan must stay inside the documented bound"
);
debug_assert!(
self.knots[index] <= coordinate,
"the located knot must not sit above the coordinate"
);
(index, comparisons)
}
}
impl<const N: usize> KnotArray<N> for LinearAxis<N> {
fn knots(&self) -> &'static [u16; N] {
self.knots
}
}
impl<const N: usize> AxisLookup<N> for LinearAxis<N> {
const KNOT_BYTES: usize = 2 * N;
const INDEX_BYTES: usize = 0;
const MAX_SEARCH_COMPARISONS: u32 = (N - 1) as u32;
fn first(&self) -> u16 {
self.knots[0]
}
fn last(&self) -> u16 {
self.knots[N - 1]
}
fn knot(&self, index: usize) -> u16 {
self.knots[index]
}
}
#[cfg(test)]
mod tests {
use super::LinearAxis;
use crate::axis::{AxisLookup, BinaryAxis, KnotArray, probes};
static X_TINY: [u16; 2] = [7, 9];
static X_MAIN: [u16; 5] = [10, 20, 30, 40, 50];
static X_SPARSE: [u16; 6] = [3, 4, 5, 1_000, 40_000, 65_000];
static X_FULL: [u16; 4] = [0, 1, 32_768, 65_535];
const TINY: LinearAxis<2> = LinearAxis::new(&X_TINY);
const MAIN: LinearAxis<5> = LinearAxis::new(&X_MAIN);
const SPARSE: LinearAxis<6> = LinearAxis::new(&X_SPARSE);
const FULL: LinearAxis<4> = LinearAxis::new(&X_FULL);
#[test]
fn the_scan_locates_the_same_index_as_the_binary_search() {
macro_rules! agrees_with_binary {
($linear:expr, $knots:expr, $stride:expr) => {
let linear = $linear;
let binary = BinaryAxis::new($knots);
for coordinate in probes($knots, $stride) {
assert_eq!(
linear.search(coordinate).0,
binary.search(coordinate).0,
"at {coordinate}"
);
}
};
}
agrees_with_binary!(TINY, &X_TINY, 1);
agrees_with_binary!(MAIN, &X_MAIN, 1);
agrees_with_binary!(SPARSE, &X_SPARSE, 97);
agrees_with_binary!(FULL, &X_FULL, 97);
}
#[test]
fn every_knot_locates_itself() {
for (index, &knot) in X_MAIN.iter().enumerate() {
assert_eq!(MAIN.search(knot).0, index, "at knot {knot}");
}
for (index, &knot) in X_SPARSE.iter().enumerate() {
assert_eq!(SPARSE.search(knot).0, index, "at knot {knot}");
}
}
#[test]
fn the_scan_never_exceeds_its_declared_bound() {
assert_eq!(<LinearAxis<2>>::MAX_SEARCH_COMPARISONS, 1);
assert_eq!(<LinearAxis<5>>::MAX_SEARCH_COMPARISONS, 4);
assert_eq!(<LinearAxis<6>>::MAX_SEARCH_COMPARISONS, 5);
for coordinate in 10u16..=50 {
assert!(MAIN.search(coordinate).1 <= <LinearAxis<5>>::MAX_SEARCH_COMPARISONS);
}
assert_eq!(MAIN.search(50).1, 4);
assert_eq!(MAIN.search(10).1, 1);
assert_eq!(TINY.search(9).1, 1);
}
#[test]
fn the_knot_array_is_referenced_and_never_copied() {
assert!(core::ptr::eq(MAIN.knots(), &X_MAIN));
assert!(core::ptr::eq(KnotArray::knots(&MAIN), &X_MAIN));
assert_eq!(<LinearAxis<5>>::KNOT_BYTES, 10);
assert_eq!(<LinearAxis<5>>::INDEX_BYTES, 0);
}
#[test]
#[should_panic(expected = "an axis must declare at least two knots")]
fn a_one_knot_axis_is_rejected() {
static ONE: [u16; 1] = [3];
let _ = LinearAxis::new(&ONE);
}
#[test]
#[should_panic(expected = "axis knots must be strictly increasing")]
fn a_descending_knot_is_rejected() {
static DESCENDING: [u16; 3] = [0, 100, 50];
let _ = LinearAxis::new(&DESCENDING);
}
}