1use crate::{provider::*, LocaleTransformError};
6
7use icu_locid::subtags::{Language, Region, Script};
8use icu_locid::LanguageIdentifier;
9use icu_provider::prelude::*;
10
11use crate::TransformResult;
12
13#[derive(Debug, Clone)]
68pub struct LocaleExpander {
69    likely_subtags_l: DataPayload<LikelySubtagsForLanguageV1Marker>,
70    likely_subtags_sr: DataPayload<LikelySubtagsForScriptRegionV1Marker>,
71    likely_subtags_ext: Option<DataPayload<LikelySubtagsExtendedV1Marker>>,
72}
73
74struct LocaleExpanderBorrowed<'a> {
75    likely_subtags_l: &'a LikelySubtagsForLanguageV1<'a>,
76    likely_subtags_sr: &'a LikelySubtagsForScriptRegionV1<'a>,
77    likely_subtags_ext: Option<&'a LikelySubtagsExtendedV1<'a>>,
78}
79
80impl LocaleExpanderBorrowed<'_> {
81    fn get_l(&self, l: Language) -> Option<(Script, Region)> {
82        let key = &l.into_tinystr().to_unvalidated();
83        self.likely_subtags_l.language.get_copied(key).or_else(|| {
84            self.likely_subtags_ext
85                .and_then(|ext| ext.language.get_copied(key))
86        })
87    }
88
89    fn get_ls(&self, l: Language, s: Script) -> Option<Region> {
90        let key = &(
91            l.into_tinystr().to_unvalidated(),
92            s.into_tinystr().to_unvalidated(),
93        );
94        self.likely_subtags_l
95            .language_script
96            .get_copied(key)
97            .or_else(|| {
98                self.likely_subtags_ext
99                    .and_then(|ext| ext.language_script.get_copied(key))
100            })
101    }
102
103    fn get_lr(&self, l: Language, r: Region) -> Option<Script> {
104        let key = &(
105            l.into_tinystr().to_unvalidated(),
106            r.into_tinystr().to_unvalidated(),
107        );
108        self.likely_subtags_l
109            .language_region
110            .get_copied(key)
111            .or_else(|| {
112                self.likely_subtags_ext
113                    .and_then(|ext| ext.language_region.get_copied(key))
114            })
115    }
116
117    fn get_s(&self, s: Script) -> Option<(Language, Region)> {
118        let key = &s.into_tinystr().to_unvalidated();
119        self.likely_subtags_sr.script.get_copied(key).or_else(|| {
120            self.likely_subtags_ext
121                .and_then(|ext| ext.script.get_copied(key))
122        })
123    }
124
125    fn get_sr(&self, s: Script, r: Region) -> Option<Language> {
126        let key = &(
127            s.into_tinystr().to_unvalidated(),
128            r.into_tinystr().to_unvalidated(),
129        );
130        self.likely_subtags_sr
131            .script_region
132            .get_copied(key)
133            .or_else(|| {
134                self.likely_subtags_ext
135                    .and_then(|ext| ext.script_region.get_copied(key))
136            })
137    }
138
139    fn get_r(&self, r: Region) -> Option<(Language, Script)> {
140        let key = &r.into_tinystr().to_unvalidated();
141        self.likely_subtags_sr.region.get_copied(key).or_else(|| {
142            self.likely_subtags_ext
143                .and_then(|ext| ext.region.get_copied(key))
144        })
145    }
146
147    fn get_und(&self) -> (Language, Script, Region) {
148        self.likely_subtags_l.und
149    }
150}
151
152#[inline]
153fn update_langid(
154    language: Language,
155    script: Option<Script>,
156    region: Option<Region>,
157    langid: &mut LanguageIdentifier,
158) -> TransformResult {
159    let mut modified = false;
160
161    if langid.language.is_empty() && !language.is_empty() {
162        langid.language = language;
163        modified = true;
164    }
165
166    if langid.script.is_none() && script.is_some() {
167        langid.script = script;
168        modified = true;
169    }
170
171    if langid.region.is_none() && region.is_some() {
172        langid.region = region;
173        modified = true;
174    }
175
176    if modified {
177        TransformResult::Modified
178    } else {
179        TransformResult::Unmodified
180    }
181}
182
183#[inline]
184fn update_langid_minimize(
185    language: Language,
186    script: Option<Script>,
187    region: Option<Region>,
188    langid: &mut LanguageIdentifier,
189) -> TransformResult {
190    let mut modified = false;
191
192    if langid.language != language {
193        langid.language = language;
194        modified = true;
195    }
196
197    if langid.script != script {
198        langid.script = script;
199        modified = true;
200    }
201
202    if langid.region != region {
203        langid.region = region;
204        modified = true;
205    }
206
207    if modified {
208        TransformResult::Modified
209    } else {
210        TransformResult::Unmodified
211    }
212}
213
214#[cfg(feature = "compiled_data")]
215impl Default for LocaleExpander {
216    fn default() -> Self {
217        Self::new()
218    }
219}
220
221impl LocaleExpander {
222    #[cfg(feature = "compiled_data")]
233    pub const fn new() -> Self {
234        LocaleExpander {
235            likely_subtags_l: DataPayload::from_static_ref(
236                crate::provider::Baked::SINGLETON_LOCID_TRANSFORM_LIKELYSUBTAGS_L_V1,
237            ),
238            likely_subtags_sr: DataPayload::from_static_ref(
239                crate::provider::Baked::SINGLETON_LOCID_TRANSFORM_LIKELYSUBTAGS_SR_V1,
240            ),
241            likely_subtags_ext: None,
242        }
243    }
244
245    #[cfg(feature = "compiled_data")]
256    pub const fn new_extended() -> Self {
257        LocaleExpander {
258            likely_subtags_l: DataPayload::from_static_ref(
259                crate::provider::Baked::SINGLETON_LOCID_TRANSFORM_LIKELYSUBTAGS_L_V1,
260            ),
261            likely_subtags_sr: DataPayload::from_static_ref(
262                crate::provider::Baked::SINGLETON_LOCID_TRANSFORM_LIKELYSUBTAGS_SR_V1,
263            ),
264            likely_subtags_ext: Some(DataPayload::from_static_ref(
265                crate::provider::Baked::SINGLETON_LOCID_TRANSFORM_LIKELYSUBTAGS_EXT_V1,
266            )),
267        }
268    }
269
270    #[doc = icu_provider::gen_any_buffer_unstable_docs!(UNSTABLE, Self::new_extended)]
271    pub fn try_new_extended_unstable<P>(
272        provider: &P,
273    ) -> Result<LocaleExpander, LocaleTransformError>
274    where
275        P: DataProvider<LikelySubtagsForLanguageV1Marker>
276            + DataProvider<LikelySubtagsForScriptRegionV1Marker>
277            + DataProvider<LikelySubtagsExtendedV1Marker>
278            + ?Sized,
279    {
280        let likely_subtags_l = provider.load(Default::default())?.take_payload()?;
281        let likely_subtags_sr = provider.load(Default::default())?.take_payload()?;
282        let likely_subtags_ext = Some(provider.load(Default::default())?.take_payload()?);
283
284        Ok(LocaleExpander {
285            likely_subtags_l,
286            likely_subtags_sr,
287            likely_subtags_ext,
288        })
289    }
290
291    icu_provider::gen_any_buffer_data_constructors!(locale: skip, options: skip, error: LocaleTransformError,
292        #[cfg(skip)]
293        functions: [
294        new_extended,
295        try_new_extended_with_any_provider,
296        try_new_extended_with_buffer_provider,
297        try_new_extended_unstable,
298        Self
299    ]);
300
301    #[doc = icu_provider::gen_any_buffer_unstable_docs!(ANY, Self::new)]
302    pub fn try_new_with_any_provider(
303        provider: &(impl AnyProvider + ?Sized),
304    ) -> Result<LocaleExpander, LocaleTransformError> {
305        Self::try_new_compat(&provider.as_downcasting())
306    }
307
308    #[doc = icu_provider::gen_any_buffer_unstable_docs!(BUFFER, Self::new)]
309    #[cfg(feature = "serde")]
310    pub fn try_new_with_buffer_provider(
311        provider: &(impl BufferProvider + ?Sized),
312    ) -> Result<LocaleExpander, LocaleTransformError> {
313        Self::try_new_compat(&provider.as_deserializing())
314    }
315
316    #[doc = icu_provider::gen_any_buffer_unstable_docs!(UNSTABLE, Self::new)]
317    pub fn try_new_unstable<P>(provider: &P) -> Result<LocaleExpander, LocaleTransformError>
318    where
319        P: DataProvider<LikelySubtagsForLanguageV1Marker>
320            + DataProvider<LikelySubtagsForScriptRegionV1Marker>
321            + ?Sized,
322    {
323        let likely_subtags_l = provider.load(Default::default())?.take_payload()?;
324        let likely_subtags_sr = provider.load(Default::default())?.take_payload()?;
325
326        Ok(LocaleExpander {
327            likely_subtags_l,
328            likely_subtags_sr,
329            likely_subtags_ext: None,
330        })
331    }
332
333    fn try_new_compat<P>(provider: &P) -> Result<LocaleExpander, LocaleTransformError>
334    where
335        P: DataProvider<LikelySubtagsForLanguageV1Marker>
336            + DataProvider<LikelySubtagsForScriptRegionV1Marker>
337            + DataProvider<LikelySubtagsExtendedV1Marker>
338            + DataProvider<LikelySubtagsV1Marker>
339            + ?Sized,
340    {
341        let payload_l = provider
342            .load(Default::default())
343            .and_then(DataResponse::take_payload);
344        let payload_sr = provider
345            .load(Default::default())
346            .and_then(DataResponse::take_payload);
347        let payload_ext = provider
348            .load(Default::default())
349            .and_then(DataResponse::take_payload);
350
351        let (likely_subtags_l, likely_subtags_sr, likely_subtags_ext) =
352            match (payload_l, payload_sr, payload_ext) {
353                (Ok(l), Ok(sr), Err(_)) => (l, sr, None),
354                (Ok(l), Ok(sr), Ok(ext)) => (l, sr, Some(ext)),
355                _ => {
356                    let result: DataPayload<LikelySubtagsV1Marker> =
357                        provider.load(Default::default())?.take_payload()?;
358                    (
359                        result.map_project_cloned(|st, _| {
360                            LikelySubtagsForLanguageV1::clone_from_borrowed(st)
361                        }),
362                        result.map_project(|st, _| st.into()),
363                        None,
364                    )
365                }
366            };
367
368        Ok(LocaleExpander {
369            likely_subtags_l,
370            likely_subtags_sr,
371            likely_subtags_ext,
372        })
373    }
374
375    fn as_borrowed(&self) -> LocaleExpanderBorrowed {
376        LocaleExpanderBorrowed {
377            likely_subtags_l: self.likely_subtags_l.get(),
378            likely_subtags_sr: self.likely_subtags_sr.get(),
379            likely_subtags_ext: self.likely_subtags_ext.as_ref().map(|p| p.get()),
380        }
381    }
382
383    pub fn maximize<T: AsMut<LanguageIdentifier>>(&self, mut langid: T) -> TransformResult {
440        let langid = langid.as_mut();
441        let data = self.as_borrowed();
442
443        if !langid.language.is_empty() && langid.script.is_some() && langid.region.is_some() {
444            return TransformResult::Unmodified;
445        }
446
447        if !langid.language.is_empty() {
448            if let Some(region) = langid.region {
449                if let Some(script) = data.get_lr(langid.language, region) {
450                    return update_langid(Language::UND, Some(script), None, langid);
451                }
452            }
453            if let Some(script) = langid.script {
454                if let Some(region) = data.get_ls(langid.language, script) {
455                    return update_langid(Language::UND, None, Some(region), langid);
456                }
457            }
458            if let Some((script, region)) = data.get_l(langid.language) {
459                return update_langid(Language::UND, Some(script), Some(region), langid);
460            }
461            return TransformResult::Unmodified;
463        }
464        if let Some(script) = langid.script {
465            if let Some(region) = langid.region {
466                if let Some(language) = data.get_sr(script, region) {
467                    return update_langid(language, None, None, langid);
468                }
469            }
470            if let Some((language, region)) = data.get_s(script) {
471                return update_langid(language, None, Some(region), langid);
472            }
473        }
474        if let Some(region) = langid.region {
475            if let Some((language, script)) = data.get_r(region) {
476                return update_langid(language, Some(script), None, langid);
477            }
478        }
479
480        debug_assert!(langid.language.is_empty());
483        update_langid(
484            data.get_und().0,
485            Some(data.get_und().1),
486            Some(data.get_und().2),
487            langid,
488        )
489    }
490
491    pub fn minimize<T: AsMut<LanguageIdentifier>>(&self, langid: T) -> TransformResult {
518        self.minimize_impl(langid, true)
519    }
520
521    pub fn minimize_favor_script<T: AsMut<LanguageIdentifier>>(
547        &self,
548        langid: T,
549    ) -> TransformResult {
550        self.minimize_impl(langid, false)
551    }
552
553    fn minimize_impl<T: AsMut<LanguageIdentifier>>(
554        &self,
555        mut langid: T,
556        favor_region: bool,
557    ) -> TransformResult {
558        let langid = langid.as_mut();
559
560        let mut max = langid.clone();
561        self.maximize(&mut max);
562
563        let mut trial = max.clone();
564
565        trial.script = None;
566        trial.region = None;
567        self.maximize(&mut trial);
568        if trial == max {
569            return update_langid_minimize(max.language, None, None, langid);
570        }
571
572        if favor_region {
573            trial.script = None;
574            trial.region = max.region;
575            self.maximize(&mut trial);
576
577            if trial == max {
578                return update_langid_minimize(max.language, None, max.region, langid);
579            }
580
581            trial.script = max.script;
582            trial.region = None;
583            self.maximize(&mut trial);
584            if trial == max {
585                return update_langid_minimize(max.language, max.script, None, langid);
586            }
587        } else {
588            trial.script = max.script;
589            trial.region = None;
590            self.maximize(&mut trial);
591            if trial == max {
592                return update_langid_minimize(max.language, max.script, None, langid);
593            }
594
595            trial.script = None;
596            trial.region = max.region;
597            self.maximize(&mut trial);
598
599            if trial == max {
600                return update_langid_minimize(max.language, None, max.region, langid);
601            }
602        }
603
604        update_langid_minimize(max.language, max.script, max.region, langid)
605    }
606
607    #[inline]
609    pub(crate) fn get_likely_script<T: AsRef<LanguageIdentifier>>(
610        &self,
611        langid: T,
612    ) -> Option<Script> {
613        let langid = langid.as_ref();
614        langid
615            .script
616            .or_else(|| self.infer_likely_script(langid.language, langid.region))
617    }
618
619    fn infer_likely_script(&self, language: Language, region: Option<Region>) -> Option<Script> {
620        let data = self.as_borrowed();
621
622        if language != Language::UND {
630            if let Some(region) = region {
631                if let Some(script) = data.get_lr(language, region) {
633                    return Some(script);
634                }
635            }
636            if let Some((script, _)) = data.get_l(language) {
638                return Some(script);
639            }
640        }
641        if let Some(region) = region {
642            if let Some((_, script)) = data.get_r(region) {
644                return Some(script);
645            }
646        }
647        None
649    }
650}
651
652#[cfg(feature = "serde")]
653#[cfg(test)]
654mod tests {
655    use super::*;
656    use icu_locid::locale;
657
658    struct RejectByKeyProvider {
659        keys: Vec<DataKey>,
660    }
661
662    impl AnyProvider for RejectByKeyProvider {
663        fn load_any(&self, key: DataKey, _: DataRequest) -> Result<AnyResponse, DataError> {
664            if self.keys.contains(&key) {
665                return Err(DataErrorKind::MissingDataKey.with_str_context("rejected"));
666            }
667
668            let l = crate::provider::Baked::SINGLETON_LOCID_TRANSFORM_LIKELYSUBTAGS_L_V1;
669            let ext = crate::provider::Baked::SINGLETON_LOCID_TRANSFORM_LIKELYSUBTAGS_EXT_V1;
670            let sr = crate::provider::Baked::SINGLETON_LOCID_TRANSFORM_LIKELYSUBTAGS_SR_V1;
671
672            let payload = if key.hashed() == LikelySubtagsV1Marker::KEY.hashed() {
673                DataPayload::<LikelySubtagsV1Marker>::from_owned(LikelySubtagsV1 {
674                    language_script: l
675                        .language_script
676                        .iter_copied()
677                        .chain(ext.language_script.iter_copied())
678                        .collect(),
679                    language_region: l
680                        .language_region
681                        .iter_copied()
682                        .chain(ext.language_region.iter_copied())
683                        .collect(),
684                    language: l
685                        .language
686                        .iter_copied()
687                        .chain(ext.language.iter_copied())
688                        .collect(),
689                    script_region: ext.script_region.clone(),
690                    script: ext.script.clone(),
691                    region: ext.region.clone(),
692                    und: l.und,
693                })
694                .wrap_into_any_payload()
695            } else if key.hashed() == LikelySubtagsForLanguageV1Marker::KEY.hashed() {
696                DataPayload::<LikelySubtagsForLanguageV1Marker>::from_static_ref(l)
697                    .wrap_into_any_payload()
698            } else if key.hashed() == LikelySubtagsExtendedV1Marker::KEY.hashed() {
699                DataPayload::<LikelySubtagsExtendedV1Marker>::from_static_ref(ext)
700                    .wrap_into_any_payload()
701            } else if key.hashed() == LikelySubtagsForScriptRegionV1Marker::KEY.hashed() {
702                DataPayload::<LikelySubtagsForScriptRegionV1Marker>::from_static_ref(sr)
703                    .wrap_into_any_payload()
704            } else {
705                return Err(DataErrorKind::MissingDataKey.into_error());
706            };
707
708            Ok(AnyResponse {
709                payload: Some(payload),
710                metadata: Default::default(),
711            })
712        }
713    }
714
715    #[test]
716    fn test_old_keys() {
717        let provider = RejectByKeyProvider {
718            keys: vec![
719                LikelySubtagsForLanguageV1Marker::KEY,
720                LikelySubtagsForScriptRegionV1Marker::KEY,
721                LikelySubtagsExtendedV1Marker::KEY,
722            ],
723        };
724        let lc = LocaleExpander::try_new_with_any_provider(&provider)
725            .expect("should create with old keys");
726        let mut locale = locale!("zh-CN");
727        assert_eq!(lc.maximize(&mut locale), TransformResult::Modified);
728        assert_eq!(locale, locale!("zh-Hans-CN"));
729    }
730
731    #[test]
732    fn test_new_keys() {
733        let provider = RejectByKeyProvider {
734            keys: vec![LikelySubtagsV1Marker::KEY],
735        };
736        let lc = LocaleExpander::try_new_with_any_provider(&provider)
737            .expect("should create with new keys");
738        let mut locale = locale!("zh-CN");
739        assert_eq!(lc.maximize(&mut locale), TransformResult::Modified);
740        assert_eq!(locale, locale!("zh-Hans-CN"));
741    }
742
743    #[test]
744    fn test_mixed_keys() {
745        let provider = RejectByKeyProvider {
748            keys: vec![LikelySubtagsForScriptRegionV1Marker::KEY],
749        };
750        let lc = LocaleExpander::try_new_with_any_provider(&provider)
751            .expect("should create with mixed keys");
752        let mut locale = locale!("zh-CN");
753        assert_eq!(lc.maximize(&mut locale), TransformResult::Modified);
754        assert_eq!(locale, locale!("zh-Hans-CN"));
755    }
756
757    #[test]
758    fn test_no_keys() {
759        let provider = RejectByKeyProvider {
760            keys: vec![
761                LikelySubtagsForLanguageV1Marker::KEY,
762                LikelySubtagsForScriptRegionV1Marker::KEY,
763                LikelySubtagsV1Marker::KEY,
764            ],
765        };
766        if LocaleExpander::try_new_with_any_provider(&provider).is_ok() {
767            panic!("should not create: no data present")
768        };
769    }
770
771    #[test]
772    fn test_new_small_keys() {
773        let provider = RejectByKeyProvider {
775            keys: vec![
776                LikelySubtagsExtendedV1Marker::KEY,
777                LikelySubtagsV1Marker::KEY,
778            ],
779        };
780        let lc = LocaleExpander::try_new_with_any_provider(&provider)
781            .expect("should create with mixed keys");
782        let mut locale = locale!("zh-CN");
783        assert_eq!(lc.maximize(&mut locale), TransformResult::Modified);
784        assert_eq!(locale, locale!("zh-Hans-CN"));
785    }
786
787    #[test]
788    fn test_minimize_favor_script() {
789        let lc = LocaleExpander::new();
790        let mut locale = locale!("yue-Hans");
791        assert_eq!(
792            lc.minimize_favor_script(&mut locale),
793            TransformResult::Unmodified
794        );
795        assert_eq!(locale, locale!("yue-Hans"));
796    }
797
798    #[test]
799    fn test_minimize_favor_region() {
800        let lc = LocaleExpander::new();
801        let mut locale = locale!("yue-Hans");
802        assert_eq!(lc.minimize(&mut locale), TransformResult::Modified);
803        assert_eq!(locale, locale!("yue-CN"));
804    }
805}