use dataprof_core::{SemanticHintBinding, SemanticHintKind, SemanticHints};
use dataprof_metrics::{is_null_like_token, value_matches_hint};
#[derive(Debug, Clone)]
struct BindingCounter {
binding: SemanticHintBinding,
}
#[derive(Debug, Clone, Default)]
pub struct ValueHintBindingAccumulator {
counters: Vec<BindingCounter>,
}
impl ValueHintBindingAccumulator {
pub fn new(hints: &SemanticHints) -> Self {
let positive = hints.positive_columns.iter().map(|column| BindingCounter {
binding: SemanticHintBinding {
column: column.clone(),
kind: SemanticHintKind::Positive,
checked_values: 0,
matched_values: 0,
exact: true,
},
});
let temporal = hints.temporal_columns.iter().map(|column| BindingCounter {
binding: SemanticHintBinding {
column: column.clone(),
kind: SemanticHintKind::Temporal,
checked_values: 0,
matched_values: 0,
exact: true,
},
});
Self {
counters: positive.chain(temporal).collect(),
}
}
pub fn observe(&mut self, column: &str, value: &str) {
if is_null_like_token(value.trim()) {
return;
}
for counter in self
.counters
.iter_mut()
.filter(|counter| counter.binding.column == column)
{
counter.binding.checked_values += 1;
if value_matches_hint(value, counter.binding.kind) {
counter.binding.matched_values += 1;
}
}
}
pub fn bindings<'a>(
&self,
column_names: impl IntoIterator<Item = &'a str>,
) -> Vec<SemanticHintBinding> {
let names: std::collections::HashSet<&str> = column_names.into_iter().collect();
self.counters
.iter()
.filter(|counter| names.contains(counter.binding.column.as_str()))
.map(|counter| counter.binding.clone())
.collect()
}
pub fn merge(&mut self, other: &Self) {
for other_counter in &other.counters {
if let Some(counter) = self.counters.iter_mut().find(|candidate| {
candidate.binding.column == other_counter.binding.column
&& candidate.binding.kind == other_counter.binding.kind
}) {
counter.binding.checked_values += other_counter.binding.checked_values;
counter.binding.matched_values += other_counter.binding.matched_values;
} else {
self.counters.push(other_counter.clone());
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn accumulates_exact_counts_with_calculator_semantics() {
let hints = SemanticHints::new(vec!["amount".to_string()], vec![])
.with_temporal_columns(vec!["event".to_string()]);
let mut accumulator = ValueHintBindingAccumulator::new(&hints);
for value in ["1", " 2", "NULL"] {
accumulator.observe("amount", value);
}
for value in ["2020-01-01", "not-a-date", " 2021-01-01"] {
accumulator.observe("event", value);
}
let bindings = accumulator.bindings(["amount", "event"]);
assert_eq!(bindings[0].checked_values, 2);
assert_eq!(bindings[0].matched_values, 1);
assert!(bindings[0].exact);
assert_eq!(bindings[1].checked_values, 3);
assert_eq!(bindings[1].matched_values, 1);
assert!(bindings[1].exact);
}
#[test]
fn omits_unknown_columns() {
let hints = SemanticHints::new(vec!["missing".to_string()], vec![]);
let accumulator = ValueHintBindingAccumulator::new(&hints);
assert!(accumulator.bindings(["present"]).is_empty());
}
}