1use std::ops::Deref;
5
6use topcoat_core::fnv1a::Fnv1a;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
15pub struct UnicodeCodePoint(u32);
16
17impl UnicodeCodePoint {
18 #[must_use]
25 #[track_caller]
26 pub const fn new(code_point: u32) -> Self {
27 assert!(
28 code_point <= 0x10_FFFF,
29 "unicode code point exceeds U+10FFFF"
30 );
31 Self(code_point)
32 }
33
34 pub(crate) const fn hash(self, h: Fnv1a<u64>) -> Fnv1a<u64> {
36 h.write(&self.0.to_le_bytes())
37 }
38}
39
40impl From<UnicodeCodePoint> for u32 {
41 fn from(value: UnicodeCodePoint) -> Self {
42 value.0
43 }
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub struct CodePointOutOfRangeError;
50
51impl std::fmt::Display for CodePointOutOfRangeError {
52 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53 f.write_str("code point exceeds U+10FFFF")
54 }
55}
56
57impl std::error::Error for CodePointOutOfRangeError {}
58
59impl TryFrom<u32> for UnicodeCodePoint {
60 type Error = CodePointOutOfRangeError;
61
62 fn try_from(value: u32) -> Result<Self, Self::Error> {
63 if value > 0x10_FFFF {
64 return Err(CodePointOutOfRangeError);
65 }
66 Ok(Self(value))
67 }
68}
69
70impl std::fmt::Display for UnicodeCodePoint {
71 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72 let code_point = self.0;
73 write!(f, "U+{code_point:04X}")
74 }
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
82pub struct UnicodeRange {
83 start: UnicodeCodePoint,
84 end: UnicodeCodePoint,
85}
86
87impl UnicodeRange {
88 #[must_use]
94 #[track_caller]
95 pub const fn new(start: UnicodeCodePoint, end: UnicodeCodePoint) -> Self {
96 assert!(end.0 >= start.0, "unicode range must not be empty");
97 Self { start, end }
98 }
99
100 #[must_use]
107 #[track_caller]
108 pub const fn from_u32(start: u32, end: u32) -> Self {
109 Self::new(UnicodeCodePoint::new(start), UnicodeCodePoint::new(end))
110 }
111
112 #[must_use]
114 pub const fn start(&self) -> UnicodeCodePoint {
115 self.start
116 }
117
118 #[must_use]
120 pub const fn end(&self) -> UnicodeCodePoint {
121 self.end
122 }
123
124 pub(crate) const fn hash(self, h: Fnv1a<u64>) -> Fnv1a<u64> {
126 self.end.hash(self.start.hash(h))
127 }
128}
129
130impl std::fmt::Display for UnicodeRange {
131 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
132 if self.start == self.end {
133 self.start.fmt(f)
134 } else {
135 let start = self.start.0;
136 let end = self.end.0;
137 write!(f, "U+{start:04X}-{end:04X}")
138 }
139 }
140}
141
142#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
147pub struct UnicodeRanges(&'static [UnicodeRange]);
148
149impl UnicodeRanges {
150 #[must_use]
152 pub const fn new(ranges: &'static [UnicodeRange]) -> Self {
153 Self(ranges)
154 }
155
156 pub(crate) const fn hash(self, mut h: Fnv1a<u64>) -> Fnv1a<u64> {
158 let mut i = 0;
159 while i < self.0.len() {
160 h = self.0[i].hash(h);
161 i += 1;
162 }
163 h
164 }
165}
166
167impl std::fmt::Display for UnicodeRanges {
168 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
169 for (i, range) in self.0.iter().enumerate() {
170 if i > 0 {
171 f.write_str(", ")?;
172 }
173 range.fmt(f)?;
174 }
175 Ok(())
176 }
177}
178
179impl Deref for UnicodeRanges {
180 type Target = [UnicodeRange];
181
182 fn deref(&self) -> &Self::Target {
183 self.0
184 }
185}
186
187#[cfg(test)]
188mod tests {
189 use super::*;
190
191 fn cp(value: u32) -> UnicodeCodePoint {
192 UnicodeCodePoint::new(value)
193 }
194
195 #[test]
196 fn code_point_displays_as_padded_hex() {
197 assert_eq!(cp(0x41).to_string(), "U+0041");
198 assert_eq!(cp(0x10_FFFF).to_string(), "U+10FFFF");
199 }
200
201 #[test]
202 fn code_point_converts_to_u32() {
203 assert_eq!(u32::from(cp(0x1F600)), 0x1F600);
204 }
205
206 #[test]
207 fn try_from_accepts_surrogates_and_the_maximum() {
208 assert_eq!(UnicodeCodePoint::try_from(0xD800), Ok(cp(0xD800)));
209 assert_eq!(UnicodeCodePoint::try_from(0x10_FFFF), Ok(cp(0x10_FFFF)));
210 }
211
212 #[test]
213 fn try_from_rejects_out_of_range() {
214 assert_eq!(
215 UnicodeCodePoint::try_from(0x11_0000),
216 Err(CodePointOutOfRangeError),
217 );
218 }
219
220 #[test]
221 #[should_panic = "exceeds"]
222 fn new_panics_on_out_of_range_code_point() {
223 let _ = UnicodeCodePoint::new(0x11_0000);
224 }
225
226 #[test]
227 fn single_code_point_range_omits_the_dash() {
228 assert_eq!(UnicodeRange::new(cp(0x41), cp(0x41)).to_string(), "U+0041");
229 }
230
231 #[test]
232 fn multi_code_point_range_includes_the_dash() {
233 assert_eq!(
234 UnicodeRange::new(cp(0x41), cp(0x5A)).to_string(),
235 "U+0041-005A",
236 );
237 }
238
239 #[test]
240 #[should_panic = "empty"]
241 fn range_panics_when_end_precedes_start() {
242 let _ = UnicodeRange::new(cp(0x5A), cp(0x41));
243 }
244
245 #[test]
246 fn range_from_u32_matches_new() {
247 assert_eq!(
248 UnicodeRange::from_u32(0x41, 0x5A),
249 UnicodeRange::new(cp(0x41), cp(0x5A)),
250 );
251 }
252
253 #[test]
254 #[should_panic = "exceeds"]
255 fn range_from_u32_panics_on_out_of_range() {
256 let _ = UnicodeRange::from_u32(0x00, 0x11_0000);
257 }
258
259 #[test]
260 fn ranges_display_comma_separated() {
261 const RANGES: UnicodeRanges = UnicodeRanges::new(&[
262 UnicodeRange::new(UnicodeCodePoint::new(0x00), UnicodeCodePoint::new(0xFF)),
263 UnicodeRange::new(UnicodeCodePoint::new(0x131), UnicodeCodePoint::new(0x131)),
264 UnicodeRange::new(UnicodeCodePoint::new(0x152), UnicodeCodePoint::new(0x153)),
265 ]);
266 assert_eq!(RANGES.to_string(), "U+0000-00FF, U+0131, U+0152-0153");
267 }
268
269 #[test]
270 fn empty_ranges_display_as_empty_string() {
271 const RANGES: UnicodeRanges = UnicodeRanges::new(&[]);
272 assert_eq!(RANGES.to_string(), "");
273 }
274
275 #[test]
276 fn ranges_deref_to_their_slice() {
277 const RANGES: UnicodeRanges = UnicodeRanges::new(&[UnicodeRange::new(
278 UnicodeCodePoint::new(0x00),
279 UnicodeCodePoint::new(0xFF),
280 )]);
281 assert_eq!(RANGES.len(), 1);
282 assert_eq!(RANGES[0].start(), cp(0x00));
283 }
284}