use core::str::FromStr;
use std::{
collections::BTreeSet,
string::{String, ToString},
vec::Vec,
};
use smol_bytes::Utf8Bytes;
use super::{LanguageId, ParseLanguageIdError};
use crate::lang::{Language, ParseLanguageError, ParseRegionError, Region, ScriptSubtag, registry};
fn door(sent: &str) -> Result<LanguageId, ParseLanguageIdError> {
LanguageId::new(sent)
}
fn admitted(sent: &str) -> LanguageId {
door(sent).unwrap_or_else(|refused| panic!("`{sent}` is a language tag: {refused}"))
}
fn canonical(sent: &str) -> String {
admitted(sent).to_string()
}
fn corpus() -> Vec<String> {
let mut tags: Vec<String> = Vec::new();
for (subtag, _) in registry::table::LANGUAGES {
tags.push(String::from(*subtag));
}
for (subtag, script) in registry::table::LANGUAGE_SUPPRESS_SCRIPT {
tags.push(std::format!("{subtag}-{script}"));
tags.push(std::format!("{subtag}-{script}-US"));
tags.push(std::format!("{subtag}-{script}-419"));
tags.push(std::format!("{subtag}-{script}-Cyrl"));
tags.push(std::format!("{subtag}-{script}-{script}"));
tags.push(std::format!("{subtag}-{script}-US-Cyrl"));
tags.push(std::format!("{subtag}-{script}-1901"));
tags.push(std::format!("{subtag}-{script}-x-a"));
tags.push(std::format!(
"{}-{}",
subtag.to_uppercase(),
script.to_lowercase()
));
}
for (subtag, _) in registry::table::SCRIPTS {
tags.push(std::format!("zh-{subtag}"));
}
for (subtag, _) in registry::table::REGIONS {
tags.push(std::format!("zh-Hans-{subtag}"));
}
for (tag, _) in registry::table::GRANDFATHERED {
tags.push(String::from(*tag));
}
for tag in registry::table::GRANDFATHERED_KEPT {
tags.push(String::from(*tag));
}
tags.extend(interactions());
for tag in [
"en-Latn-Cyrl",
"en-Latn-Latn",
"en-Latn",
"en-Latn-US",
"en-Latn-US-Cyrl",
"en-Latn-x-Cyrl",
"zh_Hans_CN",
"GER-latn-de",
"deu-Latn-DE",
"EN-us",
"iw-IL",
"de-BU",
"und",
"en-US-posix",
"de-u-co-phonebk",
"en-x-lorem",
"en-US-x-Foo",
"en-US-x-foo",
"sl-rozaj-biske",
"hy-Latn-IT-arevela",
"en-a-bbb-x-a-ccc",
"de-CH-1901",
"en-USA",
"de-1a1",
"zh-Hans-XYZ",
"zh-yue",
"zh-yue-Hant-HK",
"xx-yy-zz-bogus",
"abcd",
"abcd-Latn",
"en-abcd",
"en-Latn-abcd",
"en-Latn-ABCD",
] {
tags.push(String::from(tag));
}
tags
}
fn interactions() -> Vec<String> {
let mut tags: Vec<String> = Vec::new();
for tag in registry::table::GRANDFATHERED
.iter()
.map(|(tag, _)| *tag)
.chain(registry::table::GRANDFATHERED_KEPT.iter().copied())
{
let (language, after) = tag.split_once('-').unwrap_or((tag, ""));
let scripts: Vec<Option<&str>> = match registry::language_suppress_script(language) {
Some(script) => std::vec![None, Some(script)],
None => std::vec![None],
};
for spelling in language_preimages(language) {
for script in scripts.iter().copied() {
for after in region_preimages(after) {
let mut built = spelling.clone();
if let Some(script) = script {
built.push('-');
built.push_str(script);
}
if !after.is_empty() {
built.push('-');
built.push_str(&after);
}
tags.push(built.to_uppercase());
tags.push(built);
}
}
}
}
let reached = registry::table::ALPHA3
.iter()
.map(|(code, shortest)| {
(
*code,
registry::language_preferred(shortest).unwrap_or(shortest),
)
})
.chain(
registry::table::LANGUAGE_PREFERRED
.iter()
.map(|(subtag, preferred)| (*subtag, *preferred)),
);
for (sent, folded) in reached {
let Some(script) = registry::language_suppress_script(folded) else {
continue;
};
tags.push(std::format!("{sent}-{script}"));
tags.push(std::format!("{sent}-{script}-US"));
tags.push(std::format!("{sent}-{script}-Cyrl"));
tags.push(std::format!("{sent}-{script}-{script}"));
}
for (deprecated, _) in registry::table::REGION_PREFERRED {
for (subtag, script) in registry::table::LANGUAGE_SUPPRESS_SCRIPT {
tags.push(std::format!("{subtag}-{script}-{deprecated}"));
}
}
tags
}
fn language_preimages(subtag: &str) -> Vec<String> {
let mut spellings = std::vec![String::from(subtag)];
spellings.extend(
registry::table::LANGUAGE_PREFERRED
.iter()
.filter(|(_, preferred)| *preferred == subtag)
.map(|(deprecated, _)| String::from(*deprecated)),
);
let through: Vec<String> = spellings.clone();
spellings.extend(
registry::table::ALPHA3
.iter()
.filter(|(_, shortest)| through.iter().any(|hop| hop == shortest))
.map(|(code, _)| String::from(*code)),
);
spellings
}
fn region_preimages(after: &str) -> Vec<String> {
let mut spellings = std::vec![String::from(after)];
let (head, rest) = after.split_once('-').unwrap_or((after, ""));
if !super::region_shaped(head) {
return spellings;
}
let canonical = head.to_uppercase();
for (deprecated, preferred) in registry::table::REGION_PREFERRED {
if *preferred != canonical {
continue;
}
spellings.push(match rest.is_empty() {
true => String::from(*deprecated),
false => std::format!("{deprecated}-{rest}"),
});
}
spellings
}
#[test]
fn the_door_applies_every_composition_rule_the_ruling_names() {
for (sent, held, why) in [
("zh_Hans_CN", "zh-Hans-CN", "underscores"),
("en_US", "en-US", "underscores"),
("en-Latn", "en", "`en` implies `Latn`"),
("en-Latn-US", "en-US", "and the region survives it"),
("fr-latn", "fr", "any case"),
("he-Hebr", "he", "not only Latin"),
("zh-Hans", "zh-Hans", "`zh` implies nothing"),
("zh-Hant", "zh-Hant", "and the two stay apart"),
("zh-Hant-TW", "zh-Hant-TW", "with a region beside it"),
("i-klingon", "tlh", "grandfathered"),
("I-KLINGON", "tlh", "grandfathered, any case"),
("zh-guoyu", "cmn", "grandfathered"),
("art-lojban", "jbo", "grandfathered"),
("no-bok", "nb", "grandfathered"),
(
"en-GB-oed",
"en-GB-oxendict",
"a multi-subtag preferred value",
),
(
"GER-latn-de",
"de-DE",
"alpha-3, suppression and region case, at once",
),
("deu-DE", "de-DE", "an mp4's language subtag"),
("iw-IL", "he-IL", "a deprecated language"),
("de-BU", "de-MM", "a deprecated region"),
("EN-us", "en-US", "case on two seats"),
] {
assert_eq!(canonical(sent), held, "`{sent}` — {why}");
}
}
#[test]
fn each_seat_is_filled_by_the_shape_that_belongs_in_it() {
let full = admitted("sr-Cyrl-RS");
assert_eq!(full.language().as_str(), "sr");
assert_eq!(
full.script().as_ref().map(ScriptSubtag::as_str),
Some("Cyrl")
);
assert_eq!(full.region().as_ref().map(Region::as_str), Some("RS"));
assert_eq!(full.rest(), None);
let bare = admitted("de");
assert_eq!(bare.language().as_str(), "de");
assert_eq!(bare.script(), None);
assert_eq!(bare.region(), None);
let regional = admitted("de-AT");
assert_eq!(regional.script(), None);
assert_eq!(regional.region().as_ref().map(Region::as_str), Some("AT"));
let area = admitted("es-419");
assert_eq!(area.region().as_ref().map(Region::as_str), Some("419"));
assert!(area.region().expect("a region").is_area());
}
#[test]
fn the_tail_is_held_verbatim_and_rendered_back() {
for tag in [
"en-US-posix",
"de-u-co-phonebk",
"en-x-lorem",
"sl-rozaj-biske",
"en-US-x-Foo",
"hy-Latn-IT-arevela",
] {
assert_eq!(canonical(tag), tag, "`{tag}` did not round-trip");
}
let held = admitted("en-US-x-Foo");
assert_eq!(held.language().as_str(), "en");
assert_eq!(held.region().as_ref().map(Region::as_str), Some("US"));
assert_eq!(held.rest().map(Utf8Bytes::as_str), Some("x-Foo"));
}
#[test]
fn two_spellings_of_one_tail_are_two_values() {
let upper = admitted("en-US-x-Foo");
let lower = admitted("en-US-x-foo");
assert_ne!(upper, lower);
assert_eq!(upper.language(), lower.language());
assert_eq!(upper.region(), lower.region());
assert_eq!(admitted("EN-us-x-Foo"), upper);
}
#[test]
fn an_extlang_rides_the_tail_and_takes_the_later_seats_with_it() {
let cantonese = admitted("zh-yue");
assert_eq!(cantonese.language().as_str(), "zh");
assert_eq!(cantonese.rest().map(Utf8Bytes::as_str), Some("yue"));
assert_eq!(cantonese.to_string(), "zh-yue");
let swallowed = admitted("zh-yue-Hant-HK");
assert_eq!(swallowed.language().as_str(), "zh");
assert_eq!(swallowed.script(), None, "the script is IN the tail");
assert_eq!(swallowed.region(), None, "and so is the region");
assert_eq!(swallowed.rest().map(Utf8Bytes::as_str), Some("yue-Hant-HK"));
assert_eq!(swallowed.to_string(), "zh-yue-Hant-HK", "lossless anyway");
let plain = admitted("zh-Hant-HK");
assert_eq!(
plain.script().as_ref().map(ScriptSubtag::as_str),
Some("Hant")
);
assert_eq!(plain.region().as_ref().map(Region::as_str), Some("HK"));
}
#[test]
fn the_tail_admits_what_is_alphanumeric_and_refuses_what_is_not() {
assert_eq!(
canonical("en-USA"),
"en-USA",
"not a legal variant, and carried"
);
assert_eq!(canonical("de-1901"), "de-1901");
assert_eq!(canonical("en-a-bbb-x-a-ccc"), "en-a-bbb-x-a-ccc");
assert_eq!(door("en-US-!!"), Err(ParseLanguageIdError::Tail('!')));
assert_eq!(door("en-US-po six"), Err(ParseLanguageIdError::Tail(' ')));
assert_eq!(
door("en-US-abcdefghi"),
Err(ParseLanguageIdError::TailWidth)
);
assert!(door("en-US-abcdefgh").is_ok(), "eight is the ceiling");
}
#[test]
fn a_grandfathered_tag_with_no_replacement_falls_through_to_the_ordinary_parse() {
assert_eq!(canonical("cel-gaulish"), "cel-gaulish");
assert_eq!(admitted("cel-gaulish").language().as_str(), "cel");
assert_eq!(
admitted("cel-gaulish").rest().map(Utf8Bytes::as_str),
Some("gaulish")
);
assert_eq!(canonical("zh-min"), "zh-min");
assert_eq!(
admitted("zh-min").rest().map(Utf8Bytes::as_str),
Some("min")
);
for refused in ["i-default", "i-mingo", "i-enochian"] {
assert_eq!(
door(refused),
Err(ParseLanguageIdError::Language(ParseLanguageError::TooShort)),
"`{refused}`"
);
}
}
#[test]
fn the_grandfathered_table_fits_the_lookup_buffer() {
let widest = registry::table::GRANDFATHERED
.iter()
.map(|(tag, _)| tag.len())
.chain(
registry::table::GRANDFATHERED_KEPT
.iter()
.map(|tag| tag.len()),
)
.max()
.expect("twenty-six of them");
assert!(
widest <= super::GRANDFATHERED_MAX,
"the widest grandfathered tag is {widest} bytes and the buffer is {}",
super::GRANDFATHERED_MAX
);
}
#[test]
fn a_refusal_names_the_seat_and_carries_its_error() {
assert_eq!(door(""), Err(ParseLanguageIdError::Empty));
assert_eq!(door("en-"), Err(ParseLanguageIdError::EmptySubtag));
assert_eq!(door("en--US"), Err(ParseLanguageIdError::EmptySubtag));
assert_eq!(door("-en"), Err(ParseLanguageIdError::EmptySubtag));
assert_eq!(
door("e-US"),
Err(ParseLanguageIdError::Language(ParseLanguageError::TooShort))
);
assert_eq!(
door("日本語-JP"),
Err(ParseLanguageIdError::Language(
ParseLanguageError::NotAlphabetic('日')
))
);
assert_eq!(canonical("zh-Hans-XYZ"), "zh-Hans-XYZ", "the tail took it");
assert_eq!(
canonical("de-1a1"),
"de-1a1",
"neither two letters nor three digits"
);
assert_eq!(
admitted("de-1a1").rest().map(Utf8Bytes::as_str),
Some("1a1")
);
assert_eq!(admitted("de-1a1").region(), None);
}
#[test]
fn two_spellings_of_one_identity_are_one_value() {
let mkv = admitted("GER-latn-de");
let mp4 = admitted("deu-Latn-DE");
let bcp47 = admitted("de-DE");
assert_eq!(mkv, mp4);
assert_eq!(mp4, bcp47);
let mut distinct = BTreeSet::new();
distinct.insert(mkv);
distinct.insert(mp4);
distinct.insert(bcp47);
assert_eq!(distinct.len(), 1, "one identity, one hash bucket");
assert_ne!(admitted("de"), admitted("de-DE"));
}
#[test]
fn the_rendering_and_the_parse_are_inverse() {
for sent in [
"de",
"zh-Hans",
"en-US",
"sr-Cyrl-RS",
"en-US-x-Foo",
"GER-latn-de",
"i-klingon",
"zh_Hans_CN",
] {
let held = admitted(sent);
let rendered = held.to_string();
assert_eq!(LanguageId::from_str(&rendered).expect("canonical"), held);
assert_eq!(admitted(&rendered), held, "the door is idempotent");
}
assert_eq!(
std::format!("{:?}", admitted("GER-latn-de")),
r#"LanguageId("de-DE")"#
);
}
#[test]
fn building_an_identity_by_parts_applies_the_same_rules() {
let english = Language::new("en").expect("a language");
let latin = ScriptSubtag::new("Latn").expect("a script");
let composed = LanguageId::compose(english, Some(latin), None, None);
assert_eq!(composed, admitted("en"));
assert_eq!(composed.script(), None, "the suppression fired");
let chinese = Language::new("zh").expect("a language");
let simplified = ScriptSubtag::new("Hans").expect("a script");
let composed = LanguageId::compose(chinese, Some(simplified), None, None);
assert_eq!(composed, admitted("zh-Hans"));
assert_eq!(LanguageId::from(english), admitted("en"));
}
#[test]
fn the_public_road_from_parts_is_the_standard_door() {
let seats = [
("zh", Some("Hans"), Some("CN"), None),
("en", Some("Latn"), Some("US"), None),
("de", None, None, Some("1901")),
("und", None, None, None),
];
for (language, script, region, rest) in seats {
let mut spelled = String::from(Language::new(language).expect("a language").as_str());
for part in [script, region, rest].into_iter().flatten() {
spelled.push('-');
spelled.push_str(part);
}
let composed = LanguageId::compose(
Language::new(language).expect("a language"),
script.map(|s| ScriptSubtag::new(s).expect("a script")),
region.map(|r| Region::new(r).expect("a region")),
rest.map(Utf8Bytes::from),
);
assert_eq!(
LanguageId::try_from(spelled.as_str()).expect("the spelled seats parse"),
composed,
"`{spelled}` — the two roads part"
);
}
}
#[test]
fn the_try_from_rows_are_the_door() {
assert_eq!(
LanguageId::try_from("GER-latn-de").expect("a tag"),
admitted("de-DE"),
"the folds apply — it is the same door"
);
assert_eq!(
LanguageId::try_from(Utf8Bytes::from("zh_Hans_CN")).expect("a tag"),
admitted("zh-Hans-CN"),
);
assert_eq!(
LanguageId::try_from("en-US-!!"),
Err(ParseLanguageIdError::Tail('!')),
"and so do the refusals"
);
assert_eq!(
LanguageId::try_from(Utf8Bytes::from("")),
Err(ParseLanguageIdError::Empty)
);
}
#[test]
fn the_region_seats_error_has_a_route_to_the_surface() {
let refused = ParseLanguageIdError::Region(ParseRegionError::Mixed);
assert!(refused.to_string().contains("region subtag"));
assert!(core::error::Error::source(&refused).is_some());
let language = ParseLanguageIdError::Language(ParseLanguageError::Empty);
assert!(core::error::Error::source(&language).is_some());
assert!(core::error::Error::source(&ParseLanguageIdError::Empty).is_none());
}
#[test]
fn a_suppression_that_would_not_reparse_does_not_fire() {
for (sent, tail, why) in [
(
"en-Latn-Cyrl",
"Cyrl",
"the tail's head would be read as the script",
),
(
"en-Latn-Latn",
"Latn",
"and the same where the head IS the suppressed script",
),
] {
let held = admitted(sent);
assert_eq!(held.to_string(), sent, "`{sent}` — {why}");
assert_eq!(
held.script().as_ref().map(ScriptSubtag::as_str),
Some("Latn"),
"`{sent}` — the script is retained"
);
assert_eq!(held.region(), None, "`{sent}`");
assert_eq!(held.rest().map(Utf8Bytes::as_str), Some(tail), "`{sent}`");
let rendered = held.to_string();
assert_eq!(
LanguageId::from_str(&rendered).expect("the rendering is a tag"),
held,
"`{sent}` — the rendering read back as another value"
);
}
let suppressed_text = admitted("en-Cyrl");
assert_ne!(admitted("en-Latn-Cyrl"), suppressed_text);
assert_eq!(
suppressed_text.script().as_ref().map(ScriptSubtag::as_str),
Some("Cyrl"),
"the reparser's script slot takes what the fold would have vacated"
);
assert_eq!(suppressed_text.rest(), None, "and the tail is gone with it");
assert_ne!(admitted("en-Latn-Latn"), admitted("en-Latn"));
assert_eq!(canonical("en-Latn"), "en", "the tail is empty — safe");
assert_eq!(
canonical("en-Latn-US"),
"en-US",
"the region is a seat — safe"
);
assert_eq!(
canonical("en-Latn-1901"),
"en-1901",
"a variant-shaped head is not a script — safe"
);
assert_eq!(
canonical("en-Latn-x-Cyrl"),
"en-x-Cyrl",
"the tail is a contiguous suffix, so only its HEAD can be reclassified"
);
assert_eq!(canonical("en-Latn-US-Cyrl"), "en-Latn-US-Cyrl");
}
#[test]
fn the_region_slot_takes_its_subtag_before_a_tail_can_open_with_one() {
for (sent, region, rest) in [
("en-Latn-US", Some("US"), None),
("en-Latn-419", Some("419"), None),
("en-Latn-US-Cyrl", Some("US"), Some("Cyrl")),
("en-US-posix", Some("US"), Some("posix")),
("es-419-x-a", Some("419"), Some("x-a")),
("zh-Hans-CN-x-a", Some("CN"), Some("x-a")),
] {
let held = admitted(sent);
assert_eq!(
held.region().as_ref().map(Region::as_str),
region,
"`{sent}` — the region seat"
);
assert_eq!(
held.rest().map(Utf8Bytes::as_str),
rest,
"`{sent}` — the tail begins after it"
);
}
let mut walked = 0usize;
for tag in corpus() {
let Ok(held) = door(&tag) else { continue };
walked += 1;
if held.region().is_some() {
continue;
}
let Some(rest) = held.rest() else { continue };
let head = rest
.as_str()
.split('-')
.next()
.expect("a tail has at least one subtag");
assert!(
!super::region_shaped(head),
"`{tag}` holds an empty region beside a region-shaped tail head `{head}` — the sibling case \
is REACHABLE and the suppression's guard must grow an arm for it"
);
}
assert!(walked > 11_000, "the corpus walked only {walked} tags");
}
#[test]
fn every_canonicalisation_is_reparse_stable() {
let mut walked = 0usize;
for tag in corpus() {
let Ok(held) = door(&tag) else { continue };
let rendered = held.to_string();
let reread = admitted(&rendered);
assert_eq!(
reread, held,
"`{tag}` rendered `{rendered}`, which reads back as a different identity"
);
assert_eq!(
reread.to_string(),
rendered,
"`{tag}` — the rendering is not a fixed point"
);
assert_eq!(
registry::grandfathered_preferred(&rendered.to_ascii_lowercase()),
None,
"`{tag}` rendered `{rendered}`, which the whole-tag table folds again — the \
canonicalisation stopped short of its fixed point"
);
walked += 1;
}
assert!(
walked > 11_000,
"the sweep is the registry's corpus and its interaction preimages, and it walked only \
{walked} tags"
);
}
#[test]
fn the_whole_tag_fold_is_applied_until_the_rendering_stops_moving() {
for (sent, held, through) in [
(
"en-Latn-GB-oed",
"en-GB-oxendict",
"the suppression drops `Latn` and leaves a grandfathered tag",
),
(
"eng-GB-oed",
"en-GB-oxendict",
"an mp4's alpha-3 reaches the same text",
),
(
"ENG-GB-OED",
"en-GB-oxendict",
"and the case fold is part of the road there",
),
("nor-bok", "nb", "alpha-3, then the whole tag"),
(
"NOR-BOK",
"nb",
"the tail keeps its case, and the lookup folds it anyway",
),
("zho-guoyu", "cmn", "the terminological Chinese"),
("chi-guoyu", "cmn", "and the bibliographic one"),
("zho-hakka", "hak", "another row of the same table"),
("nor-nyn", "nn", "and the sibling of the first"),
] {
assert_eq!(canonical(sent), held, "`{sent}` — {through}");
let value = admitted(sent);
assert_eq!(value, admitted(held), "`{sent}` — two identities, not one");
let mut distinct = BTreeSet::new();
distinct.insert(value.clone());
distinct.insert(admitted(held));
assert_eq!(distinct.len(), 1, "`{sent}`");
let rendered = value.to_string();
assert_eq!(admitted(&rendered), value, "`{sent}` — the value moved");
assert_eq!(
admitted(&rendered).to_string(),
rendered,
"`{sent}` — the text moved"
);
}
assert_eq!(canonical("en-Latn-Cyrl"), "en-Latn-Cyrl");
assert_eq!(canonical("en-Latn-Latn"), "en-Latn-Latn");
assert_eq!(canonical("en-GB-oed"), "en-GB-oxendict");
assert_eq!(canonical("i-klingon"), "tlh");
assert_eq!(canonical("zh-min"), "zh-min", "kept, so nothing folds");
assert_eq!(
canonical("zho-min"),
"zh-min",
"…and a fold that LANDS on a kept row stops there too"
);
}
#[test]
fn the_generated_hop_bound_is_the_chain_this_registry_has() {
let mut deepest = 0usize;
for (start, preferred) in registry::table::GRANDFATHERED {
let mut chain: Vec<String> = std::vec![String::from(*start)];
let mut held = LanguageId::composed(preferred).unwrap_or_else(|refused| {
panic!("`{start}` prefers `{preferred}`, which the door refuses: {refused}")
});
while let Some(next) = held.folds_onto() {
let key = held.to_string().to_ascii_lowercase();
assert!(
!chain.contains(&key),
"the whole-tag fold CYCLES: {} → {key} — canonicalisation has no fixed point to reach",
chain.join(" → ")
);
chain.push(key);
held = LanguageId::composed(next).unwrap_or_else(|refused| {
panic!("`{next}` is a `Preferred-Value` the door refuses: {refused}")
});
}
deepest = deepest.max(chain.len());
}
assert_eq!(
deepest,
registry::MAX_GRANDFATHERED_HOPS,
"the generator proved a bound of {} and the fold's own chain is {deepest} — regenerate with \
`cargo xtask gen-lang`",
registry::MAX_GRANDFATHERED_HOPS
);
}
#[test]
fn ordering_walks_the_seats_in_order() {
let mut sorted = [
admitted("de-DE"),
admitted("de"),
admitted("de-AT"),
admitted("zh-Hans"),
admitted("zh"),
];
sorted.sort();
let tags: Vec<String> = sorted.iter().map(LanguageId::to_string).collect();
assert_eq!(tags, ["de", "de-AT", "de-DE", "zh", "zh-Hans"]);
}