Skip to main content

icu_normalizer/
properties.rs

1// This file is part of ICU4X. For terms of use, please see the file
2// called LICENSE at the top level of the ICU4X source tree
3// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ).
4
5//! Access to the Unicode properties or property-based operations that
6//! are required for NFC and NFD.
7//!
8//! Applications should generally use the full normalizers that are
9//! provided at the top level of this crate. However, the APIs in this
10//! module are provided for callers such as HarfBuzz that specifically
11//! want access to the raw canonical composition operation e.g. for use in a
12//! glyph-availability-guided custom normalizer.
13
14use crate::char_from_u16;
15use crate::char_from_u32;
16use crate::in_inclusive_range;
17use crate::provider::DecompositionData;
18use crate::provider::DecompositionTables;
19use crate::provider::NonRecursiveDecompositionSupplement;
20use crate::provider::NormalizerNfcV2;
21use crate::provider::NormalizerNfdDataV1;
22use crate::provider::NormalizerNfdSupplementV1;
23use crate::provider::NormalizerNfdTablesV1;
24use crate::trie_value_has_ccc;
25use crate::CanonicalCombiningClass;
26use crate::CanonicalCompositionsBorrowed;
27use crate::CanonicalCompositionsPayload;
28use crate::BACKWARD_COMBINING_MARKER;
29use crate::FDFA_MARKER;
30use crate::HANGUL_L_BASE;
31use crate::HANGUL_N_COUNT;
32use crate::HANGUL_S_BASE;
33use crate::HANGUL_S_COUNT;
34use crate::HANGUL_T_BASE;
35use crate::HANGUL_T_COUNT;
36use crate::HANGUL_V_BASE;
37use crate::HIGH_ZEROS_MASK;
38use crate::LOW_ZEROS_MASK;
39use crate::NON_ROUND_TRIP_MARKER;
40use icu_provider::prelude::*;
41
42/// Borrowed version of the raw canonical composition operation.
43///
44/// Callers should generally use `ComposingNormalizer` instead of this API.
45/// However, this API is provided for callers such as HarfBuzz that specifically
46/// want access to the raw canonical composition operation e.g. for use in a
47/// glyph-availability-guided custom normalizer.
48#[derive(Debug, Copy, Clone)]
49pub struct CanonicalCompositionBorrowed<'a> {
50    canonical_compositions: CanonicalCompositionsBorrowed<'a>,
51}
52
53#[cfg(feature = "compiled_data")]
54impl Default for CanonicalCompositionBorrowed<'static> {
55    fn default() -> Self {
56        Self::new()
57    }
58}
59
60impl CanonicalCompositionBorrowed<'static> {
61    /// Cheaply converts a [`CanonicalCompositionBorrowed<'static>`] into a [`CanonicalComposition`].
62    ///
63    /// Note: Due to branching and indirection, using [`CanonicalComposition`] might inhibit some
64    /// compile-time optimizations that are possible with [`CanonicalCompositionBorrowed`].
65    pub const fn static_to_owned(self) -> CanonicalComposition {
66        CanonicalComposition {
67            canonical_compositions: self.canonical_compositions.static_to_owned(),
68        }
69    }
70
71    /// Constructs a new `CanonicalComposition` using compiled data.
72    ///
73    /// ✨ *Enabled with the `compiled_data` Cargo feature.*
74    ///
75    /// [πŸ“š Help choosing a constructor](icu_provider::constructors)
76    #[cfg(feature = "compiled_data")]
77    pub const fn new() -> Self {
78        Self {
79            canonical_compositions: CanonicalCompositionsBorrowed::Current(
80                crate::provider::Baked::SINGLETON_NORMALIZER_NFC_V2,
81            ),
82        }
83    }
84}
85
86impl CanonicalCompositionBorrowed<'_> {
87    /// Performs canonical composition (including Hangul) on a pair of
88    /// characters or returns `None` if these characters don't compose.
89    /// Composition exclusions are taken into account.
90    ///
91    /// # Examples
92    ///
93    /// ```
94    /// let comp = icu::normalizer::properties::CanonicalCompositionBorrowed::new();
95    ///
96    /// assert_eq!(comp.compose('a', 'b'), None); // Just two non-composing starters
97    /// assert_eq!(comp.compose('a', '\u{0308}'), Some('Γ€'));
98    /// assert_eq!(comp.compose('αΊΉ', '\u{0302}'), Some('ệ'));
99    /// assert_eq!(comp.compose('𝅗', 'π…₯'), None); // Composition exclusion
100    /// assert_eq!(comp.compose('ে', 'ΰ¦Ύ'), Some('ΰ§‹')); // Second is starter
101    /// assert_eq!(comp.compose('α„€', 'α…‘'), Some('κ°€')); // Hangul LV
102    /// assert_eq!(comp.compose('κ°€', 'ᆨ'), Some('각')); // Hangul LVT
103    /// ```
104    #[inline(always)]
105    pub fn compose(self, starter: char, second: char) -> Option<char> {
106        self.canonical_compositions
107            .as_ref()
108            .compose(starter, second)
109    }
110}
111
112/// The raw canonical composition operation.
113///
114/// Callers should generally use `ComposingNormalizer` instead of this API.
115/// However, this API is provided for callers such as HarfBuzz that specifically
116/// want access to the raw canonical composition operation e.g. for use in a
117/// glyph-availability-guided custom normalizer.
118#[derive(Debug)]
119pub struct CanonicalComposition {
120    canonical_compositions: CanonicalCompositionsPayload,
121}
122
123#[cfg(feature = "compiled_data")]
124impl Default for CanonicalComposition {
125    fn default() -> Self {
126        Self::new().static_to_owned()
127    }
128}
129
130impl CanonicalComposition {
131    /// Constructs a borrowed version of this type for more efficient querying.
132    pub fn as_borrowed(&self) -> CanonicalCompositionBorrowed<'_> {
133        CanonicalCompositionBorrowed {
134            canonical_compositions: self.canonical_compositions.as_borrowed(),
135        }
136    }
137
138    /// Constructs a new `CanonicalCompositionBorrowed` using compiled data.
139    ///
140    /// ✨ *Enabled with the `compiled_data` Cargo feature.*
141    ///
142    /// [πŸ“š Help choosing a constructor](icu_provider::constructors)
143    #[cfg(feature = "compiled_data")]
144    #[expect(clippy::new_ret_no_self)]
145    pub const fn new() -> CanonicalCompositionBorrowed<'static> {
146        CanonicalCompositionBorrowed::new()
147    }
148
149    icu_provider::gen_buffer_data_constructors!(() -> error: DataError,
150        functions: [
151            new: skip,
152            try_new_with_buffer_provider,
153            try_new_unstable,
154            Self,
155        ]
156    );
157
158    #[doc = icu_provider::gen_buffer_unstable_docs!(UNSTABLE, Self::new)]
159    pub fn try_new_unstable<D>(provider: &D) -> Result<Self, DataError>
160    where
161        D: DataProvider<NormalizerNfcV2> + ?Sized,
162    {
163        let canonical_compositions: DataPayload<NormalizerNfcV2> =
164            provider.load(Default::default())?.payload;
165        Ok(CanonicalComposition {
166            canonical_compositions: CanonicalCompositionsPayload::Current(canonical_compositions),
167        })
168    }
169}
170
171/// The outcome of non-recursive canonical decomposition of a character.
172#[allow(clippy::exhaustive_enums)]
173#[derive(Debug, PartialEq, Eq)]
174pub enum Decomposed {
175    /// The character is its own canonical decomposition.
176    Default,
177    /// The character decomposes to a single different character.
178    Singleton(char),
179    /// The character decomposes to two characters.
180    Expansion(char, char),
181}
182
183/// Borrowed version of the raw (non-recursive) canonical decomposition operation.
184///
185/// Callers should generally use `DecomposingNormalizer` instead of this API.
186/// However, this API is provided for callers such as HarfBuzz that specifically
187/// want access to non-recursive canonical decomposition e.g. for use in a
188/// glyph-availability-guided custom normalizer.
189#[derive(Debug)]
190pub struct CanonicalDecompositionBorrowed<'a> {
191    decompositions: &'a DecompositionData<'a>,
192    tables: &'a DecompositionTables<'a>,
193    non_recursive: &'a NonRecursiveDecompositionSupplement<'a>,
194}
195
196#[cfg(feature = "compiled_data")]
197impl Default for CanonicalDecompositionBorrowed<'static> {
198    fn default() -> Self {
199        Self::new()
200    }
201}
202
203impl CanonicalDecompositionBorrowed<'static> {
204    /// Cheaply converts a [`CanonicalDecompositionBorrowed<'static>`] into a [`CanonicalDecomposition`].
205    ///
206    /// Note: Due to branching and indirection, using [`CanonicalDecomposition`] might inhibit some
207    /// compile-time optimizations that are possible with [`CanonicalDecompositionBorrowed`].
208    pub const fn static_to_owned(self) -> CanonicalDecomposition {
209        CanonicalDecomposition {
210            decompositions: DataPayload::from_static_ref(self.decompositions),
211            tables: DataPayload::from_static_ref(self.tables),
212            non_recursive: DataPayload::from_static_ref(self.non_recursive),
213        }
214    }
215
216    /// Construct from compiled data.
217    ///
218    /// ✨ *Enabled with the `compiled_data` Cargo feature.*
219    ///
220    /// [πŸ“š Help choosing a constructor](icu_provider::constructors)
221    #[cfg(feature = "compiled_data")]
222    pub const fn new() -> Self {
223        const _: () = assert!(
224            crate::provider::Baked::SINGLETON_NORMALIZER_NFD_TABLES_V1
225                .scalars16
226                .const_len()
227                + crate::provider::Baked::SINGLETON_NORMALIZER_NFD_TABLES_V1
228                    .scalars24
229                    .const_len()
230                <= 0xFFF,
231            "future extension"
232        );
233
234        Self {
235            decompositions: crate::provider::Baked::SINGLETON_NORMALIZER_NFD_DATA_V1,
236            tables: crate::provider::Baked::SINGLETON_NORMALIZER_NFD_TABLES_V1,
237            non_recursive: crate::provider::Baked::SINGLETON_NORMALIZER_NFD_SUPPLEMENT_V1,
238        }
239    }
240}
241
242impl CanonicalDecompositionBorrowed<'_> {
243    /// Performs non-recursive canonical decomposition (including for Hangul).
244    ///
245    /// ```
246    ///     use icu::normalizer::properties::Decomposed;
247    ///     let decomp = icu::normalizer::properties::CanonicalDecompositionBorrowed::new();
248    ///
249    ///     assert_eq!(decomp.decompose('e'), Decomposed::Default);
250    ///     assert_eq!(
251    ///         decomp.decompose('ệ'),
252    ///         Decomposed::Expansion('αΊΉ', '\u{0302}')
253    ///     );
254    ///     assert_eq!(decomp.decompose('각'), Decomposed::Expansion('κ°€', 'ᆨ'));
255    ///     assert_eq!(decomp.decompose('\u{212B}'), Decomposed::Singleton('Γ…')); // ANGSTROM SIGN
256    ///     assert_eq!(decomp.decompose('\u{2126}'), Decomposed::Singleton('Ξ©')); // OHM SIGN
257    ///     assert_eq!(decomp.decompose('\u{1F71}'), Decomposed::Singleton('Ξ¬')); // oxia
258    /// ```
259    #[inline]
260    pub fn decompose(&self, c: char) -> Decomposed {
261        let lvt = u32::from(c).wrapping_sub(HANGUL_S_BASE);
262        if lvt >= HANGUL_S_COUNT {
263            return self.decompose_non_hangul(c);
264        }
265        // Invariant: lvt ≀ HANGUL_S_COUNT = 1172
266        let t = lvt % HANGUL_T_COUNT;
267        // Invariant: t ≀ (1172 / HANGUL_T_COUNT = 1172 / 28 = 41)
268        if t == 0 {
269            let l = lvt / HANGUL_N_COUNT;
270            // Invariant: v ≀ (1172 / HANGUL_N_COUNT = 1172 / 588 β‰ˆ 2)
271            let v = (lvt % HANGUL_N_COUNT) / HANGUL_T_COUNT;
272            // Invariant: v < (HANGUL_N_COUNT / HANGUL_T_COUNT = 588 / 28 = 21)
273            return Decomposed::Expansion(
274                // Safety: HANGUL_*_BASE are 0x1nnn, addding numbers that are 21 and 41
275                // max will keep it in range, less than 0xD800
276                unsafe { char::from_u32_unchecked(HANGUL_L_BASE + l) },
277                unsafe { char::from_u32_unchecked(HANGUL_V_BASE + v) },
278            );
279        }
280        let lv = lvt - t;
281        // Invariant: lvt < 1172
282        // Safe because values known to be in range
283        Decomposed::Expansion(
284            // Safety: HANGUL_*_BASE are 0x1nnn, addding numbers that are 1172 and 41
285            // max will keep it in range, less than 0xD800
286            unsafe { char::from_u32_unchecked(HANGUL_S_BASE + lv) },
287            unsafe { char::from_u32_unchecked(HANGUL_T_BASE + t) },
288        )
289    }
290
291    /// Performs non-recursive canonical decomposition except Hangul syllables
292    /// are reported as `Decomposed::Default`.
293    #[inline(always)]
294    fn decompose_non_hangul(&self, c: char) -> Decomposed {
295        let decomposition = self.decompositions.trie.get(c);
296        // The REPLACEMENT CHARACTER has `NON_ROUND_TRIP_MARKER` set,
297        // and that flag needs to be ignored here.
298        if (decomposition & !(BACKWARD_COMBINING_MARKER | NON_ROUND_TRIP_MARKER)) == 0 {
299            return Decomposed::Default;
300        }
301        // The loop is only broken out of as goto forward
302        #[expect(clippy::never_loop)]
303        loop {
304            let high_zeros = (decomposition & HIGH_ZEROS_MASK) == 0;
305            let low_zeros = (decomposition & LOW_ZEROS_MASK) == 0;
306            if !high_zeros && !low_zeros {
307                // Decomposition into two BMP characters: starter and non-starter
308                if in_inclusive_range(c, '\u{1F71}', '\u{1FFB}') {
309                    // Look in the other trie due to oxia singleton
310                    // mappings to corresponding character with tonos.
311                    break;
312                }
313                let starter = char_from_u32(decomposition & 0x7FFF);
314                let combining = char_from_u32((decomposition >> 15) & 0x7FFF);
315                return Decomposed::Expansion(starter, combining);
316            }
317            if high_zeros {
318                // Decomposition into one BMP character or non-starter
319                if trie_value_has_ccc(decomposition) {
320                    // Non-starter
321                    if !in_inclusive_range(c, '\u{0340}', '\u{0F81}') {
322                        return Decomposed::Default;
323                    }
324                    return match c {
325                        '\u{0340}' => {
326                            // COMBINING GRAVE TONE MARK
327                            Decomposed::Singleton('\u{0300}')
328                        }
329                        '\u{0341}' => {
330                            // COMBINING ACUTE TONE MARK
331                            Decomposed::Singleton('\u{0301}')
332                        }
333                        '\u{0343}' => {
334                            // COMBINING GREEK KORONIS
335                            Decomposed::Singleton('\u{0313}')
336                        }
337                        '\u{0344}' => {
338                            // COMBINING GREEK DIALYTIKA TONOS
339                            Decomposed::Expansion('\u{0308}', '\u{0301}')
340                        }
341                        '\u{0F73}' => {
342                            // TIBETAN VOWEL SIGN II
343                            Decomposed::Expansion('\u{0F71}', '\u{0F72}')
344                        }
345                        '\u{0F75}' => {
346                            // TIBETAN VOWEL SIGN UU
347                            Decomposed::Expansion('\u{0F71}', '\u{0F74}')
348                        }
349                        '\u{0F81}' => {
350                            // TIBETAN VOWEL SIGN REVERSED II
351                            Decomposed::Expansion('\u{0F71}', '\u{0F80}')
352                        }
353                        _ => Decomposed::Default,
354                    };
355                }
356                let singleton = decomposition as u16;
357                debug_assert_ne!(
358                    singleton, FDFA_MARKER,
359                    "How come we got the U+FDFA NFKD marker here?"
360                );
361                return Decomposed::Singleton(char_from_u16(singleton));
362            }
363            if c == '\u{212B}' {
364                // ANGSTROM SIGN
365                return Decomposed::Singleton('\u{00C5}');
366            }
367            // Only 12 of 14 bits used as of Unicode 16.
368            let offset = (((decomposition & !(0b11 << 30)) >> 16) as usize) - 1;
369            // Only 3 of 4 bits used as of Unicode 16.
370            let len_bits = decomposition & 0b1111;
371            let tables = self.tables;
372            if offset < tables.scalars16.len() {
373                if len_bits != 0 {
374                    // i.e. logical len isn't 2
375                    break;
376                }
377                if let Some(first) = tables.scalars16.get(offset) {
378                    if let Some(second) = tables.scalars16.get(offset + 1) {
379                        // Two BMP starters
380                        return Decomposed::Expansion(char_from_u16(first), char_from_u16(second));
381                    }
382                }
383                // GIGO case
384                debug_assert!(false);
385                return Decomposed::Default;
386            }
387            let len = len_bits + 1;
388            if len > 2 {
389                break;
390            }
391            let offset24 = offset - tables.scalars16.len();
392            if let Some(first_c) = tables.scalars24.get(offset24) {
393                if len == 1 {
394                    return Decomposed::Singleton(first_c);
395                }
396                if let Some(second_c) = tables.scalars24.get(offset24 + 1) {
397                    return Decomposed::Expansion(first_c, second_c);
398                }
399            }
400            // GIGO case
401            debug_assert!(false);
402            return Decomposed::Default;
403        }
404        let non_recursive = self.non_recursive;
405        let non_recursive_decomposition = non_recursive.trie.get(c);
406        if non_recursive_decomposition == 0 {
407            // GIGO case
408            debug_assert!(false);
409            return Decomposed::Default;
410        }
411        let trail_or_complex = (non_recursive_decomposition >> 16) as u16;
412        let lead = non_recursive_decomposition as u16;
413        if lead != 0 && trail_or_complex != 0 {
414            // Decomposition into two BMP characters
415            return Decomposed::Expansion(char_from_u16(lead), char_from_u16(trail_or_complex));
416        }
417        if lead != 0 {
418            // Decomposition into one BMP character
419            return Decomposed::Singleton(char_from_u16(lead));
420        }
421        // Decomposition into two non-BMP characters
422        // Low is offset into a table plus one to keep it non-zero.
423        let offset = usize::from(trail_or_complex - 1);
424        if let Some(first) = non_recursive.scalars24.get(offset) {
425            if let Some(second) = non_recursive.scalars24.get(offset + 1) {
426                return Decomposed::Expansion(first, second);
427            }
428        }
429        // GIGO case
430        debug_assert!(false);
431        Decomposed::Default
432    }
433}
434
435/// The raw (non-recursive) canonical decomposition operation.
436///
437/// Callers should generally use `DecomposingNormalizer` instead of this API.
438/// However, this API is provided for callers such as HarfBuzz that specifically
439/// want access to non-recursive canonical decomposition e.g. for use in a
440/// glyph-availability-guided custom normalizer.
441#[derive(Debug)]
442pub struct CanonicalDecomposition {
443    decompositions: DataPayload<NormalizerNfdDataV1>,
444    tables: DataPayload<NormalizerNfdTablesV1>,
445    non_recursive: DataPayload<NormalizerNfdSupplementV1>,
446}
447
448#[cfg(feature = "compiled_data")]
449impl Default for CanonicalDecomposition {
450    fn default() -> Self {
451        Self::new().static_to_owned()
452    }
453}
454
455impl CanonicalDecomposition {
456    /// Constructs a borrowed version of this type for more efficient querying.
457    pub fn as_borrowed(&self) -> CanonicalDecompositionBorrowed<'_> {
458        CanonicalDecompositionBorrowed {
459            decompositions: self.decompositions.get(),
460            tables: self.tables.get(),
461            non_recursive: self.non_recursive.get(),
462        }
463    }
464
465    /// Construct from compiled data.
466    ///
467    /// ✨ *Enabled with the `compiled_data` Cargo feature.*
468    ///
469    /// [πŸ“š Help choosing a constructor](icu_provider::constructors)
470    #[cfg(feature = "compiled_data")]
471    #[expect(clippy::new_ret_no_self)]
472    pub const fn new() -> CanonicalDecompositionBorrowed<'static> {
473        CanonicalDecompositionBorrowed::new()
474    }
475
476    icu_provider::gen_buffer_data_constructors!(() -> error: DataError,
477        functions: [
478            new: skip,
479            try_new_with_buffer_provider,
480            try_new_unstable,
481            Self,
482        ]
483    );
484
485    #[doc = icu_provider::gen_buffer_unstable_docs!(UNSTABLE, Self::new)]
486    pub fn try_new_unstable<D>(provider: &D) -> Result<Self, DataError>
487    where
488        D: DataProvider<NormalizerNfdDataV1>
489            + DataProvider<NormalizerNfdTablesV1>
490            + DataProvider<NormalizerNfdSupplementV1>
491            + ?Sized,
492    {
493        let decompositions: DataPayload<NormalizerNfdDataV1> =
494            provider.load(Default::default())?.payload;
495        let tables: DataPayload<NormalizerNfdTablesV1> = provider.load(Default::default())?.payload;
496
497        if tables.get().scalars16.len() + tables.get().scalars24.len() > 0xFFF {
498            // The data is from a future where there exists a normalization flavor whose
499            // complex decompositions take more than 0xFFF but fewer than 0x1FFF code points
500            // of space. If a good use case from such a decomposition flavor arises, we can
501            // dynamically change the bit masks so that the length mask becomes 0x1FFF instead
502            // of 0xFFF and the all-non-starters mask becomes 0 instead of 0x1000. However,
503            // since for now the masks are hard-coded, error out.
504            return Err(DataError::custom("future extension"));
505        }
506
507        let non_recursive: DataPayload<NormalizerNfdSupplementV1> =
508            provider.load(Default::default())?.payload;
509
510        Ok(CanonicalDecomposition {
511            decompositions,
512            tables,
513            non_recursive,
514        })
515    }
516}
517
518/// Borrowed version of lookup of the `Canonical_Combining_Class` Unicode property.
519///
520/// # Example
521///
522/// ```
523/// use icu::properties::props::CanonicalCombiningClass;
524/// use icu::normalizer::properties::CanonicalCombiningClassMapBorrowed;
525///
526/// let map = CanonicalCombiningClassMapBorrowed::new();
527/// assert_eq!(map.get('a'), CanonicalCombiningClass::NotReordered); // U+0061: LATIN SMALL LETTER A
528/// assert_eq!(map.get32(0x0301), CanonicalCombiningClass::Above); // U+0301: COMBINING ACUTE ACCENT
529/// ```
530#[derive(Debug)]
531pub struct CanonicalCombiningClassMapBorrowed<'a> {
532    /// The data trie
533    decompositions: &'a DecompositionData<'a>,
534}
535
536#[cfg(feature = "compiled_data")]
537impl Default for CanonicalCombiningClassMapBorrowed<'static> {
538    fn default() -> Self {
539        Self::new()
540    }
541}
542
543impl CanonicalCombiningClassMapBorrowed<'static> {
544    /// Cheaply converts a [`CanonicalCombiningClassMapBorrowed<'static>`] into a [`CanonicalCombiningClassMap`].
545    ///
546    /// Note: Due to branching and indirection, using [`CanonicalCombiningClassMap`] might inhibit some
547    /// compile-time optimizations that are possible with [`CanonicalCombiningClassMapBorrowed`].
548    pub const fn static_to_owned(self) -> CanonicalCombiningClassMap {
549        CanonicalCombiningClassMap {
550            decompositions: DataPayload::from_static_ref(self.decompositions),
551        }
552    }
553
554    /// Construct from compiled data.
555    ///
556    /// ✨ *Enabled with the `compiled_data` Cargo feature.*
557    ///
558    /// [πŸ“š Help choosing a constructor](icu_provider::constructors)
559    #[cfg(feature = "compiled_data")]
560    pub const fn new() -> Self {
561        CanonicalCombiningClassMapBorrowed {
562            decompositions: crate::provider::Baked::SINGLETON_NORMALIZER_NFD_DATA_V1,
563        }
564    }
565}
566
567impl CanonicalCombiningClassMapBorrowed<'_> {
568    /// Look up the canonical combining class for a scalar value.
569    ///
570    /// The return value is a u8 representing the canonical combining class,
571    /// you may enable the `"icu_properties"` feature if you would like to use a typed
572    /// `CanonicalCombiningClass`.
573    #[inline(always)]
574    pub fn get_u8(&self, c: char) -> u8 {
575        self.get32_u8(u32::from(c))
576    }
577
578    /// Look up the canonical combining class for a scalar value
579    /// represented as `u32`. If the argument is outside the scalar
580    /// value range, `Not_Reordered` is returned.
581    ///
582    /// The return value is a u8 representing the canonical combining class,
583    /// you may enable the `"icu_properties"` feature if you would like to use a typed
584    /// `CanonicalCombiningClass`.
585    pub fn get32_u8(&self, c: u32) -> u8 {
586        let trie_value = self.decompositions.trie.get32(c);
587        if trie_value_has_ccc(trie_value) {
588            trie_value as u8
589        } else {
590            ccc!(NotReordered, 0).to_icu4c_value()
591        }
592    }
593
594    /// Look up the canonical combining class for a scalar value
595    ///
596    /// ✨ *Enabled with the `icu_properties` Cargo feature.*
597    #[inline(always)]
598    #[cfg(feature = "icu_properties")]
599    pub fn get(&self, c: char) -> CanonicalCombiningClass {
600        CanonicalCombiningClass::from_icu4c_value(self.get_u8(c))
601    }
602
603    /// Look up the canonical combining class for a scalar value
604    /// represented as `u32`. If the argument is outside the scalar
605    /// value range, `CanonicalCombiningClass::NotReordered` is returned.
606    ///
607    /// ✨ *Enabled with the `icu_properties` Cargo feature.*
608    #[cfg(feature = "icu_properties")]
609    pub fn get32(&self, c: u32) -> CanonicalCombiningClass {
610        CanonicalCombiningClass::from_icu4c_value(self.get32_u8(c))
611    }
612}
613
614/// Lookup of the `Canonical_Combining_Class` Unicode property.
615#[derive(Debug)]
616pub struct CanonicalCombiningClassMap {
617    /// The data trie
618    decompositions: DataPayload<NormalizerNfdDataV1>,
619}
620
621#[cfg(feature = "compiled_data")]
622impl Default for CanonicalCombiningClassMap {
623    fn default() -> Self {
624        Self::new().static_to_owned()
625    }
626}
627
628impl CanonicalCombiningClassMap {
629    /// Constructs a borrowed version of this type for more efficient querying.
630    pub fn as_borrowed(&self) -> CanonicalCombiningClassMapBorrowed<'_> {
631        CanonicalCombiningClassMapBorrowed {
632            decompositions: self.decompositions.get(),
633        }
634    }
635
636    /// Construct from compiled data.
637    ///
638    /// ✨ *Enabled with the `compiled_data` Cargo feature.*
639    ///
640    /// [πŸ“š Help choosing a constructor](icu_provider::constructors)
641    #[cfg(feature = "compiled_data")]
642    #[expect(clippy::new_ret_no_self)]
643    pub const fn new() -> CanonicalCombiningClassMapBorrowed<'static> {
644        CanonicalCombiningClassMapBorrowed::new()
645    }
646
647    icu_provider::gen_buffer_data_constructors!(() -> error: DataError,
648        functions: [
649            new: skip,
650            try_new_with_buffer_provider,
651            try_new_unstable,
652            Self,
653    ]);
654
655    #[doc = icu_provider::gen_buffer_unstable_docs!(UNSTABLE, Self::new)]
656    pub fn try_new_unstable<D>(provider: &D) -> Result<Self, DataError>
657    where
658        D: DataProvider<NormalizerNfdDataV1> + ?Sized,
659    {
660        let decompositions: DataPayload<NormalizerNfdDataV1> =
661            provider.load(Default::default())?.payload;
662        Ok(CanonicalCombiningClassMap { decompositions })
663    }
664}