Skip to main content

qubit_codec_text/charset/
utf16.rs

1// =============================================================================
2//    Copyright (c) 2026 Haixing Hu.
3//
4//    SPDX-License-Identifier: Apache-2.0
5//
6//    Licensed under the Apache License, Version 2.0.
7// =============================================================================
8use crate::{
9    ByteOrder,
10    Unicode,
11    UnicodeBom,
12};
13
14/// Namespace for UTF-16 constants and code-unit classification helpers.
15pub enum Utf16 {}
16
17impl Utf16 {
18    /// Maximum number of UTF-16 code units needed for one Unicode scalar value.
19    pub const MAX_UNITS_PER_CHAR: usize = 2;
20
21    /// Maximum number of serialized UTF-16 bytes needed for one Unicode scalar
22    /// value.
23    pub const MAX_BYTES_PER_CHAR: usize = 4;
24
25    /// Tests whether a UTF-16 unit is a high surrogate.
26    ///
27    /// # Parameters
28    ///
29    /// - `unit`: The UTF-16 unit to test.
30    ///
31    /// # Returns
32    ///
33    /// Returns `true` if `unit` is in `0xD800..=0xDBFF`.
34    #[inline(always)]
35    pub const fn is_high_surrogate(unit: u16) -> bool {
36        (unit as u32) >= Unicode::HIGH_SURROGATE_MIN
37            && (unit as u32) <= Unicode::HIGH_SURROGATE_MAX
38    }
39
40    /// Tests whether a UTF-16 unit is a low surrogate.
41    ///
42    /// # Parameters
43    ///
44    /// - `unit`: The UTF-16 unit to test.
45    ///
46    /// # Returns
47    ///
48    /// Returns `true` if `unit` is in `0xDC00..=0xDFFF`.
49    #[inline(always)]
50    pub const fn is_low_surrogate(unit: u16) -> bool {
51        (unit as u32) >= Unicode::LOW_SURROGATE_MIN
52            && (unit as u32) <= Unicode::LOW_SURROGATE_MAX
53    }
54
55    /// Tests whether a UTF-16 unit is any surrogate.
56    ///
57    /// # Parameters
58    ///
59    /// - `unit`: The UTF-16 unit to test.
60    ///
61    /// # Returns
62    ///
63    /// Returns `true` if `unit` is in `0xD800..=0xDFFF`.
64    #[inline(always)]
65    pub const fn is_surrogate(unit: u16) -> bool {
66        (unit as u32) >= Unicode::SURROGATE_MIN
67            && (unit as u32) <= Unicode::SURROGATE_MAX
68    }
69
70    /// Tests whether a UTF-16 unit encodes a scalar value by itself.
71    ///
72    /// # Parameters
73    ///
74    /// - `unit`: The UTF-16 unit to test.
75    ///
76    /// # Returns
77    ///
78    /// Returns `true` for non-surrogate units.
79    #[inline(always)]
80    pub const fn is_single_unit(unit: u16) -> bool {
81        !Self::is_surrogate(unit)
82    }
83
84    /// Tests whether two UTF-16 units form a surrogate pair.
85    ///
86    /// # Parameters
87    ///
88    /// - `high`: The candidate high surrogate.
89    /// - `low`: The candidate low surrogate.
90    ///
91    /// # Returns
92    ///
93    /// Returns `true` if `high` is a high surrogate and `low` is a low
94    /// surrogate.
95    #[inline(always)]
96    pub const fn is_surrogate_pair(high: u16, low: u16) -> bool {
97        Self::is_high_surrogate(high) && Self::is_low_surrogate(low)
98    }
99
100    /// Returns the UTF-16 unit count needed for `ch`.
101    ///
102    /// # Parameters
103    ///
104    /// - `ch`: The character to size.
105    ///
106    /// # Returns
107    ///
108    /// Returns `1` for BMP scalar values and `2` for supplementary scalar
109    /// values.
110    #[inline(always)]
111    pub const fn unit_len(ch: char) -> usize {
112        if (ch as u32) >= Unicode::SUPPLEMENTARY_MIN {
113            2
114        } else {
115            1
116        }
117    }
118
119    /// Returns the UTF-16 unit count needed for a raw code point.
120    ///
121    /// # Parameters
122    ///
123    /// - `code_point`: The raw code point to size.
124    ///
125    /// # Returns
126    ///
127    /// Returns `Some(1)` or `Some(2)` for scalar values and `None` otherwise.
128    #[inline(always)]
129    pub const fn unit_len_code_point(code_point: u32) -> Option<usize> {
130        if !Unicode::is_scalar_value(code_point) {
131            None
132        } else if code_point >= Unicode::SUPPLEMENTARY_MIN {
133            Some(2)
134        } else {
135            Some(1)
136        }
137    }
138
139    /// Composes a surrogate pair into a Unicode code point.
140    ///
141    /// # Parameters
142    ///
143    /// - `high`: The high surrogate.
144    /// - `low`: The low surrogate.
145    ///
146    /// # Returns
147    ///
148    /// Returns `Some(code_point)` when the pair is valid, or `None` otherwise.
149    #[inline(always)]
150    pub const fn compose_pair(high: u16, low: u16) -> Option<u32> {
151        if Self::is_surrogate_pair(high, low) {
152            let high_payload = (high as u32) - Unicode::HIGH_SURROGATE_MIN;
153            let low_payload = (low as u32) - Unicode::LOW_SURROGATE_MIN;
154            Some(
155                (high_payload << 10) + low_payload + Unicode::SUPPLEMENTARY_MIN,
156            )
157        } else {
158            None
159        }
160    }
161
162    /// Returns the high surrogate for a supplementary code point.
163    ///
164    /// # Parameters
165    ///
166    /// - `code_point`: The supplementary code point.
167    ///
168    /// # Returns
169    ///
170    /// Returns `Some(high_surrogate)` for supplementary code points and `None`
171    /// otherwise.
172    #[inline(always)]
173    pub const fn high_surrogate(code_point: u32) -> Option<u16> {
174        if Unicode::is_supplementary(code_point) {
175            Some(
176                (((code_point - Unicode::SUPPLEMENTARY_MIN) >> 10)
177                    + Unicode::HIGH_SURROGATE_MIN) as u16,
178            )
179        } else {
180            None
181        }
182    }
183
184    /// Returns the low surrogate for a supplementary code point.
185    ///
186    /// # Parameters
187    ///
188    /// - `code_point`: The supplementary code point.
189    ///
190    /// # Returns
191    ///
192    /// Returns `Some(low_surrogate)` for supplementary code points and `None`
193    /// otherwise.
194    #[inline(always)]
195    pub const fn low_surrogate(code_point: u32) -> Option<u16> {
196        if Unicode::is_supplementary(code_point) {
197            Some(
198                (((code_point - Unicode::SUPPLEMENTARY_MIN) & 0x3ff)
199                    + Unicode::LOW_SURROGATE_MIN) as u16,
200            )
201        } else {
202            None
203        }
204    }
205
206    /// Detects a UTF-16 BOM and returns its byte order.
207    ///
208    /// # Parameters
209    ///
210    /// - `bytes`: The byte buffer to inspect.
211    ///
212    /// # Returns
213    ///
214    /// Returns `Some(ByteOrder)` for UTF-16 BOM prefixes, or `None` otherwise.
215    #[inline]
216    pub fn detect_bom(bytes: &[u8]) -> Option<ByteOrder> {
217        match UnicodeBom::detect(bytes) {
218            Some(UnicodeBom::Utf16BigEndian) => Some(ByteOrder::BigEndian),
219            Some(UnicodeBom::Utf16LittleEndian) => {
220                Some(ByteOrder::LittleEndian)
221            }
222            _ => None,
223        }
224    }
225}