packed_char/
lib.rs

1//! [`PackedChar`] stores either a [`char`] or a [`U22`] in 32 bits of space.
2//!
3//! # Examples
4//!
5//! ```
6//! use packed_char::{PackedChar, U22, Contents};
7//! # use packed_char::U22FromU32Error;
8//! # fn main() -> Result<(), U22FromU32Error> {
9//! assert_eq!(PackedChar::from('a').contents(), Contents::Char('a'));
10//! assert_eq!(PackedChar::try_from(42)?.contents(), Contents::U22(U22::from_u32(42)?));
11//! # Ok(()) }
12//! ```
13
14#![no_std]
15
16mod u22;
17pub use u22::{U22FromU32Error, U22};
18
19use core::fmt::{self, Debug, Formatter};
20
21/// Stores either a `char` or a [`U22`] in 32 bits of space.
22#[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(); // 11
30    const LEADING_MASK: u32 = !(u32::MAX >> Self::LEADING);
31    const TRAILING: u32 = Self::SURROGATE_LOW.trailing_zeros(); // 11
32    const TRAILING_MASK: u32 = !(u32::MAX << Self::TRAILING);
33    const MAX_U22_LEADING: u32 = U22::MAX.leading_zeros();
34
35    /// Creates a new value from the given `char`.
36    ///
37    /// # Examples
38    ///
39    /// ```
40    /// # use packed_char::{PackedChar, Contents};
41    /// let pack = PackedChar::from_char('a');
42    /// assert_eq!(pack.contents(), Contents::Char('a'));
43    /// ```
44    pub const fn from_char(c: char) -> Self {
45        Self(c as u32)
46    }
47
48    /// Creates a new value from the given `u22`.
49    ///
50    /// # Examples
51    ///
52    /// ```
53    /// # use packed_char::{PackedChar, Contents, U22};
54    /// let u22 = U22::from_u32(42).unwrap();
55    /// let pack = PackedChar::from_u22(u22);
56    /// assert_eq!(pack.contents(), Contents::U22(u22));
57    /// ```
58    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    /// Gets the stored value.
66    ///
67    /// # Examples
68    ///
69    /// ```
70    /// # use packed_char::{PackedChar, Contents, U22, U22FromU32Error};
71    /// # fn main() -> Result<(), U22FromU32Error> {
72    /// let pack = PackedChar::try_from(42)?;
73    /// assert_eq!(pack.contents(), Contents::U22(U22::from_u32(42)?));
74    ///
75    /// let pack = PackedChar::from('a');
76    /// assert_eq!(pack.contents(), Contents::Char('a'));
77    /// # Ok(())
78    /// # }
79    /// ```
80    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                // SAFETY: Valid by construction since we reversed the storage procedure.
88                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/// The contents of a [`PackedChar`].
122///
123/// Returned from [`PackedChar::contents`].
124#[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            // Char containing surrogate mask
141            #[allow(clippy::unusual_byte_groupings)]
142            char::from_u32(0b1_11011_11111111111).unwrap(),
143            // Char not containing surrogate mask
144            #[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}