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 ffi::{Py_UCS1, Py_UCS4},
16 Bound, Py, Python,
17};
18use rustc_hash::FxHashMap;
19
20use crate::dtype::{clone_methods_impl, Element, PyArrayDescr, PyArrayDescrMethods};
21use crate::npyffi::{_PyDataType_GET_ITEM_DATA, PyDataType_SET_ELSIZE, NPY_TYPES};
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 = &mut *dtype.as_dtype_ptr();
192 PyDataType_SET_ELSIZE(py, descr, size.try_into().unwrap());
193 (*_PyDataType_GET_ITEM_DATA(descr)).byteorder = byteorder;
194
195 entry.insert(dtype.into())
196 }
197 };
198
199 dtype.bind(py).to_owned()
200 }
201}
202
203#[cfg(test)]
204mod tests {
205 use super::*;
206
207 #[test]
208 #[allow(clippy::byte_char_slices)]
209 fn format_fixed_string() {
210 assert_eq!(
211 PyFixedString([b'f', b'o', b'o', 0, 0, 0]).to_string(),
212 "foo"
213 );
214 assert_eq!(PyFixedString(*b"foobar").to_string(), "foobar");
215 }
216
217 #[test]
218 fn format_fixed_unicode() {
219 assert_eq!(
220 PyFixedUnicode([b'f' as _, b'o' as _, b'o' as _, 0, 0, 0]).to_string(),
221 "foo"
222 );
223 assert_eq!(
224 PyFixedUnicode([0x1F980, 0x1F40D, 0, 0, 0, 0]).to_string(),
225 "🦀🐍"
226 );
227 assert_eq!(
228 PyFixedUnicode([b'f' as _, b'o' as _, b'o' as _, b'b' as _, b'a' as _, b'r' as _])
229 .to_string(),
230 "foobar"
231 );
232 }
233}