use std::collections::{BTreeMap, BTreeSet};
use std::sync::Arc;
use std::time::Duration;
use arrow_array::cast::AsArray;
use arrow_array::types::{Float16Type, Float32Type, Float64Type};
use arrow_array::{
new_null_array, Array, ArrayRef, ArrowPrimitiveType, Decimal128Array, Float64Array,
RecordBatch, RecordBatchOptions, UInt64Array,
};
use arrow_schema::{DataType, Field, Schema};
use arrow_select::interleave::interleave;
use crate::bucket::{float_starts, whole_starts, Bucket};
use crate::dictionary::dictionary_index;
use crate::filter::{resolve, Filter, Predicate};
use crate::sorted::{Entry, SortedRows};
use crate::value::{canonical, prefix, unscale, Kind, Value, ValueFn};
use crate::view::{entry, entry_order, Rows, SortColumn};
use crate::window::Span;
use crate::{DataError, RowId, RowRef};
const DECIMAL_LIMIT: u128 = 10u128.pow(38);
const fn power_of_two(exponent: i32) -> f64 {
f64::from_bits(((1023 + exponent) as u64) << 52)
}
const HUGE: f64 = power_of_two(960);
const UNSCALE: f64 = power_of_two(-128);
const SCALE: f64 = power_of_two(128);
#[derive(Debug, Clone, PartialEq, Eq)]
enum Function {
Count,
Sum(String),
Min(String),
Max(String),
Mean(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Aggregate {
function: Function,
name: Option<String>,
}
impl Aggregate {
#[must_use]
pub fn count() -> Self {
Self::of(Function::Count)
}
#[must_use]
pub fn sum(column: impl Into<String>) -> Self {
Self::of(Function::Sum(column.into()))
}
#[must_use]
pub fn min(column: impl Into<String>) -> Self {
Self::of(Function::Min(column.into()))
}
#[must_use]
pub fn max(column: impl Into<String>) -> Self {
Self::of(Function::Max(column.into()))
}
#[must_use]
pub fn mean(column: impl Into<String>) -> Self {
Self::of(Function::Mean(column.into()))
}
#[must_use]
pub fn named(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
fn of(function: Function) -> Self {
Self {
function,
name: None,
}
}
fn name(&self) -> String {
if let Some(name) = &self.name {
return name.clone();
}
let (function, column) = match &self.function {
Function::Count => return "count".to_owned(),
Function::Sum(column) => ("sum", column),
Function::Min(column) => ("min", column),
Function::Max(column) => ("max", column),
Function::Mean(column) => ("mean", column),
};
format!("{function}({column})")
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct GroupSpec {
keys: Vec<KeySpec>,
aggregates: Vec<Aggregate>,
filter: Option<Filter>,
}
impl GroupSpec {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn group_by(self, column: impl Into<String>) -> Self {
self.key(column, None)
}
#[must_use]
pub fn group_by_time(self, column: impl Into<String>, every: Duration) -> Self {
self.key(column, Some(Span::Duration(every)))
}
#[must_use]
pub fn group_by_step(self, column: impl Into<String>, step: f64) -> Self {
self.key(column, Some(Span::Value(step)))
}
fn key(mut self, column: impl Into<String>, bucket: Option<Span>) -> Self {
self.keys.push(KeySpec {
column: column.into(),
bucket,
});
self
}
#[must_use]
pub fn aggregate(mut self, aggregate: Aggregate) -> Self {
self.aggregates.push(aggregate);
self
}
#[must_use]
pub fn filter(mut self, filter: Filter) -> Self {
self.filter = Some(filter);
self
}
}
#[derive(Debug, Clone, PartialEq)]
struct KeySpec {
column: String,
bucket: Option<Span>,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
enum Part {
Bool(bool),
Int(i64),
UInt(u64),
Decimal(i128),
Float(u64),
Str(Box<str>),
Null,
}
impl Part {
fn of(value: Value<'_>) -> Self {
match value {
Value::Null => Self::Null,
Value::Bool(v) => Self::Bool(v),
Value::Int(v) => Self::Int(v),
Value::UInt(v) => Self::UInt(v),
Value::Decimal(v) => Self::Decimal(v),
Value::Float(_) => Self::Float(prefix(value, false)),
Value::Str(v) => Self::Str(v.into()),
}
}
}
fn from_prefix(bits: u64) -> f64 {
const SIGN: u64 = 1 << 63;
f64::from_bits(if bits & SIGN != 0 {
bits & !SIGN
} else {
!bits
})
}
#[derive(Debug, Clone)]
struct Source {
column: usize,
read: ValueFn,
kind: Kind,
data_type: DataType,
name: String,
}
impl Source {
fn new(schema: &Schema, name: &str) -> Result<Self, DataError> {
let (column, kind, read) = resolve(schema, name)?;
Ok(Self {
column,
read,
kind,
data_type: schema.field(column).data_type().clone(),
name: name.to_owned(),
})
}
fn read<'a>(&self, batches: &'a [RecordBatch], at: RowRef) -> Value<'a> {
(self.read)(batches[at.batch].column(self.column).as_ref(), at.row)
}
fn output_type(&self) -> &DataType {
match &self.data_type {
DataType::Dictionary(_, values) => values,
other => other,
}
}
}
#[derive(Debug, Clone)]
struct Key {
source: Source,
bucket: Option<Bucket>,
}
impl Key {
fn new(schema: &Schema, spec: &KeySpec) -> Result<Self, DataError> {
let source = Source::new(schema, &spec.column)?;
let bucket = spec
.bucket
.map(|width| {
Bucket::new(width, &source.data_type, source.kind)
.ok_or_else(|| DataError::BucketWidth(spec.column.clone()))
})
.transpose()?;
Ok(Self { source, bucket })
}
fn part(&self, batches: &[RecordBatch], at: RowRef) -> Part {
let value = self.source.read(batches, at);
Part::of(match &self.bucket {
Some(bucket) => bucket.start(value),
None => value,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Numeric {
Integer,
Decimal { scale: i8 },
Float,
}
impl Numeric {
fn of(data_type: &DataType) -> Option<Self> {
Some(match data_type {
DataType::Decimal128(_, scale) => Self::Decimal { scale: *scale },
t if t.is_integer() => Self::Integer,
t if t.is_floating() => Self::Float,
_ => return None,
})
}
}
#[derive(Debug, Clone)]
enum Track {
Sum(Numeric),
Order(SortColumn),
}
#[derive(Debug, Clone)]
struct Tracker {
source: Source,
track: Track,
}
impl Tracker {
fn apply(&self, state: &mut State, rows: Rows<'_>, id: RowId, at: RowRef, sign: i8) {
match (&self.track, state, self.source.read(rows.batches, at)) {
(_, _, Value::Null) => {}
(Track::Order(column), State::Ordered(sorted), _) => {
let sort = std::slice::from_ref(column);
let probe = entry(sort, rows.batches, id, at);
let order = |other: Entry| entry_order(sort, rows, other, probe, at);
if sign > 0 {
sorted.insert(probe, order);
} else {
let found = sorted.remove(id, order);
debug_assert!(found, "a counted value is among the sorted values");
}
}
(_, State::Exact(sum), Value::Int(v)) => sum.apply(i128::from(v), sign),
(_, State::Exact(sum), Value::UInt(v)) => sum.apply(i128::from(v), sign),
(_, State::Exact(sum), Value::Decimal(v)) => sum.apply(v, sign),
(_, State::Float(sum), Value::Float(v)) => sum.apply(v, sign),
_ => debug_assert!(false, "a tracker's state does not fit its column"),
}
}
}
fn step(count: u64, sign: i8) -> u64 {
if sign > 0 {
count + 1
} else {
count - 1
}
}
#[derive(Debug, Clone, Copy, Default)]
struct ExactSum {
sum: i128,
carry: i64,
values: u64,
}
impl ExactSum {
fn apply(&mut self, value: i128, sign: i8) {
let (sum, wrapped) = if sign > 0 {
self.sum.overflowing_add(value)
} else {
self.sum.overflowing_sub(value)
};
if wrapped {
self.carry += if (sign > 0) == (value > 0) { 1 } else { -1 };
}
self.sum = sum;
self.values = step(self.values, sign);
}
fn total(&self) -> Option<i128> {
(self.values > 0 && self.carry == 0).then_some(self.sum)
}
fn mean(&self) -> Option<f64> {
let total = self.sum as f64 + self.carry as f64 * SCALE;
(self.values > 0).then(|| total / self.values as f64)
}
}
#[derive(Debug, Clone, Copy, Default)]
struct Compensated {
sum: f64,
compensation: f64,
values: u64,
}
impl Compensated {
fn apply(&mut self, value: f64, sign: i8) {
self.values = step(self.values, sign);
if self.values == 0 {
*self = Self::default();
return;
}
let value = if sign > 0 { value } else { -value };
let total = self.sum + value;
if self.sum.abs() >= value.abs() {
self.compensation += (self.sum - total) + value;
} else {
self.compensation += (value - total) + self.sum;
}
self.sum = total;
}
fn total(&self) -> f64 {
self.sum + self.compensation
}
}
#[derive(Debug, Clone, Copy, Default)]
struct FloatSum {
normal: Compensated,
huge: Compensated,
nan: u64,
infinity: u64,
negative_infinity: u64,
}
impl FloatSum {
fn apply(&mut self, value: f64, sign: i8) {
if value.is_nan() {
self.nan = step(self.nan, sign);
} else if value == f64::INFINITY {
self.infinity = step(self.infinity, sign);
} else if value == f64::NEG_INFINITY {
self.negative_infinity = step(self.negative_infinity, sign);
} else if value.abs() >= HUGE {
self.huge.apply(value * UNSCALE, sign);
} else {
self.normal.apply(value, sign);
}
}
fn values(&self) -> u64 {
self.normal.values + self.huge.values + self.nan + self.infinity + self.negative_infinity
}
fn sum(&self) -> Option<f64> {
(self.values() > 0).then(|| self.divided(1.0))
}
fn mean(&self) -> Option<f64> {
let values = self.values();
(values > 0).then(|| self.divided(values as f64))
}
fn divided(&self, divisor: f64) -> f64 {
if self.nan > 0 || (self.infinity > 0 && self.negative_infinity > 0) {
return f64::NAN;
}
if self.infinity > 0 {
return f64::INFINITY;
}
if self.negative_infinity > 0 {
return f64::NEG_INFINITY;
}
let normal = self.normal.total() / divisor;
let huge = self.huge.total() / divisor;
if huge == 0.0 {
normal
} else {
(huge + normal * UNSCALE) * SCALE
}
}
}
#[derive(Debug, Clone)]
enum State {
Exact(ExactSum),
Float(FloatSum),
Ordered(SortedRows),
}
impl State {
fn new(track: &Track) -> Self {
match track {
Track::Sum(Numeric::Float) => Self::Float(FloatSum::default()),
Track::Sum(_) => Self::Exact(ExactSum::default()),
Track::Order(_) => Self::Ordered(SortedRows::default()),
}
}
fn exact(&self) -> Option<&ExactSum> {
match self {
Self::Exact(sum) => Some(sum),
_ => None,
}
}
fn float(&self) -> Option<&FloatSum> {
match self {
Self::Float(sum) => Some(sum),
_ => None,
}
}
fn extreme(&self, max: bool) -> Option<RowId> {
match self {
Self::Ordered(sorted) if max => sorted.last(),
Self::Ordered(sorted) => sorted.first(),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy)]
enum Output {
Count,
Sum(usize, Numeric),
Mean(usize, Numeric),
Min(usize),
Max(usize),
}
#[derive(Debug, Clone)]
struct Compiled {
output: Output,
name: String,
}
#[derive(Debug, Clone)]
struct Group {
rows: BTreeSet<RowId>,
states: Vec<State>,
}
impl Group {
fn new(trackers: &[Tracker]) -> Self {
Self {
rows: BTreeSet::new(),
states: trackers.iter().map(|t| State::new(&t.track)).collect(),
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct GroupState {
keys: Vec<Key>,
trackers: Vec<Tracker>,
aggregates: Vec<Compiled>,
filter: Option<Predicate>,
groups: BTreeMap<Vec<Part>, Group>,
member: Vec<bool>,
revision: u64,
}
impl GroupState {
pub(crate) fn compile(spec: &GroupSpec, schema: &Schema) -> Result<Self, DataError> {
let keys = spec
.keys
.iter()
.map(|k| Key::new(schema, k))
.collect::<Result<Vec<_>, _>>()?;
let mut trackers = Vec::new();
let aggregates = spec
.aggregates
.iter()
.map(|aggregate| {
let output = match &aggregate.function {
Function::Count => Output::Count,
Function::Sum(column) => {
let (index, numeric) = sum_tracker(&mut trackers, schema, column)?;
Output::Sum(index, numeric)
}
Function::Mean(column) => {
let (index, numeric) = sum_tracker(&mut trackers, schema, column)?;
Output::Mean(index, numeric)
}
Function::Min(column) => {
Output::Min(order_tracker(&mut trackers, schema, column)?)
}
Function::Max(column) => {
Output::Max(order_tracker(&mut trackers, schema, column)?)
}
};
Ok(Compiled {
output,
name: aggregate.name(),
})
})
.collect::<Result<Vec<_>, DataError>>()?;
let mut names = BTreeSet::new();
let all_names = keys
.iter()
.map(|k| &k.source.name)
.chain(aggregates.iter().map(|a| &a.name));
for name in all_names {
if !names.insert(name) {
return Err(DataError::DuplicateName(name.clone()));
}
}
let filter = spec
.filter
.as_ref()
.map(|f| Predicate::compile(f, schema))
.transpose()?;
let mut state = Self {
keys,
trackers,
aggregates,
filter,
groups: BTreeMap::new(),
member: Vec::new(),
revision: 0,
};
state.clear();
Ok(state)
}
fn clear(&mut self) {
self.groups.clear();
self.member.clear();
if self.keys.is_empty() {
self.groups.insert(Vec::new(), Group::new(&self.trackers));
}
}
fn passes(&self, batches: &[RecordBatch], at: RowRef) -> bool {
self.filter
.as_ref()
.is_none_or(|f| f.eval(&batches[at.batch], at.row) == Some(true))
}
fn key(&self, batches: &[RecordBatch], at: RowRef) -> Vec<Part> {
self.keys.iter().map(|k| k.part(batches, at)).collect()
}
fn count(&mut self, rows: Rows<'_>, id: RowId, at: RowRef, sorted: bool) -> bool {
if !self.passes(rows.batches, at) {
return false;
}
let key = self.key(rows.batches, at);
let trackers = &self.trackers;
let group = self
.groups
.entry(key)
.or_insert_with(|| Group::new(trackers));
group.rows.insert(id);
for (tracker, state) in trackers.iter().zip(&mut group.states) {
if sorted || !matches!(tracker.track, Track::Order(_)) {
tracker.apply(state, rows, id, at, 1);
}
}
if self.member.len() <= id as usize {
self.member.resize(id as usize + 1, false);
}
self.member[id as usize] = true;
true
}
pub(crate) fn insert(&mut self, rows: Rows<'_>, id: RowId, at: RowRef) {
if self.count(rows, id, at, true) {
self.revision += 1;
}
}
pub(crate) fn remove(&mut self, rows: Rows<'_>, id: RowId, at: RowRef) {
if !self.member.get(id as usize).copied().unwrap_or(false) {
return;
}
let key = self.key(rows.batches, at);
let Some(group) = self.groups.get_mut(&key) else {
debug_assert!(false, "a counted row's group exists");
return;
};
group.rows.remove(&id);
for (tracker, state) in self.trackers.iter().zip(&mut group.states) {
tracker.apply(state, rows, id, at, -1);
}
if group.rows.is_empty() && !self.keys.is_empty() {
self.groups.remove(&key);
}
self.member[id as usize] = false;
self.revision += 1;
}
pub(crate) fn rebuild(&mut self, rows: Rows<'_>) {
let had_rows = self.groups.values().any(|g| !g.rows.is_empty());
self.clear();
let mut counted = false;
for (id, slot) in rows.slots.iter().enumerate() {
if let (Ok(id), Some(slot)) = (RowId::try_from(id), slot) {
counted |= self.count(rows, id, slot.row_ref(), false);
}
}
self.sort_values(rows);
if had_rows || counted {
self.revision += 1;
}
}
fn sort_values(&mut self, rows: Rows<'_>) {
for (index, tracker) in self.trackers.iter().enumerate() {
let Track::Order(column) = &tracker.track else {
continue;
};
let sort = std::slice::from_ref(column);
for group in self.groups.values_mut() {
let mut entries: Vec<Entry> = group
.rows
.iter()
.map(|&id| (id, rows.at(id)))
.filter(|&(_, at)| tracker.source.read(rows.batches, at) != Value::Null)
.map(|(id, at)| entry(sort, rows.batches, id, at))
.collect();
entries.sort_unstable_by(|&a, &b| entry_order(sort, rows, a, b, rows.at(b.id)));
if let State::Ordered(sorted) = &mut group.states[index] {
sorted.rebuild(&entries);
}
}
}
}
}
fn sum_tracker(
trackers: &mut Vec<Tracker>,
schema: &Schema,
column: &str,
) -> Result<(usize, Numeric), DataError> {
let source = Source::new(schema, column)?;
let numeric =
Numeric::of(&source.data_type).ok_or_else(|| DataError::NotNumeric(column.to_owned()))?;
Ok((tracker(trackers, source, Track::Sum(numeric)), numeric))
}
fn order_tracker(
trackers: &mut Vec<Tracker>,
schema: &Schema,
column: &str,
) -> Result<usize, DataError> {
let source = Source::new(schema, column)?;
let sort = SortColumn::ascending(source.column, source.read);
Ok(tracker(trackers, source, Track::Order(sort)))
}
fn tracker(trackers: &mut Vec<Tracker>, source: Source, track: Track) -> usize {
let same = |t: &Tracker| {
t.source.column == source.column
&& std::mem::discriminant(&t.track) == std::mem::discriminant(&track)
};
if let Some(index) = trackers.iter().position(same) {
return index;
}
trackers.push(Tracker { source, track });
trackers.len() - 1
}
#[derive(Debug, Clone, Copy)]
pub struct Groups<'a> {
pub(crate) state: &'a GroupState,
pub(crate) rows: Rows<'a>,
}
impl Groups<'_> {
pub fn len(&self) -> usize {
self.state.groups.len()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn revision(&self) -> u64 {
self.state.revision
}
pub fn to_batch(&self) -> Result<RecordBatch, DataError> {
let groups: Vec<&Group> = self.state.groups.values().collect();
let mut fields = Vec::new();
let mut columns = Vec::new();
for (index, key) in self.state.keys.iter().enumerate() {
let source = &key.source;
columns.push(match key.bucket {
Some(_) => self.starts(index, source),
None => {
let rows = groups.iter().map(|g| g.rows.first().copied());
self.gather(source, rows)?
}
});
fields.push(Field::new(&source.name, source.output_type().clone(), true));
}
for aggregate in &self.state.aggregates {
let (column, data_type) = self.column(aggregate.output, &groups)?;
let nullable = !matches!(aggregate.output, Output::Count);
fields.push(Field::new(&aggregate.name, data_type, nullable));
columns.push(column);
}
let schema = Arc::new(Schema::new(fields));
let options = RecordBatchOptions::new().with_row_count(Some(groups.len()));
RecordBatch::try_new_with_options(schema, columns, &options).map_err(DataError::arrow)
}
fn column(&self, output: Output, groups: &[&Group]) -> Result<(ArrayRef, DataType), DataError> {
Ok(match output {
Output::Count => {
let counts: UInt64Array =
groups.iter().map(|g| Some(g.rows.len() as u64)).collect();
(Arc::new(counts), DataType::UInt64)
}
Output::Sum(index, numeric) => sums(numeric, groups.iter().map(|g| &g.states[index]))?,
Output::Mean(index, numeric) => {
let means: Float64Array = groups
.iter()
.map(|g| mean(&g.states[index], numeric))
.collect();
(Arc::new(means), DataType::Float64)
}
Output::Min(index) | Output::Max(index) => {
let max = matches!(output, Output::Max(_));
let rows = groups.iter().map(|g| g.states[index].extreme(max));
let source = &self.state.trackers[index].source;
(self.gather(source, rows)?, source.output_type().clone())
}
})
}
fn starts(&self, index: usize, source: &Source) -> ArrayRef {
let parts = self.state.groups.keys().map(|key| &key[index]);
if source.kind == Kind::Float {
let starts: Vec<Option<f64>> = parts
.map(|part| match part {
Part::Float(bits) => Some(from_prefix(*bits)),
_ => None,
})
.collect();
return float_starts(&source.data_type, &starts);
}
let starts: Vec<Option<i128>> = parts
.map(|part| match part {
Part::Decimal(start) => Some(*start),
_ => None,
})
.collect();
whole_starts(&source.data_type, &starts)
}
fn gather(
&self,
source: &Source,
ids: impl Iterator<Item = Option<RowId>>,
) -> Result<ArrayRef, DataError> {
let batches = self.rows.batches;
let column = |batch: usize| batches[batch].column(source.column).as_ref();
let decode = matches!(source.data_type, DataType::Dictionary(..));
let null = new_null_array(source.output_type(), 1);
let mut arrays: Vec<&dyn Array> = (0..batches.len())
.map(|batch| {
let array = column(batch);
match array.as_any_dictionary_opt() {
Some(dictionary) if decode => dictionary.values().as_ref(),
_ => array,
}
})
.collect();
arrays.push(null.as_ref());
let null_at = (arrays.len() - 1, 0);
let indices: Vec<(usize, usize)> = ids
.map(|id| {
let place = id.and_then(|id| {
let at = self.rows.at(id);
let row = if decode {
dictionary_index(column(at.batch), at.row)?
} else {
at.row
};
Some((at.batch, row))
});
place.unwrap_or(null_at)
})
.collect();
let array = interleave(&arrays, &indices).map_err(DataError::arrow)?;
Ok(if source.kind == Kind::Float {
canonical_floats(&array)
} else {
array
})
}
}
fn canonical_floats(array: &ArrayRef) -> ArrayRef {
type F16 = <Float16Type as ArrowPrimitiveType>::Native;
if let Some(a) = array.as_primitive_opt::<Float64Type>() {
Arc::new(a.unary::<_, Float64Type>(canonical))
} else if let Some(a) = array.as_primitive_opt::<Float32Type>() {
Arc::new(a.unary::<_, Float32Type>(|v| canonical(f64::from(v)) as f32))
} else if let Some(a) = array.as_primitive_opt::<Float16Type>() {
Arc::new(a.unary::<_, Float16Type>(|v| F16::from_f64(canonical(v.into()))))
} else {
Arc::clone(array)
}
}
fn mean(state: &State, numeric: Numeric) -> Option<f64> {
match (state, numeric) {
(State::Float(sum), _) => sum.mean(),
(State::Exact(sum), Numeric::Decimal { scale }) => sum.mean().map(|m| unscale(m, scale)),
(State::Exact(sum), _) => sum.mean(),
(State::Ordered(_), _) => None,
}
}
fn sums<'g>(
numeric: Numeric,
states: impl Iterator<Item = &'g State>,
) -> Result<(ArrayRef, DataType), DataError> {
let scale = match numeric {
Numeric::Float => {
let sums: Float64Array = states.map(|s| s.float().and_then(FloatSum::sum)).collect();
return Ok((Arc::new(sums), DataType::Float64));
}
Numeric::Integer => 0,
Numeric::Decimal { scale } => scale,
};
let sums: Decimal128Array = states
.map(|s| {
let sum = s.exact().and_then(ExactSum::total);
sum.filter(|v| v.unsigned_abs() < DECIMAL_LIMIT)
})
.collect();
let sums = sums
.with_precision_and_scale(38, scale)
.map_err(DataError::arrow)?;
Ok((Arc::new(sums), DataType::Decimal128(38, scale)))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_compensated_sum_keeps_what_a_plain_sum_loses() {
for (first, second) in [(1e16, 1.0), (1.0, 1e16)] {
let mut sum = FloatSum::default();
sum.apply(first, 1);
sum.apply(second, 1);
sum.apply(1e16, -1);
assert_eq!(sum.sum(), Some(1.0), "{first} then {second}");
}
}
#[test]
fn a_float_sum_forgets_its_rounding_once_its_values_are_gone() {
let huge = [1.7e308, 1.3e308, 0.9e308, 1.1e308];
let mut sum = FloatSum::default();
sum.apply(1.0, 1);
for v in huge {
sum.apply(v, 1);
}
for v in huge.iter().rev() {
sum.apply(*v, -1);
}
assert_eq!(sum.sum(), Some(1.0));
let mut sum = FloatSum::default();
for v in [0.1, 0.2, 0.3] {
sum.apply(v, 1);
}
for v in [0.2, 0.1, 0.3] {
sum.apply(v, -1);
}
sum.apply(1e-30, 1);
assert_eq!(sum.sum(), Some(1e-30));
}
#[test]
fn nan_and_infinities_leave_a_float_sum_with_their_values() {
let mut sum = FloatSum::default();
for _ in 0..100 {
sum.apply(1.0, 1);
}
sum.apply(f64::NAN, 1);
assert!(sum.sum().is_some_and(f64::is_nan));
sum.apply(f64::NAN, -1);
assert_eq!(sum.sum(), Some(100.0));
sum.apply(f64::INFINITY, 1);
assert_eq!(sum.sum(), Some(f64::INFINITY));
sum.apply(f64::NEG_INFINITY, 1);
assert!(sum.sum().is_some_and(f64::is_nan), "inf + -inf");
sum.apply(f64::INFINITY, -1);
assert_eq!(sum.sum(), Some(f64::NEG_INFINITY));
sum.apply(f64::NEG_INFINITY, -1);
assert_eq!(sum.sum(), Some(100.0));
assert_eq!(sum.mean(), Some(1.0));
}
#[test]
fn huge_floats_sum_past_f64_and_back() {
let mut sum = FloatSum::default();
sum.apply(1.0, 1);
sum.apply(f64::MAX, 1);
sum.apply(f64::MAX, 1);
assert_eq!(sum.sum(), Some(f64::INFINITY));
let mean = sum.mean().unwrap();
let expected = f64::MAX / 3.0 * 2.0;
assert!(
(mean - expected).abs() <= expected * 1e-15,
"{mean} vs {expected}"
);
sum.apply(f64::MAX, -1);
assert_eq!(sum.sum(), Some(f64::MAX), "f64::MAX + 1");
sum.apply(f64::MAX, -1);
assert_eq!(sum.sum(), Some(1.0));
}
#[test]
fn a_sum_just_below_overflow_does_not_round_to_infinity() {
let mut sum = FloatSum::default();
sum.apply(power_of_two(1023), 1);
sum.apply(power_of_two(1023), 1);
for _ in 0..4_096 {
sum.apply(-power_of_two(959), 1);
}
assert_eq!(sum.sum(), Some(f64::MAX));
}
#[test]
fn huge_values_keep_their_rounding_too() {
let (a, b, c) = (f64::MAX, 1e308, -1.5e308);
let mut sum = FloatSum::default();
for v in [a, b, c] {
sum.apply(v, 1);
}
assert_eq!(sum.sum(), Some((a + c) + b));
}
#[test]
fn an_exact_sum_past_i128_comes_back_when_the_values_leave() {
for big in [i128::MAX, i128::MIN] {
let mut sum = ExactSum::default();
sum.apply(7, 1);
sum.apply(big, 1);
sum.apply(big, 1);
assert_eq!(sum.total(), None, "7 + 2 * {big}");
let mean = sum.mean().unwrap();
assert!((mean - big as f64 * 2.0 / 3.0).abs() <= 1e25, "{mean}");
sum.apply(big, -1);
assert_eq!(sum.total(), big.checked_add(7));
sum.apply(big, -1);
assert_eq!(sum.total(), Some(7));
}
}
}