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}