tensor4all_core/smallstring.rs
1/// Trait for character types that can be stored in SmallString.
2///
3/// This trait abstracts over different character representations:
4/// - `u16`: UTF-16 code unit (2 bytes), compatible with ITensors.jl's SmallString
5/// - `char`: Full Unicode code point (4 bytes), supports all Unicode characters
6///
7/// Default is `u16` for memory efficiency and ITensors.jl compatibility.
8pub trait SmallChar: Copy + Default + Ord + Eq + std::hash::Hash + std::fmt::Debug {
9 /// The null/zero value for this character type.
10 const ZERO: Self;
11
12 /// Convert from a Rust char.
13 /// Returns None if the character cannot be represented in this type.
14 fn from_char(c: char) -> Option<Self>;
15
16 /// Convert to a Rust char.
17 fn to_char(self) -> char;
18}
19
20impl SmallChar for u16 {
21 const ZERO: Self = 0;
22
23 fn from_char(c: char) -> Option<Self> {
24 // u16 can represent BMP characters (U+0000 to U+FFFF)
25 let code = c as u32;
26 if code <= 0xFFFF {
27 Some(code as u16)
28 } else {
29 None // Surrogate pairs not supported
30 }
31 }
32
33 fn to_char(self) -> char {
34 // Safe because we only store valid BMP characters
35 char::from_u32(self as u32).unwrap_or('\u{FFFD}')
36 }
37}
38
39impl SmallChar for char {
40 const ZERO: Self = '\0';
41
42 fn from_char(c: char) -> Option<Self> {
43 Some(c)
44 }
45
46 fn to_char(self) -> char {
47 self
48 }
49}
50
51/// A stack-allocated fixed-capacity string with explicit length.
52///
53/// This type stores characters in a fixed-size array and maintains
54/// an explicit length field, similar to ITensors.jl's `SmallString`.
55///
56/// # Type Parameters
57/// - `MAX_LEN`: Maximum number of characters (default: 16, matching ITensors.jl)
58/// - `C`: Character type (default: `u16` for ITensors.jl compatibility)
59///
60/// # Character Type Options
61/// - `u16` (default): 2 bytes per character, supports BMP (Basic Multilingual Plane)
62///
63/// - Covers ASCII, Japanese, Chinese, Korean, and most practical characters
64/// - Does NOT support emoji or rare characters outside BMP
65/// - `char`: 4 bytes per character, full Unicode support
66///
67/// # Example
68/// ```
69/// use tensor4all_core::smallstring::SmallString;
70///
71/// // Default: u16 characters (ITensors.jl compatible)
72/// let s1 = SmallString::<16>::from_str("hello").unwrap();
73///
74/// // Explicit char type for full Unicode support
75/// let s2 = SmallString::<16, char>::from_str("hello 😀").unwrap();
76/// ```
77#[derive(Debug, Clone, Copy)]
78pub struct SmallString<const MAX_LEN: usize, C: SmallChar = u16> {
79 data: [C; MAX_LEN],
80 len: usize, // Explicit length (0 ≤ len ≤ MAX_LEN)
81}
82
83/// Error type for SmallString operations.
84#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq)]
85pub enum SmallStringError {
86 /// The string exceeds the maximum length.
87 #[error("String too long ({actual} > {max})")]
88 TooLong {
89 /// The actual length of the string.
90 actual: usize,
91 /// The maximum allowed length.
92 max: usize,
93 },
94 /// A character cannot be represented in the target character type.
95 #[error("Invalid character: {char_value:?}")]
96 InvalidChar {
97 /// The character that could not be converted.
98 char_value: char,
99 },
100}
101
102impl<const MAX_LEN: usize, C: SmallChar> SmallString<MAX_LEN, C> {
103 /// Create an empty SmallString.
104 pub fn new() -> Self {
105 Self {
106 data: [C::ZERO; MAX_LEN],
107 len: 0,
108 }
109 }
110
111 /// Create a SmallString from a string slice.
112 ///
113 /// Returns an error if:
114 /// - The string is longer than MAX_LEN characters
115 /// - Any character cannot be represented in the character type C
116 ///
117 /// This function is allocation-free (no heap allocation).
118 #[allow(clippy::should_implement_trait)]
119 /// # Errors
120 ///
121 /// Returns [`SmallStringError::TooLong`] when `s` exceeds `MAX_LEN`
122 /// characters, or [`SmallStringError::InvalidChar`] when a character cannot
123 /// be represented in `C`.
124 ///
125 pub fn from_str(s: &str) -> Result<Self, SmallStringError> {
126 let mut data = [C::ZERO; MAX_LEN];
127 let mut len = 0;
128
129 for ch in s.chars() {
130 if len >= MAX_LEN {
131 // Count total characters for error message
132 let actual = len + 1 + s.chars().skip(len + 1).count();
133 return Err(SmallStringError::TooLong {
134 actual,
135 max: MAX_LEN,
136 });
137 }
138 data[len] = C::from_char(ch).ok_or(SmallStringError::InvalidChar { char_value: ch })?;
139 len += 1;
140 }
141
142 Ok(Self { data, len })
143 }
144
145 /// Convert to a String.
146 pub fn as_str(&self) -> String {
147 self.data[..self.len].iter().map(|c| c.to_char()).collect()
148 }
149
150 /// Check if the string is empty.
151 pub fn is_empty(&self) -> bool {
152 self.len == 0
153 }
154
155 /// Get the length of the string.
156 pub fn len(&self) -> usize {
157 self.len
158 }
159
160 /// Get the maximum capacity.
161 pub fn capacity(&self) -> usize {
162 MAX_LEN
163 }
164
165 /// Get a character at the given index.
166 pub fn get(&self, index: usize) -> Option<char> {
167 if index < self.len {
168 Some(self.data[index].to_char())
169 } else {
170 None
171 }
172 }
173
174 /// Get a reference to the internal data slice.
175 pub fn as_slice(&self) -> &[C] {
176 &self.data[..self.len]
177 }
178}
179
180impl<const MAX_LEN: usize, C: SmallChar> Default for SmallString<MAX_LEN, C> {
181 fn default() -> Self {
182 Self::new()
183 }
184}
185
186impl<const MAX_LEN: usize, C: SmallChar> PartialEq for SmallString<MAX_LEN, C> {
187 fn eq(&self, other: &Self) -> bool {
188 self.len == other.len && self.data[..self.len] == other.data[..other.len]
189 }
190}
191
192impl<const MAX_LEN: usize, C: SmallChar> Eq for SmallString<MAX_LEN, C> {}
193
194impl<const MAX_LEN: usize, C: SmallChar> std::hash::Hash for SmallString<MAX_LEN, C> {
195 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
196 self.data[..self.len].hash(state);
197 }
198}
199
200impl<const MAX_LEN: usize, C: SmallChar> PartialOrd for SmallString<MAX_LEN, C> {
201 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
202 Some(self.cmp(other))
203 }
204}
205
206impl<const MAX_LEN: usize, C: SmallChar> Ord for SmallString<MAX_LEN, C> {
207 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
208 self.data[..self.len].cmp(&other.data[..other.len])
209 }
210}
211
212impl<const MAX_LEN: usize, C: SmallChar> std::fmt::Display for SmallString<MAX_LEN, C> {
213 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
214 write!(f, "{}", self.as_str())
215 }
216}
217
218#[cfg(test)]
219mod tests;