use crate::inference::{CharClassifier, ClassificationResult, InferenceError};
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct ColumnConfig {
pub sample_size: usize,
pub min_agreement: f32,
}
impl Default for ColumnConfig {
fn default() -> Self {
Self {
sample_size: 100,
min_agreement: 0.3,
}
}
}
#[derive(Debug, Clone)]
pub struct ColumnResult {
pub label: String,
pub confidence: f32,
pub vote_distribution: Vec<(String, f32)>,
pub disambiguation_applied: bool,
pub disambiguation_rule: Option<String>,
pub samples_used: usize,
}
pub struct ColumnClassifier {
classifier: CharClassifier,
config: ColumnConfig,
}
impl ColumnClassifier {
pub fn new(classifier: CharClassifier, config: ColumnConfig) -> Self {
Self { classifier, config }
}
pub fn with_defaults(classifier: CharClassifier) -> Self {
Self::new(classifier, ColumnConfig::default())
}
pub fn classify_column(&self, values: &[String]) -> Result<ColumnResult, InferenceError> {
if values.is_empty() {
return Ok(ColumnResult {
label: "unknown".to_string(),
confidence: 0.0,
vote_distribution: vec![],
disambiguation_applied: false,
disambiguation_rule: None,
samples_used: 0,
});
}
let sample = if values.len() <= self.config.sample_size {
values.to_vec()
} else {
let step = values.len() as f64 / self.config.sample_size as f64;
(0..self.config.sample_size)
.map(|i| values[(i as f64 * step) as usize].clone())
.collect()
};
let n_samples = sample.len();
let results = self.classifier.classify_batch(&sample)?;
let mut vote_counts: HashMap<String, usize> = HashMap::new();
for result in &results {
*vote_counts.entry(result.label.clone()).or_default() += 1;
}
let mut votes: Vec<(String, usize)> = vote_counts.into_iter().collect();
votes.sort_by(|a, b| b.1.cmp(&a.1));
let vote_distribution: Vec<(String, f32)> = votes
.iter()
.map(|(label, count)| (label.clone(), *count as f32 / n_samples as f32))
.collect();
let (majority_label, majority_count) = votes.first().cloned().unwrap_or_default();
let majority_fraction = majority_count as f32 / n_samples as f32;
let disambiguation = disambiguate(&sample, &results, &votes, n_samples);
if let Some((label, rule_name)) = disambiguation {
Ok(ColumnResult {
label,
confidence: majority_fraction.max(0.8), vote_distribution,
disambiguation_applied: true,
disambiguation_rule: Some(rule_name),
samples_used: n_samples,
})
} else {
let confidence = if majority_fraction >= self.config.min_agreement {
majority_fraction
} else {
majority_fraction * 0.5 };
Ok(ColumnResult {
label: majority_label,
confidence,
vote_distribution,
disambiguation_applied: false,
disambiguation_rule: None,
samples_used: n_samples,
})
}
}
pub fn classifier(&self) -> &CharClassifier {
&self.classifier
}
pub fn config(&self) -> &ColumnConfig {
&self.config
}
}
const DATE_SLASH_PAIR: (&str, &str) = ("datetime.date.us_slash", "datetime.date.eu_slash");
const SHORT_DATE_PAIR: (&str, &str) = ("datetime.date.short_mdy", "datetime.date.short_dmy");
const COORDINATE_PAIR: (&str, &str) = (
"geography.coordinate.latitude",
"geography.coordinate.longitude",
);
fn disambiguate(
values: &[String],
results: &[ClassificationResult],
votes: &[(String, usize)],
_n_samples: usize,
) -> Option<(String, String)> {
let top_labels: Vec<&str> = votes.iter().take(3).map(|(l, _)| l.as_str()).collect();
if contains_pair(&top_labels, DATE_SLASH_PAIR.0, DATE_SLASH_PAIR.1) {
if let Some(label) = disambiguate_slash_dates(values) {
return Some((label, "date_slash_disambiguation".to_string()));
}
}
if contains_pair(&top_labels, SHORT_DATE_PAIR.0, SHORT_DATE_PAIR.1) {
if let Some(label) = disambiguate_short_dates(values) {
return Some((label, "short_date_disambiguation".to_string()));
}
}
if contains_pair(&top_labels, COORDINATE_PAIR.0, COORDINATE_PAIR.1) {
if let Some(label) = disambiguate_coordinates(values) {
return Some((label, "coordinate_disambiguation".to_string()));
}
}
if let Some((label, rule)) = disambiguate_numeric(values, results, &top_labels) {
return Some((label, rule));
}
None
}
fn contains_pair(labels: &[&str], a: &str, b: &str) -> bool {
labels.contains(&a) && labels.contains(&b)
}
fn disambiguate_slash_dates(values: &[String]) -> Option<String> {
let mut first_over_12 = false;
let mut second_over_12 = false;
for val in values {
let parts: Vec<&str> = val.split('/').collect();
if parts.len() >= 2 {
if let Ok(first) = parts[0].parse::<u32>() {
if first > 12 {
first_over_12 = true;
}
}
if let Ok(second) = parts[1].parse::<u32>() {
if second > 12 {
second_over_12 = true;
}
}
}
}
if first_over_12 && !second_over_12 {
Some("datetime.date.eu_slash".to_string())
} else if second_over_12 && !first_over_12 {
Some("datetime.date.us_slash".to_string())
} else {
None
}
}
fn disambiguate_short_dates(values: &[String]) -> Option<String> {
let mut first_over_12 = false;
let mut second_over_12 = false;
for val in values {
let parts: Vec<&str> = val.split('-').collect();
if parts.len() >= 2 {
if let Ok(first) = parts[0].parse::<u32>() {
if first > 12 {
first_over_12 = true;
}
}
if let Ok(second) = parts[1].parse::<u32>() {
if second > 12 {
second_over_12 = true;
}
}
}
}
if first_over_12 && !second_over_12 {
Some("datetime.date.short_dmy".to_string())
} else if second_over_12 && !first_over_12 {
Some("datetime.date.short_mdy".to_string())
} else {
None
}
}
fn disambiguate_coordinates(values: &[String]) -> Option<String> {
let mut any_over_90 = false;
let mut all_parseable = true;
let mut parsed_count = 0;
for val in values {
if let Ok(v) = val.trim().parse::<f64>() {
parsed_count += 1;
if v.abs() > 90.0 {
any_over_90 = true;
}
} else {
all_parseable = false;
}
}
if parsed_count < 3 {
return None;
}
if any_over_90 {
Some("geography.coordinate.longitude".to_string())
} else if all_parseable {
Some("geography.coordinate.latitude".to_string())
} else {
None
}
}
fn disambiguate_numeric(
values: &[String],
results: &[ClassificationResult],
top_labels: &[&str],
) -> Option<(String, String)> {
let numeric_types = [
"technology.internet.port",
"representation.numeric.increment",
"representation.numeric.integer_number",
"representation.numeric.decimal_number",
"geography.address.postal_code",
"geography.address.street_number",
"datetime.component.year",
];
let has_numeric_confusion = top_labels.iter().any(|l| numeric_types.contains(l));
if !has_numeric_confusion {
return None;
}
let parsed: Vec<i64> = values
.iter()
.filter_map(|v| v.trim().parse::<i64>().ok())
.collect();
if parsed.len() < 3 {
return None;
}
let min = *parsed.iter().min().unwrap();
let max = *parsed.iter().max().unwrap();
let range = max - min;
let mut sorted = parsed.clone();
sorted.sort();
sorted.dedup();
let is_sequential = if sorted.len() >= 3 {
let diffs: Vec<i64> = sorted.windows(2).map(|w| w[1] - w[0]).collect();
let avg_diff = diffs.iter().sum::<i64>() as f64 / diffs.len() as f64;
let variance = diffs
.iter()
.map(|d| (*d as f64 - avg_diff).powi(2))
.sum::<f64>()
/ diffs.len() as f64;
variance < (avg_diff * 0.5).powi(2) && avg_diff > 0.0
} else {
false
};
let all_in_port_range = min >= 0 && max <= 65535;
let has_common_ports = parsed
.iter()
.any(|v| [80, 443, 8080, 3306, 5432, 22, 21, 25, 53, 3000, 8000, 8443].contains(v));
let all_positive = min > 0;
let typical_postal_range = all_positive && max <= 99999 && min >= 100;
let digit_lengths: Vec<usize> = values
.iter()
.filter_map(|v| {
let trimmed = v.trim();
if trimmed.chars().all(|c| c.is_ascii_digit()) {
Some(trimmed.len())
} else {
None
}
})
.collect();
let consistent_digits = if !digit_lengths.is_empty() {
let first_len = digit_lengths[0];
digit_lengths.iter().all(|&l| l == first_len)
} else {
false
};
let year_candidates: Vec<i64> = parsed
.iter()
.filter(|&&v| (1900..=2100).contains(&v))
.copied()
.collect();
let count_trimmed_4digit = values
.iter()
.filter(|v| {
let t = v.trim();
t.len() == 4 && t.chars().all(|c| c.is_ascii_digit())
})
.count();
let fraction_4digit = if values.is_empty() {
0.0
} else {
count_trimmed_4digit as f64 / values.len() as f64
};
let mostly_4digit = fraction_4digit >= 0.8;
let year_fraction = if parsed.is_empty() {
0.0
} else {
year_candidates.len() as f64 / parsed.len() as f64
};
let is_year_column = year_fraction >= 0.8 && parsed.len() >= 3 && mostly_4digit;
if is_year_column {
return Some((
"datetime.component.year".to_string(),
"numeric_year_detection".to_string(),
));
}
if is_sequential && min >= 0 && range > 0 {
return Some((
"representation.numeric.increment".to_string(),
"numeric_sequential_detection".to_string(),
));
}
if has_common_ports && all_in_port_range && !is_sequential {
return Some((
"technology.internet.port".to_string(),
"numeric_port_detection".to_string(),
));
}
if consistent_digits && typical_postal_range && !is_sequential {
if mostly_4digit && year_fraction >= 0.8 {
return Some((
"datetime.component.year".to_string(),
"numeric_year_detection".to_string(),
));
}
return Some((
"geography.address.postal_code".to_string(),
"numeric_postal_code_detection".to_string(),
));
}
let street_range = all_positive && max < 100000 && min >= 1;
let is_street_candidate = top_labels.contains(&"geography.address.street_number");
if is_street_candidate
&& street_range
&& !is_sequential
&& !has_common_ports
&& !consistent_digits
{
return Some((
"geography.address.street_number".to_string(),
"numeric_street_number_detection".to_string(),
));
}
let _ = results; None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_slash_date_eu_detected() {
let values: Vec<String> = vec![
"15/01/2024",
"28/06/2023",
"03/11/2022",
"31/12/2019",
"12/05/2020",
]
.into_iter()
.map(String::from)
.collect();
let result = disambiguate_slash_dates(&values);
assert_eq!(result, Some("datetime.date.eu_slash".to_string()));
}
#[test]
fn test_slash_date_us_detected() {
let values: Vec<String> = vec![
"01/15/2024",
"06/28/2023",
"11/03/2022",
"12/31/2019",
"05/12/2020",
]
.into_iter()
.map(String::from)
.collect();
let result = disambiguate_slash_dates(&values);
assert_eq!(result, Some("datetime.date.us_slash".to_string()));
}
#[test]
fn test_slash_date_ambiguous() {
let values: Vec<String> = vec![
"01/02/2024",
"03/04/2023",
"05/06/2022",
"07/08/2021",
"09/10/2020",
]
.into_iter()
.map(String::from)
.collect();
let result = disambiguate_slash_dates(&values);
assert_eq!(result, None);
}
#[test]
fn test_short_date_dmy_detected() {
let values: Vec<String> = vec!["15-01-24", "28-06-23", "31-12-19"]
.into_iter()
.map(String::from)
.collect();
let result = disambiguate_short_dates(&values);
assert_eq!(result, Some("datetime.date.short_dmy".to_string()));
}
#[test]
fn test_short_date_mdy_detected() {
let values: Vec<String> = vec!["01-15-24", "06-28-23", "12-31-19"]
.into_iter()
.map(String::from)
.collect();
let result = disambiguate_short_dates(&values);
assert_eq!(result, Some("datetime.date.short_mdy".to_string()));
}
#[test]
fn test_coordinates_longitude_detected() {
let values: Vec<String> = vec!["-74.0060", "151.2093", "-0.1278", "139.6917", "2.3522"]
.into_iter()
.map(String::from)
.collect();
let result = disambiguate_coordinates(&values);
assert_eq!(result, Some("geography.coordinate.longitude".to_string()));
}
#[test]
fn test_coordinates_latitude_detected() {
let values: Vec<String> = vec!["40.7128", "-33.8688", "51.5074", "35.6762", "-22.9068"]
.into_iter()
.map(String::from)
.collect();
let result = disambiguate_coordinates(&values);
assert_eq!(result, Some("geography.coordinate.latitude".to_string()));
}
#[test]
fn test_numeric_sequential_detection() {
let values: Vec<String> = vec!["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]
.into_iter()
.map(String::from)
.collect();
let results: Vec<ClassificationResult> = values
.iter()
.map(|_| ClassificationResult {
label: "representation.numeric.increment".to_string(),
confidence: 0.8,
all_scores: vec![],
})
.collect();
let votes = vec![
("representation.numeric.increment".to_string(), 8),
("representation.numeric.integer_number".to_string(), 2),
];
let top_labels: Vec<&str> = votes.iter().map(|(l, _)| l.as_str()).collect();
let result = disambiguate_numeric(&values, &results, &top_labels);
assert!(result.is_some());
let (label, _rule) = result.unwrap();
assert_eq!(label, "representation.numeric.increment");
}
#[test]
fn test_numeric_port_detection() {
let values: Vec<String> = vec![
"80", "443", "8080", "3306", "22", "5432", "3000", "8443", "25", "53",
]
.into_iter()
.map(String::from)
.collect();
let results: Vec<ClassificationResult> = values
.iter()
.map(|_| ClassificationResult {
label: "technology.internet.port".to_string(),
confidence: 0.7,
all_scores: vec![],
})
.collect();
let votes = vec![
("technology.internet.port".to_string(), 7),
("representation.numeric.integer_number".to_string(), 3),
];
let top_labels: Vec<&str> = votes.iter().map(|(l, _)| l.as_str()).collect();
let result = disambiguate_numeric(&values, &results, &top_labels);
assert!(result.is_some());
let (label, _rule) = result.unwrap();
assert_eq!(label, "technology.internet.port");
}
#[test]
fn test_numeric_postal_code_detection() {
let values: Vec<String> = vec![
"10001", "90210", "30301", "60601", "02101", "75001", "33101", "94102", "20001",
"98101",
]
.into_iter()
.map(String::from)
.collect();
let results: Vec<ClassificationResult> = values
.iter()
.map(|_| ClassificationResult {
label: "geography.address.postal_code".to_string(),
confidence: 0.6,
all_scores: vec![],
})
.collect();
let votes = vec![
("geography.address.postal_code".to_string(), 6),
("representation.numeric.integer_number".to_string(), 4),
];
let top_labels: Vec<&str> = votes.iter().map(|(l, _)| l.as_str()).collect();
let result = disambiguate_numeric(&values, &results, &top_labels);
assert!(result.is_some());
let (label, _rule) = result.unwrap();
assert_eq!(label, "geography.address.postal_code");
}
#[test]
fn test_year_detection() {
let values: Vec<String> = vec![
"2020", "2019", "2021", "2018", "2023", "2015", "2022", "2017", "2024", "2016",
]
.into_iter()
.map(String::from)
.collect();
let results: Vec<ClassificationResult> = values
.iter()
.map(|_| ClassificationResult {
label: "representation.numeric.integer_number".to_string(),
confidence: 0.6,
all_scores: vec![],
})
.collect();
let votes = vec![
("representation.numeric.integer_number".to_string(), 5),
("geography.address.street_number".to_string(), 3),
("datetime.component.year".to_string(), 2),
];
let top_labels: Vec<&str> = votes.iter().map(|(l, _)| l.as_str()).collect();
let result = disambiguate_numeric(&values, &results, &top_labels);
assert!(result.is_some());
let (label, rule) = result.unwrap();
assert_eq!(label, "datetime.component.year");
assert_eq!(rule, "numeric_year_detection");
}
#[test]
fn test_year_detection_historical() {
let values: Vec<String> = vec!["1945", "1918", "1969", "1989", "2001"]
.into_iter()
.map(String::from)
.collect();
let results: Vec<ClassificationResult> = values
.iter()
.map(|_| ClassificationResult {
label: "representation.numeric.decimal_number".to_string(),
confidence: 0.5,
all_scores: vec![],
})
.collect();
let votes = vec![
("representation.numeric.decimal_number".to_string(), 3),
("representation.numeric.integer_number".to_string(), 2),
];
let top_labels: Vec<&str> = votes.iter().map(|(l, _)| l.as_str()).collect();
let result = disambiguate_numeric(&values, &results, &top_labels);
assert!(result.is_some());
let (label, _) = result.unwrap();
assert_eq!(label, "datetime.component.year");
}
#[test]
fn test_year_not_triggered_for_5digit_postal() {
let values: Vec<String> = vec![
"10001", "90210", "30301", "60601", "02101", "75001", "33101", "94102", "20001",
"98101",
]
.into_iter()
.map(String::from)
.collect();
let results: Vec<ClassificationResult> = values
.iter()
.map(|_| ClassificationResult {
label: "geography.address.postal_code".to_string(),
confidence: 0.6,
all_scores: vec![],
})
.collect();
let votes = vec![
("geography.address.postal_code".to_string(), 6),
("representation.numeric.integer_number".to_string(), 4),
];
let top_labels: Vec<&str> = votes.iter().map(|(l, _)| l.as_str()).collect();
let result = disambiguate_numeric(&values, &results, &top_labels);
assert!(result.is_some());
let (label, _) = result.unwrap();
assert_eq!(label, "geography.address.postal_code");
}
#[test]
fn test_sequential_years_still_detected_as_year() {
let values: Vec<String> = vec![
"2001", "2002", "2003", "2004", "2005", "2006", "2007", "2008", "2009", "2010",
]
.into_iter()
.map(String::from)
.collect();
let results: Vec<ClassificationResult> = values
.iter()
.map(|_| ClassificationResult {
label: "representation.numeric.increment".to_string(),
confidence: 0.7,
all_scores: vec![],
})
.collect();
let votes = vec![
("representation.numeric.increment".to_string(), 7),
("representation.numeric.integer_number".to_string(), 3),
];
let top_labels: Vec<&str> = votes.iter().map(|(l, _)| l.as_str()).collect();
let result = disambiguate_numeric(&values, &results, &top_labels);
assert!(result.is_some());
let (label, _) = result.unwrap();
assert_eq!(label, "datetime.component.year");
}
#[test]
fn test_sequential_non_year_still_increment() {
let values: Vec<String> = vec!["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]
.into_iter()
.map(String::from)
.collect();
let results: Vec<ClassificationResult> = values
.iter()
.map(|_| ClassificationResult {
label: "representation.numeric.increment".to_string(),
confidence: 0.8,
all_scores: vec![],
})
.collect();
let votes = vec![
("representation.numeric.increment".to_string(), 8),
("representation.numeric.integer_number".to_string(), 2),
];
let top_labels: Vec<&str> = votes.iter().map(|(l, _)| l.as_str()).collect();
let result = disambiguate_numeric(&values, &results, &top_labels);
assert!(result.is_some());
let (label, _) = result.unwrap();
assert_eq!(label, "representation.numeric.increment");
}
#[test]
fn test_year_not_triggered_for_ports() {
let values: Vec<String> = vec!["80", "443", "8080", "3306", "22"]
.into_iter()
.map(String::from)
.collect();
let results: Vec<ClassificationResult> = values
.iter()
.map(|_| ClassificationResult {
label: "technology.internet.port".to_string(),
confidence: 0.8,
all_scores: vec![],
})
.collect();
let votes = vec![
("technology.internet.port".to_string(), 4),
("representation.numeric.integer_number".to_string(), 1),
];
let top_labels: Vec<&str> = votes.iter().map(|(l, _)| l.as_str()).collect();
let result = disambiguate_numeric(&values, &results, &top_labels);
if let Some((label, _)) = result {
assert_ne!(label, "datetime.component.year");
}
}
#[test]
fn test_year_with_outlier_not_postal_code() {
let values: Vec<String> = vec![
"2020", "2019", "2021", "2018", "2023", "2015", "2022", "2017", "2024", "1776",
]
.into_iter()
.map(String::from)
.collect();
let results: Vec<ClassificationResult> = values
.iter()
.map(|_| ClassificationResult {
label: "geography.address.postal_code".to_string(),
confidence: 0.6,
all_scores: vec![],
})
.collect();
let votes = vec![
("geography.address.postal_code".to_string(), 5),
("representation.numeric.decimal_number".to_string(), 3),
("datetime.component.year".to_string(), 2),
];
let top_labels: Vec<&str> = votes.iter().map(|(l, _)| l.as_str()).collect();
let result = disambiguate_numeric(&values, &results, &top_labels);
assert!(result.is_some());
let (label, _rule) = result.unwrap();
assert_eq!(label, "datetime.component.year");
}
#[test]
fn test_year_with_many_outliers_not_year() {
let values: Vec<String> = vec![
"2020", "2019", "2021", "1500", "1600", "1700", "1800", "2022", "2023", "2024",
]
.into_iter()
.map(String::from)
.collect();
let results: Vec<ClassificationResult> = values
.iter()
.map(|_| ClassificationResult {
label: "representation.numeric.integer_number".to_string(),
confidence: 0.6,
all_scores: vec![],
})
.collect();
let votes = vec![
("representation.numeric.integer_number".to_string(), 5),
("geography.address.postal_code".to_string(), 3),
("datetime.component.year".to_string(), 2),
];
let top_labels: Vec<&str> = votes.iter().map(|(l, _)| l.as_str()).collect();
let result = disambiguate_numeric(&values, &results, &top_labels);
if let Some((label, _)) = result {
assert_ne!(label, "datetime.component.year");
}
}
#[test]
fn test_year_with_non4digit_outlier() {
let values: Vec<String> = vec![
"2020", "2019", "2021", "2018", "2023", "2015", "2022", "2017", "2024", "NA",
]
.into_iter()
.map(String::from)
.collect();
let results: Vec<ClassificationResult> = values
.iter()
.map(|_| ClassificationResult {
label: "representation.numeric.decimal_number".to_string(),
confidence: 0.6,
all_scores: vec![],
})
.collect();
let votes = vec![
("representation.numeric.decimal_number".to_string(), 8),
("datetime.component.year".to_string(), 2),
];
let top_labels: Vec<&str> = votes.iter().map(|(l, _)| l.as_str()).collect();
let result = disambiguate_numeric(&values, &results, &top_labels);
assert!(result.is_some());
let (label, rule) = result.unwrap();
assert_eq!(label, "datetime.component.year");
assert_eq!(rule, "numeric_year_detection");
}
#[test]
fn test_year_with_decimal_format() {
let values: Vec<String> = vec![
"2020", "2019", "2021.0", "2018", "2023", "2015", "2022", "2017.0", "2024", "2016",
]
.into_iter()
.map(String::from)
.collect();
let results: Vec<ClassificationResult> = values
.iter()
.map(|_| ClassificationResult {
label: "representation.numeric.decimal_number".to_string(),
confidence: 0.7,
all_scores: vec![],
})
.collect();
let votes = vec![
("representation.numeric.decimal_number".to_string(), 10),
];
let top_labels: Vec<&str> = votes.iter().map(|(l, _)| l.as_str()).collect();
let result = disambiguate_numeric(&values, &results, &top_labels);
assert!(result.is_some());
let (label, _) = result.unwrap();
assert_eq!(label, "datetime.component.year");
}
#[test]
fn test_not_year_when_too_few_4digit() {
let values: Vec<String> = vec![
"2020", "2019", "NA", "N/A", "", "2015", "2022", "null", "2024", "missing",
]
.into_iter()
.map(String::from)
.collect();
let results: Vec<ClassificationResult> = values
.iter()
.map(|_| ClassificationResult {
label: "representation.numeric.decimal_number".to_string(),
confidence: 0.5,
all_scores: vec![],
})
.collect();
let votes = vec![
("representation.numeric.decimal_number".to_string(), 10),
];
let top_labels: Vec<&str> = votes.iter().map(|(l, _)| l.as_str()).collect();
let result = disambiguate_numeric(&values, &results, &top_labels);
if let Some((label, _)) = result {
assert_ne!(label, "datetime.component.year");
}
}
#[test]
fn test_empty_column() {
let result = ColumnResult {
label: "unknown".to_string(),
confidence: 0.0,
vote_distribution: vec![],
disambiguation_applied: false,
disambiguation_rule: None,
samples_used: 0,
};
assert_eq!(result.label, "unknown");
assert_eq!(result.samples_used, 0);
}
}