added tests and benchmarks

This commit is contained in:
Logan 2023-07-01 12:13:05 -05:00
parent 893849870c
commit ed78c4e210
8 changed files with 281 additions and 205 deletions

2
Cargo.lock generated
View file

@ -3,5 +3,5 @@
version = 3
[[package]]
name = "ndrange"
name = "nrange"
version = "0.1.0"

View file

@ -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]

1
src/bounds_ext.rs Normal file
View file

@ -0,0 +1 @@
use std::ops::RangeBounds;

View file

@ -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<R, T: Step, const N: usize>
pub enum NRangeIter<R, T: Step, const N: usize>
where
R: RangeBounds<T>,
R: ExactSizeIterator,
Self: Sized,
{
Iterating {
ndrange: NdRange<R, T, N>,
nrange: NRange<R, T, N>,
current: [T; N],
},
Done,
}
impl<R, T, const N: usize> Iterator for NdIter<R, T, N>
impl<R, T, const N: usize> Iterator for NRangeIter<R, T, N>
where
R: RangeBounds<T>,
R: ExactSizeIterator,
@ -28,16 +28,19 @@ where
fn next(&mut self) -> Option<Self::Item> {
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<usize>) {
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<R, T, const N: usize> ExactSizeIterator for NdIter<R, T, N>
impl<R, T, const N: usize> ExactSizeIterator for NRangeIter<R, T, N>
where
R: RangeBounds<T>,
R: ExactSizeIterator,
@ -74,7 +79,7 @@ where
}
}
impl<R, T, const N: usize> FusedIterator for NdIter<R, T, N>
impl<R, T, const N: usize> FusedIterator for NRangeIter<R, T, N>
where
R: RangeBounds<T>,
R: ExactSizeIterator,

View file

@ -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<T>(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;

View file

@ -1,186 +0,0 @@
use std::{iter::Step, marker::PhantomData, ops::RangeBounds};
use crate::nditer::NdIter;
pub(crate) fn get_bound_start<R, T>(s: &R) -> T
where
R: RangeBounds<T>,
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<R, Idx, const N: usize>
where
R: RangeBounds<Idx>,
{
pub(crate) ranges: [R; N],
pub(crate) _phantom: PhantomData<Idx>,
}
impl<R, Idx, const N: usize> std::fmt::Debug for NdRange<R, Idx, N>
where
R: RangeBounds<Idx>,
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<R, Idx, const N: usize> PartialEq for NdRange<R, Idx, N>
where
R: RangeBounds<Idx>,
R: PartialEq<R>,
{
fn eq(&self, other: &Self) -> bool {
for i in 0..N {
if self.ranges[i] != other.ranges[i] {
return false;
}
}
return true;
}
}
impl<R, Idx, const N: usize> NdRange<R, Idx, N>
where
R: RangeBounds<Idx>,
{
pub fn new(ranges: [R; N]) -> Self {
Self {
ranges,
_phantom: PhantomData,
}
}
}
impl<R, Idx, const N: usize> NdRange<R, Idx, N>
where
R: RangeBounds<Idx>,
Idx: PartialOrd<Idx>,
{
pub fn contains<U>(&self, other: &[U; N]) -> bool
where
Idx: PartialOrd<U>,
U: PartialOrd<Idx>,
{
for i in 0..N {
if !self.ranges[i].contains(&other[i]) {
return false;
}
}
return true;
}
}
impl<R, Idx, const N: usize> NdRange<R, Idx, N>
where
Self: Sized,
R: RangeBounds<Idx>,
R: ExactSizeIterator,
R: Clone,
Idx: Step,
{
pub fn len(&self) -> usize {
self.clone().into_iter().len()
}
}
impl<R, Idx, const N: usize> IntoIterator for NdRange<R, Idx, N>
where
R: RangeBounds<Idx>,
R: ExactSizeIterator,
Idx: Step,
{
type IntoIter = NdIter<R, Idx, N>;
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<R, Idx, const N: usize> Default for NdRange<R, Idx, N>
where
R: RangeBounds<Idx>,
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,)* ])
};
}

147
src/range.rs Normal file
View file

@ -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>
/// Idx: range bound type (see [`std::ops::Range`])
/// N: dimensionality of the range (the *N* in `NRange`)
#[derive(Clone, Eq, Hash)]
pub struct NRange<R, Idx, const N: usize>
where
R: RangeBounds<Idx>,
{
pub bounds: [R; N],
pub(crate) _phantom: PhantomData<Idx>,
}
impl<R, Idx, const N: usize> NRange<R, Idx, N>
where
R: RangeBounds<Idx>,
{
pub fn new(ranges: [R; N]) -> Self {
Self {
bounds: ranges,
_phantom: PhantomData,
}
}
}
impl<R, Idx, const N: usize> std::fmt::Debug for NRange<R, Idx, N>
where
R: RangeBounds<Idx>,
R: std::fmt::Debug,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.bounds.fmt(f)?;
Ok(())
}
}
impl<R, Idx, const N: usize> PartialEq for NRange<R, Idx, N>
where
R: RangeBounds<Idx>,
R: PartialEq<R>,
{
fn eq(&self, other: &Self) -> bool {
for i in 0..N {
if self.bounds[i] != other.bounds[i] {
return false;
}
}
return true;
}
}
impl<R, Idx, const N: usize> NRange<R, Idx, N>
where
R: RangeBounds<Idx>,
Idx: PartialOrd<Idx>,
{
pub fn contains<U>(&self, other: &[U; N]) -> bool
where
Idx: PartialOrd<U>,
U: PartialOrd<Idx>,
{
for i in 0..N {
if !self.bounds[i].contains(&other[i]) {
return false;
}
}
return true;
}
}
impl<R, Idx, const N: usize> NRange<R, Idx, N>
where
Self: Sized,
R: RangeBounds<Idx>,
R: ExactSizeIterator,
R: Clone,
Idx: Step,
{
pub fn len(&self) -> usize {
self.clone().into_iter().len()
}
}
impl<R, Idx, const N: usize> IntoIterator for NRange<R, Idx, N>
where
R: RangeBounds<Idx>,
R: ExactSizeIterator,
Idx: Step,
{
type IntoIter = NRangeIter<R, Idx, N>;
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<R, Idx, const N: usize> Default for NRange<R, Idx, N>
where
R: RangeBounds<Idx>,
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)
}
);
}
}

61
src/tests.rs Normal file
View file

@ -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]);
}
}
}
});
}