use super::{AxisLookup, sealed};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct UniformAxis<const N: usize, const ORIGIN: u16, const STEP: u16>(());
impl<const N: usize, const ORIGIN: u16, const STEP: u16> Default for UniformAxis<N, ORIGIN, STEP> {
fn default() -> Self {
Self::new()
}
}
impl<const N: usize, const ORIGIN: u16, const STEP: u16> UniformAxis<N, ORIGIN, STEP> {
#[must_use]
pub const fn new() -> Self {
assert!(N >= 2, "an axis must declare at least two knots");
assert!(
N <= 65_536,
"a uniform u16 axis declares at most 65_536 knots"
);
assert!(
STEP >= 1,
"a uniform axis must declare a step of at least 1"
);
assert!(
(ORIGIN as usize) + (N - 1) * (STEP as usize) <= u16::MAX as usize,
"the last uniform knot must be representable in u16"
);
Self(())
}
#[must_use]
pub const fn origin(&self) -> u16 {
ORIGIN
}
#[must_use]
pub const fn step(&self) -> u16 {
STEP
}
const fn nth(index: usize) -> u16 {
((ORIGIN as u32) + (index as u32) * (STEP as u32)) as u16
}
#[must_use]
pub const fn knot(&self, index: usize) -> u16 {
assert!(index < N, "knot index is outside the axis");
Self::nth(index)
}
#[must_use]
#[inline(always)]
pub const fn last(&self) -> u16 {
Self::nth(N - 1)
}
}
impl<const N: usize, const ORIGIN: u16, const STEP: u16> sealed::Sealed<N>
for UniformAxis<N, ORIGIN, STEP>
{
#[inline(always)]
fn search_in_domain(&self, coordinate: u16) -> (usize, u32) {
debug_assert!(
ORIGIN <= coordinate && coordinate <= <Self as AxisLookup<N>>::last(self),
"the sealed search is only called on an in-domain coordinate"
);
let index = ((coordinate - ORIGIN) / STEP) as usize;
debug_assert!(index < N, "a located index must stay inside the axis");
(index, 0)
}
}
impl<const N: usize, const ORIGIN: u16, const STEP: u16> AxisLookup<N>
for UniformAxis<N, ORIGIN, STEP>
{
const KNOT_BYTES: usize = 0;
const INDEX_BYTES: usize = 0;
const MAX_SEARCH_COMPARISONS: u32 = 0;
fn first(&self) -> u16 {
ORIGIN
}
#[inline(always)]
fn last(&self) -> u16 {
Self::last(self)
}
#[inline(always)]
fn knot(&self, index: usize) -> u16 {
Self::knot(self, index)
}
}
#[cfg(test)]
mod tests {
use super::UniformAxis;
use crate::axis::{AxisLookup, BinaryAxis};
use core::mem::size_of;
const SMALL: UniformAxis<5, 0, 25> = UniformAxis::new();
const OFFSET: UniformAxis<9, 100, 50> = UniformAxis::new();
const UNIT: UniformAxis<2, 7, 1> = UniformAxis::new();
const WIDE: UniformAxis<2, 0, 65_535> = UniformAxis::new();
const FULL_COUNT: UniformAxis<65_536, 0, 1> = UniformAxis::new();
static SMALL_KNOTS: [u16; 5] = [0, 25, 50, 75, 100];
static OFFSET_KNOTS: [u16; 9] = [100, 150, 200, 250, 300, 350, 400, 450, 500];
static WIDE_KNOTS: [u16; 2] = [0, 65_535];
#[test]
fn the_declared_knots_are_the_stored_knots_of_the_equivalent_axis() {
for (index, &knot) in SMALL_KNOTS.iter().enumerate() {
assert_eq!(SMALL.knot(index), knot);
}
for (index, &knot) in OFFSET_KNOTS.iter().enumerate() {
assert_eq!(OFFSET.knot(index), knot);
}
assert_eq!((SMALL.first(), SMALL.last()), (0, 100));
assert_eq!((OFFSET.first(), OFFSET.last()), (100, 500));
assert_eq!((UNIT.first(), UNIT.last()), (7, 8));
assert_eq!((WIDE.first(), WIDE.last()), (0, 65_535));
assert_eq!((FULL_COUNT.first(), FULL_COUNT.last()), (0, 65_535));
assert_eq!(FULL_COUNT.knot(65_535), 65_535);
}
#[test]
fn arithmetic_location_agrees_with_the_binary_search_of_the_same_axis() {
let binary = BinaryAxis::new(&SMALL_KNOTS);
for coordinate in 0u16..=100 {
assert_eq!(
SMALL.search(coordinate).0,
binary.search(coordinate).0,
"at {coordinate}"
);
}
let binary = BinaryAxis::new(&OFFSET_KNOTS);
for coordinate in 100u16..=500 {
assert_eq!(
OFFSET.search(coordinate).0,
binary.search(coordinate).0,
"at {coordinate}"
);
}
let binary = BinaryAxis::new(&WIDE_KNOTS);
for coordinate in [0u16, 1, 32_767, 32_768, 65_534, 65_535] {
assert_eq!(WIDE.search(coordinate).0, binary.search(coordinate).0);
}
}
#[test]
fn a_uniform_search_compares_no_knots_at_all() {
assert_eq!(<UniformAxis<9, 100, 50>>::MAX_SEARCH_COMPARISONS, 0);
for coordinate in 100u16..=500 {
assert_eq!(OFFSET.search(coordinate).1, 0);
}
}
#[test]
fn a_uniform_axis_stores_nothing() {
assert_eq!(<UniformAxis<9, 100, 50>>::KNOT_BYTES, 0);
assert_eq!(<UniformAxis<9, 100, 50>>::INDEX_BYTES, 0);
assert_eq!(size_of::<UniformAxis<9, 100, 50>>(), 0);
assert_eq!(size_of::<UniformAxis<65_536, 0, 1>>(), 0);
}
#[test]
fn the_descriptor_is_readable_without_a_knot_array() {
assert_eq!(OFFSET.origin(), 100);
assert_eq!(OFFSET.step(), 50);
assert_eq!(WIDE.step(), 65_535);
}
#[test]
#[should_panic(expected = "an axis must declare at least two knots")]
fn a_one_knot_descriptor_is_rejected() {
let _ = <UniformAxis<1, 0, 25>>::new();
}
#[test]
#[should_panic(expected = "a uniform axis must declare a step of at least 1")]
fn a_zero_step_is_rejected() {
let _ = <UniformAxis<5, 0, 0>>::new();
}
#[test]
#[should_panic(expected = "the last uniform knot must be representable in u16")]
fn a_descriptor_whose_last_knot_leaves_u16_is_rejected() {
let _ = <UniformAxis<5, 60_000, 2_000>>::new();
}
#[cfg(target_pointer_width = "64")]
#[test]
#[should_panic(expected = "a uniform u16 axis declares at most 65_536 knots")]
fn an_oversized_count_is_rejected_before_narrowing() {
let _ = <UniformAxis<{ (u32::MAX as usize) + 2 }, 0, 1>>::new();
}
#[test]
#[should_panic(expected = "knot index is outside the axis")]
fn a_knot_index_outside_the_axis_is_rejected() {
let _ = SMALL.knot(5);
}
}