1use std::collections::hash_map::Entry;
7use std::ffi::c_char;
8use std::fmt;
9use std::mem::size_of;
10use std::str;
11use std::sync::Mutex;
12
13use pyo3::sync::MutexExt;
14use pyo3::{
15 Bound, Py, Python,
16 ffi::{Py_UCS1, Py_UCS4},
17};
18use rustc_hash::FxHashMap;
19
20use crate::dtype::{Element, PyArrayDescr, PyArrayDescrMethods, clone_methods_impl};
21use crate::npyffi::{_PyDataType_GET_ITEM_DATA, NPY_TYPES, PyDataType_SET_ELSIZE};
22
23#[repr(transparent)]
61#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
62pub struct PyFixedString<const N: usize>(pub [Py_UCS1; N]);
63
64impl<const N: usize> fmt::Display for PyFixedString<N> {
65 fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
66 fmt.write_str(str::from_utf8(&self.0).unwrap().trim_end_matches('\0'))
67 }
68}
69
70impl<const N: usize> From<[Py_UCS1; N]> for PyFixedString<N> {
71 fn from(val: [Py_UCS1; N]) -> Self {
72 Self(val)
73 }
74}
75
76unsafe impl<const N: usize> Element for PyFixedString<N> {
77 const IS_COPY: bool = true;
78
79 fn get_dtype(py: Python<'_>) -> Bound<'_, PyArrayDescr> {
80 static DTYPES: TypeDescriptors = TypeDescriptors::new();
81
82 unsafe { DTYPES.from_size(py, NPY_TYPES::NPY_STRING, b'|' as _, size_of::<Self>()) }
83 }
84
85 clone_methods_impl!(Self);
86}
87
88#[repr(transparent)]
126#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
127pub struct PyFixedUnicode<const N: usize>(pub [Py_UCS4; N]);
128
129impl<const N: usize> fmt::Display for PyFixedUnicode<N> {
130 fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
131 for character in self.0 {
132 if character == 0 {
133 break;
134 }
135
136 write!(fmt, "{}", char::from_u32(character).unwrap())?;
137 }
138
139 Ok(())
140 }
141}
142
143impl<const N: usize> From<[Py_UCS4; N]> for PyFixedUnicode<N> {
144 fn from(val: [Py_UCS4; N]) -> Self {
145 Self(val)
146 }
147}
148
149unsafe impl<const N: usize> Element for PyFixedUnicode<N> {
150 const IS_COPY: bool = true;
151
152 fn get_dtype(py: Python<'_>) -> Bound<'_, PyArrayDescr> {
153 static DTYPES: TypeDescriptors = TypeDescriptors::new();
154
155 unsafe { DTYPES.from_size(py, NPY_TYPES::NPY_UNICODE, b'=' as _, size_of::<Self>()) }
156 }
157
158 clone_methods_impl!(Self);
159}
160
161struct TypeDescriptors {
162 dtypes: Mutex<Option<FxHashMap<usize, Py<PyArrayDescr>>>>,
163}
164
165impl TypeDescriptors {
166 const fn new() -> Self {
167 Self {
168 dtypes: Mutex::new(None),
169 }
170 }
171
172 #[allow(clippy::wrong_self_convention)]
174 unsafe fn from_size<'py>(
175 &self,
176 py: Python<'py>,
177 npy_type: NPY_TYPES,
178 byteorder: c_char,
179 size: usize,
180 ) -> Bound<'py, PyArrayDescr> {
181 let mut dtypes = self
182 .dtypes
183 .lock_py_attached(py)
184 .expect("dtype cache poisoned");
185
186 let dtype = match dtypes.get_or_insert_with(Default::default).entry(size) {
187 Entry::Occupied(entry) => entry.into_mut(),
188 Entry::Vacant(entry) => {
189 let dtype = PyArrayDescr::new_from_npy_type(py, npy_type);
190
191 let descr = dtype.as_dtype_ptr();
192 unsafe {
193 PyDataType_SET_ELSIZE(py, descr, size.try_into().unwrap());
194 (*_PyDataType_GET_ITEM_DATA(descr)).byteorder = byteorder;
195 }
196
197 entry.insert(dtype.into())
198 }
199 };
200
201 dtype.bind(py).to_owned()
202 }
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208
209 #[test]
210 #[allow(clippy::byte_char_slices)]
211 fn format_fixed_string() {
212 assert_eq!(
213 PyFixedString([b'f', b'o', b'o', 0, 0, 0]).to_string(),
214 "foo"
215 );
216 assert_eq!(PyFixedString(*b"foobar").to_string(), "foobar");
217 }
218
219 #[test]
220 fn format_fixed_unicode() {
221 assert_eq!(
222 PyFixedUnicode([b'f' as _, b'o' as _, b'o' as _, 0, 0, 0]).to_string(),
223 "foo"
224 );
225 assert_eq!(
226 PyFixedUnicode([0x1F980, 0x1F40D, 0, 0, 0, 0]).to_string(),
227 "🦀🐍"
228 );
229 assert_eq!(
230 PyFixedUnicode([
231 b'f' as _, b'o' as _, b'o' as _, b'b' as _, b'a' as _, b'r' as _
232 ])
233 .to_string(),
234 "foobar"
235 );
236 }
237}