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 an error when the operation fails (a shape or index mismatch, or
122    /// /// a backend failure).
123    ///
124    pub fn from_str(s: &str) -> Result<Self, SmallStringError> {
125        let mut data = [C::ZERO; MAX_LEN];
126        let mut len = 0;
127
128        for ch in s.chars() {
129            if len >= MAX_LEN {
130                // Count total characters for error message
131                let actual = len + 1 + s.chars().skip(len + 1).count();
132                return Err(SmallStringError::TooLong {
133                    actual,
134                    max: MAX_LEN,
135                });
136            }
137            data[len] = C::from_char(ch).ok_or(SmallStringError::InvalidChar { char_value: ch })?;
138            len += 1;
139        }
140
141        Ok(Self { data, len })
142    }
143
144    /// Convert to a String.
145    pub fn as_str(&self) -> String {
146        self.data[..self.len].iter().map(|c| c.to_char()).collect()
147    }
148
149    /// Check if the string is empty.
150    pub fn is_empty(&self) -> bool {
151        self.len == 0
152    }
153
154    /// Get the length of the string.
155    pub fn len(&self) -> usize {
156        self.len
157    }
158
159    /// Get the maximum capacity.
160    pub fn capacity(&self) -> usize {
161        MAX_LEN
162    }
163
164    /// Get a character at the given index.
165    pub fn get(&self, index: usize) -> Option<char> {
166        if index < self.len {
167            Some(self.data[index].to_char())
168        } else {
169            None
170        }
171    }
172
173    /// Get a reference to the internal data slice.
174    pub fn as_slice(&self) -> &[C] {
175        &self.data[..self.len]
176    }
177}
178
179impl<const MAX_LEN: usize, C: SmallChar> Default for SmallString<MAX_LEN, C> {
180    fn default() -> Self {
181        Self::new()
182    }
183}
184
185impl<const MAX_LEN: usize, C: SmallChar> PartialEq for SmallString<MAX_LEN, C> {
186    fn eq(&self, other: &Self) -> bool {
187        self.len == other.len && self.data[..self.len] == other.data[..other.len]
188    }
189}
190
191impl<const MAX_LEN: usize, C: SmallChar> Eq for SmallString<MAX_LEN, C> {}
192
193impl<const MAX_LEN: usize, C: SmallChar> std::hash::Hash for SmallString<MAX_LEN, C> {
194    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
195        self.data[..self.len].hash(state);
196    }
197}
198
199impl<const MAX_LEN: usize, C: SmallChar> PartialOrd for SmallString<MAX_LEN, C> {
200    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
201        Some(self.cmp(other))
202    }
203}
204
205impl<const MAX_LEN: usize, C: SmallChar> Ord for SmallString<MAX_LEN, C> {
206    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
207        self.data[..self.len].cmp(&other.data[..other.len])
208    }
209}
210
211impl<const MAX_LEN: usize, C: SmallChar> std::fmt::Display for SmallString<MAX_LEN, C> {
212    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
213        write!(f, "{}", self.as_str())
214    }
215}
216
217#[cfg(test)]
218mod tests;