1use crate::prelude::*;
3
4use super::tag::{self, LangCode};
5
6const CHINESE: &str = "zh";
8
9fn chinese_script(code: &LangCode) -> Option<&'static str> {
13 if code.language() != CHINESE || code.script().is_some() {
14 return None;
15 }
16 match code.region()? {
17 "CN" | "SG" => Some("Hans"),
18 "TW" | "HK" | "MO" => Some("Hant"),
19 _ => None,
20 }
21}
22
23fn offered_along<'a>(tag: &str, offered: &'a [LangCode]) -> Option<&'a LangCode> {
25 tag::ancestry(tag).find_map(|step| offered.iter().find(|code| code.as_str() == step))
26}
27
28fn match_one(wanted: &LangCode, offered: &[LangCode]) -> Option<LangCode> {
30 if let Some(found) = offered_along(wanted.as_str(), offered) {
31 return Some(found.clone());
32 }
33 let language = wanted.language();
34 let region_script = chinese_script(wanted);
35 if let Some(script) = region_script {
36 let rest = &wanted.as_str()[language.len()..];
37 if let Some(found) = offered_along(&format!("{language}-{script}{rest}"), offered) {
38 return Some(found.clone());
39 }
40 }
41 let mut same = offered.iter().filter(|code| code.language() == language);
42 let only = same.next()?;
43 if same.next().is_some() {
44 return None;
45 }
46 let script = wanted.script().or(region_script);
47 match (script, only.script().or_else(|| chinese_script(only))) {
48 (Some(want), Some(have)) if want != have => None,
49 _ => Some(only.clone()),
50 }
51}
52
53pub fn best_match(preferred: &[LangCode], offered: &[LangCode]) -> Option<LangCode> {
71 preferred
72 .iter()
73 .find_map(|wanted| match_one(wanted, offered))
74}
75
76#[cfg(test)]
77mod tests {
78 use super::*;
79
80 fn tags(texts: &[&str]) -> Vec<LangCode> {
81 texts
82 .iter()
83 .map(|text| LangCode::new(text).unwrap_or_else(|| panic!("{text}")))
84 .collect()
85 }
86
87 fn best(preferred: &[&str], offered: &[&str]) -> Option<String> {
88 best_match(&tags(preferred), &tags(offered)).map(|code| code.as_str().to_owned())
89 }
90
91 #[test]
92 fn a_tag_matches_itself_or_a_parent() {
93 assert_eq!(best(&["pt-BR"], &["en", "pt"]), Some("pt".into()));
94 assert_eq!(best(&["en-GB"], &["en"]), Some("en".into()));
95 assert_eq!(best(&["pt-BR"], &["pt", "pt-BR"]), Some("pt-BR".into()));
96 assert_eq!(best(&["ja"], &["en", "ja"]), Some("ja".into()));
97 }
98
99 #[test]
100 fn chinese_takes_its_script_from_its_region() {
101 assert_eq!(best(&["zh-CN"], &["en", "zh-Hans"]), Some("zh-Hans".into()));
102 assert_eq!(best(&["zh-TW"], &["en", "zh-Hans"]), None);
103 assert_eq!(
104 best(&["zh-CN"], &["zh-Hant", "zh-Hans-CN"]),
105 Some("zh-Hans-CN".into())
106 );
107 for (region, script) in [
108 ("CN", "Hans"),
109 ("SG", "Hans"),
110 ("TW", "Hant"),
111 ("HK", "Hant"),
112 ("MO", "Hant"),
113 ] {
114 let offered = ["en", "zh-Hans", "zh-Hant"];
115 let wanted = format!("zh-{region}");
116 assert_eq!(
117 best(&[&wanted], &offered),
118 Some(format!("zh-{script}")),
119 "{wanted}"
120 );
121 }
122 assert_eq!(best(&["zh-MY"], &["zh-Hans", "zh-Hant"]), None);
123 }
124
125 #[test]
126 fn only_chinese_without_a_script_takes_one_from_its_region() {
127 let script = |text: &str| chinese_script(&tags(&[text])[0]);
128 assert_eq!(script("zh-SG"), Some("Hans"));
129 assert_eq!(script("ms-SG"), None);
130 assert_eq!(script("zh-Hant-CN"), None);
131 assert_eq!(best(&["ms-SG"], &["en", "ms-Arab"]), Some("ms-Arab".into()));
132 }
133
134 #[test]
135 fn a_lone_tag_of_the_language_matches_unless_the_scripts_differ() {
136 assert_eq!(best(&["pt-PT"], &["en", "pt-BR"]), Some("pt-BR".into()));
137 assert_eq!(best(&["pt-PT"], &["en", "pt-BR", "pt-AO"]), None);
138 assert_eq!(best(&["sr-Latn"], &["en", "sr-Cyrl"]), None);
139 assert_eq!(best(&["sr-Latn"], &["en", "sr-RS"]), Some("sr-RS".into()));
140 assert_eq!(best(&["zh-HK"], &["en", "zh-CN"]), None);
141 assert_eq!(best(&["zh-HK"], &["en", "zh-TW"]), Some("zh-TW".into()));
142 assert_eq!(best(&["zh"], &["en", "zh-Hant"]), Some("zh-Hant".into()));
143 }
144
145 #[test]
146 fn the_first_preferred_tag_that_matches_wins() {
147 assert_eq!(best(&["de", "fr-CA"], &["en", "fr"]), Some("fr".into()));
148 assert_eq!(best(&["fr-CA", "en"], &["en", "fr"]), Some("fr".into()));
149 assert_eq!(best(&["de", "it"], &["en", "fr"]), None);
150 assert_eq!(best(&[], &["en", "fr"]), None);
151 assert_eq!(best(&["en"], &[]), None);
152 }
153}