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}