#[derive(Debug, Clone, Copy)]
struct Run<Identity> {
first: (f64, f64),
last: (f64, f64),
min: (f64, f64),
max: (f64, f64),
break_before: bool,
identity: Identity,
}
impl<Identity: Copy + Eq> Run<Identity> {
fn new(point: (f64, f64), break_before: bool, identity: Identity) -> Self {
Run {
first: point,
last: point,
min: point,
max: point,
break_before,
identity,
}
}
fn add(&mut self, point: (f64, f64)) {
self.last = point;
if point.1 < self.min.1 {
self.min = point;
}
if point.1 > self.max.1 {
self.max = point;
}
}
fn merge(&mut self, later: Run<Identity>) {
debug_assert!(self.identity == later.identity);
self.last = later.last;
if later.min.1 < self.min.1 {
self.min = later.min;
}
if later.max.1 > self.max.1 {
self.max = later.max;
}
}
fn points(self) -> [(f64, f64); 4] {
let mut points = [self.first, self.min, self.max, self.last];
points.sort_by(|a, b| a.0.total_cmp(&b.0));
points
}
}
#[derive(Debug, Clone)]
struct Bucket<Identity> {
completed: Vec<Run<Identity>>,
current: Run<Identity>,
}
impl<Identity: Copy + Eq> Bucket<Identity> {
fn new(run: Run<Identity>) -> Self {
Bucket {
completed: Vec::new(),
current: run,
}
}
fn last_mut(&mut self) -> &mut Run<Identity> {
&mut self.current
}
fn first(&self) -> &Run<Identity> {
match self.completed.first() {
Some(run) => run,
None => &self.current,
}
}
fn first_mut(&mut self) -> &mut Run<Identity> {
match self.completed.first_mut() {
Some(run) => run,
None => &mut self.current,
}
}
fn push(&mut self, run: Run<Identity>) {
let completed = std::mem::replace(&mut self.current, run);
self.completed.push(completed);
}
fn append(&mut self, mut later: Bucket<Identity>) {
let completed = std::mem::replace(&mut self.current, later.current);
self.completed.push(completed);
self.completed.append(&mut later.completed);
}
fn merge_first_and_append(&mut self, later: Bucket<Identity>) {
let mut runs = later.into_runs();
self.current
.merge(runs.next().expect("a bucket always contains one run"));
for run in runs {
self.push(run);
}
}
fn into_runs(self) -> impl Iterator<Item = Run<Identity>> {
self.completed
.into_iter()
.chain(std::iter::once(self.current))
}
}
#[derive(Debug, Clone, Copy)]
enum DomainMap {
Direct { start: f64, span: f64 },
Scaled { scale: f64, start: f64, span: f64 },
}
impl DomainMap {
fn new((start, end): (f64, f64)) -> Self {
let span = end - start;
if span.is_finite() {
return Self::Direct { start, span };
}
let scale = start.abs().max(end.abs());
let scaled_start = start / scale;
Self::Scaled {
scale,
start: scaled_start,
span: end / scale - scaled_start,
}
}
fn normalize(self, value: f64) -> f64 {
match self {
DomainMap::Direct { start, span } => (value - start) / span,
DomainMap::Scaled { scale, start, span } => (value / scale - start) / span,
}
}
}
#[derive(Debug, Clone)]
struct Aggregate<Identity> {
domain: (f64, f64),
domain_map: DomainMap,
buckets: Vec<Option<Bucket<Identity>>>,
pending_gap: bool,
}
impl<Identity: Copy + Eq> Aggregate<Identity> {
fn try_new(domain: (f64, f64), columns: usize) -> crate::Result<Self> {
if !(domain.0.is_finite() && domain.1.is_finite()) {
return Err(crate::Error::InvalidParameter {
detail: "M4 needs a finite domain",
});
}
if columns == 0 {
return Err(crate::Error::EmptyDimension { what: "M4 columns" });
}
if columns > super::MAX_STAT_ELEMENTS {
return Err(crate::Error::DimensionTooLarge {
what: "M4 column count",
requested: columns,
limit: super::MAX_STAT_ELEMENTS,
});
}
let mut buckets = Vec::new();
buckets
.try_reserve_exact(columns)
.map_err(|_| crate::Error::AllocationFailed { what: "M4 buckets" })?;
buckets.resize(columns, None);
Ok(Self {
domain,
domain_map: DomainMap::new(domain),
buckets,
pending_gap: false,
})
}
fn add(&mut self, x: f64, y: f64, identity: Identity) {
if !x.is_finite() {
self.gap();
return;
}
if let Some(index) = self.bucket_index(x) {
self.record(index, x, y, identity);
}
}
fn record(&mut self, index: usize, x: f64, y: f64, identity: Identity) {
if !y.is_finite() {
self.gap();
return;
}
let point = (x, y);
let break_before = std::mem::take(&mut self.pending_gap);
match &mut self.buckets[index] {
Some(bucket) if break_before => bucket.push(Run::new(point, true, identity)),
Some(bucket) => {
let last = bucket.last_mut();
if last.identity == identity {
last.add(point);
} else {
bucket.push(Run::new(point, true, identity));
}
}
None => {
self.buckets[index] = Some(Bucket::new(Run::new(point, break_before, identity)));
}
}
}
fn gap(&mut self) {
self.pending_gap = true;
}
fn merge(&mut self, later: &Self) {
assert!(
self.domain == later.domain && self.buckets.len() == later.buckets.len(),
"M4::merge requires identical domains and column counts"
);
let self_last = self.buckets.iter().rposition(Option::is_some);
let later_first = later.buckets.iter().position(Option::is_some);
let boundary_gap = self.pending_gap;
for (index, (mine, theirs)) in self
.buckets
.iter_mut()
.zip(later.buckets.iter())
.enumerate()
{
let Some(theirs) = theirs else { continue };
let mut theirs = theirs.clone();
if Some(index) == later_first && boundary_gap {
theirs.first_mut().break_before = true;
}
match mine {
Some(bucket) => {
let same_boundary_bucket =
Some(index) == self_last && Some(index) == later_first;
let same_identity = bucket.last_mut().identity == theirs.first().identity;
if same_boundary_bucket && !theirs.first().break_before && same_identity {
bucket.merge_first_and_append(theirs);
} else {
if same_boundary_bucket && !same_identity {
theirs.first_mut().break_before = true;
}
bucket.append(theirs);
}
}
None => *mine = Some(theirs),
}
}
self.pending_gap = if later_first.is_some() {
later.pending_gap
} else {
self.pending_gap || later.pending_gap
};
}
fn into_runs(self) -> impl Iterator<Item = Run<Identity>> {
self.buckets
.into_iter()
.flatten()
.flat_map(Bucket::into_runs)
}
fn bucket_index(&self, x: f64) -> Option<usize> {
let (lo, hi) = self.domain;
if x < lo || x > hi {
return None;
}
if hi == lo {
return Some(0);
}
let position = self.domain_map.normalize(x) * self.buckets.len() as f64;
Some((position as usize).min(self.buckets.len() - 1))
}
}
#[derive(Debug, Clone)]
pub struct M4 {
aggregate: Aggregate<()>,
}
impl M4 {
pub fn new(domain: (f64, f64), columns: usize) -> M4 {
M4::try_new(domain, columns)
.expect("M4::new requires a finite domain and a bounded non-empty grid")
}
pub fn try_new(domain: (f64, f64), columns: usize) -> crate::Result<M4> {
Ok(M4 {
aggregate: Aggregate::try_new(domain, columns)?,
})
}
pub fn add(&mut self, x: f64, y: f64) {
self.aggregate.add(x, y, ());
}
fn record(&mut self, index: usize, x: f64, y: f64) {
self.aggregate.record(index, x, y, ());
}
fn gap(&mut self) {
self.aggregate.gap();
}
pub fn merge(&mut self, later: &M4) {
self.aggregate.merge(&later.aggregate);
}
pub fn emit(self) -> (Vec<f64>, Vec<f64>) {
emit(self.aggregate)
}
}
fn push_point(x: &mut Vec<f64>, y: &mut Vec<f64>, point: (f64, f64)) -> bool {
if x.last() == Some(&point.0) && y.last() == Some(&point.1) {
return false;
}
x.push(point.0);
y.push(point.1);
true
}
fn emit(aggregate: Aggregate<()>) -> (Vec<f64>, Vec<f64>) {
let capacity = aggregate.buckets.len() * 4;
let mut x = Vec::with_capacity(capacity);
let mut y = Vec::with_capacity(capacity);
for run in aggregate.into_runs() {
if run.break_before && y.last().is_none_or(|value: &f64| !value.is_nan()) {
x.push(f64::NAN);
y.push(f64::NAN);
}
for point in run.points() {
push_point(&mut x, &mut y, point);
}
}
(x, y)
}
fn emit_categories(aggregate: Aggregate<usize>) -> (Vec<f64>, Vec<f64>, Vec<usize>) {
let capacity = aggregate.buckets.len() * 4;
let mut x = Vec::with_capacity(capacity);
let mut y = Vec::with_capacity(capacity);
let mut categories = Vec::with_capacity(capacity);
for run in aggregate.into_runs() {
if run.break_before && y.last().is_none_or(|value: &f64| !value.is_nan()) {
x.push(f64::NAN);
y.push(f64::NAN);
categories.push(run.identity);
}
for point in run.points() {
if push_point(&mut x, &mut y, point) {
categories.push(run.identity);
}
}
}
(x, y, categories)
}
pub fn m4(x: &[f64], y: &[f64], columns: usize) -> Option<(Vec<f64>, Vec<f64>)> {
assert_eq!(x.len(), y.len(), "m4 requires series of equal length");
let columns = columns.max(1);
if columns > super::MAX_STAT_ELEMENTS {
return None;
}
let mut lo = f64::INFINITY;
let mut hi = f64::NEG_INFINITY;
let mut previous = f64::NEG_INFINITY;
for &value in x {
if !value.is_finite() {
continue;
}
if value < previous {
return None;
}
previous = value;
lo = lo.min(value);
hi = hi.max(value);
}
if !lo.is_finite() {
return None;
}
let mut aggregate = M4::try_new((lo, hi), columns).ok()?;
for (&xv, &yv) in x.iter().zip(y.iter()) {
aggregate.add(xv, yv);
}
Some(aggregate.emit())
}
pub(crate) fn m4_mapped(
x: Option<&[f64]>,
y: &[f64],
columns: usize,
map: impl Fn(f64) -> f64,
) -> Option<(Vec<f64>, Vec<f64>)> {
if columns == 0 || columns > super::MAX_STAT_ELEMENTS {
return None;
}
let mut aggregate = M4::try_new((0.0, 1.0), columns).ok()?;
let mut previous = f64::NEG_INFINITY;
let length = x.map_or(y.len(), |values| values.len().min(y.len()));
for (index, &yv) in y.iter().take(length).enumerate() {
let xv = match x {
Some(values) => values[index],
None => index as f64,
};
if !xv.is_finite() {
aggregate.gap();
continue;
}
if xv < previous {
return None;
}
previous = xv;
if !yv.is_finite() {
aggregate.gap();
continue;
}
let position = map(xv);
if !position.is_finite() {
aggregate.gap();
continue;
}
let column = position.round();
if (0.0..columns as f64).contains(&column) {
aggregate.record(column as usize, xv, yv);
}
}
Some(aggregate.emit())
}
pub(crate) fn m4_mapped_categories(
x: Option<&[f64]>,
y: &[f64],
categories: &[usize],
columns: usize,
map: impl Fn(f64) -> f64,
) -> Option<(Vec<f64>, Vec<f64>, Vec<usize>)> {
if columns == 0 || columns > super::MAX_STAT_ELEMENTS {
return None;
}
let mut aggregate: Aggregate<usize> = Aggregate::try_new((0.0, 1.0), columns).ok()?;
let mut previous_x = f64::NEG_INFINITY;
let mut previous_category = None;
let length = x
.map_or(y.len(), |values| values.len().min(y.len()))
.min(categories.len());
for (index, &yv) in y.iter().take(length).enumerate() {
let category = categories[index];
if previous_category.is_some_and(|previous| previous != category) {
aggregate.gap();
}
previous_category = Some(category);
let xv = match x {
Some(values) => values[index],
None => index as f64,
};
if !xv.is_finite() {
aggregate.gap();
continue;
}
if xv < previous_x {
return None;
}
previous_x = xv;
if !yv.is_finite() {
aggregate.gap();
continue;
}
let position = map(xv);
if !position.is_finite() {
aggregate.gap();
continue;
}
let column = position.round();
if (0.0..columns as f64).contains(&column) {
aggregate.record(column as usize, xv, yv, category);
}
}
Some(emit_categories(aggregate))
}
#[cfg(test)]
#[path = "tests/m4_tests.rs"]
mod tests;