From ed78c4e210164cce4ea9cfad6843868bc126c0c5 Mon Sep 17 00:00:00 2001 From: Logan Date: Sat, 1 Jul 2023 12:13:05 -0500 Subject: [PATCH] added tests and benchmarks --- Cargo.lock | 2 +- Cargo.toml | 5 +- src/bounds_ext.rs | 1 + src/{nditer.rs => iter.rs} | 33 ++++--- src/lib.rs | 51 +++++++++- src/ndrange.rs | 186 ------------------------------------- src/range.rs | 147 +++++++++++++++++++++++++++++ src/tests.rs | 61 ++++++++++++ 8 files changed, 281 insertions(+), 205 deletions(-) create mode 100644 src/bounds_ext.rs rename src/{nditer.rs => iter.rs} (59%) delete mode 100644 src/ndrange.rs create mode 100644 src/range.rs create mode 100644 src/tests.rs diff --git a/Cargo.lock b/Cargo.lock index fb7f5e2..63b32e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3,5 +3,5 @@ version = 3 [[package]] -name = "ndrange" +name = "nrange" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 09b4d28..28fc7de 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,11 @@ [package] -name = "ndrange" +name = "nrange" version = "0.1.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[profile.bench] +opt-level = 3 + [dependencies] diff --git a/src/bounds_ext.rs b/src/bounds_ext.rs new file mode 100644 index 0000000..3b1039e --- /dev/null +++ b/src/bounds_ext.rs @@ -0,0 +1 @@ +use std::ops::RangeBounds; diff --git a/src/nditer.rs b/src/iter.rs similarity index 59% rename from src/nditer.rs rename to src/iter.rs index 51f7c47..698284a 100644 --- a/src/nditer.rs +++ b/src/iter.rs @@ -3,22 +3,22 @@ use std::{ ops::RangeBounds, }; -use crate::{get_bound_start, ndrange::NdRange}; +use crate::{get_real_bound, range::NRange}; -pub enum NdIter +pub enum NRangeIter where R: RangeBounds, R: ExactSizeIterator, Self: Sized, { Iterating { - ndrange: NdRange, + nrange: NRange, current: [T; N], }, Done, } -impl Iterator for NdIter +impl Iterator for NRangeIter where R: RangeBounds, R: ExactSizeIterator, @@ -28,16 +28,19 @@ where fn next(&mut self) -> Option { match self { - NdIter::Iterating { ndrange, current } => { + NRangeIter::Iterating { + nrange: ndrange, + current, + } => { let ret = current.clone(); for i in 0..current.len() { let next = T::forward(current[i].clone(), 1); - if ndrange.ranges[i].contains(&next) { + if ndrange.bounds[i].contains(&next) { current[i] = next.clone(); break; } else { - current[i] = get_bound_start(&ndrange.ranges[i]); - if i == (current.len() - 1) { + current[i] = get_real_bound(ndrange.bounds[i].start_bound()); + if i == (N - 1) { *self = Self::Done; break; } @@ -45,25 +48,27 @@ where } Some(ret) }, - NdIter::Done => None, + NRangeIter::Done => None, } } fn size_hint(&self) -> (usize, Option) { let mut size = 1; match self { - NdIter::Iterating { ndrange, .. } => { - for i in &ndrange.ranges { + NRangeIter::Iterating { + nrange: ndrange, .. + } => { + for i in &ndrange.bounds { size *= i.len(); } (size, Some(size)) }, - NdIter::Done => (0, None), + NRangeIter::Done => (0, None), } } } -impl ExactSizeIterator for NdIter +impl ExactSizeIterator for NRangeIter where R: RangeBounds, R: ExactSizeIterator, @@ -74,7 +79,7 @@ where } } -impl FusedIterator for NdIter +impl FusedIterator for NRangeIter where R: RangeBounds, R: ExactSizeIterator, diff --git a/src/lib.rs b/src/lib.rs index 26378b3..05c4f1f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,50 @@ +#![feature(test)] #![feature(step_trait)] -pub mod nditer; -pub mod ndrange; -pub use ndrange::*; +//! The `nrange` crate provides an abstraction of the +//! standard range types over *n*-dimensions +//! While the traditional range types cover a set of linear +//! values, the [`NRange`](crate::range::NRange) type +//! cover a region in vector space. `NRange` can represent +//! the points in a 2D rectangle, 3D cube, 4D hypercube, or +//! extend to any number of dimensions using rust's const +//! generics. +//! # Getting started +//! The preferred way of initializing `NRange` is using +//! the provided macro. The syntax is similar to the `vec!` +//! macro: +//! ``` +//! use nrange::*; +//! let range_a = nrange!(0..3, 0..3); +//! let range_b = nrange!(0..3; 2); +//! assert_eq!(range_a, range_b); +//! +//! assert!(range_a.contains(&[0, 2])); +//! +//! for v in range_a { +//! print!("{v:?}, "); // [0, 0], [1, 0], [2, 0], [0, 1], ... +//! } +//! ``` + +pub(crate) fn get_real_bound(bound: Bound<&T>) -> T +where + T: Step, + T: Clone, +{ + match bound { + Bound::Included(s) => s.clone(), + Bound::Excluded(s) => Step::forward(s.clone(), 1), + Bound::Unbounded => panic!("Cannot get real bound"), + } +} + +pub mod bounds_ext; +pub mod iter; +pub mod range; + +use std::{iter::Step, ops::Bound}; + +pub use range::*; + +#[cfg(test)] +mod tests; diff --git a/src/ndrange.rs b/src/ndrange.rs deleted file mode 100644 index fde8350..0000000 --- a/src/ndrange.rs +++ /dev/null @@ -1,186 +0,0 @@ -use std::{iter::Step, marker::PhantomData, ops::RangeBounds}; - -use crate::nditer::NdIter; - -pub(crate) fn get_bound_start(s: &R) -> T -where - R: RangeBounds, - R: ExactSizeIterator, - T: Step, - T: Clone, -{ - match s.start_bound() { - std::ops::Bound::Included(s) => s.clone(), - std::ops::Bound::Excluded(s) => Step::forward(s.clone(), 1), - std::ops::Bound::Unbounded => unreachable!( - "Implementing exact size iterator means the range cannot be unbounded \ - at the start" - ), - } -} - -/// A N-Dimensional extension to Range and its derivatives -/// -/// # Examples -/// ``` -/// assert_eq!(ndrange!(0..5, 0..5), NdRange::new([0..5, 0..5])); -/// assert_eq!(ndrange!(0..3, 0..3).len(), 9); -/// -/// let mut iter = ndrange!(0..2, 0..2).into_iter(); -/// assert_eq!(iter.next(), Some([0, 0])); -/// assert_eq!(iter.next(), Some([1, 0])); -/// assert_eq!(iter.next(), Some([0, 1])); -/// assert_eq!(iter.next(), Some([1, 1])); -/// assert_eq!(iter.next(), None); -/// -/// assert!(ndrange!(0..10, 0..10).contains(&[2, 4])); -/// -/// assert_eq!(ndrange!().len(), 0); -/// assert_eq!(ndrange!(0..7, 0..0).len(), 0); -/// assert!(!ndrange!(0..3, 0..2, 0..0).contains(&[2, 1, 0])); -/// -/// for i in NdRange::new([0..5, 0..5]) { -/// println!("{:?}", i); -/// } -/// -/// for _ in ndrange!() { -/// panic!("Unreachable, empty NdRange returns an empty -/// iterator") } -/// -/// for i in ndrange!(0..5, 0..5) { -/// println!("{i:?}"); -/// } -/// ``` -#[derive(Clone, Eq, Hash)] -pub struct NdRange -where - R: RangeBounds, -{ - pub(crate) ranges: [R; N], - pub(crate) _phantom: PhantomData, -} - -impl std::fmt::Debug for NdRange -where - R: RangeBounds, - R: std::fmt::Debug, -{ - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "[ ")?; - for r in &self.ranges { - r.fmt(f)?; - write!(f, ", ")?; - } - write!(f, "]")?; - Ok(()) - } -} - -impl PartialEq for NdRange -where - R: RangeBounds, - R: PartialEq, -{ - fn eq(&self, other: &Self) -> bool { - for i in 0..N { - if self.ranges[i] != other.ranges[i] { - return false; - } - } - return true; - } -} - -impl NdRange -where - R: RangeBounds, -{ - pub fn new(ranges: [R; N]) -> Self { - Self { - ranges, - _phantom: PhantomData, - } - } -} - -impl NdRange -where - R: RangeBounds, - Idx: PartialOrd, -{ - pub fn contains(&self, other: &[U; N]) -> bool - where - Idx: PartialOrd, - U: PartialOrd, - { - for i in 0..N { - if !self.ranges[i].contains(&other[i]) { - return false; - } - } - return true; - } -} - -impl NdRange -where - Self: Sized, - R: RangeBounds, - R: ExactSizeIterator, - R: Clone, - Idx: Step, -{ - pub fn len(&self) -> usize { - self.clone().into_iter().len() - } -} - -impl IntoIterator for NdRange -where - R: RangeBounds, - R: ExactSizeIterator, - Idx: Step, -{ - type IntoIter = NdIter; - type Item = [Idx; N]; - - fn into_iter(self) -> Self::IntoIter { - let mut empty = N == 0; - for i in 0..N { - if self.ranges[i].len() == 0 { - empty = true; - break; - } - } - - match empty { - true => NdIter::Done, - false => NdIter::Iterating { - current: std::array::from_fn(|i| get_bound_start(&self.ranges[i])), - ndrange: self, - }, - } - } -} - -impl Default for NdRange -where - R: RangeBounds, - R: Default, -{ - fn default() -> Self { - Self { - ranges: std::array::from_fn(|_| R::default()), - _phantom: PhantomData, - } - } -} - -#[macro_export] -macro_rules! ndrange { - - ( $( $x:expr ), + $(,)? ) => { - NdRange::new([ $($x,)* ]) - }; - -} diff --git a/src/range.rs b/src/range.rs new file mode 100644 index 0000000..b097a68 --- /dev/null +++ b/src/range.rs @@ -0,0 +1,147 @@ +use std::{iter::Step, marker::PhantomData, ops::RangeBounds}; + +use crate::{get_real_bound, iter::NRangeIter}; + +/// N-dimensional range +/// R: any range type implementing RangeBounds +/// Idx: range bound type (see [`std::ops::Range`]) +/// N: dimensionality of the range (the *N* in `NRange`) +#[derive(Clone, Eq, Hash)] +pub struct NRange +where + R: RangeBounds, +{ + pub bounds: [R; N], + pub(crate) _phantom: PhantomData, +} + +impl NRange +where + R: RangeBounds, +{ + pub fn new(ranges: [R; N]) -> Self { + Self { + bounds: ranges, + _phantom: PhantomData, + } + } +} + +impl std::fmt::Debug for NRange +where + R: RangeBounds, + R: std::fmt::Debug, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.bounds.fmt(f)?; + Ok(()) + } +} + +impl PartialEq for NRange +where + R: RangeBounds, + R: PartialEq, +{ + fn eq(&self, other: &Self) -> bool { + for i in 0..N { + if self.bounds[i] != other.bounds[i] { + return false; + } + } + return true; + } +} + +impl NRange +where + R: RangeBounds, + Idx: PartialOrd, +{ + pub fn contains(&self, other: &[U; N]) -> bool + where + Idx: PartialOrd, + U: PartialOrd, + { + for i in 0..N { + if !self.bounds[i].contains(&other[i]) { + return false; + } + } + return true; + } +} + +impl NRange +where + Self: Sized, + R: RangeBounds, + R: ExactSizeIterator, + R: Clone, + Idx: Step, +{ + pub fn len(&self) -> usize { + self.clone().into_iter().len() + } +} + +impl IntoIterator for NRange +where + R: RangeBounds, + R: ExactSizeIterator, + Idx: Step, +{ + type IntoIter = NRangeIter; + type Item = [Idx; N]; + + fn into_iter(self) -> Self::IntoIter { + let mut empty = N == 0; + for i in 0..N { + if self.bounds[i].len() == 0 { + empty = true; + break; + } + } + + match empty { + true => NRangeIter::Done, + false => NRangeIter::Iterating { + current: std::array::from_fn(|i| { + get_real_bound(self.bounds[i].start_bound()) + }), + nrange: self, + }, + } + } +} + +impl Default for NRange +where + R: RangeBounds, + R: Default, +{ + fn default() -> Self { + Self { + bounds: std::array::from_fn(|_| R::default()), + _phantom: PhantomData, + } + } +} +#[macro_use] +pub mod create_macro { + #[macro_export] + macro_rules! nrange { + () => {compile_error!("NRange cannot have 0 dimensions!")}; + + ($( $x:expr ), + $(,)?) => { + $crate::range::NRange::new([ $($x,)* ]) + }; + + ($elem:expr; $n:expr) => ( + { + let arr: [_; $n] = std::array::from_fn(|_| {$elem}); + $crate::range::NRange::new(arr) + } + ); + } +} diff --git a/src/tests.rs b/src/tests.rs new file mode 100644 index 0000000..10de555 --- /dev/null +++ b/src/tests.rs @@ -0,0 +1,61 @@ +use super::*; +extern crate test; +use std::hint::black_box; +use test::Bencher; + +#[test] +fn eq() { + let arr = [ + nrange!(1..3, 1..3), + nrange!(1..3; 2), + NRange::new([1..3, 1..3]), + ]; + + for a in &arr { + for b in &arr { + assert_eq!(a, b); + } + } +} + +#[test] +fn order() { + let mut nd = nrange!(0..2, 0..2).into_iter(); + assert_eq!(nd.next(), Some([0, 0])); + assert_eq!(nd.next(), Some([1, 0])); + assert_eq!(nd.next(), Some([0, 1])); +} + +#[test] +fn emptiness() { + let nd = nrange!(0..100, 0..99999, 0..0).into_iter(); + assert_eq!(nd.len(), 0); +} + +#[test] +fn length() { + let nd = nrange!(0..100, 0..100, 0..33).into_iter(); + assert_eq!(nd.len(), 100 * 100 * 33); +} + +#[bench] +fn nd_benchmark(b: &mut Bencher) { + b.iter(|| { + for i in nrange!(0..100; 3).into_iter() { + black_box(i); + } + }); +} + +#[bench] +fn linear_benchmark(b: &mut Bencher) { + b.iter(|| { + for x in (0..100).into_iter() { + for y in (0..100).into_iter() { + for z in (0..100).into_iter() { + black_box(&[x, y, z]); + } + } + } + }); +}