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}