Skip to main content

molframe_cif/document/
model.rs

1//! The lossless layer: what the file said, before anything interprets it.
2//!
3//! A document keeps every category, every item and the order they appeared in,
4//! including the ones molframe has no interpretation for. That is what lets a file
5//! survive a read and a write even where this library predates the dictionary
6//! extension it uses, and it is the only layer that can honestly claim to
7//! preserve what was written.
8//!
9//! Values keep the distinction the format draws between "does not apply", "not
10//! recorded" and "here is the value", because those mean different things and
11//! collapsing them into one absent value throws away a statement the depositor
12//! made deliberately.
13
14use super::number::fixed_point;
15use crate::lexer::Quoting;
16use indexmap::IndexMap;
17use molframe_core::span::ByteSpan;
18use num_traits::ToPrimitive;
19use std::borrow::Cow;
20use std::sync::Arc;
21
22/// One value of one item.
23#[derive(Clone, PartialEq, Debug)]
24pub enum CifValue {
25    /// Written `.` — the item does not apply here.
26    Inapplicable,
27    /// Written `?` — the value exists but was not recorded.
28    Unknown,
29    /// Text.
30    Text(Arc<str>),
31    /// A whole number.
32    ///
33    /// Kept as an integer rather than as a float: a serial number past two to
34    /// the fifty-third would otherwise stop round-tripping exactly, and the
35    /// files that reach those numbers are precisely the large ones.
36    Integer(i64),
37    /// A number with a fractional part.
38    Float(f64),
39}
40
41/// True when the text starts like a number, so `nan`, `inf` and `infinity`,
42/// which the float parser accepts, are never mistaken for one.
43fn numeric_lead(text: &str) -> bool {
44    let digits = match text.as_bytes() {
45        [b'+' | b'-', rest @ ..] => rest,
46        all => all,
47    };
48    matches!(digits.first(), Some(b'0'..=b'9' | b'.'))
49}
50
51/// True when `value` renders back to exactly `text`, decided without rendering.
52///
53/// `i64` parsing accepts a leading `+` and leading zeros; the canonical
54/// spelling has neither, and `-0` is not the spelling of zero.
55fn spells_integer(text: &str, value: i64) -> bool {
56    let digits = match text.strip_prefix('-') {
57        Some(digits) => digits,
58        None => text,
59    };
60    let canonical = !digits.starts_with('+') && (digits == "0" || !digits.starts_with('0'));
61    canonical && (value != 0 || text == "0")
62}
63
64#[derive(Clone, Copy)]
65pub(crate) enum CifValueRef<'a> {
66    Inapplicable,
67    Unknown,
68    Text(&'a str),
69    Integer(i64),
70    Float(f64),
71}
72
73impl<'a> CifValueRef<'a> {
74    pub(crate) fn parse(text: &'a str, quoting: Quoting) -> Self {
75        if quoting != Quoting::Bare {
76            return Self::Text(text);
77        }
78        match text {
79            "." => Self::Inapplicable,
80            "?" => Self::Unknown,
81            _ => {
82                if text.as_bytes().contains(&b'.') {
83                    if let Some(value) = fixed_point(text) {
84                        return Self::Float(value);
85                    }
86                    if numeric_lead(text)
87                        && let Ok(value) = text.parse::<f64>()
88                    {
89                        return Self::Float(value);
90                    }
91                } else if let Ok(value) = text.parse::<i64>()
92                    && spells_integer(text, value)
93                {
94                    // Only a numeral that its value spells back exactly is a
95                    // number. `098`, `+5` and `0E0` are names that happen to
96                    // look numeric, and re-rendering them would silently turn
97                    // one identifier into another.
98                    return Self::Integer(value);
99                }
100                Self::Text(text)
101            }
102        }
103    }
104
105    pub(crate) const fn as_str(self) -> Option<&'a str> {
106        match self {
107            Self::Text(text) => Some(text),
108            _ => None,
109        }
110    }
111
112    pub(crate) fn as_identifier(self) -> Option<Cow<'a, str>> {
113        match self {
114            Self::Text(text) => Some(Cow::Borrowed(text)),
115            Self::Integer(value) => Some(Cow::Owned(value.to_string())),
116            Self::Float(value) => Some(Cow::Owned(value.to_string())),
117            Self::Inapplicable | Self::Unknown => None,
118        }
119    }
120
121    pub(crate) fn as_integer(self) -> Option<i64> {
122        match self {
123            Self::Integer(value) => Some(value),
124            Self::Text(text) if numeric_lead(text) => text.parse().ok(),
125            _ => None,
126        }
127    }
128
129    pub(crate) fn as_float(self) -> Option<f64> {
130        match self {
131            Self::Integer(value) => value.to_f64(),
132            Self::Float(value) => Some(value),
133            Self::Text(text) if numeric_lead(text) => text.parse().ok(),
134            _ => None,
135        }
136    }
137
138    pub(crate) const fn is_recorded(self) -> bool {
139        !matches!(self, Self::Inapplicable | Self::Unknown)
140    }
141}
142
143impl CifValue {
144    /// Interprets a lexed value, deciding what kind of thing it is.
145    ///
146    /// Only a bare value can be a number or a sentinel. `'.'` written in quotes
147    /// is the one-character string, not the sentinel, and the format is explicit
148    /// about the difference.
149    #[must_use]
150    pub fn parse(text: &str, quoting: Quoting) -> Self {
151        Self::parse_with_text(text, quoting, |text| Arc::from(text))
152    }
153
154    pub(crate) fn parse_with_text(
155        text: &str,
156        quoting: Quoting,
157        retain: impl FnOnce(&str) -> Arc<str>,
158    ) -> Self {
159        match CifValueRef::parse(text, quoting) {
160            CifValueRef::Inapplicable => Self::Inapplicable,
161            CifValueRef::Unknown => Self::Unknown,
162            CifValueRef::Text(text) => Self::Text(retain(text)),
163            CifValueRef::Integer(value) => Self::Integer(value),
164            CifValueRef::Float(value) => Self::Float(value),
165        }
166    }
167
168    /// The value as text, or `None` when it is a sentinel.
169    #[must_use]
170    pub fn as_str(&self) -> Option<&str> {
171        match self {
172            Self::Text(text) => Some(text),
173            _ => None,
174        }
175    }
176
177    /// The value as an identifier, whatever kind of thing it was written as.
178    ///
179    /// Identifiers in this format are frequently numeric — entity numbers and
180    /// chain labels especially — and reading them only as text drops them
181    /// silently. Nothing is allocated unless the value really was a number.
182    #[must_use]
183    pub fn as_identifier(&self) -> Option<Cow<'_, str>> {
184        match self {
185            Self::Text(text) => Some(Cow::Borrowed(text)),
186            Self::Integer(value) => Some(Cow::Owned(value.to_string())),
187            Self::Float(value) => Some(Cow::Owned(value.to_string())),
188            Self::Inapplicable | Self::Unknown => None,
189        }
190    }
191
192    /// The value as a whole number, where it is one.
193    ///
194    /// A numeral that is not its own canonical spelling (`+5`, `007`) is kept
195    /// as text so identifiers survive; it still reads as the number it denotes.
196    #[must_use]
197    pub fn as_integer(&self) -> Option<i64> {
198        match self {
199            Self::Integer(value) => Some(*value),
200            Self::Text(text) if numeric_lead(text) => text.parse().ok(),
201            _ => None,
202        }
203    }
204
205    /// The value as a number, whether it was written with a fractional part.
206    #[must_use]
207    pub fn as_float(&self) -> Option<f64> {
208        match self {
209            Self::Integer(value) => value.to_f64(),
210            Self::Float(value) => Some(*value),
211            Self::Text(text) if numeric_lead(text) => text.parse().ok(),
212            _ => None,
213        }
214    }
215
216    /// Returns true for a value that was recorded.
217    #[must_use]
218    pub const fn is_recorded(&self) -> bool {
219        !matches!(self, Self::Inapplicable | Self::Unknown)
220    }
221}
222
223/// One item's values, in row order.
224#[derive(Clone, Debug, Default)]
225pub struct Column {
226    values: Vec<CifValue>,
227    quoting: Vec<Quoting>,
228}
229
230impl Column {
231    /// Reserves space for additional values and their original quoting.
232    pub fn reserve(&mut self, additional: usize) {
233        self.values.reserve(additional);
234        self.quoting.reserve(additional);
235    }
236
237    /// Appends a value and how it was written.
238    pub fn push(&mut self, value: CifValue, quoting: Quoting) {
239        self.values.push(value);
240        self.quoting.push(quoting);
241    }
242
243    /// The number of rows.
244    #[must_use]
245    pub fn len(&self) -> usize {
246        self.values.len()
247    }
248
249    /// Returns true when the column holds nothing.
250    #[must_use]
251    pub fn is_empty(&self) -> bool {
252        self.values.is_empty()
253    }
254
255    /// The value in one row.
256    #[must_use]
257    pub fn get(&self, row: usize) -> Option<&CifValue> {
258        self.values.get(row)
259    }
260
261    /// How the value in one row was written, so a preserving write can put it
262    /// back the way it found it.
263    #[must_use]
264    pub fn quoting(&self, row: usize) -> Option<Quoting> {
265        self.quoting.get(row).copied()
266    }
267
268    /// Every value, in row order.
269    pub fn iter(&self) -> impl Iterator<Item = &CifValue> {
270        self.values.iter()
271    }
272}
273
274/// One category and its items.
275#[derive(Clone, Debug)]
276pub struct Category {
277    name: Box<str>,
278    columns: IndexMap<Box<str>, Column>,
279    span: ByteSpan,
280}
281
282impl Category {
283    /// Creates an empty category.
284    #[must_use]
285    pub fn new(name: impl Into<Box<str>>, span: ByteSpan) -> Self {
286        Self {
287            name: name.into(),
288            columns: IndexMap::new(),
289            span,
290        }
291    }
292
293    /// The category's name, without a leading underscore.
294    #[must_use]
295    pub fn name(&self) -> &str {
296        &self.name
297    }
298
299    /// Where the category was found.
300    #[must_use]
301    pub const fn span(&self) -> ByteSpan {
302        self.span
303    }
304
305    /// The number of rows, taken from the longest item.
306    #[must_use]
307    pub fn row_count(&self) -> usize {
308        // A category with no items has no rows, rather than an unknown number.
309        let Some(rows) = self.columns.values().map(Column::len).max() else {
310            return 0;
311        };
312        rows
313    }
314
315    /// The number of items.
316    #[must_use]
317    pub fn len(&self) -> usize {
318        self.columns.len()
319    }
320
321    /// Returns true when the category holds no items.
322    #[must_use]
323    pub fn is_empty(&self) -> bool {
324        self.columns.is_empty()
325    }
326
327    /// One item's values.
328    #[must_use]
329    pub fn column(&self, item: &str) -> Option<&Column> {
330        self.columns.get(item)
331    }
332
333    /// One item's values, creating the item if it is new.
334    pub fn column_mut(&mut self, item: &str) -> &mut Column {
335        if let Some(index) = self.columns.get_index_of(item) {
336            return &mut self.columns[index];
337        }
338        let (index, _) = self.columns.insert_full(item.into(), Column::default());
339        &mut self.columns[index]
340    }
341
342    /// The item names, in the order the file gave them.
343    pub fn items(&self) -> impl Iterator<Item = &str> {
344        self.columns.keys().map(Box::as_ref)
345    }
346
347    /// One value, by item and row.
348    #[must_use]
349    pub fn value(&self, item: &str, row: usize) -> Option<&CifValue> {
350        self.column(item)?.get(row)
351    }
352
353    /// One value as text, by item and row.
354    #[must_use]
355    pub fn text(&self, item: &str, row: usize) -> Option<&str> {
356        self.value(item, row)?.as_str()
357    }
358
359    /// One value as an identifier, by item and row.
360    #[must_use]
361    pub fn identifier(&self, item: &str, row: usize) -> Option<Cow<'_, str>> {
362        self.value(item, row)?.as_identifier()
363    }
364}
365
366/// One data block.
367#[derive(Clone, Debug)]
368pub struct DataBlock {
369    name: Box<str>,
370    categories: IndexMap<Box<str>, Category>,
371}
372
373impl DataBlock {
374    /// Creates an empty block.
375    #[must_use]
376    pub fn new(name: impl Into<Box<str>>) -> Self {
377        Self {
378            name: name.into(),
379            categories: IndexMap::new(),
380        }
381    }
382
383    /// The block's name.
384    #[must_use]
385    pub fn name(&self) -> &str {
386        &self.name
387    }
388
389    /// One category.
390    #[must_use]
391    pub fn category(&self, name: &str) -> Option<&Category> {
392        self.categories.get(name)
393    }
394
395    /// One category, creating it if it is new.
396    pub fn category_mut(&mut self, name: &str, span: ByteSpan) -> &mut Category {
397        if let Some(index) = self.categories.get_index_of(name) {
398            return &mut self.categories[index];
399        }
400        let (index, _) = self
401            .categories
402            .insert_full(name.into(), Category::new(name, span));
403        &mut self.categories[index]
404    }
405
406    /// Removes one category and hands it over, keeping the order of the rest.
407    ///
408    /// Moving a category out costs `O(items)` and copies none of its values.
409    pub fn take_category(&mut self, name: &str) -> Option<Category> {
410        self.categories.shift_remove(name)
411    }
412
413    /// The categories, in the order the file gave them.
414    pub fn categories(&self) -> impl Iterator<Item = &Category> {
415        self.categories.values()
416    }
417
418    /// The number of categories.
419    #[must_use]
420    pub fn len(&self) -> usize {
421        self.categories.len()
422    }
423
424    /// Returns true when the block holds no categories.
425    #[must_use]
426    pub fn is_empty(&self) -> bool {
427        self.categories.is_empty()
428    }
429}
430
431/// A whole file, losslessly.
432///
433/// # Examples
434///
435/// ```
436/// use molframe_cif::{Document, parse};
437/// use molframe_core::io::InputBuffer;
438///
439/// let input = InputBuffer::from_bytes(b"data_test\n_entry.id 1ABC\n".to_vec());
440/// let (document, _) = parse(&input)?;
441///
442/// let id = document.first_block().and_then(|block| block.category("entry"))
443///     .and_then(|entry| entry.text("id", 0));
444/// assert_eq!(id, Some("1ABC"));
445/// # Ok::<(), Vec<molframe_core::diagnostic::Diagnostic>>(())
446/// ```
447#[derive(Clone, Debug, Default)]
448pub struct Document {
449    blocks: Vec<DataBlock>,
450}
451
452impl Document {
453    /// Creates an empty document.
454    #[must_use]
455    pub const fn new() -> Self {
456        Self { blocks: Vec::new() }
457    }
458
459    /// Appends a block.
460    pub fn push(&mut self, block: DataBlock) {
461        self.blocks.push(block);
462    }
463
464    /// The first block, which is the one a single-entry file has.
465    #[must_use]
466    pub fn first_block(&self) -> Option<&DataBlock> {
467        self.blocks.first()
468    }
469
470    /// The last block, which is the one still being filled while parsing.
471    pub fn last_block_mut(&mut self) -> Option<&mut DataBlock> {
472        self.blocks.last_mut()
473    }
474
475    /// Every block, in order.
476    pub fn blocks(&self) -> impl Iterator<Item = &DataBlock> {
477        self.blocks.iter()
478    }
479
480    /// Every block, in order, for taking categories out of them.
481    pub fn blocks_mut(&mut self) -> impl Iterator<Item = &mut DataBlock> {
482        self.blocks.iter_mut()
483    }
484
485    /// The number of blocks.
486    #[must_use]
487    pub fn len(&self) -> usize {
488        self.blocks.len()
489    }
490
491    /// Returns true when the document holds no blocks.
492    #[must_use]
493    pub fn is_empty(&self) -> bool {
494        self.blocks.is_empty()
495    }
496}
497
498#[cfg(test)]
499#[path = "model_tests.rs"]
500mod tests;