1use std::collections::HashMap;
31
32#[derive(Debug, Clone)]
34pub struct ChrfScore {
35 pub score: f32,
37 pub order: usize,
39 pub beta: f32,
41 pub word_order: usize,
43}
44
45pub fn chrf(candidate: &str, reference: &str) -> ChrfScore {
47 chrf_with(candidate, reference, 6, 2.0, 0)
48}
49
50pub fn chrf_plus_plus(candidate: &str, reference: &str) -> ChrfScore {
52 chrf_with(candidate, reference, 6, 2.0, 2)
53}
54
55pub fn chrf_with(
61 candidate: &str,
62 reference: &str,
63 order: usize,
64 beta: f32,
65 word_order: usize,
66) -> ChrfScore {
67 let order = order.max(1);
68 let cand_chars: Vec<char> = candidate.chars().collect();
69 let ref_chars: Vec<char> = reference.chars().collect();
70
71 let both_empty = cand_chars.is_empty() && ref_chars.is_empty();
73 let one_empty = cand_chars.is_empty() ^ ref_chars.is_empty();
74 if both_empty {
75 return ChrfScore {
76 score: 1.0,
77 order,
78 beta,
79 word_order,
80 };
81 }
82 if one_empty {
83 return ChrfScore {
84 score: 0.0,
85 order,
86 beta,
87 word_order,
88 };
89 }
90
91 let cand_words: Vec<&str> = candidate.split_whitespace().collect();
92 let ref_words: Vec<&str> = reference.split_whitespace().collect();
93
94 let mut f_values: Vec<f32> = Vec::new();
96
97 for n in 1..=order {
99 if let Some(f) = order_f_beta_chars(&cand_chars, &ref_chars, n, beta) {
100 f_values.push(f);
101 } else {
102 f_values.push(0.0);
103 }
104 }
105
106 if word_order >= 1 {
108 for n in 1..=word_order {
109 if let Some(f) = order_f_beta_words(&cand_words, &ref_words, n, beta) {
110 f_values.push(f);
111 } else {
112 f_values.push(0.0);
113 }
114 }
115 }
116
117 let score = if f_values.is_empty() {
118 0.0
119 } else {
120 let sum: f32 = f_values.iter().sum();
121 sum / f_values.len() as f32
122 };
123
124 ChrfScore {
125 score: score.clamp(0.0, 1.0),
126 order,
127 beta,
128 word_order,
129 }
130}
131
132fn order_f_beta_chars(cand: &[char], reference: &[char], n: usize, beta: f32) -> Option<f32> {
133 if cand.len() < n || reference.len() < n {
134 return None;
135 }
136 let cand_counts = ngram_counts_char(cand, n);
137 let ref_counts = ngram_counts_char(reference, n);
138 Some(f_beta_from_counts(&cand_counts, &ref_counts, beta))
139}
140
141fn order_f_beta_words(cand: &[&str], reference: &[&str], n: usize, beta: f32) -> Option<f32> {
142 if cand.len() < n || reference.len() < n {
143 return None;
144 }
145 let cand_counts = ngram_counts_words(cand, n);
146 let ref_counts = ngram_counts_words(reference, n);
147 Some(f_beta_from_counts(&cand_counts, &ref_counts, beta))
148}
149
150fn f_beta_from_counts<K: std::hash::Hash + Eq + Clone>(
151 cand: &HashMap<K, usize>,
152 reference: &HashMap<K, usize>,
153 beta: f32,
154) -> f32 {
155 let cand_total: usize = cand.values().sum();
156 let ref_total: usize = reference.values().sum();
157 if cand_total == 0 || ref_total == 0 {
158 return 0.0;
159 }
160 let mut overlap = 0usize;
161 for (k, &v) in cand {
162 if let Some(&rv) = reference.get(k) {
163 overlap += v.min(rv);
164 }
165 }
166 if overlap == 0 {
167 return 0.0;
168 }
169 let p = overlap as f32 / cand_total as f32;
170 let r = overlap as f32 / ref_total as f32;
171 let b2 = beta * beta;
172 let denom = b2 * p + r;
173 if denom <= 0.0 {
174 0.0
175 } else {
176 ((1.0 + b2) * p * r) / denom
177 }
178}
179
180fn ngram_counts_char(chars: &[char], n: usize) -> HashMap<Vec<char>, usize> {
181 let mut counts: HashMap<Vec<char>, usize> = HashMap::new();
182 if n == 0 || chars.len() < n {
183 return counts;
184 }
185 for w in chars.windows(n) {
186 *counts.entry(w.to_vec()).or_insert(0) += 1;
187 }
188 counts
189}
190
191fn ngram_counts_words(words: &[&str], n: usize) -> HashMap<Vec<String>, usize> {
192 let mut counts: HashMap<Vec<String>, usize> = HashMap::new();
193 if n == 0 || words.len() < n {
194 return counts;
195 }
196 for w in words.windows(n) {
197 let key: Vec<String> = w.iter().map(|s| s.to_string()).collect();
198 *counts.entry(key).or_insert(0) += 1;
199 }
200 counts
201}