Skip to main content

sim_text/
exact.rs

1use core::fmt;
2use core::ops::Range;
3
4/// The largest code-unit allocation representable by Rust's collection APIs.
5///
6/// This bound also makes multiplication by the size of a code unit safe.
7pub const MAX_CODE_UNITS: usize = isize::MAX as usize / size_of::<u16>();
8
9/// An offset in the exact UTF-16 code-unit sequence.
10///
11/// This is intentionally not interchangeable with [`ScalarOffset`].
12///
13/// ```compile_fail
14/// use sim_text::{CodeUnitOffset, CodeUnitString, ScalarOffset};
15/// let text = CodeUnitString::from_scalar("abc");
16/// let scalar = ScalarOffset::new(1);
17/// let _ = text.code_unit_at(scalar);
18/// ```
19#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
20pub struct CodeUnitOffset(usize);
21
22impl CodeUnitOffset {
23    /// Construct an offset. Bounds are checked when it is used with a value.
24    pub const fn new(value: usize) -> Self {
25        Self(value)
26    }
27
28    /// Return the numeric code-unit offset.
29    pub const fn get(self) -> usize {
30        self.0
31    }
32}
33
34/// An offset in the Unicode scalar sequence.
35#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
36pub struct ScalarOffset(usize);
37
38impl ScalarOffset {
39    /// Construct an offset. Bounds are checked during conversion.
40    pub const fn new(value: usize) -> Self {
41        Self(value)
42    }
43
44    /// Return the numeric scalar offset.
45    pub const fn get(self) -> usize {
46        self.0
47    }
48}
49
50/// A half-open range in the exact UTF-16 code-unit sequence.
51#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
52pub struct CodeUnitRange {
53    /// Inclusive start offset.
54    pub start: CodeUnitOffset,
55    /// Exclusive end offset.
56    pub end: CodeUnitOffset,
57}
58
59impl CodeUnitRange {
60    /// Construct a half-open code-unit range.
61    pub const fn new(start: CodeUnitOffset, end: CodeUnitOffset) -> Self {
62        Self { start, end }
63    }
64}
65
66/// A half-open range in the Unicode scalar sequence.
67#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
68pub struct ScalarRange {
69    /// Inclusive start offset.
70    pub start: ScalarOffset,
71    /// Exclusive end offset.
72    pub end: ScalarOffset,
73}
74
75impl ScalarRange {
76    /// Construct a half-open scalar range.
77    pub const fn new(start: ScalarOffset, end: ScalarOffset) -> Self {
78        Self { start, end }
79    }
80}
81
82/// Located evidence for the first invalid UTF-16 code unit.
83#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
84pub struct InvalidSurrogate {
85    /// Code-unit offset of the invalid surrogate.
86    pub offset: CodeUnitOffset,
87    /// Invalid raw code unit.
88    pub unit: u16,
89}
90
91/// Failure to construct or project an exact code-unit string.
92#[derive(Clone, Debug, Eq, PartialEq)]
93pub enum CodeUnitStringError {
94    /// The requested code-unit count cannot be represented by an allocation.
95    TooLong {
96        /// Requested number of code units.
97        len: usize,
98        /// Maximum supported number of code units.
99        max: usize,
100    },
101    /// The unit sequence contains an unpaired surrogate.
102    LoneSurrogate(InvalidSurrogate),
103}
104
105impl fmt::Display for CodeUnitStringError {
106    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
107        match self {
108            Self::TooLong { len, max } => {
109                write!(
110                    f,
111                    "code-unit length {len} exceeds the supported maximum {max}"
112                )
113            }
114            Self::LoneSurrogate(invalid) => {
115                write!(
116                    f,
117                    "lone surrogate {:#06x} at code-unit index {}",
118                    invalid.unit,
119                    invalid.offset.get()
120                )
121            }
122        }
123    }
124}
125
126impl std::error::Error for CodeUnitStringError {}
127
128/// An exact sequence of UTF-16 code units.
129///
130/// Unlike [`String`], this value admits lone surrogates. Conversion to scalar
131/// Unicode is therefore explicit and checked.
132#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
133pub struct CodeUnitString {
134    units: Vec<u16>,
135}
136
137impl CodeUnitString {
138    /// Encode scalar Unicode text as UTF-16 code units.
139    pub fn from_scalar(text: &str) -> Self {
140        Self {
141            units: text.encode_utf16().collect(),
142        }
143    }
144
145    /// Preserve an exact sequence, rejecting an unrepresentable allocation.
146    pub fn try_from_code_units(units: Vec<u16>) -> Result<Self, CodeUnitStringError> {
147        if units.len() > MAX_CODE_UNITS {
148            return Err(CodeUnitStringError::TooLong {
149                len: units.len(),
150                max: MAX_CODE_UNITS,
151            });
152        }
153        Ok(Self { units })
154    }
155
156    /// Preserve an exact sequence including lone surrogates.
157    ///
158    /// A Rust `Vec<u16>` that already exists necessarily satisfies the
159    /// allocation limit, so this compatibility constructor is infallible.
160    pub fn from_code_units(units: Vec<u16>) -> Self {
161        Self::try_from_code_units(units).expect("an existing Vec satisfies allocation limits")
162    }
163
164    /// Borrow all exact code units.
165    pub fn as_code_units(&self) -> &[u16] {
166        &self.units
167    }
168
169    /// Return the length in code units.
170    pub fn len(&self) -> usize {
171        self.units.len()
172    }
173
174    /// Whether the value has no code units.
175    pub fn is_empty(&self) -> bool {
176        self.units.is_empty()
177    }
178
179    /// Index one code unit.
180    pub fn code_unit_at(&self, offset: CodeUnitOffset) -> Option<u16> {
181        self.units.get(offset.get()).copied()
182    }
183
184    /// Slice by a code-unit range, clamping both bounds.
185    pub fn slice(&self, range: CodeUnitRange) -> Self {
186        let start = range.start.get().min(self.len());
187        let end = range.end.get().max(start).min(self.len());
188        Self::from_code_units(self.units[Range { start, end }].to_vec())
189    }
190
191    /// Iterate exact code units (the indexing face).
192    pub fn code_units(&self) -> impl ExactSizeIterator<Item = u16> + '_ {
193        self.units.iter().copied()
194    }
195
196    /// Iterate chunks consisting of one surrogate pair or one unpaired unit.
197    pub fn iter_code_points(&self) -> CodePointIter<'_> {
198        CodePointIter {
199            units: &self.units,
200            at: 0,
201        }
202    }
203
204    /// Convert well-formed UTF-16 to scalar Unicode text.
205    pub fn to_scalar(&self) -> Result<String, CodeUnitStringError> {
206        String::from_utf16(&self.units).map_err(|_| {
207            let invalid = first_lone(&self.units).expect("invalid UTF-16 has a lone surrogate");
208            CodeUnitStringError::LoneSurrogate(invalid)
209        })
210    }
211
212    /// Convert a bounded scalar offset to its code-unit offset.
213    pub fn code_unit_offset(
214        &self,
215        scalar: ScalarOffset,
216    ) -> Result<CodeUnitOffset, OffsetConversionError> {
217        let text = self
218            .to_scalar()
219            .map_err(OffsetConversionError::InvalidText)?;
220        let mut scalar_at = 0;
221        for (byte_at, _) in text.char_indices() {
222            if scalar_at == scalar.get() {
223                return Ok(CodeUnitOffset::new(text[..byte_at].encode_utf16().count()));
224            }
225            scalar_at += 1;
226        }
227        if scalar_at == scalar.get() {
228            return Ok(CodeUnitOffset::new(self.len()));
229        }
230        Err(OffsetConversionError::ScalarOutOfBounds {
231            offset: scalar,
232            len: scalar_at,
233        })
234    }
235
236    /// Convert a bounded code-unit offset at a scalar boundary.
237    pub fn scalar_offset(
238        &self,
239        code_unit: CodeUnitOffset,
240    ) -> Result<ScalarOffset, OffsetConversionError> {
241        if code_unit.get() > self.len() {
242            return Err(OffsetConversionError::CodeUnitOutOfBounds {
243                offset: code_unit,
244                len: self.len(),
245            });
246        }
247        let text = self
248            .to_scalar()
249            .map_err(OffsetConversionError::InvalidText)?;
250        let mut units = 0;
251        let mut scalars = 0;
252        for scalar in text.chars() {
253            if units == code_unit.get() {
254                return Ok(ScalarOffset::new(scalars));
255            }
256            units += scalar.len_utf16();
257            scalars += 1;
258            if units > code_unit.get() {
259                return Err(OffsetConversionError::NotScalarBoundary(code_unit));
260            }
261        }
262        Ok(ScalarOffset::new(scalars))
263    }
264}
265
266/// Failure to convert between bounded offset domains.
267#[derive(Clone, Debug, Eq, PartialEq)]
268pub enum OffsetConversionError {
269    /// The exact unit sequence is not scalar Unicode text.
270    InvalidText(CodeUnitStringError),
271    /// The requested code-unit offset exceeds the sequence.
272    CodeUnitOutOfBounds { offset: CodeUnitOffset, len: usize },
273    /// The requested scalar offset exceeds the sequence.
274    ScalarOutOfBounds { offset: ScalarOffset, len: usize },
275    /// The code-unit offset splits a surrogate pair.
276    NotScalarBoundary(CodeUnitOffset),
277}
278
279impl AsRef<[u16]> for CodeUnitString {
280    fn as_ref(&self) -> &[u16] {
281        self.as_code_units()
282    }
283}
284
285/// Iterator over code-point chunks represented as exact code-unit strings.
286pub struct CodePointIter<'a> {
287    units: &'a [u16],
288    at: usize,
289}
290
291impl Iterator for CodePointIter<'_> {
292    type Item = CodeUnitString;
293
294    fn next(&mut self) -> Option<Self::Item> {
295        let first = *self.units.get(self.at)?;
296        let width = if (0xd800..=0xdbff).contains(&first)
297            && self
298                .units
299                .get(self.at + 1)
300                .is_some_and(|unit| (0xdc00..=0xdfff).contains(unit))
301        {
302            2
303        } else {
304            1
305        };
306        let out = CodeUnitString::from_code_units(self.units[self.at..self.at + width].to_vec());
307        self.at += width;
308        Some(out)
309    }
310}
311
312fn first_lone(units: &[u16]) -> Option<InvalidSurrogate> {
313    let mut index = 0;
314    while index < units.len() {
315        let unit = units[index];
316        if (0xd800..=0xdbff).contains(&unit) {
317            if units
318                .get(index + 1)
319                .is_some_and(|next| (0xdc00..=0xdfff).contains(next))
320            {
321                index += 2;
322                continue;
323            }
324            return Some(InvalidSurrogate {
325                offset: CodeUnitOffset::new(index),
326                unit,
327            });
328        }
329        if (0xdc00..=0xdfff).contains(&unit) {
330            return Some(InvalidSurrogate {
331                offset: CodeUnitOffset::new(index),
332                unit,
333            });
334        }
335        index += 1;
336    }
337    None
338}