use std::collections::HashSet;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Bucket {
Faithful,
Divergent,
Reported,
}
const FAITHFUL_THRESHOLD: f64 = 0.90;
const WORD_FAITHFUL_THRESHOLD: f64 = 0.97;
const SHINGLE_K: usize = 3;
pub fn classify(
wikrs_text: &str,
truth_text: &str,
has_unsupported: bool,
has_table: bool,
) -> Bucket {
if has_unsupported {
Bucket::Reported
} else if is_faithful(wikrs_text, truth_text, has_table) {
Bucket::Faithful
} else {
Bucket::Divergent
}
}
pub fn is_faithful(wikrs_text: &str, truth_text: &str, table_evidence: bool) -> bool {
precision(wikrs_text, truth_text) >= FAITHFUL_THRESHOLD
|| (table_evidence && word_precision(wikrs_text, truth_text) >= WORD_FAITHFUL_THRESHOLD)
}
pub fn precision(wikrs_text: &str, truth_text: &str) -> f64 {
let got = shingles(wikrs_text);
if got.is_empty() {
return 1.0;
}
let truth = shingles(truth_text);
let hits = got.iter().filter(|s| truth.contains(*s)).count();
hits as f64 / got.len() as f64
}
pub fn coverage(wikrs_text: &str, truth_text: &str) -> f64 {
let truth = shingles(truth_text);
if truth.is_empty() {
return 1.0;
}
let got = shingles(wikrs_text);
let hits = truth.iter().filter(|s| got.contains(*s)).count();
hits as f64 / truth.len() as f64
}
pub fn word_precision(wikrs_text: &str, truth_text: &str) -> f64 {
let got: HashSet<String> = word_vec(wikrs_text).into_iter().collect();
if got.is_empty() {
return 1.0;
}
let truth: HashSet<String> = word_vec(truth_text).into_iter().collect();
let hits = got.iter().filter(|w| truth.contains(*w)).count();
hits as f64 / got.len() as f64
}
fn word_vec(text: &str) -> Vec<String> {
text.split(|c: char| !c.is_alphanumeric())
.filter(|w| !w.is_empty())
.map(str::to_lowercase)
.collect()
}
fn shingles(text: &str) -> HashSet<String> {
let words = word_vec(text);
let mut set = HashSet::new();
if words.is_empty() {
return set;
}
if words.len() < SHINGLE_K {
set.insert(words.join(" "));
return set;
}
for w in words.windows(SHINGLE_K) {
set.insert(w.join(" "));
}
set
}
#[derive(Debug, Clone, Copy, Default, PartialEq)]
pub struct Report {
pub faithful: usize,
pub divergent: usize,
pub reported: usize,
}
impl Report {
pub fn record(&mut self, bucket: Bucket) {
match bucket {
Bucket::Faithful => self.faithful += 1,
Bucket::Divergent => self.divergent += 1,
Bucket::Reported => self.reported += 1,
}
}
pub fn total(&self) -> usize {
self.faithful + self.divergent + self.reported
}
pub fn percentages(&self) -> (f64, f64, f64) {
let total = self.total();
if total == 0 {
return (0.0, 0.0, 0.0);
}
let pct = |n: usize| 100.0 * n as f64 / total as f64;
(pct(self.faithful), pct(self.divergent), pct(self.reported))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn faithful_subset_with_no_diagnostics_is_faithful() {
let wikrs = "Earth is the third planet from the Sun";
let truth = "Earth is the third planet from the Sun . It has one moon and abundant water .";
assert_eq!(classify(wikrs, truth, false, false), Bucket::Faithful);
}
#[test]
fn any_unsupported_diagnostic_is_reported_even_if_text_matches() {
let text = "Earth is the third planet from the Sun";
assert_eq!(classify(text, text, true, false), Bucket::Reported);
}
#[test]
fn fabricated_phrase_without_diagnostic_is_divergent() {
let wikrs = "Earth is the flat center of the universe";
let truth = "Earth is the third planet from the Sun in the Solar System";
assert_eq!(classify(wikrs, truth, false, false), Bucket::Divergent);
}
#[test]
fn precision_is_one_for_subset_and_for_empty_output() {
let wikrs = "Earth is the third planet";
let truth = "Earth is the third planet from the Sun and more";
assert!((precision(wikrs, truth) - 1.0).abs() < 1e-9);
assert!((precision("", truth) - 1.0).abs() < 1e-9);
assert!(is_faithful("", truth, false));
}
#[test]
fn low_coverage_does_not_make_a_faithful_page_divergent() {
let wikrs = "Earth is the third planet";
let truth =
"Earth is the third planet from the Sun and it has a large natural satellite moon";
assert!(coverage(wikrs, truth) < 0.5);
assert!(coverage(wikrs, truth) > 0.0);
assert!((precision(wikrs, truth) - 1.0).abs() < 1e-9);
assert_eq!(classify(wikrs, truth, false, false), Bucket::Faithful);
}
#[test]
fn report_tallies_and_percentages_sum_to_one_hundred() {
let mut r = Report::default();
r.record(Bucket::Faithful);
r.record(Bucket::Faithful);
r.record(Bucket::Reported);
r.record(Bucket::Divergent);
assert_eq!(r.total(), 4);
let (x, y, z) = r.percentages();
assert!((x - 50.0).abs() < 1e-9);
assert!((y - 25.0).abs() < 1e-9);
assert!((z - 25.0).abs() < 1e-9);
assert!((x + y + z - 100.0).abs() < 1e-9);
}
#[test]
fn empty_report_is_all_zero() {
let r = Report::default();
assert_eq!(r.total(), 0);
assert_eq!(r.percentages(), (0.0, 0.0, 0.0));
}
#[test]
fn word_precision_rescues_reordered_table_cells() {
let wikrs = "Alice 30 Bob 25";
let truth = "Name Age Alice Bob 30 25 and more rows of data here";
assert!(precision(wikrs, truth) < 0.90, "shingles should differ");
assert!(word_precision(wikrs, truth) > 0.97, "every word is present");
assert!(is_faithful(wikrs, truth, true), "faithful via the fallback");
}
#[test]
fn word_fallback_needs_table_evidence() {
let wikrs = "planet third the is Earth Sun the from";
let truth = "Earth is the third planet from the Sun";
assert!(word_precision(wikrs, truth) > 0.97, "same word set");
assert!(
!is_faithful(wikrs, truth, false),
"no table -> no order-robust fallback"
);
assert_eq!(classify(wikrs, truth, false, false), Bucket::Divergent);
assert_eq!(classify(wikrs, truth, false, true), Bucket::Faithful);
}
#[test]
fn genuinely_different_words_stay_divergent() {
let wikrs = "Berlin is the capital of Germany";
let truth = "Paris is the capital of France and a city";
assert!(word_precision(wikrs, truth) < 0.97);
assert!(!is_faithful(wikrs, truth, true));
}
}