pub fn wer(reference: &str, hypothesis: &str) -> f64 {
let r: Vec<&str> = reference.split_whitespace().collect();
let h: Vec<&str> = hypothesis.split_whitespace().collect();
if r.is_empty() {
return f64::NAN;
}
let dist = levenshtein(&r, &h);
dist as f64 / r.len() as f64
}
pub fn cer(reference: &str, hypothesis: &str) -> f64 {
let r: Vec<char> = reference.chars().filter(|c| !c.is_whitespace()).collect();
let h: Vec<char> = hypothesis.chars().filter(|c| !c.is_whitespace()).collect();
if r.is_empty() {
return f64::NAN;
}
let dist = levenshtein(&r, &h);
dist as f64 / r.len() as f64
}
pub fn wer_lenient(reference: &str, hypothesis: &str) -> f64 {
let r_norm = normalize_lenient(reference);
let h_norm = normalize_lenient(hypothesis);
let r: Vec<&str> = r_norm.split_whitespace().collect();
let h: Vec<&str> = h_norm.split_whitespace().collect();
if r.is_empty() {
return f64::NAN;
}
let dist = levenshtein(&r, &h);
dist as f64 / r.len() as f64
}
pub fn cer_lenient(reference: &str, hypothesis: &str) -> f64 {
let r: Vec<char> = normalize_lenient(reference)
.chars()
.filter(|c| !c.is_whitespace())
.collect();
let h: Vec<char> = normalize_lenient(hypothesis)
.chars()
.filter(|c| !c.is_whitespace())
.collect();
if r.is_empty() {
return f64::NAN;
}
let dist = levenshtein(&r, &h);
dist as f64 / r.len() as f64
}
pub fn normalize_lenient(text: &str) -> String {
const DROP_PUNCT: &[char] = &[
'.', ',', '?', '!', ':', ';', '"', '\'', '(', ')', '[', ']', '{', '}',
'。', '、', '「', '」', '『', '』', '・', '?', '!', ':', ';', '(', ')', ',', '.',
'《', '》', '〈', '〉', '〝', '〟', '…', '\u{2018}', '\u{2019}', '\u{201C}', '\u{201D}', ];
const SPLIT_PUNCT: &[char] = &['-', '–', '—', '−', '―'];
let digits_normalised = normalize_chinese_numerals(text);
let digits_normalised = normalize_korean_numerals(&digits_normalised);
let mut out = String::with_capacity(digits_normalised.len());
let mut last_space = true; for c in digits_normalised.chars() {
if DROP_PUNCT.contains(&c) {
continue;
}
if SPLIT_PUNCT.contains(&c) || c.is_whitespace() {
if !last_space {
out.push(' ');
last_space = true;
}
continue;
}
last_space = false;
for lower in c.to_lowercase() {
out.push(lower);
}
}
if out.ends_with(' ') {
out.pop();
}
normalize_english_number_words(&out)
}
fn normalize_chinese_numerals(text: &str) -> String {
let mut out = String::with_capacity(text.len());
let mut run = String::new();
for c in text.chars() {
if is_zh_numeric(c) {
run.push(c);
} else {
if !run.is_empty() {
out.push_str(&convert_zh_numeric_run(&run));
run.clear();
}
out.push(c);
}
}
if !run.is_empty() {
out.push_str(&convert_zh_numeric_run(&run));
}
out
}
fn is_zh_numeric(c: char) -> bool {
matches!(
c,
'零' | '〇'
| '一'
| '二'
| '三'
| '四'
| '五'
| '六'
| '七'
| '八'
| '九'
| '十'
| '百'
| '千'
| '万'
| '亿'
)
}
fn zh_digit_value(c: char) -> Option<u64> {
match c {
'零' | '〇' => Some(0),
'一' => Some(1),
'二' => Some(2),
'三' => Some(3),
'四' => Some(4),
'五' => Some(5),
'六' => Some(6),
'七' => Some(7),
'八' => Some(8),
'九' => Some(9),
_ => None,
}
}
fn convert_zh_numeric_run(run: &str) -> String {
let has_positional = run
.chars()
.any(|c| matches!(c, '十' | '百' | '千' | '万' | '亿'));
if has_positional {
parse_positional(run).to_string()
} else {
run.chars()
.filter_map(zh_digit_value)
.filter_map(|d| char::from_digit(d as u32, 10))
.collect()
}
}
fn parse_positional(s: &str) -> u64 {
let mut total: u64 = 0;
let mut section: u64 = 0;
let mut last: u64 = 0;
let mut have_last = false;
for c in s.chars() {
if let Some(d) = zh_digit_value(c) {
last = d;
have_last = true;
} else {
match c {
'十' => {
section += if have_last { last } else { 1 } * 10;
last = 0;
have_last = false;
}
'百' => {
section += last * 100;
last = 0;
have_last = false;
}
'千' => {
section += last * 1000;
last = 0;
have_last = false;
}
'万' => {
total += (section + last) * 10_000;
section = 0;
last = 0;
have_last = false;
}
'亿' => {
total += (section + last) * 100_000_000;
section = 0;
last = 0;
have_last = false;
}
_ => {}
}
}
}
total + section + last
}
fn normalize_korean_numerals(text: &str) -> String {
let mut out = String::with_capacity(text.len());
let mut run = String::new();
let mut last_was_token = false;
for (idx, c) in text.char_indices() {
if is_kr_token(c) || c.is_ascii_digit() {
run.push(c);
last_was_token = true;
} else if c.is_whitespace() && last_was_token {
run.push(c);
last_was_token = false;
} else {
flush_kr_run(&mut out, &run, Some(&text[idx..]));
run.clear();
last_was_token = false;
out.push(c);
}
}
flush_kr_run(&mut out, &run, None);
out
}
fn flush_kr_run(out: &mut String, run: &str, follow_up: Option<&str>) {
if run.is_empty() {
return;
}
let trimmed = run.trim_end();
let trailing_ws = &run[trimmed.len()..];
if trimmed.is_empty() {
out.push_str(run);
return;
}
let cleaned: String = trimmed.chars().filter(|c| !c.is_whitespace()).collect();
let segments = split_kr_run_at_korean_digit_boundaries(&cleaned);
let n_segs = segments.len();
for (i, seg) in segments.iter().enumerate() {
let seg_follow_up: Option<&str> = if i + 1 < n_segs {
Some(segments[i + 1].as_str())
} else {
follow_up
};
flush_kr_segment(out, seg, seg_follow_up);
if i + 1 < n_segs {
out.push(' ');
}
}
out.push_str(trailing_ws);
}
fn split_kr_run_at_korean_digit_boundaries(cleaned: &str) -> Vec<String> {
let mut out = Vec::new();
let mut cur = String::new();
let mut prev_was_korean_digit = false;
for c in cleaned.chars() {
let is_kr_digit = kr_digit_value(c).is_some();
if is_kr_digit && prev_was_korean_digit && !cur.is_empty() {
out.push(std::mem::take(&mut cur));
}
cur.push(c);
prev_was_korean_digit = is_kr_digit;
}
if !cur.is_empty() {
out.push(cur);
}
out
}
fn flush_kr_segment(out: &mut String, segment: &str, follow_up: Option<&str>) {
if segment.is_empty() {
return;
}
let has_digit = segment
.chars()
.any(|c| kr_digit_value(c).is_some() || c.is_ascii_digit());
let has_unit = segment.chars().any(is_kr_unit);
if has_digit && has_unit {
out.push_str(&parse_kr_positional(segment).to_string());
} else if segment.chars().count() == 1
&& follow_up.map(starts_with_kr_classifier).unwrap_or(false)
{
let single = segment.chars().next().unwrap();
if let Some(d) = kr_digit_value(single) {
out.push_str(&d.to_string());
} else {
out.push_str(segment);
}
} else {
out.push_str(segment);
}
}
fn starts_with_kr_classifier(s: &str) -> bool {
if let Some(c) = s.chars().next() {
if is_kr_date_time_classifier(c) {
return true;
}
}
s.starts_with("세트")
}
fn is_kr_date_time_classifier(c: char) -> bool {
matches!(c, '년' | '월' | '일' | '시' | '분' | '초')
}
fn is_kr_token(c: char) -> bool {
kr_digit_value(c).is_some() || is_kr_unit(c)
}
fn is_kr_unit(c: char) -> bool {
matches!(c, '십' | '백' | '천' | '만' | '억' | '조')
}
fn kr_digit_value(c: char) -> Option<u64> {
match c {
'영' | '공' => Some(0),
'일' => Some(1),
'이' => Some(2),
'삼' => Some(3),
'사' => Some(4),
'오' => Some(5),
'육' => Some(6),
'칠' => Some(7),
'팔' => Some(8),
'구' => Some(9),
_ => None,
}
}
fn parse_kr_positional(s: &str) -> u64 {
let mut total: u64 = 0;
let mut section: u64 = 0;
let mut current: u64 = 0;
let mut have_current = false;
for c in s.chars() {
if let Some(d) = c.to_digit(10) {
current = current * 10 + d as u64;
have_current = true;
} else if let Some(d) = kr_digit_value(c) {
current = d;
have_current = true;
} else {
match c {
'십' => {
let v = if have_current { current } else { 1 };
section += v * 10;
current = 0;
have_current = false;
}
'백' => {
let v = if have_current { current } else { 1 };
section += v * 100;
current = 0;
have_current = false;
}
'천' => {
let v = if have_current { current } else { 1 };
section += v * 1000;
current = 0;
have_current = false;
}
'만' => {
let v = section + current;
let v = if v == 0 { 1 } else { v };
total += v * 10_000;
section = 0;
current = 0;
have_current = false;
}
'억' => {
let v = section + current;
let v = if v == 0 { 1 } else { v };
total += v * 100_000_000;
section = 0;
current = 0;
have_current = false;
}
'조' => {
let v = section + current;
let v = if v == 0 { 1 } else { v };
total += v * 1_000_000_000_000;
section = 0;
current = 0;
have_current = false;
}
_ => {}
}
}
}
total + section + current
}
fn levenshtein<T: Eq>(a: &[T], b: &[T]) -> usize {
if a.is_empty() {
return b.len();
}
if b.is_empty() {
return a.len();
}
let mut prev: Vec<usize> = (0..=b.len()).collect();
let mut curr: Vec<usize> = vec![0; b.len() + 1];
for (i, ai) in a.iter().enumerate() {
curr[0] = i + 1;
for (j, bj) in b.iter().enumerate() {
let cost = if ai == bj { 0 } else { 1 };
curr[j + 1] = (curr[j] + 1).min(prev[j + 1] + 1).min(prev[j] + cost);
}
std::mem::swap(&mut prev, &mut curr);
}
prev[b.len()]
}
fn normalize_english_number_words(text: &str) -> String {
if text.is_empty() {
return String::new();
}
text.split(' ')
.map(|tok| english_number_replacement(tok).unwrap_or(tok))
.collect::<Vec<_>>()
.join(" ")
}
fn english_number_replacement(tok: &str) -> Option<&'static str> {
match tok {
"zero" => Some("0"),
"one" => Some("1"),
"two" => Some("2"),
"three" => Some("3"),
"four" => Some("4"),
"five" => Some("5"),
"six" => Some("6"),
"seven" => Some("7"),
"eight" => Some("8"),
"nine" => Some("9"),
"ten" => Some("10"),
"eleven" => Some("11"),
"twelve" => Some("12"),
"thirteen" => Some("13"),
"fourteen" => Some("14"),
"fifteen" => Some("15"),
"sixteen" => Some("16"),
"seventeen" => Some("17"),
"eighteen" => Some("18"),
"nineteen" => Some("19"),
"twenty" => Some("20"),
"thirty" => Some("30"),
"forty" => Some("40"),
"fifty" => Some("50"),
"sixty" => Some("60"),
"seventy" => Some("70"),
"eighty" => Some("80"),
"ninety" => Some("90"),
"hundred" => Some("100"),
"thousand" => Some("1000"),
"million" => Some("1000000"),
"first" => Some("1st"),
"second" => Some("2nd"),
"third" => Some("3rd"),
"fourth" => Some("4th"),
"fifth" => Some("5th"),
"sixth" => Some("6th"),
"seventh" => Some("7th"),
"eighth" => Some("8th"),
"ninth" => Some("9th"),
"tenth" => Some("10th"),
"eleventh" => Some("11th"),
"twelfth" => Some("12th"),
"thirteenth" => Some("13th"),
"fourteenth" => Some("14th"),
"fifteenth" => Some("15th"),
"sixteenth" => Some("16th"),
"seventeenth" => Some("17th"),
"eighteenth" => Some("18th"),
"nineteenth" => Some("19th"),
"twentieth" => Some("20th"),
"thirtieth" => Some("30th"),
"fortieth" => Some("40th"),
"fiftieth" => Some("50th"),
"sixtieth" => Some("60th"),
"seventieth" => Some("70th"),
"eightieth" => Some("80th"),
"ninetieth" => Some("90th"),
"hundredth" => Some("100th"),
"thousandth" => Some("1000th"),
"millionth" => Some("1000000th"),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn wer_identical_is_zero() {
assert_eq!(wer("hello world", "hello world"), 0.0);
}
#[test]
fn wer_one_substitution_in_three_words() {
assert!((wer("hello dark world", "hello world world") - 1.0 / 3.0).abs() < 1e-9);
}
#[test]
fn wer_deletion() {
assert!((wer("the quick brown fox", "the brown fox") - 1.0 / 4.0).abs() < 1e-9);
}
#[test]
fn wer_lenient_normalises_punctuation_and_case() {
assert_eq!(wer_lenient("Hello, world!", "hello world"), 0.0);
}
#[test]
fn wer_empty_reference_is_nan() {
assert!(wer("", "anything").is_nan());
}
#[test]
fn cer_identical_japanese() {
assert_eq!(cer("今日は天気がいい", "今日は天気がいい"), 0.0);
}
#[test]
fn cer_ignores_whitespace_differences() {
assert_eq!(cer("今日は", "今日 は"), 0.0);
}
#[test]
fn cer_one_char_substitution_in_three() {
assert!((cer("今日は", "今夜は") - 1.0 / 3.0).abs() < 1e-9);
}
#[test]
fn cer_lenient_strips_japanese_punctuation() {
assert_eq!(cer_lenient("今日は、天気がいい。", "今日は天気がいい"), 0.0);
}
#[test]
fn normalize_lenient_collapses_whitespace() {
assert_eq!(normalize_lenient(" hello world "), "hello world");
}
#[test]
fn normalize_lenient_lowercases_ascii_only() {
assert_eq!(normalize_lenient("Hello CAFÉ"), "hello café");
}
#[test]
fn zh_digit_by_digit_year_form() {
assert_eq!(normalize_lenient("二零一一年"), "2011年");
assert_eq!(normalize_lenient("一九六三年"), "1963年");
assert_eq!(normalize_lenient("二零零二"), "2002");
}
#[test]
fn zh_positional_simple() {
assert_eq!(normalize_lenient("十五米"), "15米");
assert_eq!(normalize_lenient("七十多颗"), "70多颗");
assert_eq!(normalize_lenient("二十"), "20");
assert_eq!(normalize_lenient("二十五"), "25");
}
#[test]
fn zh_positional_with_hundreds_and_thousands() {
assert_eq!(normalize_lenient("一百二十三"), "123");
assert_eq!(normalize_lenient("一千二百"), "1200");
assert_eq!(normalize_lenient("三千五百"), "3500");
}
#[test]
fn zh_positional_scaling_with_wan_and_yi() {
assert_eq!(normalize_lenient("一万二千三百"), "12300");
assert_eq!(normalize_lenient("二亿"), "200000000");
}
#[test]
fn zh_lone_ten_and_zero_variants() {
assert_eq!(normalize_lenient("十"), "10");
assert_eq!(normalize_lenient("〇"), "0");
assert_eq!(normalize_lenient("〇〇"), "00");
}
#[test]
fn cer_lenient_zero_after_numeral_normalisation_zh() {
let r = "桥下垂直净空15米该项目于2011年8月完工但直到2017年3月才开始通车";
let h = "桥下垂直净空十五米该项目于二零一一年八月完工但直到二零一七年三月才开始通车";
assert!(cer_lenient(r, h).abs() < 1e-9, "got {}", cer_lenient(r, h));
}
#[test]
fn cer_lenient_isolates_real_errors_after_normalisation_zh() {
let r = "1963年大坝建成后季节性洪水被控制住了沉积物不再冲散到河流里";
let h = "一九六三年大坝建成后季节性洪水被控制住了沉积雾不再冲散到河流里";
let c = cer_lenient(r, h);
let r_chars = normalize_lenient(r)
.chars()
.filter(|x| !x.is_whitespace())
.count();
let expected = 1.0 / r_chars as f64;
assert!((c - expected).abs() < 1e-9, "expected {expected} got {c}");
}
#[test]
fn cer_lenient_handles_mixed_pos_and_digit_forms_zh() {
let r = "scotturb 403 路 公 共 汽 车";
let h = "scotburb 四零三路 公交汽车";
let r_norm: String = normalize_lenient(r)
.chars()
.filter(|x| !x.is_whitespace())
.collect();
let h_norm: String = normalize_lenient(h)
.chars()
.filter(|x| !x.is_whitespace())
.collect();
assert!(
r_norm.contains("403"),
"ref should contain 403, got {r_norm}"
);
assert!(
h_norm.contains("403"),
"hyp should contain 403, got {h_norm}"
);
}
#[test]
fn normalize_lenient_case_fold_is_locked_in() {
assert_eq!(wer_lenient("Hello World", "hello world"), 0.0);
assert_eq!(cer_lenient("Many People", "many people"), 0.0);
}
#[test]
fn cer_strips_internal_whitespace_for_cjk() {
let r = "这 并 不 是 告 别";
let h = "这并不是告别";
assert_eq!(cer(r, h), 0.0);
}
#[test]
fn en_lenient_cardinals_round_trip_to_arabic() {
assert_eq!(wer_lenient("twelve", "12"), 0.0);
assert_eq!(wer_lenient("twenty", "20"), 0.0);
assert_eq!(wer_lenient("hundred", "100"), 0.0);
assert_eq!(wer_lenient("thousand", "1000"), 0.0);
}
#[test]
fn en_lenient_ordinals_round_trip_to_th_form() {
assert_eq!(wer_lenient("twentieth century", "20th century"), 0.0);
assert_eq!(wer_lenient("first place", "1st place"), 0.0);
assert_eq!(wer_lenient("twelfth night", "12th night"), 0.0);
assert_eq!(
wer_lenient("third time's the charm", "3rd time's the charm"),
0.0
);
}
#[test]
fn en_lenient_hyphen_splits_compound_words() {
assert_eq!(wer_lenient("whole-number ratio", "whole number ratio"), 0.0);
}
#[test]
fn en_lenient_hyphen_splits_numeric_ranges() {
let r = "25 to 30 years";
let h = "25-30 years";
let w = wer_lenient(r, h);
assert!((w - 0.25).abs() < 1e-9, "expected 0.25, got {w}");
}
#[test]
fn en_lenient_dump_regressions_align_after_normalisation() {
let r = "twentieth century research has shown that there are two pools of genetic variation hidden and expressed";
let h = "20th century research has shown that there are two pools of genetic variation hidden and expressed";
assert_eq!(wer_lenient(r, h), 0.0);
}
#[test]
fn en_lenient_compound_separator_does_not_glue_tokens() {
let normalized = normalize_lenient("25-30");
assert_eq!(normalized, "25 30");
}
#[test]
fn en_lenient_em_and_en_dash_treated_as_split() {
assert_eq!(normalize_lenient("alpha\u{2014}beta"), "alpha beta"); assert_eq!(normalize_lenient("alpha\u{2013}beta"), "alpha beta"); }
#[test]
fn en_lenient_number_words_only_replace_whole_tokens() {
assert_eq!(normalize_lenient("oneness"), "oneness");
assert_eq!(normalize_lenient("twenties"), "twenties");
assert_eq!(normalize_lenient("hundreds"), "hundreds");
}
#[test]
fn en_lenient_normalisation_preserves_non_number_tokens() {
assert_eq!(
normalize_lenient("the quick brown fox"),
"the quick brown fox"
);
}
#[test]
fn lenient_drop_punct_covers_fullwidth_ff_block() {
assert_eq!(cer_lenient("你好,世界", "你好世界"), 0.0);
assert_eq!(cer_lenient("你好世界", "你好,世界"), 0.0);
for p in &['?', '!', ':', ';', '(', ')', '.'] {
let with_punct = format!("hello{p}world");
let without = "helloworld";
assert_eq!(
cer_lenient(&with_punct, without),
0.0,
"punct {p:?} not stripped"
);
}
}
#[test]
fn lenient_drop_punct_covers_japanese_brackets_and_dot() {
assert_eq!(cer_lenient("「今日は」、晴れだ。", "今日は晴れだ"), 0.0);
assert_eq!(cer_lenient("『良』『書』", "良書"), 0.0);
assert_eq!(cer_lenient("カタログ・データ", "カタログデータ"), 0.0);
}
#[test]
fn lenient_drop_punct_covers_smart_quotes_and_ellipsis() {
assert_eq!(wer_lenient("\u{2018}hello\u{2019}", "hello"), 0.0);
assert_eq!(wer_lenient("\u{201C}hello\u{201D}", "hello"), 0.0);
assert_eq!(cer_lenient("そうです…", "そうです"), 0.0);
}
#[test]
fn lenient_normalisation_is_symmetric_across_ref_and_hyp() {
assert_eq!(wer_lenient("Hello, World!", "hello world"), 0.0);
assert_eq!(wer_lenient("hello world", "Hello, World!"), 0.0);
assert_eq!(cer_lenient("你好,世界", "你好世界"), 0.0);
assert_eq!(cer_lenient("你好世界", "你好,世界"), 0.0);
assert_eq!(cer_lenient("二零二一年", "2021年"), 0.0);
assert_eq!(cer_lenient("2021年", "二零二一年"), 0.0);
assert_eq!(wer_lenient("twentieth century", "20th century"), 0.0);
assert_eq!(wer_lenient("20th century", "twentieth century"), 0.0);
}
#[test]
fn fullwidth_space_is_collapsed_for_wer() {
assert_eq!(wer("hello\u{3000}world", "hello world"), 0.0);
}
#[test]
fn fullwidth_space_is_stripped_for_cer() {
let with = "今日\u{3000}は\u{3000}晴れだ";
let without = "今日は晴れだ";
assert_eq!(cer(with, without), 0.0);
}
#[test]
fn mixed_whitespace_kinds_collapse_uniformly() {
assert_eq!(wer("a\tb", "a b"), 0.0);
assert_eq!(wer("a\u{00A0}b", "a b"), 0.0);
assert_eq!(wer("a\u{3000}b", "a b"), 0.0);
}
#[test]
fn strict_wer_preserves_punctuation_difference() {
let r = "hello world";
let h = "hello world.";
assert!(
(wer(r, h) - 0.5).abs() < 1e-9,
"strict wer should see `.` token, got {}",
wer(r, h)
);
}
#[test]
fn strict_cer_preserves_punctuation_difference() {
let r = "hello world";
let h = "hello world.";
let c = cer(r, h);
assert!(
(c - 0.1).abs() < 1e-9,
"strict cer should see `.` char, got {c}"
);
}
#[test]
fn strict_wer_preserves_capitalisation_difference() {
let r = "hello world";
let h = "Hello world";
assert!((wer(r, h) - 0.5).abs() < 1e-9);
}
#[test]
fn strict_cer_preserves_capitalisation_difference() {
let r = "hello world";
let h = "Hello world";
let c = cer(r, h);
assert!((c - 0.1).abs() < 1e-9, "got {c}");
}
#[test]
fn strict_cer_preserves_chinese_comma() {
let r = "你好世界";
let h = "你好,世界";
let c = cer(r, h);
assert!(
(c - 0.25).abs() < 1e-9,
"strict cer should see fullwidth comma, got {c}"
);
}
#[test]
fn strict_metrics_still_collapse_whitespace() {
assert_eq!(wer("hello world", "hello world"), 0.0);
assert_eq!(cer("今日は", "今日 は"), 0.0);
}
#[test]
fn lenient_wer_strips_punctuation_and_case() {
assert_eq!(wer_lenient("Hello, World!", "hello world"), 0.0);
assert_eq!(wer_lenient("hello world", "Hello, World!"), 0.0);
}
#[test]
fn lenient_cer_strips_chinese_comma_and_normalises_numerals() {
assert_eq!(cer_lenient("你好,世界", "你好世界"), 0.0);
assert_eq!(cer_lenient("二零二一年", "2021年"), 0.0);
}
#[test]
fn lenient_wer_normalises_english_number_words_and_hyphens() {
assert_eq!(wer_lenient("twentieth century", "20th century"), 0.0);
assert_eq!(wer_lenient("whole-number ratio", "whole number ratio"), 0.0);
}
#[test]
fn normalize_lenient_pipeline_matches_legacy_normalize_contract() {
assert_eq!(normalize_lenient("Hello CAFÉ"), "hello café");
assert_eq!(normalize_lenient("25-30"), "25 30");
assert_eq!(normalize_lenient("十五米"), "15米");
assert_eq!(normalize_lenient("twelve"), "12");
assert_eq!(normalize_lenient("\u{2018}hello\u{2019}"), "hello");
assert_eq!(normalize_lenient("そうです…"), "そうです");
assert_eq!(normalize_lenient("hello\u{3000}world"), "hello world");
}
#[test]
fn normalize_korean_numerals_two_token_run() {
assert_eq!(normalize_korean_numerals("십 오 미터"), "15 미터");
}
#[test]
fn normalize_korean_numerals_year_with_unit_chain() {
assert_eq!(normalize_korean_numerals("이천 십 일 년"), "2011 년");
assert_eq!(normalize_korean_numerals("이천 십 칠 년"), "2017 년");
}
#[test]
fn normalize_korean_numerals_full_thousand_chain() {
assert_eq!(normalize_korean_numerals("천 구백 사십 년"), "1940 년");
}
#[test]
fn normalize_korean_numerals_man_compound() {
assert_eq!(normalize_korean_numerals("일만 년"), "10000 년");
assert_eq!(normalize_korean_numerals("일 만 년"), "10000 년");
}
#[test]
fn normalize_korean_numerals_mixed_arabic_with_man() {
assert_eq!(normalize_korean_numerals("15만"), "150000");
assert_eq!(normalize_korean_numerals("12만 명"), "120000 명");
}
#[test]
fn normalize_korean_numerals_eok_billion() {
assert_eq!(normalize_korean_numerals("일억 원"), "100000000 원");
assert_eq!(normalize_korean_numerals("삼억 오천만 원"), "350000000 원");
}
#[test]
fn normalize_korean_numerals_skips_homographs_without_unit() {
assert_eq!(normalize_korean_numerals("이것은"), "이것은");
assert_eq!(normalize_korean_numerals("구매"), "구매");
assert_eq!(normalize_korean_numerals("사람"), "사람");
}
#[test]
fn normalize_korean_numerals_skips_unit_only_homographs() {
assert_eq!(normalize_korean_numerals("천천히"), "천천히");
assert_eq!(normalize_korean_numerals("만나다"), "만나다");
}
#[test]
fn normalize_korean_numerals_single_digit_alone_kept() {
assert_eq!(normalize_korean_numerals("팔"), "팔");
}
#[test]
fn normalize_korean_numerals_single_digit_with_date_classifier() {
assert_eq!(normalize_korean_numerals("팔 월"), "8 월");
assert_eq!(normalize_korean_numerals("삼 월에"), "3 월에");
assert_eq!(normalize_korean_numerals("일 일"), "1 일");
assert_eq!(normalize_korean_numerals("오 시"), "5 시");
assert_eq!(normalize_korean_numerals("구 분"), "9 분");
assert_eq!(normalize_korean_numerals("육 초"), "6 초");
assert_eq!(normalize_korean_numerals("이 년"), "2 년");
}
#[test]
fn normalize_korean_numerals_classifier_only_widens_for_dates() {
assert_eq!(normalize_korean_numerals("팔 명"), "팔 명");
assert_eq!(normalize_korean_numerals("이 개"), "이 개");
assert_eq!(normalize_korean_numerals("삼 대"), "삼 대");
}
#[test]
fn normalize_korean_numerals_run_followed_by_classifier_unaffected() {
assert_eq!(normalize_korean_numerals("이천 십 일 년"), "2011 년");
}
#[test]
fn normalize_korean_numerals_split_at_consecutive_korean_digits() {
assert_eq!(normalize_korean_numerals("십 오 일"), "15 일");
assert_eq!(normalize_korean_numerals("십오일"), "15 일");
}
#[test]
fn normalize_korean_numerals_split_doesnt_break_normal_positional() {
assert_eq!(normalize_korean_numerals("이천 십 일"), "2011");
assert_eq!(normalize_korean_numerals("15만"), "150000");
}
#[test]
fn normalize_korean_numerals_split_with_classifier_followup() {
assert_eq!(normalize_korean_numerals("십오일년"), "15 1년");
}
#[test]
fn normalize_korean_numerals_multi_char_classifier_set() {
assert_eq!(normalize_korean_numerals("이 세트"), "2 세트");
assert_eq!(normalize_korean_numerals("이 세트에서"), "2 세트에서");
assert_eq!(
normalize_korean_numerals("이 세트가 끝나고"),
"2 세트가 끝나고"
);
}
#[test]
fn normalize_korean_numerals_bare_se_is_not_classifier() {
assert_eq!(normalize_korean_numerals("이 세"), "이 세");
assert_eq!(normalize_korean_numerals("이 세상"), "이 세상");
}
#[test]
fn normalize_korean_numerals_classifier_skips_arabic_digit() {
assert_eq!(normalize_korean_numerals("2 세트"), "2 세트");
assert_eq!(normalize_korean_numerals("2세트"), "2세트");
}
#[test]
fn normalize_korean_numerals_single_internal_space_absorbed() {
assert_eq!(normalize_korean_numerals("이천 십"), "2010");
}
#[test]
fn normalize_korean_numerals_double_space_terminates_run() {
assert_eq!(normalize_korean_numerals("이천 십"), "2000 십");
}
#[test]
fn normalize_korean_numerals_passes_through_non_numeric() {
assert_eq!(
normalize_korean_numerals("다리 밑 수직 간격은 미터이며"),
"다리 밑 수직 간격은 미터이며"
);
}
#[test]
fn lenient_cer_normalises_korean_numerals_against_arabic_reference() {
let reference = "공사는 2011년 8월에 마무리되었으며";
let hypothesis = "공사는 이천 십 일 년 8월에 마무리되었으며";
let cer = cer_lenient(reference, hypothesis);
assert!(
cer < 0.05,
"expected cer < 0.05 after kr numeral norm, got {cer}"
);
}
}