Skip to main content

extxyz_types/
lib.rs

1#![allow(clippy::match_bool)]
2
3use std::{borrow::Cow, collections::HashMap, ops::Deref};
4
5/// checking special characters escape and escape as needed, using Cow because most string won't
6/// need quoting.
7#[must_use]
8pub fn escape(s: &str) -> Cow<'_, str> {
9    let needs_quoting = s.chars().any(|c| {
10        matches!(
11            c,
12            '"' | '\\' | '\n' | ' ' | '=' | ',' | '[' | ']' | '{' | '}'
13        )
14    });
15
16    if !needs_quoting {
17        return Cow::Borrowed(s);
18    }
19
20    // +4 is a fair guess for capacity with x2 quotes and possibly 2 escapes
21    let mut out = String::with_capacity(s.len() + 4);
22    out.push('"');
23    for c in s.chars() {
24        match c {
25            '\n' => out.push_str("\\n"),
26            '\\' => out.push_str("\\\\"),
27            '"' => out.push_str("\\\""),
28            _ => out.push(c),
29        }
30    }
31    out.push('"');
32
33    Cow::Owned(out)
34}
35
36/// A newtype wrapper around `i32` that dereferences to `i32`.
37///
38/// # Deref coercion
39///
40/// `Integer` implements `Deref<Target = i32>`, allowing `&Integer` to be used
41/// wherever `&i32` is expected.
42///
43/// ```
44/// use extxyz_types::Integer;
45///
46/// fn takes_i32(x: &i32) {}
47///
48/// let n = Integer::from(42);
49/// takes_i32(&n);
50/// ```
51#[derive(Debug, Default, Copy, Clone)]
52pub struct Integer(i32);
53
54/// A newtype wrapper around `f64` that dereferences to `f64`.
55///
56/// # Deref coercion
57///
58/// `FloatNum` implements `Deref<Target = f64>`, allowing `&FloatNum` to be used
59/// wherever `&f64` is expected.
60///
61/// ```
62/// use extxyz_types::FloatNum;
63///
64/// fn takes_f64(x: &f64) {}
65///
66/// let x = FloatNum::from(3.14);
67/// takes_f64(&x);
68/// ```
69#[derive(Debug, Default, Copy, Clone)]
70pub struct FloatNum(f64);
71
72/// A newtype wrapper around `bool` that dereferences to `bool`.
73///
74/// # Deref coercion
75///
76/// `Boolean` implements `Deref<Target = bool>`, allowing `&Boolean` to be used
77/// wherever `&bool` is expected.
78///
79/// ```
80/// use extxyz_types::Boolean;
81///
82/// fn takes_bool(x: &bool) {}
83///
84/// let b = Boolean::from(true);
85/// takes_bool(&b);
86/// ```
87#[derive(Debug, Default, Copy, Clone)]
88pub struct Boolean(bool);
89
90/// A newtype wrapper around `String` that dereferences to `str`.
91///
92/// # Deref coercion
93///
94/// `Text` implements `Deref<Target = str>`, allowing `&Text` to be used
95/// wherever `&str` is expected.
96///
97/// ```
98/// use extxyz_types::Text;
99///
100/// fn takes_str(s: &str) {}
101///
102/// let t = Text::from("hello");
103/// takes_str(&t);
104/// ```
105#[derive(Debug, Default, Clone)]
106pub struct Text(String);
107
108impl Deref for Integer {
109    type Target = i32;
110
111    fn deref(&self) -> &Self::Target {
112        &self.0
113    }
114}
115impl Deref for FloatNum {
116    type Target = f64;
117
118    fn deref(&self) -> &Self::Target {
119        &self.0
120    }
121}
122impl Deref for Boolean {
123    type Target = bool;
124
125    fn deref(&self) -> &Self::Target {
126        &self.0
127    }
128}
129impl Deref for Text {
130    type Target = str;
131
132    fn deref(&self) -> &Self::Target {
133        &self.0
134    }
135}
136
137impl From<i32> for Integer {
138    fn from(value: i32) -> Self {
139        Self(value)
140    }
141}
142impl From<f64> for FloatNum {
143    fn from(value: f64) -> Self {
144        Self(value)
145    }
146}
147impl From<bool> for Boolean {
148    fn from(value: bool) -> Self {
149        Self(value)
150    }
151}
152impl From<String> for Text {
153    fn from(value: String) -> Self {
154        Self(value)
155    }
156}
157impl From<&str> for Text {
158    fn from(value: &str) -> Self {
159        Self(value.to_string())
160    }
161}
162
163impl std::fmt::Display for Integer {
164    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
165        write!(f, "{}", self.0)
166    }
167}
168impl std::fmt::Display for FloatNum {
169    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
170        // default .8 precision and no other formatter if not override
171        if f.precision().is_some() {
172            std::fmt::Display::fmt(&self.0, f)
173        } else {
174            write!(f, "{:.8}", self.0)
175        }
176    }
177}
178impl std::fmt::Display for Boolean {
179    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
180        match self.0 {
181            true => write!(f, "T"),
182            false => write!(f, "F"),
183        }
184    }
185}
186impl std::fmt::Display for Text {
187    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
188        let escaped = escape(&self.0);
189        f.pad(&escaped)
190    }
191}
192
193/// A dynamically-typed container for extended XYZ property values.
194///
195/// `Value` represents the different data types that can appear in extended
196/// XYZ metadata or per-atom properties. It supports scalar values, vectors,
197/// and matrices across several primitive types.
198///
199/// # Variants
200/// ## Scalar values
201/// - `Integer`: A single integer value.
202/// - `Float`: A single floating-point value.
203/// - `Bool`: A boolean value.
204/// - `Str`: A string value.
205///
206/// ## Vector values
207/// - `VecInteger(Vec<Integer>, u32)`: A vector of integers and its length.
208/// - `VecFloat(Vec<FloatNum>, u32)`: A vector of floats and its length.
209/// - `VecBool(Vec<Boolean>, u32)`: A vector of booleans and its length.
210/// - `VecText(Vec<Text>, u32)`: A vector of strings and its length.
211///
212/// ## Matrix values
213/// - `MatrixInteger(Vec<Vec<Integer>>, (u32, u32))`: A 2D array of integers
214///   with shape `(rows, cols)`.
215/// - `MatrixFloat(Vec<Vec<FloatNum>>, (u32, u32))`: A 2D array of floats
216///   with shape `(rows, cols)`.
217/// - `MatrixBool(Vec<Vec<Boolean>>, (u32, u32))`: A 2D array of booleans
218///   with shape `(rows, cols)`.
219/// - `MatrixText(Vec<Vec<Text>>, (u32, u32))`: A 2D array of strings
220///   with shape `(rows, cols)`.
221///
222/// ## Fallback
223/// - `Unsupported`: Represents values that could not be parsed or are not
224///   supported by the current implementation. This is also the default variant.
225///
226/// # Notes
227/// - Vector variants store their length explicitly to preserve shape
228///   information from the original input.
229/// - Matrix variants store both the data and its `(rows, cols)` dimensions.
230/// - This enum is designed for flexibility when parsing loosely-typed
231///   formats such as extended XYZ.
232///
233/// # Derives
234/// - `Debug`, `Clone`, and `Default` are implemented.
235/// - The default value is [`Value::Unsupported`].
236#[derive(Debug, Clone, Default)]
237pub enum Value {
238    Integer(Integer),
239    Float(FloatNum),
240    Bool(Boolean),
241    Str(Text),
242    VecInteger(Vec<Integer>, u32),
243    VecFloat(Vec<FloatNum>, u32),
244    VecBool(Vec<Boolean>, u32),
245    VecText(Vec<Text>, u32),
246    MatrixInteger(Vec<Vec<Integer>>, (u32, u32)),
247    MatrixFloat(Vec<Vec<FloatNum>>, (u32, u32)),
248    MatrixBool(Vec<Vec<Boolean>>, (u32, u32)),
249    MatrixText(Vec<Vec<Text>>, (u32, u32)),
250    #[default]
251    Unsupported,
252}
253
254impl Value {
255    /// Attempts to extract the underlying integer value.
256    ///
257    /// Consumes `self` and returns the contained [`Integer`] if this is
258    /// [`Value::Integer`], otherwise returns `None`.
259    ///
260    /// # Examples
261    /// ```
262    /// use extxyz_types::{Value, Integer};
263    ///
264    /// let v = Value::Integer(Integer::from(42));
265    /// ```
266    pub fn as_integer(self) -> Option<Integer> {
267        match self {
268            Value::Integer(i) => Some(i),
269            _ => None,
270        }
271    }
272
273    /// Attempts to extract the underlying floating-point value.
274    ///
275    /// Consumes `self` and returns the contained [`FloatNum`] if this is
276    /// [`Value::Float`], otherwise returns `None`.
277    ///
278    /// # Examples
279    /// ```
280    /// use extxyz_types::{Value, FloatNum};
281    ///
282    /// let v = Value::Float(FloatNum::from(3.14));
283    /// ```
284    pub fn as_float(self) -> Option<FloatNum> {
285        match self {
286            Value::Float(i) => Some(i),
287            _ => None,
288        }
289    }
290
291    /// Attempts to extract the underlying boolean value.
292    ///
293    /// Consumes `self` and returns the contained [`Boolean`] if this is
294    /// [`Value::Bool`], otherwise returns `None`.
295    pub fn as_bool(self) -> Option<Boolean> {
296        match self {
297            Value::Bool(i) => Some(i),
298            _ => None,
299        }
300    }
301
302    /// Attempts to extract the underlying string value.
303    ///
304    /// Consumes `self` and returns the contained [`Text`] if this is
305    /// [`Value::Str`], otherwise returns `None`.
306    pub fn as_text(self) -> Option<Text> {
307        match self {
308            Value::Str(i) => Some(i),
309            _ => None,
310        }
311    }
312}
313
314impl std::fmt::Display for Value {
315    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
316        fn fmt_array<T: std::fmt::Display>(arr: &[T]) -> String {
317            arr.iter()
318                .map(std::string::ToString::to_string)
319                .collect::<Vec<_>>()
320                .join(", ")
321        }
322
323        fn fmt_matrix<T: std::fmt::Display>(matrix: &[Vec<T>]) -> String {
324            matrix
325                .iter()
326                .map(|row| format!("[{}]", fmt_array(row)))
327                .collect::<Vec<_>>()
328                .join(", ")
329        }
330
331        match self {
332            Value::Integer(v) => write!(f, "{v}"),
333            Value::Float(v) => write!(f, "{v}"),
334            Value::Bool(v) => write!(f, "{v}"),
335            Value::Str(v) => write!(f, "{v}"),
336            Value::VecInteger(arr, _) => write!(f, "[{}]", fmt_array(arr)),
337            Value::VecFloat(arr, _) => write!(f, "[{}]", fmt_array(arr)),
338            Value::VecBool(arr, _) => write!(f, "[{}]", fmt_array(arr)),
339            Value::VecText(arr, _) => write!(f, "[{}]", fmt_array(arr)),
340            Value::MatrixInteger(matrix, _) => write!(f, "[{}]", fmt_matrix(matrix)),
341            Value::MatrixFloat(matrix, _) => write!(f, "[{}]", fmt_matrix(matrix)),
342            Value::MatrixBool(matrix, _) => write!(f, "[{}]", fmt_matrix(matrix)),
343            Value::MatrixText(matrix, _) => write!(f, "[{}]", fmt_matrix(matrix)),
344            Value::Unsupported => write!(f, "<unsupported>"),
345        }
346    }
347}
348
349// Safe hardler for `DictEntry`
350#[derive(Debug)]
351pub struct DictHandler(pub Vec<(String, Value)>);
352
353impl DictHandler {
354    /// Get the value by key.
355    /// Since internally extxyz dict stores not as a real hashmap but a linklist,
356    /// and the lookup takes O(N).
357    #[must_use]
358    pub fn get(&self, key: &str) -> Option<&Value> {
359        for (k, v) in &self.0 {
360            if k.as_str() == key {
361                return Some(v);
362            }
363        }
364
365        None
366    }
367}
368
369impl<'a> DictHandler {
370    /// return an iter of `&(String, Value)`
371    pub fn iter(&'a self) -> std::slice::Iter<'a, (String, Value)> {
372        self.into_iter()
373    }
374}
375
376impl<'a> IntoIterator for &'a DictHandler {
377    type Item = &'a (String, Value);
378    type IntoIter = std::slice::Iter<'a, (String, Value)>;
379
380    fn into_iter(self) -> Self::IntoIter {
381        self.0.iter()
382    }
383}
384
385/// A raw frame parsed from an `extxyz` file.
386///
387/// This struct represents the data for a single frame of an `extxyz` file,
388/// including the number of atoms, metadata, and per-atom arrays.  
389///
390/// You can iterate over the per-atom arrays directly:
391/// ```ignore
392/// for (name, value) in frame.arrs() {
393///     println!("{name}: {value:?}");
394/// }
395/// ```
396///
397/// Or convert the metadata info into a `HashMap` for easy lookup:
398/// ```ignore
399/// let info_map = frame.info();
400/// if let Some(temperature) = info_map.get("temperature") {
401///     println!("Temperature: {:?}", temperature);
402/// }
403/// ```
404#[derive(Debug)]
405pub struct Frame {
406    natoms: u32,
407    info: DictHandler,
408    arrs: DictHandler,
409}
410
411impl Frame {
412    #[must_use]
413    pub fn new(natoms: u32, info: Vec<(String, Value)>, arrs: Vec<(String, Value)>) -> Self {
414        Self {
415            natoms,
416            info: DictHandler(info),
417            arrs: DictHandler(arrs),
418        }
419    }
420
421    /// Returns the number of atoms in the frame.
422    #[must_use]
423    pub fn natoms(&self) -> u32 {
424        self.natoms
425    }
426
427    /// override comment, if not exist, create the comment in the info field
428    pub fn set_comment(&mut self, comment: &str) {
429        let newv = Value::Str(Text::from(comment));
430
431        if let Some((_, value)) = self.info.0.iter_mut().find(|(k, _)| k == "comment") {
432            *value = newv;
433        } else {
434            self.info.0.push(("comment".to_string(), newv));
435        };
436    }
437
438    /// Returns the frame metadata (`arrs`) as a `HashMap` for easy lookup.
439    ///
440    /// Keys are `&str` slices pointing to the original `String`s inside
441    /// `DictHandler`, and values are references to `Value`.
442    ///
443    /// # Example
444    /// ```ignore
445    /// let arrs_map = frame.arrs();
446    /// if let Some(pos) = arrs_map.get("pos") {
447    ///     println!("Positions: {:?}", pos);
448    /// }
449    /// ```
450    #[must_use]
451    pub fn arrs(&self) -> HashMap<&str, &Value> {
452        let arrs = self.arrs.iter().map(|(k, v)| (k.as_str(), v));
453        arrs.collect::<HashMap<_, _>>()
454    }
455
456    /// Returns the frame metadata (`info`) as a `HashMap` for easy lookup.
457    ///
458    /// Keys are `&str` slices pointing to the original `String`s inside
459    /// `DictHandler`, and values are references to `Value`.
460    ///
461    /// # Example
462    /// ```ignore
463    /// let info_map = frame.info();
464    /// if let Some(temperature) = info_map.get("temperature") {
465    ///     println!("Temperature: {:?}", temperature);
466    /// }
467    /// ```
468    #[must_use]
469    pub fn info(&self) -> HashMap<&str, &Value> {
470        self.info
471            .iter()
472            .map(|(k, v)| (k.as_str(), v))
473            .collect::<HashMap<_, _>>()
474    }
475
476    /// Return info as `Vec<(&str, &Value)>` keep the original order
477    pub fn info_orderd(&self) -> Vec<(&str, &Value)> {
478        self.info
479            .iter()
480            .map(|(k, v)| (k.as_str(), v))
481            .collect::<Vec<(_, _)>>()
482    }
483
484    /// Return arrs as `Vec<(&str, &Value)>` keep the original order
485    pub fn arrs_orderd(&self) -> Vec<(&str, &Value)> {
486        self.arrs
487            .iter()
488            .map(|(k, v)| (k.as_str(), v))
489            .collect::<Vec<(_, _)>>()
490    }
491}