Skip to main content

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;