1use crate::provider::*;
6
7use icu_locale_core::subtags::{Language, Region, Script};
8use icu_locale_core::LanguageIdentifier;
9use icu_provider::prelude::*;
10
11use crate::TransformResult;
12
13#[derive(Debug, Clone)]
66pub struct LocaleExpander {
67    likely_subtags_l: DataPayload<LocaleLikelySubtagsLanguageV1>,
68    likely_subtags_sr: DataPayload<LocaleLikelySubtagsScriptRegionV1>,
69    likely_subtags_ext: Option<DataPayload<LocaleLikelySubtagsExtendedV1>>,
70}
71
72struct LocaleExpanderBorrowed<'a> {
73    likely_subtags_l: &'a LikelySubtagsForLanguage<'a>,
74    likely_subtags_sr: &'a LikelySubtagsForScriptRegion<'a>,
75    likely_subtags_ext: Option<&'a LikelySubtagsExtended<'a>>,
76}
77
78impl LocaleExpanderBorrowed<'_> {
79    fn get_l(&self, l: Language) -> Option<(Script, Region)> {
80        let key = &l.to_tinystr().to_unvalidated();
81        self.likely_subtags_l.language.get_copied(key).or_else(|| {
82            self.likely_subtags_ext
83                .and_then(|ext| ext.language.get_copied(key))
84        })
85    }
86
87    fn get_ls(&self, l: Language, s: Script) -> Option<Region> {
88        let key = &(
89            l.to_tinystr().to_unvalidated(),
90            s.to_tinystr().to_unvalidated(),
91        );
92        self.likely_subtags_l
93            .language_script
94            .get_copied(key)
95            .or_else(|| {
96                self.likely_subtags_ext
97                    .and_then(|ext| ext.language_script.get_copied(key))
98            })
99    }
100
101    fn get_lr(&self, l: Language, r: Region) -> Option<Script> {
102        let key = &(
103            l.to_tinystr().to_unvalidated(),
104            r.to_tinystr().to_unvalidated(),
105        );
106        self.likely_subtags_l
107            .language_region
108            .get_copied(key)
109            .or_else(|| {
110                self.likely_subtags_ext
111                    .and_then(|ext| ext.language_region.get_copied(key))
112            })
113    }
114
115    fn get_s(&self, s: Script) -> Option<(Language, Region)> {
116        let key = &s.to_tinystr().to_unvalidated();
117        self.likely_subtags_sr.script.get_copied(key).or_else(|| {
118            self.likely_subtags_ext
119                .and_then(|ext| ext.script.get_copied(key))
120        })
121    }
122
123    fn get_sr(&self, s: Script, r: Region) -> Option<Language> {
124        let key = &(
125            s.to_tinystr().to_unvalidated(),
126            r.to_tinystr().to_unvalidated(),
127        );
128        self.likely_subtags_sr
129            .script_region
130            .get_copied(key)
131            .or_else(|| {
132                self.likely_subtags_ext
133                    .and_then(|ext| ext.script_region.get_copied(key))
134            })
135    }
136
137    fn get_r(&self, r: Region) -> Option<(Language, Script)> {
138        let key = &r.to_tinystr().to_unvalidated();
139        self.likely_subtags_sr.region.get_copied(key).or_else(|| {
140            self.likely_subtags_ext
141                .and_then(|ext| ext.region.get_copied(key))
142        })
143    }
144
145    fn get_und(&self) -> (Language, Script, Region) {
146        self.likely_subtags_l.und
147    }
148}
149
150#[inline]
151fn update_langid(
152    language: Language,
153    script: Option<Script>,
154    region: Option<Region>,
155    langid: &mut LanguageIdentifier,
156) -> TransformResult {
157    let mut modified = false;
158
159    if langid.language.is_unknown() && !language.is_unknown() {
160        langid.language = language;
161        modified = true;
162    }
163
164    if langid.script.is_none() && script.is_some() {
165        langid.script = script;
166        modified = true;
167    }
168
169    if langid.region.is_none() && region.is_some() {
170        langid.region = region;
171        modified = true;
172    }
173
174    if modified {
175        TransformResult::Modified
176    } else {
177        TransformResult::Unmodified
178    }
179}
180
181#[inline]
182fn update_langid_minimize(
183    language: Language,
184    script: Option<Script>,
185    region: Option<Region>,
186    langid: &mut LanguageIdentifier,
187) -> TransformResult {
188    let mut modified = false;
189
190    if langid.language != language {
191        langid.language = language;
192        modified = true;
193    }
194
195    if langid.script != script {
196        langid.script = script;
197        modified = true;
198    }
199
200    if langid.region != region {
201        langid.region = region;
202        modified = true;
203    }
204
205    if modified {
206        TransformResult::Modified
207    } else {
208        TransformResult::Unmodified
209    }
210}
211
212impl LocaleExpander {
213    #[cfg(feature = "compiled_data")]
224    pub const fn new_common() -> Self {
225        LocaleExpander {
226            likely_subtags_l: DataPayload::from_static_ref(
227                crate::provider::Baked::SINGLETON_LOCALE_LIKELY_SUBTAGS_LANGUAGE_V1,
228            ),
229            likely_subtags_sr: DataPayload::from_static_ref(
230                crate::provider::Baked::SINGLETON_LOCALE_LIKELY_SUBTAGS_SCRIPT_REGION_V1,
231            ),
232            likely_subtags_ext: None,
233        }
234    }
235
236    icu_provider::gen_buffer_data_constructors!(() -> error: DataError,
237        functions: [
238        new_common: skip,
239        try_new_common_with_buffer_provider,
240        try_new_common_unstable,
241        Self
242    ]);
243
244    #[doc = icu_provider::gen_buffer_unstable_docs!(UNSTABLE, Self::new_common)]
245    pub fn try_new_common_unstable<P>(provider: &P) -> Result<LocaleExpander, DataError>
246    where
247        P: DataProvider<LocaleLikelySubtagsLanguageV1>
248            + DataProvider<LocaleLikelySubtagsScriptRegionV1>
249            + ?Sized,
250    {
251        let likely_subtags_l = provider.load(Default::default())?.payload;
252        let likely_subtags_sr = provider.load(Default::default())?.payload;
253
254        Ok(LocaleExpander {
255            likely_subtags_l,
256            likely_subtags_sr,
257            likely_subtags_ext: None,
258        })
259    }
260
261    #[cfg(feature = "compiled_data")]
272    pub const fn new_extended() -> Self {
273        LocaleExpander {
274            likely_subtags_l: DataPayload::from_static_ref(
275                crate::provider::Baked::SINGLETON_LOCALE_LIKELY_SUBTAGS_LANGUAGE_V1,
276            ),
277            likely_subtags_sr: DataPayload::from_static_ref(
278                crate::provider::Baked::SINGLETON_LOCALE_LIKELY_SUBTAGS_SCRIPT_REGION_V1,
279            ),
280            likely_subtags_ext: Some(DataPayload::from_static_ref(
281                crate::provider::Baked::SINGLETON_LOCALE_LIKELY_SUBTAGS_EXTENDED_V1,
282            )),
283        }
284    }
285
286    icu_provider::gen_buffer_data_constructors!(() -> error: DataError,
287        functions: [
288        new_extended: skip,
289        try_new_extended_with_buffer_provider,
290        try_new_extended_unstable,
291        Self
292    ]);
293
294    #[doc = icu_provider::gen_buffer_unstable_docs!(UNSTABLE, Self::new_extended)]
295    pub fn try_new_extended_unstable<P>(provider: &P) -> Result<LocaleExpander, DataError>
296    where
297        P: DataProvider<LocaleLikelySubtagsLanguageV1>
298            + DataProvider<LocaleLikelySubtagsScriptRegionV1>
299            + DataProvider<LocaleLikelySubtagsExtendedV1>
300            + ?Sized,
301    {
302        let likely_subtags_l = provider.load(Default::default())?.payload;
303        let likely_subtags_sr = provider.load(Default::default())?.payload;
304        let likely_subtags_ext = Some(provider.load(Default::default())?.payload);
305
306        Ok(LocaleExpander {
307            likely_subtags_l,
308            likely_subtags_sr,
309            likely_subtags_ext,
310        })
311    }
312
313    fn as_borrowed(&self) -> LocaleExpanderBorrowed {
314        LocaleExpanderBorrowed {
315            likely_subtags_l: self.likely_subtags_l.get(),
316            likely_subtags_sr: self.likely_subtags_sr.get(),
317            likely_subtags_ext: self.likely_subtags_ext.as_ref().map(|p| p.get()),
318        }
319    }
320
321    pub fn maximize(&self, langid: &mut LanguageIdentifier) -> TransformResult {
376        let data = self.as_borrowed();
377
378        if !langid.language.is_unknown() && langid.script.is_some() && langid.region.is_some() {
379            return TransformResult::Unmodified;
380        }
381
382        if !langid.language.is_unknown() {
383            if let Some(region) = langid.region {
384                if let Some(script) = data.get_lr(langid.language, region) {
385                    return update_langid(Language::UNKNOWN, Some(script), None, langid);
386                }
387            }
388            if let Some(script) = langid.script {
389                if let Some(region) = data.get_ls(langid.language, script) {
390                    return update_langid(Language::UNKNOWN, None, Some(region), langid);
391                }
392            }
393            if let Some((script, region)) = data.get_l(langid.language) {
394                return update_langid(Language::UNKNOWN, Some(script), Some(region), langid);
395            }
396            return TransformResult::Unmodified;
398        }
399        if let Some(script) = langid.script {
400            if let Some(region) = langid.region {
401                if let Some(language) = data.get_sr(script, region) {
402                    return update_langid(language, None, None, langid);
403                }
404            }
405            if let Some((language, region)) = data.get_s(script) {
406                return update_langid(language, None, Some(region), langid);
407            }
408        }
409        if let Some(region) = langid.region {
410            if let Some((language, script)) = data.get_r(region) {
411                return update_langid(language, Some(script), None, langid);
412            }
413        }
414
415        debug_assert!(langid.language.is_unknown());
418        update_langid(
419            data.get_und().0,
420            Some(data.get_und().1),
421            Some(data.get_und().2),
422            langid,
423        )
424    }
425
426    pub fn minimize(&self, langid: &mut LanguageIdentifier) -> TransformResult {
452        self.minimize_impl(langid, true)
453    }
454
455    pub fn minimize_favor_script(&self, langid: &mut LanguageIdentifier) -> TransformResult {
480        self.minimize_impl(langid, false)
481    }
482
483    fn minimize_impl(
484        &self,
485        langid: &mut LanguageIdentifier,
486        favor_region: bool,
487    ) -> TransformResult {
488        let mut max = langid.clone();
489        self.maximize(&mut max);
490
491        let mut trial = max.clone();
492
493        trial.script = None;
494        trial.region = None;
495        self.maximize(&mut trial);
496        if trial == max {
497            return update_langid_minimize(max.language, None, None, langid);
498        }
499
500        if favor_region {
501            trial.script = None;
502            trial.region = max.region;
503            self.maximize(&mut trial);
504
505            if trial == max {
506                return update_langid_minimize(max.language, None, max.region, langid);
507            }
508
509            trial.script = max.script;
510            trial.region = None;
511            self.maximize(&mut trial);
512            if trial == max {
513                return update_langid_minimize(max.language, max.script, None, langid);
514            }
515        } else {
516            trial.script = max.script;
517            trial.region = None;
518            self.maximize(&mut trial);
519            if trial == max {
520                return update_langid_minimize(max.language, max.script, None, langid);
521            }
522
523            trial.script = None;
524            trial.region = max.region;
525            self.maximize(&mut trial);
526
527            if trial == max {
528                return update_langid_minimize(max.language, None, max.region, langid);
529            }
530        }
531
532        update_langid_minimize(max.language, max.script, max.region, langid)
533    }
534
535    #[inline]
537    pub(crate) fn get_likely_script(&self, langid: &LanguageIdentifier) -> Option<Script> {
538        langid
539            .script
540            .or_else(|| self.infer_likely_script(langid.language, langid.region))
541    }
542
543    fn infer_likely_script(&self, language: Language, region: Option<Region>) -> Option<Script> {
544        let data = self.as_borrowed();
545
546        if !language.is_unknown() {
554            if let Some(region) = region {
555                if let Some(script) = data.get_lr(language, region) {
557                    return Some(script);
558                }
559            }
560            if let Some((script, _)) = data.get_l(language) {
562                return Some(script);
563            }
564        }
565        if let Some(region) = region {
566            if let Some((_, script)) = data.get_r(region) {
568                return Some(script);
569            }
570        }
571        None
573    }
574}
575
576impl AsRef<LocaleExpander> for LocaleExpander {
577    fn as_ref(&self) -> &LocaleExpander {
578        self
579    }
580}
581
582#[cfg(feature = "serde")]
583#[cfg(test)]
584mod tests {
585    use super::*;
586    use icu_locale_core::locale;
587
588    #[test]
589    fn test_minimize_favor_script() {
590        let lc = LocaleExpander::new_common();
591        let mut locale = locale!("yue-Hans");
592        assert_eq!(
593            lc.minimize_favor_script(&mut locale.id),
594            TransformResult::Unmodified
595        );
596        assert_eq!(locale, locale!("yue-Hans"));
597    }
598
599    #[test]
600    fn test_minimize_favor_region() {
601        let lc = LocaleExpander::new_common();
602        let mut locale = locale!("yue-Hans");
603        assert_eq!(lc.minimize(&mut locale.id), TransformResult::Modified);
604        assert_eq!(locale, locale!("yue-CN"));
605    }
606}