use mongol_convert::{translate, CodeType, MongolConvertError};
#[test]
fn passthrough_text_reaches_utn57_unchanged() {
let input = "plain\u{180A}\u{180E}\u{200D}";
assert_eq!(
translate(CodeType::Zvvnmod, CodeType::Utn57, input).unwrap(),
input
);
}
#[test]
fn the_hub_boundary_becomes_an_mvs() {
assert_eq!(
translate(CodeType::Zvvnmod, CodeType::Utn57, "\u{E00D}\u{202F}\u{E04D}").unwrap(),
translate(CodeType::Zvvnmod, CodeType::Utn57, "\u{E00D}").unwrap()
+ "\u{180E}"
+ &translate(CodeType::Zvvnmod, CodeType::Utn57, "\u{E04D}").unwrap(),
);
}
#[test]
fn utn57_and_delehi_sources_agree_on_the_hub() {
let utn57 = "\u{1832}\u{1820}\u{182F}\u{180E}\u{1820}\u{180E}\u{1836}\u{1822}\u{1828}";
let delehi = "\u{1832}\u{1820}\u{182F}\u{180E}\u{1820}\u{202F}\u{1836}\u{1822}\u{1828}";
let from_delehi = translate(CodeType::Delehi, CodeType::Zvvnmod, delehi).unwrap();
assert_eq!(
from_delehi,
"\u{E042}\u{E005}\u{E03B}\u{E00D}\u{202F}\u{E04D}\u{E006}\u{E00C}",
"the chachlag folds into one code and the suffix boundary stays as NNBSP"
);
assert_eq!(
translate(CodeType::Utn57, CodeType::Zvvnmod, utn57).unwrap(),
from_delehi,
"both sources must reach the same hub"
);
for target in [CodeType::MenkLetter, CodeType::MenkShape, CodeType::Z52] {
assert_eq!(
translate(CodeType::Utn57, target, utn57).unwrap(),
translate(CodeType::Delehi, target, delehi).unwrap(),
"{target:?} must not depend on which source the word came from"
);
}
}
#[test]
fn the_boundary_is_not_doubled_on_the_way_out() {
let hub = "\u{E042}\u{E005}\u{E03B}\u{E00D}\u{202F}\u{E04D}\u{E006}\u{E00C}";
for target in [CodeType::Delehi, CodeType::MenkLetter] {
let out = translate(CodeType::Zvvnmod, target, hub).unwrap();
assert_eq!(out.matches('\u{202F}').count(), 1, "{target:?}: {out:?}");
}
for target in [CodeType::MenkShape, CodeType::Z52] {
let out = translate(CodeType::Zvvnmod, target, hub).unwrap();
assert!(!out.contains('\u{202F}'), "{target:?}: {out:?}");
assert_eq!(out.matches(' ').count(), 1, "{target:?}: {out:?}");
}
}
#[test]
fn utn57_identity_short_circuits() {
let input = "\u{1820}\u{180A}\u{202F}";
assert_eq!(
translate(CodeType::Utn57, CodeType::Utn57, input).unwrap(),
input
);
}
#[test]
fn formal_zvvnmod_nirugu_converts_in_process() {
assert_eq!(
translate(CodeType::Zvvnmod, CodeType::Utn57, "\u{E0E5}").unwrap(),
"\u{180A}"
);
}
#[test]
fn legacy_sources_route_through_the_zvvnmod_hub() {
assert_eq!(
translate(CodeType::MenkShape, CodeType::Utn57, "\u{E23E}").unwrap(),
"\u{180A}"
);
}
#[test]
fn z52_word_converts_to_canonical_unicode() {
let output = translate(
CodeType::Z52,
CodeType::Utn57,
"ᡳᡬᡦ ᢌᡭᡪᢊᡱᡱᡭᢐ ᢋᡭᡬᢎᡭᡧ",
)
.unwrap();
assert!(!output.is_empty());
assert!(
output
.chars()
.all(|c| !('\u{E000}'..='\u{F8FF}').contains(&c)),
"UTN #57 output must not contain private-use ZVVNMOD shapes: {output:?}"
);
assert!(
output.chars().any(|c| ('\u{1820}'..='\u{1842}').contains(&c)),
"UTN #57 output should contain Unicode Mongolian letters: {output:?}"
);
assert_eq!(output.matches(' ').count(), 2, "spaces pass through: {output:?}");
}
#[test]
fn legacy_controls_are_discarded() {
assert_eq!(
translate(
CodeType::Zvvnmod,
CodeType::Utn57,
"\u{E140}\u{E141}\u{E142}\u{E143}\u{E144}",
)
.unwrap(),
""
);
}
#[test]
fn utn57_decodes_back_to_the_hub() {
let hub = translate(CodeType::Delehi, CodeType::Zvvnmod, "\u{1830}\u{1820}\u{180D}\u{1822}\u{180D}\u{1822}\u{1828}").unwrap();
let utn57 = translate(CodeType::Zvvnmod, CodeType::Utn57, &hub).unwrap();
assert_eq!(translate(CodeType::Utn57, CodeType::Zvvnmod, &utn57).unwrap(), hub);
}
#[test]
fn utn57_is_a_source_for_the_other_encodings() {
let delehi = "\u{182E}\u{1823}\u{1829}\u{182D}\u{1823}\u{182F}";
let utn57 = translate(CodeType::Delehi, CodeType::Utn57, delehi).unwrap();
assert_ne!(utn57, delehi, "the two conventions spell this word differently");
assert_eq!(translate(CodeType::Utn57, CodeType::Delehi, &utn57).unwrap(), delehi);
let via_utn57 = translate(CodeType::Utn57, CodeType::MenkShape, &utn57).unwrap();
let via_delehi = translate(CodeType::Delehi, CodeType::MenkShape, delehi).unwrap();
assert_eq!(via_utn57, via_delehi, "either source should reach the same MenkShape text");
}
#[test]
fn hub_round_trip_over_the_corpus_matches_the_known_gap() {
const LOSSY: fn(char) -> bool = |c| matches!(c, '\u{E140}'..='\u{E144}' | '\u{200D}');
const MIN_OK: usize = 1020; const MAX_BROKEN: usize = 33;
let corpus = std::fs::read_to_string(
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/golden/corpus_delehi.txt"),
)
.expect("corpus should be readable");
let (mut ok, mut broken) = (0usize, 0usize);
let mut sample = Vec::new();
for word in corpus.split_whitespace() {
let Ok(hub) = translate(CodeType::Delehi, CodeType::Zvvnmod, word) else { continue };
if hub.chars().any(LOSSY) {
continue;
}
let Ok(utn57) = translate(CodeType::Zvvnmod, CodeType::Utn57, &hub) else { continue };
let Ok(back) = translate(CodeType::Utn57, CodeType::Zvvnmod, &utn57) else { continue };
if back == hub {
ok += 1;
} else {
broken += 1;
if sample.len() < 3 {
sample.push(format!("{word:?}: {hub:?} -> {utn57:?} -> {back:?}"));
}
}
}
assert!(ok >= MIN_OK, "round-trip regressed: {ok} words survived, expected at least {MIN_OK}");
assert!(
broken <= MAX_BROKEN,
"round-trip regressed: {broken} words broke, expected at most {MAX_BROKEN}\n{}",
sample.join("\n")
);
}
#[test]
fn utn57_error_variant_display_is_stable() {
assert_eq!(
MongolConvertError::Utn57("backend unavailable".to_owned()).to_string(),
"UTN #57 conversion failed: backend unavailable"
);
}
#[test]
fn a_bowl_before_teeth_is_spelled_b_o_not_dd() {
let hub = "\u{E000}\u{E005}\u{E083}\u{E005}\u{E005}\u{E03D}\u{E005}\u{E00C}";
let out = translate(CodeType::Zvvnmod, CodeType::Utn57, hub).unwrap();
assert_eq!(
out,
"\u{1820}\u{180B}\u{1820}\u{182A}\u{1823}\u{1820}\u{1820}\u{1830}\u{1820}\u{1820}\u{180C}"
);
assert!(!out.contains('\u{1833}'), "no Dd in {out:?}");
assert_eq!(
translate(CodeType::MenkLetter, CodeType::Utn57, "ᠠᠪᠤᠭᠰᠠᠨ").unwrap(),
out
);
assert_eq!(
translate(CodeType::Utn57, CodeType::Zvvnmod, &out).unwrap(),
hub
);
}
#[test]
fn a_nirugu_and_a_middle_dot_reach_menk_shape() {
let out = translate(
CodeType::Utn57,
CodeType::MenkShape,
"\u{1820}\u{180B}\u{1820}\u{180A}\u{00B7}",
)
.unwrap();
assert_eq!(out, "\u{E271}\u{E26C}\u{E23E}\u{E263}\u{E243}");
assert!(!out.contains('\u{E0E5}'), "formal nirugu leaked: {out:?}");
}