Skip to main content

reifydb_value/value/digest/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use std::collections::BTreeMap;
5
6use libm::{ceil, log, pow};
7
8use crate::{
9	error::{Error, TypeError},
10	value::{Value, duration::Duration, value_type::ValueType},
11};
12
13mod encoding;
14pub mod literal;
15
16const MIN_ACCURACY_PPM: u32 = 1_000;
17const MAX_ACCURACY_PPM: u32 = 100_000;
18
19const INNER_TAGS: [(u8, ValueType); 13] = [
20	(2, ValueType::Float4),
21	(3, ValueType::Float8),
22	(4, ValueType::Int1),
23	(5, ValueType::Int2),
24	(6, ValueType::Int4),
25	(7, ValueType::Int8),
26	(8, ValueType::Int16),
27	(10, ValueType::Uint1),
28	(11, ValueType::Uint2),
29	(12, ValueType::Uint4),
30	(13, ValueType::Uint8),
31	(14, ValueType::Uint16),
32	(18, ValueType::Duration),
33];
34
35#[derive(Debug, thiserror::Error)]
36pub enum DigestError {
37	#[error("digest does not support {inner} input")]
38	UnsupportedInnerType {
39		inner: ValueType,
40	},
41
42	#[error("digest accuracy must be between 0.001 and 0.1")]
43	AccuracyOutOfRange,
44
45	#[error("digest of {expected} cannot take a {actual} value")]
46	InputTypeMismatch {
47		expected: ValueType,
48		actual: ValueType,
49	},
50
51	#[error("digest input cannot have a month part")]
52	DurationMonthPart,
53
54	#[error(transparent)]
55	DurationConversion(#[from] Box<TypeError>),
56
57	#[error(
58		"cannot merge digest({left_inner}, {left_accuracy} ppm) with digest({right_inner}, {right_accuracy} ppm)"
59	)]
60	MergeMismatch {
61		left_inner: ValueType,
62		left_accuracy: u32,
63		right_inner: ValueType,
64		right_accuracy: u32,
65	},
66
67	#[error("digest encoding version {version} is unknown")]
68	UnknownVersion {
69		version: u64,
70	},
71
72	#[error("digest encoding ends early")]
73	Truncated,
74
75	#[error("digest encoding has {remaining} trailing bytes")]
76	TrailingBytes {
77		remaining: usize,
78	},
79
80	#[error("digest encoding has a varint that is not minimal")]
81	NonMinimalVarint,
82
83	#[error("digest encoding has a varint above u64")]
84	VarintOverflow,
85
86	#[error("digest encoding has unknown inner type tag {tag}")]
87	UnknownInnerTag {
88		tag: u64,
89	},
90
91	#[error("digest encoding has a bucket index outside i32")]
92	BucketIndexOutOfRange,
93
94	#[error("digest encoding repeats bucket index {index}")]
95	RepeatedBucketIndex {
96		index: i32,
97	},
98
99	#[error("digest encoding has a zero count at bucket {index}")]
100	ZeroBucketCount {
101		index: i32,
102	},
103
104	#[error("digest encoding counts overflow u64")]
105	CountOverflow,
106
107	#[error("digest of duration cannot hold infinity")]
108	NonFiniteDuration,
109
110	#[error("accuracy must be a number")]
111	AccuracyNotANumber,
112
113	#[error("accuracy must be a whole number of parts per million")]
114	AccuracyNotWholePpm,
115
116	#[error(transparent)]
117	PercentileNotANumber(Error),
118
119	#[error("p must be between 0 and 1")]
120	PercentileOutOfRange,
121}
122
123#[derive(Clone, Debug, PartialEq, Eq, Hash)]
124pub struct Digest {
125	inner: ValueType,
126	accuracy: u32,
127	negative: BTreeMap<i32, u64>,
128	zero: u64,
129	positive: BTreeMap<i32, u64>,
130	negative_infinity: u64,
131	positive_infinity: u64,
132}
133
134enum Slot {
135	NegativeInfinity,
136	Negative(i32),
137	Zero,
138	Positive(i32),
139	PositiveInfinity,
140}
141
142impl Digest {
143	pub fn new(inner: ValueType, accuracy: u32) -> Result<Self, DigestError> {
144		if inner_tag(&inner).is_none() {
145			return Err(DigestError::UnsupportedInnerType {
146				inner,
147			});
148		}
149		if !(MIN_ACCURACY_PPM..=MAX_ACCURACY_PPM).contains(&accuracy) {
150			return Err(DigestError::AccuracyOutOfRange);
151		}
152		Ok(Self {
153			inner,
154			accuracy,
155			negative: BTreeMap::new(),
156			zero: 0,
157			positive: BTreeMap::new(),
158			negative_infinity: 0,
159			positive_infinity: 0,
160		})
161	}
162
163	pub fn inner(&self) -> &ValueType {
164		&self.inner
165	}
166
167	pub fn accuracy(&self) -> u32 {
168		self.accuracy
169	}
170
171	pub fn count(&self) -> u64 {
172		self.checked_count().expect("digest count overflows u64")
173	}
174
175	pub fn bucket_count(&self) -> usize {
176		self.negative.len() + self.positive.len()
177	}
178
179	pub fn add_value(&mut self, value: &Value) -> Result<(), DigestError> {
180		let value = self.input(value)?;
181		self.add(value);
182		Ok(())
183	}
184
185	pub fn remove_value(&mut self, value: &Value) -> Result<(), DigestError> {
186		let value = self.input(value)?;
187		self.remove(value);
188		Ok(())
189	}
190
191	pub fn merge(&mut self, other: &Digest) -> Result<(), DigestError> {
192		self.check_compatible(other)?;
193		increment(&mut self.negative_infinity, other.negative_infinity, "-inf count");
194		increment(&mut self.zero, other.zero, "zero count");
195		increment(&mut self.positive_infinity, other.positive_infinity, "+inf count");
196		for (&index, &count) in &other.negative {
197			add_to_store(&mut self.negative, "negative", index, count);
198		}
199		for (&index, &count) in &other.positive {
200			add_to_store(&mut self.positive, "positive", index, count);
201		}
202		Ok(())
203	}
204
205	pub fn unmerge(&mut self, other: &Digest) -> Result<(), DigestError> {
206		self.check_compatible(other)?;
207		decrement(&mut self.negative_infinity, other.negative_infinity, "-inf count");
208		decrement(&mut self.zero, other.zero, "zero count");
209		decrement(&mut self.positive_infinity, other.positive_infinity, "+inf count");
210		for (&index, &count) in &other.negative {
211			remove_from_store(&mut self.negative, "negative", index, count);
212		}
213		for (&index, &count) in &other.positive {
214			remove_from_store(&mut self.positive, "positive", index, count);
215		}
216		Ok(())
217	}
218
219	pub fn percentile(&self, p: f64) -> Option<f64> {
220		assert!((0.0..=1.0).contains(&p), "digest percentile p {p} is outside 0 to 1");
221		let n = self.count();
222		if n == 0 {
223			return None;
224		}
225		let rank = ((p * n as f64).ceil() as u64).clamp(1, n);
226		let mut seen = 0u64;
227		let mut reached = |count: u64| {
228			seen += count;
229			seen >= rank
230		};
231		if reached(self.negative_infinity) {
232			return Some(f64::NEG_INFINITY);
233		}
234		for (&index, &count) in self.negative.iter().rev() {
235			if reached(count) {
236				return Some(-self.representative(index));
237			}
238		}
239		if reached(self.zero) {
240			return Some(0.0);
241		}
242		for (&index, &count) in &self.positive {
243			if reached(count) {
244				return Some(self.representative(index));
245			}
246		}
247		assert!(reached(self.positive_infinity), "digest rank {rank} is past count {n}");
248		Some(f64::INFINITY)
249	}
250
251	pub fn percentile_value(&self, p: f64) -> Result<Value, DigestError> {
252		let is_duration = self.inner == ValueType::Duration;
253		match (self.percentile(p), is_duration) {
254			(None, true) => Ok(Value::none_of(ValueType::Duration)),
255			(None, false) => Ok(Value::none_of(ValueType::Float8)),
256			(Some(value), true) => Ok(Value::Duration(Duration::from_nanoseconds(value.round() as i64)?)),
257			(Some(value), false) => Ok(Value::float8(value)),
258		}
259	}
260
261	fn checked_count(&self) -> Option<u64> {
262		[self.negative_infinity, self.zero, self.positive_infinity]
263			.into_iter()
264			.chain(self.negative.values().copied())
265			.chain(self.positive.values().copied())
266			.try_fold(0u64, |total, count| total.checked_add(count))
267	}
268
269	fn check_compatible(&self, other: &Digest) -> Result<(), DigestError> {
270		if self.inner != other.inner || self.accuracy != other.accuracy {
271			return Err(DigestError::MergeMismatch {
272				left_inner: self.inner.clone(),
273				left_accuracy: self.accuracy,
274				right_inner: other.inner.clone(),
275				right_accuracy: other.accuracy,
276			});
277		}
278		Ok(())
279	}
280
281	fn input(&self, value: &Value) -> Result<f64, DigestError> {
282		match (&self.inner, value) {
283			(ValueType::Float4, Value::Float4(v)) => Ok(f64::from(v.value())),
284			(ValueType::Float8, Value::Float8(v)) => Ok(v.value()),
285			(ValueType::Int1, Value::Int1(v)) => Ok(f64::from(*v)),
286			(ValueType::Int2, Value::Int2(v)) => Ok(f64::from(*v)),
287			(ValueType::Int4, Value::Int4(v)) => Ok(f64::from(*v)),
288			(ValueType::Int8, Value::Int8(v)) => Ok(*v as f64),
289			(ValueType::Int16, Value::Int16(v)) => Ok(*v as f64),
290			(ValueType::Uint1, Value::Uint1(v)) => Ok(f64::from(*v)),
291			(ValueType::Uint2, Value::Uint2(v)) => Ok(f64::from(*v)),
292			(ValueType::Uint4, Value::Uint4(v)) => Ok(f64::from(*v)),
293			(ValueType::Uint8, Value::Uint8(v)) => Ok(*v as f64),
294			(ValueType::Uint16, Value::Uint16(v)) => Ok(*v as f64),
295			(ValueType::Duration, Value::Duration(v)) if v.get_months() != 0 => {
296				Err(DigestError::DurationMonthPart)
297			}
298			(ValueType::Duration, Value::Duration(v)) => Ok(v.as_nanos()? as f64),
299			_ => Err(DigestError::InputTypeMismatch {
300				expected: self.inner.clone(),
301				actual: value.get_type(),
302			}),
303		}
304	}
305
306	fn add(&mut self, value: f64) {
307		match self.slot(value) {
308			Slot::NegativeInfinity => increment(&mut self.negative_infinity, 1, "-inf count"),
309			Slot::Negative(index) => add_to_store(&mut self.negative, "negative", index, 1),
310			Slot::Zero => increment(&mut self.zero, 1, "zero count"),
311			Slot::Positive(index) => add_to_store(&mut self.positive, "positive", index, 1),
312			Slot::PositiveInfinity => increment(&mut self.positive_infinity, 1, "+inf count"),
313		}
314	}
315
316	fn remove(&mut self, value: f64) {
317		match self.slot(value) {
318			Slot::NegativeInfinity => decrement(&mut self.negative_infinity, 1, "-inf count"),
319			Slot::Negative(index) => remove_from_store(&mut self.negative, "negative", index, 1),
320			Slot::Zero => decrement(&mut self.zero, 1, "zero count"),
321			Slot::Positive(index) => remove_from_store(&mut self.positive, "positive", index, 1),
322			Slot::PositiveInfinity => decrement(&mut self.positive_infinity, 1, "+inf count"),
323		}
324	}
325
326	fn slot(&self, value: f64) -> Slot {
327		assert!(!value.is_nan(), "digest input is NaN");
328		if value == f64::INFINITY {
329			Slot::PositiveInfinity
330		} else if value == f64::NEG_INFINITY {
331			Slot::NegativeInfinity
332		} else if value == 0.0 {
333			Slot::Zero
334		} else if value > 0.0 {
335			Slot::Positive(self.index(value))
336		} else {
337			Slot::Negative(self.index(-value))
338		}
339	}
340
341	fn index(&self, magnitude: f64) -> i32 {
342		ceil(log(magnitude) / log(gamma(self.accuracy))) as i32
343	}
344
345	fn representative(&self, index: i32) -> f64 {
346		let gamma = gamma(self.accuracy);
347		(pow(gamma, f64::from(index) - 1.0) * (2.0 * gamma / (gamma + 1.0))).min(f64::MAX)
348	}
349}
350
351fn gamma(accuracy: u32) -> f64 {
352	let accuracy = f64::from(accuracy) / 1_000_000.0;
353	(1.0 + accuracy) / (1.0 - accuracy)
354}
355
356fn inner_tag(inner: &ValueType) -> Option<u8> {
357	INNER_TAGS.iter().find(|(_, ty)| ty == inner).map(|(tag, _)| *tag)
358}
359
360fn inner_from_tag(tag: u64) -> Option<ValueType> {
361	INNER_TAGS.iter().find(|(t, _)| u64::from(*t) == tag).map(|(_, ty)| ty.clone())
362}
363
364fn increment(slot: &mut u64, count: u64, name: &str) {
365	let current = *slot;
366	*slot = current
367		.checked_add(count)
368		.unwrap_or_else(|| panic!("digest {name} {current} overflows u64 adding {count}"));
369}
370
371fn decrement(slot: &mut u64, count: u64, name: &str) {
372	let current = *slot;
373	*slot = current
374		.checked_sub(count)
375		.unwrap_or_else(|| panic!("digest remove of {count} from {name} holding {current}"));
376}
377
378fn add_to_store(store: &mut BTreeMap<i32, u64>, name: &str, index: i32, count: u64) {
379	let slot = store.entry(index).or_insert(0);
380	let current = *slot;
381	*slot = current.checked_add(count).unwrap_or_else(|| {
382		panic!("digest {name} bucket {index} holding {current} overflows u64 adding {count}")
383	});
384}
385
386fn remove_from_store(store: &mut BTreeMap<i32, u64>, name: &str, index: i32, count: u64) {
387	let Some(slot) = store.get_mut(&index) else {
388		panic!("digest remove of {count} from absent {name} bucket {index}");
389	};
390	let current = *slot;
391	let left = current
392		.checked_sub(count)
393		.unwrap_or_else(|| panic!("digest remove of {count} from {name} bucket {index} holding {current}"));
394	if left == 0 {
395		store.remove(&index);
396	} else {
397		*slot = left;
398	}
399}
400
401#[cfg(test)]
402pub mod tests {
403	use super::*;
404
405	const ACCURACIES: [u32; 3] = [1_000, 10_000, 100_000];
406
407	pub struct Rng(u64);
408
409	impl Rng {
410		pub fn new(seed: u64) -> Self {
411			Self(seed)
412		}
413
414		pub fn next_u64(&mut self) -> u64 {
415			self.0 = self.0.wrapping_add(0x9e37_79b9_7f4a_7c15);
416			let mut z = self.0;
417			z = (z ^ (z >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
418			z = (z ^ (z >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
419			z ^ (z >> 31)
420		}
421
422		pub fn below(&mut self, bound: u64) -> u64 {
423			self.next_u64() % bound
424		}
425
426		pub fn unit(&mut self) -> f64 {
427			(self.next_u64() >> 11) as f64 / (1u64 << 53) as f64
428		}
429	}
430
431	pub fn float_digest(accuracy: u32) -> Digest {
432		Digest::new(ValueType::Float8, accuracy).unwrap()
433	}
434
435	pub fn built(accuracy: u32, values: &[f64]) -> Digest {
436		let mut digest = float_digest(accuracy);
437		for &value in values {
438			digest.add(value);
439		}
440		digest
441	}
442
443	fn magnitude(rng: &mut Rng) -> f64 {
444		10f64.powf(rng.unit() * 24.0 - 12.0)
445	}
446
447	pub fn any_value(rng: &mut Rng) -> f64 {
448		match rng.below(40) {
449			0 => f64::NEG_INFINITY,
450			1 => f64::INFINITY,
451			2..=6 => 0.0,
452			7..=10 => [1.0, -2.5, 1000.0][rng.below(3) as usize],
453			11..=24 => -magnitude(rng),
454			_ => magnitude(rng),
455		}
456	}
457
458	fn finite_value(rng: &mut Rng, sign: i32) -> f64 {
459		match (sign, rng.below(10)) {
460			(0, 0..=1) => 0.0,
461			(0, 2..=5) => -magnitude(rng),
462			(-1, _) => -magnitude(rng),
463			_ => magnitude(rng),
464		}
465	}
466
467	fn sorted(values: &[f64]) -> Vec<f64> {
468		let mut sorted = values.to_vec();
469		sorted.sort_by(f64::total_cmp);
470		sorted
471	}
472
473	fn rank(p: f64, n: usize) -> usize {
474		((p * n as f64).ceil() as usize).max(1)
475	}
476
477	fn bucketed(digest: &Digest, value: f64) -> f64 {
478		if !value.is_finite() || value == 0.0 {
479			value
480		} else if value > 0.0 {
481			digest.representative(digest.index(value))
482		} else {
483			-digest.representative(digest.index(-value))
484		}
485	}
486
487	fn same(left: Option<f64>, right: Option<f64>) -> bool {
488		match (left, right) {
489			(Some(l), Some(r)) => l.to_bits() == r.to_bits(),
490			(None, None) => true,
491			_ => false,
492		}
493	}
494
495	#[test]
496	fn new_digest_is_empty() {
497		for accuracy in ACCURACIES {
498			let digest = float_digest(accuracy);
499			assert_eq!(digest.count(), 0);
500			assert_eq!(digest.bucket_count(), 0);
501			assert_eq!(digest.percentile(0.5), None);
502			assert_eq!(digest.accuracy(), accuracy);
503			assert_eq!(digest.inner(), &ValueType::Float8);
504		}
505	}
506
507	#[test]
508	fn new_rejects_accuracy_outside_range() {
509		for accuracy in [0, 999, 100_001, u32::MAX] {
510			assert!(matches!(
511				Digest::new(ValueType::Float8, accuracy),
512				Err(DigestError::AccuracyOutOfRange)
513			));
514		}
515		assert!(Digest::new(ValueType::Float8, 1_000).is_ok());
516		assert!(Digest::new(ValueType::Float8, 100_000).is_ok());
517	}
518
519	#[test]
520	fn new_rejects_inner_types_outside_ints_floats_and_duration() {
521		let rejected = [
522			ValueType::Utf8,
523			ValueType::Boolean,
524			ValueType::Date,
525			ValueType::DateTime,
526			ValueType::Time,
527			ValueType::IdentityId,
528			ValueType::Uuid4,
529			ValueType::Uuid7,
530			ValueType::Blob,
531			ValueType::DECIMAL,
532			ValueType::Any,
533			ValueType::DictionaryId,
534			ValueType::Option(Box::new(ValueType::Int4)),
535			ValueType::List(Box::new(ValueType::Float8)),
536		];
537		for inner in rejected {
538			let result = Digest::new(inner.clone(), 10_000);
539			assert!(
540				matches!(&result, Err(DigestError::UnsupportedInnerType { inner: got }) if *got == inner),
541				"{inner} must be rejected"
542			);
543		}
544	}
545
546	#[test]
547	fn new_accepts_every_int_float_and_duration_type() {
548		for (_, inner) in INNER_TAGS {
549			assert_eq!(Digest::new(inner.clone(), 10_000).unwrap().inner(), &inner);
550		}
551		assert_eq!(INNER_TAGS.len(), 13);
552	}
553
554	#[test]
555	fn frozen_bucket_formula() {
556		// Any change to gamma, the ceil index or the representative changes answers read from stored digests.
557		let pins: [(u32, f64, i32, u64); 24] = [
558			(1_000, 1.0, 0, 0x3feff7ced916872b),
559			(1_000, 0.5, -346, 0x3fe000999322114d),
560			(1_000, 2.0, 347, 0x3ffffecacc74c18e),
561			(1_000, 1e-9, -10361, 0x3e112f2ee8f1db2a),
562			(1_000, 123.456, 2408, 0x405ed63467efc900),
563			(1_000, 1e300, 345388, 0x7e37e269c95c4dcc),
564			(1_000, 5e-324, -372219, 0x0000000000000001),
565			(1_000, f64::MAX, 354892, 0x7fefffffffffffff),
566			(10_000, 1.0, 0, 0x3fefae147ae147af),
567			(10_000, 0.5, -34, 0x3fe00c9c77c0ac0b),
568			(10_000, 2.0, 35, 0x3fffe609cfee00de),
569			(10_000, 1e-9, -1036, 0x3e110d4af35d841d),
570			(10_000, 123.456, 241, 0x405eafb0b5c737bc),
571			(10_000, 1e300, 34538, 0x7e37d4a690e10c14),
572			(10_000, 5e-324, -37220, 0x0000000000000001),
573			(10_000, f64::MAX, 35488, 0x7fefb5c3f47e6616),
574			(100_000, 1.0, 0, 0x3feccccccccccccd),
575			(100_000, 0.5, -3, 0x3fdf8c4a51a0ad8f),
576			(100_000, 2.0, 4, 0x400011230bde91ab),
577			(100_000, 1e-9, -103, 0x3e1052a20b63bdc0),
578			(100_000, 123.456, 24, 0x405bc89289578722),
579			(100_000, 1e300, 3443, 0x7e3893e644df1e51),
580			(100_000, 5e-324, -3709, 0x0000000000000001),
581			(100_000, f64::MAX, 3538, 0x7fefffffffffffff),
582		];
583		for (accuracy, value, index, representative) in pins {
584			let digest = float_digest(accuracy);
585			assert_eq!(digest.index(value), index, "index of {value:e} at {accuracy} ppm");
586			assert_eq!(
587				digest.representative(index).to_bits(),
588				representative,
589				"representative of bucket {index} at {accuracy} ppm"
590			);
591			assert_eq!(
592				built(accuracy, &[-value]).negative,
593				BTreeMap::from([(index, 1)]),
594				"negative values bucket by magnitude"
595			);
596		}
597	}
598
599	#[test]
600	fn values_route_to_their_store() {
601		let mut digest = float_digest(10_000);
602		for value in [-3.0, -0.0, 0.0, 4.0, f64::NEG_INFINITY, f64::INFINITY] {
603			digest.add(value);
604		}
605		assert_eq!(digest.negative, BTreeMap::from([(digest.index(3.0), 1)]));
606		assert_eq!(digest.positive, BTreeMap::from([(digest.index(4.0), 1)]));
607		assert_eq!(digest.zero, 2, "-0.0 must count as zero, not as a negative bucket");
608		assert_eq!((digest.negative_infinity, digest.positive_infinity), (1, 1));
609		assert_eq!(digest.count(), 6);
610	}
611
612	#[test]
613	fn subnormal_gets_a_bucket_within_index_range() {
614		let mut digest = float_digest(1_000);
615		for value in [5e-324, -5e-324, f64::MIN_POSITIVE / 2.0] {
616			digest.add(value);
617			assert!(digest.index(value.abs()).abs() <= 372_220);
618		}
619		assert_eq!(digest.zero, 0);
620		assert_eq!(digest.bucket_count(), 3);
621	}
622
623	#[test]
624	fn largest_finite_values_read_back_finite_within_accuracy() {
625		for accuracy in ACCURACIES {
626			let relative = f64::from(accuracy) / 1_000_000.0;
627			for value in [f64::MAX, -f64::MAX, f64::MAX / 1.05, -f64::MAX / 1.05] {
628				let answer = built(accuracy, &[value]).percentile(1.0).unwrap();
629				assert!(answer.is_finite(), "{value:e} at {accuracy} ppm read back as {answer}");
630				assert!(
631					(answer - value).abs() <= relative * value.abs(),
632					"{value:e} at {accuracy} ppm read back as {answer:e}"
633				);
634			}
635		}
636	}
637
638	#[test]
639	fn one_outlier_adds_one_bucket_and_remove_drops_it() {
640		let mut digest = float_digest(10_000);
641		for i in 0..1_000 {
642			digest.add(100.0 + f64::from(i) / 1_000.0);
643		}
644		let cluster = digest.clone();
645		let before = digest.bucket_count();
646		digest.add(1e15);
647		assert_eq!(digest.bucket_count(), before + 1);
648		digest.add(-1e-15);
649		assert_eq!(digest.bucket_count(), before + 2);
650		digest.remove(1e15);
651		digest.remove(-1e-15);
652		assert_eq!(digest.bucket_count(), before);
653		assert_eq!(digest, cluster, "an emptied bucket left behind breaks add then remove equality");
654	}
655
656	#[test]
657	fn add_then_remove_returns_the_empty_digest() {
658		let mut digest = float_digest(10_000);
659		for value in [5.0, -5.0, 0.0, f64::INFINITY, f64::NEG_INFINITY] {
660			digest.add(value);
661		}
662		for value in [0.0, -5.0, f64::NEG_INFINITY, 5.0, f64::INFINITY] {
663			digest.remove(value);
664		}
665		assert_eq!(digest, float_digest(10_000));
666	}
667
668	#[test]
669	#[should_panic(expected = "absent positive bucket")]
670	fn remove_from_absent_positive_bucket_panics() {
671		let mut digest = built(10_000, &[1.0]);
672		digest.remove(50.0);
673	}
674
675	#[test]
676	#[should_panic(expected = "absent negative bucket")]
677	fn remove_from_absent_negative_bucket_panics() {
678		let mut digest = built(10_000, &[-1.0, 1.0]);
679		digest.remove(-50.0);
680	}
681
682	#[test]
683	#[should_panic(expected = "from +inf count holding 0")]
684	fn remove_from_empty_special_count_panics() {
685		let mut digest = built(10_000, &[1.0]);
686		digest.remove(f64::INFINITY);
687	}
688
689	#[test]
690	#[should_panic(expected = "digest input is NaN")]
691	fn nan_input_panics_instead_of_landing_in_a_bucket() {
692		// Without the check NaN takes the negative branch and silently counts as bucket 0.
693		let mut digest = built(10_000, &[1.0]);
694		digest.add(f64::NAN);
695	}
696
697	#[test]
698	fn add_value_converts_each_supported_type() {
699		let cases = [
700			(ValueType::Float4, Value::float4(1.5f32), 1.5),
701			(ValueType::Float8, Value::float8(-2.25), -2.25),
702			(ValueType::Float8, Value::float8(f64::INFINITY), f64::INFINITY),
703			(ValueType::Int1, Value::Int1(-7), -7.0),
704			(ValueType::Int2, Value::Int2(300), 300.0),
705			(ValueType::Int4, Value::Int4(-70_000), -70_000.0),
706			(ValueType::Int8, Value::Int8(1 << 40), (1u64 << 40) as f64),
707			(ValueType::Int16, Value::Int16(-(1i128 << 100)), -((1u128 << 100) as f64)),
708			(ValueType::Uint1, Value::Uint1(200), 200.0),
709			(ValueType::Uint2, Value::Uint2(60_000), 60_000.0),
710			(ValueType::Uint4, Value::Uint4(4_000_000_000), 4_000_000_000.0),
711			(ValueType::Uint8, Value::Uint8(u64::MAX), u64::MAX as f64),
712			(ValueType::Uint16, Value::Uint16(u128::MAX), u128::MAX as f64),
713			(ValueType::Duration, Value::Duration(Duration::from_milliseconds(1_500).unwrap()), 1.5e9),
714			(ValueType::Duration, Value::Duration(Duration::from_days(-2).unwrap()), -1.728e14),
715		];
716		for (inner, value, expected) in cases {
717			let mut through_value = Digest::new(inner.clone(), 10_000).unwrap();
718			through_value.add_value(&value).unwrap();
719			let mut direct = Digest::new(inner.clone(), 10_000).unwrap();
720			direct.add(expected);
721			assert_eq!(through_value, direct, "{inner} value {value:?}");
722			through_value.remove_value(&value).unwrap();
723			assert_eq!(through_value.count(), 0, "{inner} value {value:?}");
724		}
725	}
726
727	#[test]
728	fn add_value_rejects_a_value_of_another_type() {
729		let mut digest = built(10_000, &[1.0]);
730		let before = digest.clone();
731		let cases = [
732			(Value::Int4(1), ValueType::Int4),
733			(Value::utf8("1"), ValueType::Utf8),
734			(Value::float4(1.0f32), ValueType::Float4),
735			(Value::none_of(ValueType::Float8), ValueType::Option(Box::new(ValueType::Float8))),
736		];
737		for (value, actual) in cases {
738			let added = digest.add_value(&value);
739			assert!(
740				matches!(&added, Err(DigestError::InputTypeMismatch { expected: ValueType::Float8, actual: got }) if *got == actual),
741				"{value:?} added as {added:?}"
742			);
743			assert!(matches!(digest.remove_value(&value), Err(DigestError::InputTypeMismatch { .. })));
744		}
745		assert_eq!(digest, before);
746	}
747
748	#[test]
749	fn duration_with_a_month_part_is_rejected() {
750		// as_nanos counts a month as 30 days, so converting one would bucket a guessed length.
751		let rejected = [
752			Duration::from_months(1).unwrap(),
753			Duration::from_months(-1).unwrap(),
754			Duration::from_years(1).unwrap(),
755			Duration::new(1, 0, 5).unwrap(),
756			Duration::new(-1, -2, -5).unwrap(),
757			Duration::MAX,
758		];
759		let mut digest = Digest::new(ValueType::Duration, 10_000).unwrap();
760		digest.add_value(&Value::Duration(Duration::from_days(30).unwrap())).unwrap();
761		let before = digest.clone();
762		for duration in rejected {
763			let value = Value::Duration(duration);
764			assert!(
765				matches!(digest.add_value(&value), Err(DigestError::DurationMonthPart)),
766				"add {duration:?}"
767			);
768			assert!(
769				matches!(digest.remove_value(&value), Err(DigestError::DurationMonthPart)),
770				"remove {duration:?}"
771			);
772		}
773		assert_eq!(digest, before, "a rejected duration must not touch any count");
774	}
775
776	#[test]
777	fn duration_with_a_day_part_counts_the_day_as_86400_seconds() {
778		// Dropping the day part reads 25h back as 1h, far outside any accuracy.
779		for accuracy in ACCURACIES {
780			for hours in [25, -25, 24 * 400] {
781				let duration = Duration::from_hours(hours).unwrap();
782				assert_eq!(duration.get_days(), (hours / 24) as i32, "{hours}h must carry a day part");
783				let mut digest = Digest::new(ValueType::Duration, accuracy).unwrap();
784				digest.add_value(&Value::Duration(duration)).unwrap();
785				let Value::Duration(answer) = digest.percentile_value(0.5).unwrap() else {
786					panic!("duration digest must read back a duration");
787				};
788				let exact = hours as f64 * 3_600e9;
789				let nanos = answer.get_days() as f64 * 86_400e9 + answer.get_nanos() as f64;
790				assert!(
791					(nanos - exact).abs() <= f64::from(accuracy) / 1_000_000.0 * exact.abs(),
792					"{hours}h at {accuracy} ppm read back as {answer:?}"
793				);
794				assert!(answer.get_nanos().abs() < 86_400_000_000_000, "{answer:?} is not normalized");
795				digest.remove_value(&Value::Duration(duration)).unwrap();
796				assert_eq!(digest.count(), 0, "{hours}h at {accuracy} ppm");
797			}
798		}
799	}
800
801	#[test]
802	fn duration_whose_nanoseconds_overflow_i64_fails() {
803		// A wrapped or clamped total would count a far-off value instead of failing the query.
804		let overflowing = [
805			Duration::from_days(106_752).unwrap(),
806			Duration::from_days(-106_752).unwrap(),
807			Duration::new(0, 106_751, 86_399_999_999_999).unwrap(),
808			Duration::from_days(i64::from(i32::MIN)).unwrap(),
809		];
810		let mut digest = Digest::new(ValueType::Duration, 10_000).unwrap();
811		for nanos in [i64::MAX, i64::MIN] {
812			digest.add_value(&Value::Duration(Duration::from_nanoseconds(nanos).unwrap())).unwrap();
813		}
814		let before = digest.clone();
815		for duration in overflowing {
816			let value = Value::Duration(duration);
817			assert!(
818				matches!(digest.add_value(&value), Err(DigestError::DurationConversion(_))),
819				"add {duration:?}"
820			);
821			assert!(
822				matches!(digest.remove_value(&value), Err(DigestError::DurationConversion(_))),
823				"remove {duration:?}"
824			);
825		}
826		assert_eq!(digest, before, "an overflowing duration must not touch any count");
827	}
828
829	#[test]
830	fn percentile_value_is_float8_or_duration_from_the_inner_type() {
831		let mut ints = Digest::new(ValueType::Int4, 10_000).unwrap();
832		assert_eq!(ints.percentile_value(0.5).unwrap(), Value::none_of(ValueType::Float8));
833		ints.add_value(&Value::Int4(40)).unwrap();
834		assert_eq!(ints.percentile_value(0.5).unwrap(), Value::float8(ints.percentile(0.5).unwrap()));
835
836		let mut durations = Digest::new(ValueType::Duration, 10_000).unwrap();
837		assert_eq!(durations.percentile_value(0.5).unwrap(), Value::none_of(ValueType::Duration));
838		for millis in 1..=100 {
839			durations.add_value(&Value::Duration(Duration::from_milliseconds(millis).unwrap())).unwrap();
840		}
841		let nanos = durations.percentile(0.5).unwrap();
842		assert_eq!(
843			durations.percentile_value(0.5).unwrap(),
844			Value::Duration(Duration::from_nanoseconds(nanos.round() as i64).unwrap())
845		);
846		assert!((nanos - 50e6).abs() <= 0.01 * 50e6);
847	}
848
849	#[test]
850	fn percentile_value_saturates_a_duration_representative_past_i64() {
851		for accuracy in ACCURACIES {
852			let mut digest = Digest::new(ValueType::Duration, accuracy).unwrap();
853			for nanos in [i64::MAX, i64::MIN] {
854				digest.add_value(&Value::Duration(Duration::from_nanoseconds(nanos).unwrap())).unwrap();
855			}
856			for (p, nanos) in [(1.0, i64::MAX), (0.0, i64::MIN)] {
857				let Value::Duration(answer) = digest.percentile_value(p).unwrap() else {
858					panic!("duration digest must read back a duration");
859				};
860				let answer = answer.as_nanos().unwrap() as f64;
861				let exact = nanos as f64;
862				assert!((answer - exact).abs() <= f64::from(accuracy) / 1_000_000.0 * exact.abs());
863			}
864		}
865	}
866
867	#[test]
868	fn merge_commutes_and_associates() {
869		let mut rng = Rng::new(7);
870		for accuracy in ACCURACIES {
871			let parts: Vec<Vec<f64>> =
872				(0..3).map(|_| (0..300).map(|_| any_value(&mut rng)).collect()).collect();
873			let [a, b, c] = [0, 1, 2].map(|i| built(accuracy, &parts[i]));
874
875			let mut ab = a.clone();
876			ab.merge(&b).unwrap();
877			let mut ba = b.clone();
878			ba.merge(&a).unwrap();
879			assert_eq!(ab, ba);
880
881			let mut ab_c = ab.clone();
882			ab_c.merge(&c).unwrap();
883			let mut bc = b.clone();
884			bc.merge(&c).unwrap();
885			let mut a_bc = a.clone();
886			a_bc.merge(&bc).unwrap();
887			assert_eq!(ab_c, a_bc);
888
889			assert_eq!(ab_c, built(accuracy, &parts.concat()), "merge must equal adding every value");
890		}
891	}
892
893	#[test]
894	fn unmerge_undoes_merge() {
895		let mut rng = Rng::new(11);
896		for accuracy in ACCURACIES {
897			let left: Vec<f64> = (0..400).map(|_| any_value(&mut rng)).collect();
898			let right: Vec<f64> = (0..400).map(|_| any_value(&mut rng)).collect();
899			let a = built(accuracy, &left);
900			let b = built(accuracy, &right);
901			let mut total = a.clone();
902			total.merge(&b).unwrap();
903			total.unmerge(&b).unwrap();
904			assert_eq!(total, a, "buckets emptied by unmerge must be dropped");
905			total.unmerge(&a).unwrap();
906			assert_eq!(total, float_digest(accuracy));
907		}
908	}
909
910	#[test]
911	fn merge_and_unmerge_reject_another_accuracy_or_inner_type() {
912		let mut digest = built(10_000, &[1.0, 2.0]);
913		let before = digest.clone();
914		let other_accuracy = built(20_000, &[1.0]);
915		let other_inner = Digest::new(ValueType::Int4, 10_000).unwrap();
916		for other in [&other_accuracy, &other_inner] {
917			assert!(matches!(digest.merge(other), Err(DigestError::MergeMismatch { .. })));
918			assert!(matches!(digest.unmerge(other), Err(DigestError::MergeMismatch { .. })));
919		}
920		let Err(DigestError::MergeMismatch {
921			left_inner,
922			left_accuracy,
923			right_inner,
924			right_accuracy,
925		}) = digest.merge(&other_inner)
926		else {
927			panic!("inner mismatch must be an error");
928		};
929		assert_eq!(
930			(left_inner, left_accuracy, right_inner, right_accuracy),
931			(ValueType::Float8, 10_000, ValueType::Int4, 10_000)
932		);
933		assert_eq!(digest, before);
934	}
935
936	#[test]
937	#[should_panic(expected = "absent positive bucket")]
938	fn unmerge_of_a_part_not_inside_the_total_panics() {
939		let mut total = built(10_000, &[1.0, 2.0]);
940		total.unmerge(&built(10_000, &[1.0, 3.0])).unwrap();
941	}
942
943	#[test]
944	#[should_panic(expected = "positive bucket 0 holding 1")]
945	fn unmerge_of_more_than_a_bucket_holds_panics() {
946		let mut total = built(10_000, &[1.0]);
947		total.unmerge(&built(10_000, &[1.0, 1.0])).unwrap();
948	}
949
950	#[test]
951	fn seeded_add_remove_equals_never_adding_the_removed_values() {
952		for seed in 0..20 {
953			let mut rng = Rng::new(seed);
954			let accuracy = ACCURACIES[seed as usize % 3];
955			let mut digest = float_digest(accuracy);
956			let mut present = Vec::new();
957			for _ in 0..2_000 {
958				if !present.is_empty() && rng.below(3) == 0 {
959					let at = rng.below(present.len() as u64) as usize;
960					digest.remove(present.swap_remove(at));
961				} else {
962					let value = any_value(&mut rng);
963					present.push(value);
964					digest.add(value);
965				}
966			}
967			let rebuilt = built(accuracy, &present);
968			assert_eq!(digest, rebuilt, "seed {seed}");
969			assert_eq!(digest.encode(), rebuilt.encode(), "seed {seed}");
970		}
971	}
972
973	#[test]
974	fn percentile_equals_the_bucketed_oracle() {
975		let mut rng = Rng::new(42);
976		let ps = [0.0, 0.001, 0.01, 0.07, 0.1, 0.25, 0.333, 0.5, 0.75, 0.9, 0.99, 0.999, 1.0];
977		for round in 0..60 {
978			let accuracy = ACCURACIES[round % 3];
979			let n = [1, 2, 3, 10, 100, 1_000][round % 6] + rng.below(5) as usize;
980			let values: Vec<f64> = (0..n).map(|_| any_value(&mut rng)).collect();
981			let digest = built(accuracy, &values);
982			let sorted = sorted(&values);
983			let random_ps: Vec<f64> = (0..20).map(|_| rng.unit()).collect();
984			for &p in ps.iter().chain(&random_ps) {
985				let expected = Some(bucketed(&digest, sorted[rank(p, n) - 1]));
986				let answer = digest.percentile(p);
987				assert!(
988					same(answer, expected),
989					"round {round} n {n} p {p}: {answer:?} != {expected:?}"
990				);
991			}
992		}
993	}
994
995	#[test]
996	fn rank_is_the_f64_ceil_of_p_times_n() {
997		// 0.07 * 100.0 is 7.000000000000001, so the rank is 8; an exact or rounded rank reads the 7th value.
998		let values: Vec<f64> = (1..=100).map(f64::from).collect();
999		let digest = built(1_000, &values);
1000		let eighth = digest.representative(digest.index(8.0));
1001		assert_ne!(eighth, digest.representative(digest.index(7.0)));
1002		assert_eq!(digest.percentile(0.07), Some(eighth));
1003		assert_eq!(digest.percentile(0.0), Some(digest.representative(digest.index(1.0))));
1004		assert_eq!(digest.percentile(1.0), Some(digest.representative(digest.index(100.0))));
1005	}
1006
1007	#[test]
1008	fn percentile_is_within_accuracy_for_positive_negative_and_zero_values() {
1009		// A wrong bucket or representative misses this bound by up to a whole accuracy, never by rounding.
1010		let mut rng = Rng::new(3);
1011		for round in 0..90 {
1012			let accuracy = ACCURACIES[round % 3];
1013			let sign = [1, -1, 0][(round / 3) % 3];
1014			let n = 1 + rng.below(500) as usize;
1015			let values: Vec<f64> = (0..n).map(|_| finite_value(&mut rng, sign)).collect();
1016			let digest = built(accuracy, &values);
1017			let sorted = sorted(&values);
1018			let relative = f64::from(accuracy) / 1_000_000.0;
1019			for _ in 0..50 {
1020				let p = rng.unit();
1021				let exact = sorted[rank(p, n) - 1];
1022				let answer = digest.percentile(p).unwrap();
1023				assert!(
1024					(answer - exact).abs() <= relative * exact.abs(),
1025					"round {round} p {p}: {answer:e} is not within {accuracy} ppm of {exact:e}"
1026				);
1027			}
1028		}
1029	}
1030
1031	#[test]
1032	fn ranks_landing_on_infinity_return_it() {
1033		let digest = built(10_000, &[1.0, f64::INFINITY, 0.0, f64::NEG_INFINITY, -1.0]);
1034		assert_eq!(digest.percentile(0.0), Some(f64::NEG_INFINITY));
1035		assert_eq!(digest.percentile(0.1), Some(f64::NEG_INFINITY));
1036		assert_eq!(digest.percentile(0.3), Some(-digest.representative(digest.index(1.0))));
1037		assert_eq!(digest.percentile(0.5), Some(0.0));
1038		assert_eq!(digest.percentile(0.7), Some(digest.representative(digest.index(1.0))));
1039		assert_eq!(digest.percentile(0.9), Some(f64::INFINITY));
1040		assert_eq!(digest.percentile(1.0), Some(f64::INFINITY));
1041	}
1042
1043	#[test]
1044	#[should_panic(expected = "outside 0 to 1")]
1045	fn percentile_outside_zero_to_one_panics() {
1046		built(10_000, &[1.0]).percentile(1.5);
1047	}
1048}