Skip to main content

unicode_normalize_aligned/
decompose.rs

1//! Canonical and compatibility decomposition with canonical ordering.
2
3use core::iter::FusedIterator;
4
5use alloc::vec::Vec;
6
7use crate::buffer::{RunBuffer, INLINE};
8use crate::props::{self, hangul, Props};
9
10#[derive(Clone, Copy, Debug)]
11pub(crate) enum Kind {
12    Canonical,
13    Compatible,
14}
15
16/// Streaming NFD or NFKD. Yields `(char, isize)`: the output character and its
17/// alignment change count. See the crate docs for the tag contract.
18///
19/// Created by [`UnicodeNormalization::nfd`](crate::UnicodeNormalization::nfd)
20/// and [`UnicodeNormalization::nfkd`](crate::UnicodeNormalization::nfkd).
21#[derive(Clone, Debug)]
22pub struct Decompositions<I> {
23    kind: Kind,
24    iter: I,
25    /// `(ccc, char, tag)`. Items before `pos` were emitted. Items in
26    /// `pos..ready` are sorted and waiting. Items from `ready` on are the
27    /// pending run, which gets sorted when a starter closes it.
28    buffer: RunBuffer<(u8, char, isize)>,
29    ready: usize,
30    pos: usize,
31    done: bool,
32}
33
34impl<I: Iterator<Item = char>> Decompositions<I> {
35    pub(crate) fn new(iter: I, kind: Kind) -> Self {
36        Decompositions {
37            kind,
38            iter,
39            buffer: RunBuffer::new(),
40            ready: 0,
41            pos: 0,
42            done: false,
43        }
44    }
45
46    /// Stably sorts the pending run by combining class. A run that fits
47    /// the inline buffer uses an insertion sort, stable and allocation
48    /// free. A longer run uses a counting sort over the 256 possible
49    /// classes, stable and O(n).
50    fn sort_pending(&mut self) {
51        let start = self.ready;
52        let run = match self.buffer.as_mut_slice().get_mut(start..) {
53            Some(run) => run,
54            None => return,
55        };
56        if run.len() <= INLINE {
57            for i in 1..run.len() {
58                let mut j = i;
59                while j > 0 && run[j - 1].0 > run[j].0 {
60                    run.swap(j - 1, j);
61                    j -= 1;
62                }
63            }
64        } else {
65            counting_sort(run);
66        }
67        self.ready = self.buffer.len();
68    }
69
70    /// Appends through [`RunBuffer::try_push`], so the buffer only takes
71    /// new memory after the slow path reclaims the emitted prefix and live
72    /// items still need the space. The buffer therefore stays bounded on
73    /// streaming input and never spills on stream-safe text. Tags move
74    /// with their items, so alignments are unchanged.
75    #[inline]
76    fn push_item(&mut self, item: (u8, char, isize)) {
77        if !self.buffer.try_push(item) {
78            self.push_item_slow(item);
79        }
80    }
81
82    /// Only called while `pos == ready`, which every push path satisfies:
83    /// pushes run under `next_full` pulling input, and pulls happen only
84    /// once the sorted items are drained.
85    #[cold]
86    fn push_item_slow(&mut self, item: (u8, char, isize)) {
87        if self.pos > 0 {
88            self.buffer.compact_front(self.pos);
89            self.ready -= self.pos;
90            self.pos = 0;
91        }
92        if !self.buffer.try_push(item) {
93            self.buffer.push(item);
94        }
95    }
96
97    #[inline]
98    fn push(&mut self, ccc: u8, c: char, tag: isize) {
99        if ccc == 0 {
100            if self.ready != self.buffer.len() {
101                self.sort_pending();
102            }
103            if self.pos == self.buffer.len() {
104                self.buffer.clear();
105                self.pos = 0;
106            }
107            self.push_item((0, c, tag));
108            self.ready = self.buffer.len();
109        } else {
110            self.push_item((ccc, c, tag));
111        }
112    }
113
114    /// `next` plus the combining class the buffer already holds, so the
115    /// recomposition stage never repeats the property lookup.
116    #[inline]
117    pub(crate) fn next_full(&mut self) -> Option<(u8, char, isize)> {
118        loop {
119            if self.pos < self.ready {
120                let &item = self.buffer.get(self.pos)?;
121                self.pos += 1;
122                return Some(item);
123            }
124            if self.done {
125                return None;
126            }
127            match self.iter.next() {
128                Some(c) => self.push_char(c),
129                None => {
130                    self.done = true;
131                    self.sort_pending();
132                }
133            }
134        }
135    }
136
137    fn push_sequence(&mut self, seq: &[char]) {
138        for (i, &d) in seq.iter().enumerate() {
139            let tag = if i == 0 { 0 } else { 1 };
140            self.push(Props::of(d).ccc(), d, tag);
141        }
142    }
143
144    fn push_char(&mut self, c: char) {
145        if (c as u32) < 0x80 {
146            self.push(0, c, 0);
147            return;
148        }
149        let p = Props::of(c);
150        let mapping = match self.kind {
151            Kind::Canonical if p.nfd_no() => Some(Kind::Canonical),
152            Kind::Compatible if p.nfkd_no() => Some(self.kind),
153            _ => None,
154        };
155        let kind = match mapping {
156            Some(kind) => kind,
157            None => {
158                self.push(p.ccc(), c, 0);
159                return;
160            }
161        };
162        if let Some((l, v, t)) = hangul::decompose(c) {
163            self.push(0, l, 0);
164            self.push(0, v, 1);
165            if let Some(t) = t {
166                self.push(0, t, 1);
167            }
168            return;
169        }
170        let seq = match kind {
171            Kind::Compatible if p.has_compat() => props::compat_distinct(c),
172            _ => props::canonical(c),
173        };
174        match seq {
175            Some(seq) => self.push_sequence(seq),
176            None => self.push(p.ccc(), c, 0),
177        }
178    }
179}
180
181/// Stably sorts `run` by combining class with a counting pass. Out of line so
182/// the short-run path stays small. Allocates one scratch per call. Only
183/// runs longer than the inline buffer land here, and those have already
184/// spilled, so ordering them was never allocation free.
185#[cold]
186fn counting_sort(run: &mut [(u8, char, isize)]) {
187    let mut next = [0usize; 256];
188    for item in run.iter() {
189        next[item.0 as usize] += 1;
190    }
191    let mut total = 0;
192    for slot in next.iter_mut() {
193        let count = *slot;
194        *slot = total;
195        total += count;
196    }
197    let mut scratch = Vec::new();
198    scratch.resize(run.len(), (0, '\0', 0));
199    for &item in run.iter() {
200        let slot = &mut next[item.0 as usize];
201        scratch[*slot] = item;
202        *slot += 1;
203    }
204    run.copy_from_slice(&scratch);
205}
206
207impl<I: Iterator<Item = char>> Iterator for Decompositions<I> {
208    type Item = (char, isize);
209
210    #[inline]
211    fn next(&mut self) -> Option<(char, isize)> {
212        self.next_full().map(|(_, c, tag)| (c, tag))
213    }
214
215    fn size_hint(&self) -> (usize, Option<usize>) {
216        let (lower, _) = self.iter.size_hint();
217        // `saturating_add` because a legal inner iterator can report a
218        // `usize::MAX` lower bound.
219        (lower.saturating_add(self.ready - self.pos), None)
220    }
221}
222
223impl<I: Iterator<Item = char>> FusedIterator for Decompositions<I> {}