1use 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 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 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 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 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 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 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 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}