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}