1#![no_std]
15
16mod u22;
17pub use u22::{U22FromU32Error, U22};
18
19use core::fmt::{self, Debug, Formatter};
20
21#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
23pub struct PackedChar(u32);
24
25impl PackedChar {
26 const SURROGATE_LOW: u32 = 0xD800;
27 const SURROGATE_HIGH: u32 = 0xDFFF;
28 const SURROGATE_MASK: u32 = Self::SURROGATE_LOW & Self::SURROGATE_HIGH;
29 const LEADING: u32 = (char::MAX as u32).leading_zeros(); const LEADING_MASK: u32 = !(u32::MAX >> Self::LEADING);
31 const TRAILING: u32 = Self::SURROGATE_LOW.trailing_zeros(); const TRAILING_MASK: u32 = !(u32::MAX << Self::TRAILING);
33 const MAX_U22_LEADING: u32 = U22::MAX.leading_zeros();
34
35 pub const fn from_char(c: char) -> Self {
45 Self(c as u32)
46 }
47
48 pub const fn from_u22(u22: U22) -> Self {
59 let n = u22.as_u32();
60 let leading = (n << Self::MAX_U22_LEADING) & Self::LEADING_MASK;
61 let trailing = n & Self::TRAILING_MASK;
62 Self(leading | trailing | Self::SURROGATE_MASK)
63 }
64
65 pub const fn contents(self) -> Contents {
81 match char::from_u32(self.0) {
82 Some(c) => Contents::Char(c),
83 None => {
84 let trailing = self.0 & Self::TRAILING_MASK;
85 let leading = self.0 & Self::LEADING_MASK;
86 let u22 = trailing | (leading >> Self::MAX_U22_LEADING);
87 Contents::U22(unsafe { U22::from_u32_unchecked(u22) })
89 }
90 }
91 }
92}
93
94impl Debug for PackedChar {
95 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
96 write!(f, "{:?}", self.contents())
97 }
98}
99
100impl From<char> for PackedChar {
101 fn from(c: char) -> Self {
102 Self::from_char(c)
103 }
104}
105
106impl From<U22> for PackedChar {
107 fn from(u22: U22) -> Self {
108 Self::from_u22(u22)
109 }
110}
111
112impl TryFrom<u32> for PackedChar {
113 type Error = U22FromU32Error;
114
115 fn try_from(n: u32) -> Result<Self, Self::Error> {
116 let u22 = U22::from_u32(n)?;
117 Ok(Self::from_u22(u22))
118 }
119}
120
121#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
125pub enum Contents {
126 Char(char),
127 U22(U22),
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133
134 #[test]
135 fn gets_back_chars() {
136 let test_chars = [
137 '\0',
138 '\u{D7FF}',
139 '\u{E000}',
140 #[allow(clippy::unusual_byte_groupings)]
142 char::from_u32(0b1_11011_11111111111).unwrap(),
143 #[allow(clippy::unusual_byte_groupings)]
145 char::from_u32(0b1_00000_11111111111).unwrap(),
146 char::REPLACEMENT_CHARACTER,
147 char::MAX,
148 'a',
149 '1',
150 '🫠',
151 ];
152 for c in test_chars {
153 let packed = PackedChar::from_char(c);
154 assert_eq!(packed.contents(), Contents::Char(c));
155 }
156 }
157
158 #[test]
159 fn gets_back_ints() {
160 let ints = [U22::MAX, 0x3FFFFF, 0, 42, 0b1010101010101010101010];
161 for i in ints {
162 let packed = PackedChar::try_from(i).unwrap();
163 assert_eq!(packed.contents(), Contents::U22(U22::try_from(i).unwrap()));
164 }
165 }
166
167 #[test]
168 fn fails_out_of_bounds_indices() {
169 let ints = [U22::MAX + 1, u32::MAX, 0b10101010101010101010101010101010];
170 for i in ints {
171 let packed = PackedChar::try_from(i);
172 assert_eq!(packed, Err(U22FromU32Error(i)));
173 }
174 }
175}