use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Stat<T> {
Known {
value: T,
class: Class,
provenance: Provenance,
},
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Class {
Exact,
Certified {
bound: f64,
direction: Direction,
},
Estimated,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Direction {
AtMost,
AtLeast,
Within,
}
impl Direction {
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::AtMost => "at most",
Self::AtLeast => "at least",
Self::Within => "within",
}
}
}
impl fmt::Display for Direction {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.name())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Provenance {
RowCount,
ZoneMap,
NullCount,
Sketch,
FrequencySynopsis,
Quantiles,
Dictionary,
Sortedness,
Distinctness,
LinkHeader,
DegreeDistribution,
Sample,
Default,
Observed,
Propagation,
}
impl Provenance {
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::RowCount => "row count",
Self::ZoneMap => "zone map",
Self::NullCount => "null count",
Self::Sketch => "sketch",
Self::FrequencySynopsis => "frequency synopsis",
Self::Quantiles => "quantiles",
Self::Dictionary => "dictionary",
Self::Sortedness => "sortedness",
Self::Distinctness => "distinctness",
Self::LinkHeader => "link header",
Self::DegreeDistribution => "degree distribution",
Self::Sample => "sample",
Self::Default => "default",
Self::Observed => "observed",
Self::Propagation => "propagation",
}
}
#[must_use]
pub const fn is_observed(self) -> bool {
matches!(self, Self::Observed)
}
}
impl fmt::Display for Provenance {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.name())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Use {
Answer,
Enable,
Decide,
}
impl Use {
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::Answer => "answer",
Self::Enable => "enable",
Self::Decide => "decide",
}
}
#[must_use]
pub const fn permits(self, class: Option<Class>) -> bool {
match self {
Self::Decide => true,
Self::Answer => matches!(class, Some(Class::Exact | Class::Certified { .. })),
Self::Enable => matches!(class, Some(Class::Exact)),
}
}
}
impl fmt::Display for Use {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.name())
}
}
impl Class {
#[must_use]
pub const fn is_exact(self) -> bool {
matches!(self, Self::Exact)
}
#[must_use]
pub fn combine(self, other: Self) -> Self {
match (self, other) {
(Self::Exact, Self::Exact) => Self::Exact,
(Self::Exact, class) | (class, Self::Exact) => class,
(
Self::Certified { bound: left, direction: first },
Self::Certified { bound: right, direction: second },
) => Self::Certified {
bound: (left + right).min(1.0),
direction: if first == second { first } else { Direction::Within },
},
_ => Self::Estimated,
}
}
}
impl fmt::Display for Class {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Exact => f.write_str("exact"),
Self::Certified { bound, direction } => {
write!(f, "certified {direction} {:.2}%", bound * 100.0)
}
Self::Estimated => f.write_str("estimated"),
}
}
}
impl<T> Stat<T> {
pub const fn exact(value: T, provenance: Provenance) -> Self {
Self::Known { value, class: Class::Exact, provenance }
}
pub const fn certified(
value: T,
bound: f64,
direction: Direction,
provenance: Provenance,
) -> Self {
Self::Known { value, class: Class::Certified { bound, direction }, provenance }
}
pub const fn estimated(value: T, provenance: Provenance) -> Self {
Self::Known { value, class: Class::Estimated, provenance }
}
#[must_use]
pub const fn is_known(&self) -> bool {
matches!(self, Self::Known { .. })
}
#[must_use]
pub const fn is_unknown(&self) -> bool {
matches!(self, Self::Unknown)
}
#[must_use]
pub const fn value(&self) -> Option<&T> {
match self {
Self::Known { value, .. } => Some(value),
Self::Unknown => None,
}
}
#[must_use]
pub const fn decide(&self) -> Option<&T> {
self.value()
}
#[must_use]
pub const fn enable(&self) -> Option<&T> {
match self {
Self::Known { value, class: Class::Exact, .. } => Some(value),
_ => None,
}
}
#[must_use]
pub const fn answer(&self) -> Option<&T> {
self.enable()
}
#[must_use]
pub fn answer_certified(&self, discharge: impl FnOnce(f64, Direction) -> bool) -> Option<&T> {
match self {
Self::Known { value, class: Class::Exact, .. } => Some(value),
Self::Known { value, class: Class::Certified { bound, direction }, .. } => {
discharge(*bound, *direction).then_some(value)
}
_ => None,
}
}
#[must_use]
pub const fn exact_value(&self) -> Option<&T> {
self.enable()
}
#[must_use]
pub const fn class(&self) -> Option<Class> {
match self {
Self::Known { class, .. } => Some(*class),
Self::Unknown => None,
}
}
#[must_use]
pub const fn provenance(&self) -> Option<Provenance> {
match self {
Self::Known { provenance, .. } => Some(*provenance),
Self::Unknown => None,
}
}
#[must_use]
pub const fn permits(&self, use_: Use) -> bool {
use_.permits(self.class())
}
#[must_use]
pub fn unwrap_or(self, default: T) -> T {
match self {
Self::Known { value, .. } => value,
Self::Unknown => default,
}
}
#[must_use]
pub fn map<U>(self, f: impl FnOnce(T) -> U) -> Stat<U> {
match self {
Self::Known { value, class, provenance } => {
Stat::Known { value: f(value), class, provenance }
}
Self::Unknown => Stat::Unknown,
}
}
#[must_use]
pub fn zip<U, V>(self, other: Stat<U>, f: impl FnOnce(T, U) -> V) -> Stat<V> {
match (self, other) {
(
Self::Known { value: left, class: first, provenance: from },
Stat::Known { value: right, class: second, provenance: also },
) => Stat::Known {
value: f(left, right),
class: first.combine(second),
provenance: if from == also { from } else { Provenance::Propagation },
},
_ => Stat::Unknown,
}
}
}
impl<T> Default for Stat<T> {
fn default() -> Self {
Self::Unknown
}
}
impl<T: fmt::Display> fmt::Display for Stat<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Known { value, class, provenance } => {
write!(f, "{value} ({class} from {provenance})")
}
Self::Unknown => f.write_str("unknown"),
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct Classes {
exact: u64,
certified: u64,
estimated: u64,
unknown: u64,
}
impl Classes {
#[must_use]
pub const fn new() -> Self {
Self { exact: 0, certified: 0, estimated: 0, unknown: 0 }
}
pub fn record<T>(&mut self, stat: &Stat<T>) {
self.record_class(stat.class());
}
pub fn record_class(&mut self, class: Option<Class>) {
match class {
Some(Class::Exact) => self.exact += 1,
Some(Class::Certified { .. }) => self.certified += 1,
Some(Class::Estimated) => self.estimated += 1,
None => self.unknown += 1,
}
}
#[must_use]
pub const fn exact(self) -> u64 {
self.exact
}
#[must_use]
pub const fn certified(self) -> u64 {
self.certified
}
#[must_use]
pub const fn estimated(self) -> u64 {
self.estimated
}
#[must_use]
pub const fn unknown(self) -> u64 {
self.unknown
}
#[must_use]
pub const fn total(self) -> u64 {
self.exact + self.certified + self.estimated + self.unknown
}
#[must_use]
pub fn known_share(self) -> f64 {
let total = self.total();
if total == 0 {
return 0.0;
}
#[expect(clippy::cast_precision_loss, reason = "a share is a report and not an answer")]
{
(total - self.unknown) as f64 / total as f64
}
}
pub fn merge(&mut self, other: Self) {
self.exact += other.exact;
self.certified += other.certified;
self.estimated += other.estimated;
self.unknown += other.unknown;
}
}
impl fmt::Display for Classes {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"exact {}, certified {}, estimated {}, unknown {}",
self.exact, self.certified, self.estimated, self.unknown
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unknown_is_the_default() {
let stat: Stat<u64> = Stat::default();
assert!(stat.is_unknown());
assert_eq!(stat.value(), None);
assert_eq!(stat.class(), None);
assert_eq!(stat.unwrap_or(7), 7);
}
const ROUGHLY: Class = Class::Certified { bound: 0.01, direction: Direction::Within };
#[test]
fn only_an_exact_answer_comes_back_from_exact_value() {
assert_eq!(Stat::exact(4_u64, Provenance::RowCount).exact_value(), Some(&4));
assert_eq!(
Stat::certified(4_u64, 0.01, Direction::Within, Provenance::Quantiles).exact_value(),
None
);
assert_eq!(Stat::estimated(4_u64, Provenance::Sketch).exact_value(), None);
assert_eq!(Stat::<u64>::Unknown.exact_value(), None);
}
#[test]
fn the_three_uses_are_entitled_to_different_classes() {
let exact = Stat::exact(4_u64, Provenance::RowCount);
let certified =
Stat::certified(4_u64, 0.01, Direction::AtMost, Provenance::FrequencySynopsis);
let estimated = Stat::estimated(4_u64, Provenance::Default);
let unknown = Stat::<u64>::Unknown;
assert_eq!(exact.enable(), Some(&4));
assert_eq!(certified.enable(), None);
assert_eq!(estimated.enable(), None);
assert_eq!(unknown.enable(), None);
assert_eq!(certified.answer(), None);
assert_eq!(certified.answer_certified(|bound, _| bound < 0.05), Some(&4));
assert_eq!(certified.answer_certified(|bound, _| bound < 0.001), None);
assert_eq!(estimated.answer_certified(|_, _| true), None);
assert_eq!(exact.decide(), Some(&4));
assert_eq!(estimated.decide(), Some(&4));
assert_eq!(unknown.decide(), None);
assert!(exact.permits(Use::Enable));
assert!(!certified.permits(Use::Enable));
assert!(certified.permits(Use::Answer));
assert!(!estimated.permits(Use::Answer));
assert!(unknown.permits(Use::Decide));
}
#[test]
fn a_class_degrades_when_it_is_combined() {
assert_eq!(Class::Exact.combine(Class::Exact), Class::Exact);
assert_eq!(Class::Exact.combine(Class::Estimated), Class::Estimated);
assert_eq!(Class::Exact.combine(ROUGHLY), ROUGHLY);
assert_eq!(
Class::Certified { bound: 0.01, direction: Direction::AtMost }
.combine(Class::Certified { bound: 0.02, direction: Direction::AtMost }),
Class::Certified { bound: 0.03, direction: Direction::AtMost }
);
assert_eq!(Class::Estimated.combine(ROUGHLY), Class::Estimated);
assert_eq!(Class::Estimated.combine(Class::Estimated), Class::Estimated);
}
#[test]
fn two_certificates_bounding_opposite_sides_only_bound_both() {
assert_eq!(
Class::Certified { bound: 0.01, direction: Direction::AtMost }
.combine(Class::Certified { bound: 0.02, direction: Direction::AtLeast }),
Class::Certified { bound: 0.03, direction: Direction::Within }
);
}
#[test]
fn a_certified_bound_saturates_rather_than_growing_past_everything() {
assert_eq!(
Class::Certified { bound: 0.8, direction: Direction::Within }
.combine(Class::Certified { bound: 0.7, direction: Direction::Within }),
Class::Certified { bound: 1.0, direction: Direction::Within }
);
}
#[test]
fn zip_is_unknown_when_either_side_is() {
let known = Stat::exact(10_u64, Provenance::RowCount);
let unknown = Stat::<u64>::Unknown;
assert_eq!(known.zip(unknown, |left, right| left + right), Stat::Unknown);
assert_eq!(unknown.zip(known, |left, right| left + right), Stat::Unknown);
assert_eq!(
known.zip(Stat::exact(5, Provenance::RowCount), |left, right| left + right),
Stat::exact(15, Provenance::RowCount)
);
}
#[test]
fn a_derived_answer_says_it_was_derived_rather_than_naming_one_side() {
let rows = Stat::exact(10_u64, Provenance::RowCount);
let nulls = Stat::exact(2_u64, Provenance::NullCount);
let counted = rows.zip(nulls, |rows, nulls| rows - nulls);
assert_eq!(counted.value(), Some(&8));
assert_eq!(counted.class(), Some(Class::Exact));
assert_eq!(counted.provenance(), Some(Provenance::Propagation));
}
#[test]
fn map_carries_the_class_and_the_provenance() {
let bytes = Stat::certified(100_u64, 0.05, Direction::Within, Provenance::Quantiles)
.map(|rows| rows * 8);
assert_eq!(bytes, Stat::certified(800, 0.05, Direction::Within, Provenance::Quantiles));
}
#[test]
fn the_histogram_counts_what_it_was_shown() {
let mut classes = Classes::new();
classes.record(&Stat::exact(1_u64, Provenance::RowCount));
classes.record(&Stat::certified(1_u64, 0.1, Direction::Within, Provenance::Quantiles));
classes.record(&Stat::estimated(1_u64, Provenance::ZoneMap));
classes.record(&Stat::<u64>::Unknown);
assert_eq!(classes.total(), 4);
assert_eq!(classes.exact(), 1);
assert_eq!(classes.known_share(), 0.75);
assert_eq!(classes.to_string(), "exact 1, certified 1, estimated 1, unknown 1");
let mut all = Classes::new();
all.merge(classes);
all.merge(classes);
assert_eq!(all.total(), 8);
}
#[test]
fn an_empty_histogram_knows_nothing_rather_than_everything() {
assert_eq!(Classes::new().known_share(), 0.0);
assert_eq!(Classes::new().total(), 0);
}
#[test]
fn an_answer_prints_its_class_and_its_provenance() {
assert_eq!(
Stat::exact(12_u64, Provenance::RowCount).to_string(),
"12 (exact from row count)"
);
assert_eq!(
Stat::certified(12_u64, 0.025, Direction::AtMost, Provenance::FrequencySynopsis)
.to_string(),
"12 (certified at most 2.50% from frequency synopsis)"
);
assert_eq!(
Stat::estimated(12_u64, Provenance::Default).to_string(),
"12 (estimated from default)"
);
assert_eq!(Stat::<u64>::Unknown.to_string(), "unknown");
}
#[test]
fn an_exact_number_says_where_it_came_from_too() {
let counted = Stat::exact(1_000_u64, Provenance::RowCount);
let joined = Stat::exact(1_000_u64, Provenance::LinkHeader);
assert_eq!(counted.class(), joined.class());
assert_ne!(counted.provenance(), joined.provenance());
assert_ne!(counted.to_string(), joined.to_string());
}
#[test]
fn an_observation_is_distinguishable_from_a_measurement_of_the_file() {
assert!(Provenance::Observed.is_observed());
for provenance in [
Provenance::RowCount,
Provenance::ZoneMap,
Provenance::NullCount,
Provenance::Sketch,
Provenance::FrequencySynopsis,
Provenance::Quantiles,
Provenance::Dictionary,
Provenance::Sortedness,
Provenance::Distinctness,
Provenance::LinkHeader,
Provenance::DegreeDistribution,
Provenance::Sample,
Provenance::Default,
Provenance::Propagation,
] {
assert!(!provenance.is_observed(), "{provenance} is not an observation");
}
}
}