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#[derive(Clone, Copy)]
42pub(crate) enum CifValueRef<'a> {
43    Inapplicable,
44    Unknown,
45    Text(&'a str),
46    Integer(i64),
47    Float(f64),
48}
49
50impl<'a> CifValueRef<'a> {
51    pub(crate) fn parse(text: &'a str, quoting: Quoting) -> Self {
52        if quoting != Quoting::Bare {
53            return Self::Text(text);
54        }
55        match text {
56            "." => Self::Inapplicable,
57            "?" => Self::Unknown,
58            _ => {
59                let fractional = text
60                    .as_bytes()
61                    .iter()
62                    .any(|byte| matches!(byte, b'.' | b'e' | b'E'));
63                if fractional {
64                    if let Some(value) = fixed_point(text) {
65                        return Self::Float(value);
66                    }
67                    if let Ok(value) = text.parse::<f64>() {
68                        return Self::Float(value);
69                    }
70                    if let Ok(value) = text.parse::<i64>() {
71                        return Self::Integer(value);
72                    }
73                } else {
74                    if let Ok(value) = text.parse::<i64>() {
75                        return Self::Integer(value);
76                    }
77                    if let Ok(value) = text.parse::<f64>() {
78                        return Self::Float(value);
79                    }
80                }
81                Self::Text(text)
82            }
83        }
84    }
85
86    pub(crate) const fn as_str(self) -> Option<&'a str> {
87        match self {
88            Self::Text(text) => Some(text),
89            _ => None,
90        }
91    }
92
93    pub(crate) fn as_identifier(self) -> Option<Cow<'a, str>> {
94        match self {
95            Self::Text(text) => Some(Cow::Borrowed(text)),
96            Self::Integer(value) => Some(Cow::Owned(value.to_string())),
97            Self::Float(value) => Some(Cow::Owned(value.to_string())),
98            Self::Inapplicable | Self::Unknown => None,
99        }
100    }
101
102    pub(crate) const fn as_integer(self) -> Option<i64> {
103        match self {
104            Self::Integer(value) => Some(value),
105            _ => None,
106        }
107    }
108
109    pub(crate) fn as_float(self) -> Option<f64> {
110        match self {
111            Self::Integer(value) => value.to_f64(),
112            Self::Float(value) => Some(value),
113            _ => None,
114        }
115    }
116
117    pub(crate) const fn is_recorded(self) -> bool {
118        !matches!(self, Self::Inapplicable | Self::Unknown)
119    }
120}
121
122impl CifValue {
123    /// Interprets a lexed value, deciding what kind of thing it is.
124    ///
125    /// Only a bare value can be a number or a sentinel. `'.'` written in quotes
126    /// is the one-character string, not the sentinel, and the format is explicit
127    /// about the difference.
128    #[must_use]
129    pub fn parse(text: &str, quoting: Quoting) -> Self {
130        Self::parse_with_text(text, quoting, |text| Arc::from(text))
131    }
132
133    pub(crate) fn parse_with_text(
134        text: &str,
135        quoting: Quoting,
136        retain: impl FnOnce(&str) -> Arc<str>,
137    ) -> Self {
138        match CifValueRef::parse(text, quoting) {
139            CifValueRef::Inapplicable => Self::Inapplicable,
140            CifValueRef::Unknown => Self::Unknown,
141            CifValueRef::Text(text) => Self::Text(retain(text)),
142            CifValueRef::Integer(value) => Self::Integer(value),
143            CifValueRef::Float(value) => Self::Float(value),
144        }
145    }
146
147    /// The value as text, or `None` when it is a sentinel.
148    #[must_use]
149    pub fn as_str(&self) -> Option<&str> {
150        match self {
151            Self::Text(text) => Some(text),
152            _ => None,
153        }
154    }
155
156    /// The value as an identifier, whatever kind of thing it was written as.
157    ///
158    /// Identifiers in this format are frequently numeric — entity numbers and
159    /// chain labels especially — and reading them only as text drops them
160    /// silently. Nothing is allocated unless the value really was a number.
161    #[must_use]
162    pub fn as_identifier(&self) -> Option<Cow<'_, str>> {
163        match self {
164            Self::Text(text) => Some(Cow::Borrowed(text)),
165            Self::Integer(value) => Some(Cow::Owned(value.to_string())),
166            Self::Float(value) => Some(Cow::Owned(value.to_string())),
167            Self::Inapplicable | Self::Unknown => None,
168        }
169    }
170
171    /// The value as a whole number, where it is one.
172    #[must_use]
173    pub fn as_integer(&self) -> Option<i64> {
174        match self {
175            Self::Integer(value) => Some(*value),
176            _ => None,
177        }
178    }
179
180    /// The value as a number, whether it was written with a fractional part.
181    #[must_use]
182    pub fn as_float(&self) -> Option<f64> {
183        match self {
184            Self::Integer(value) => value.to_f64(),
185            Self::Float(value) => Some(*value),
186            _ => None,
187        }
188    }
189
190    /// Returns true for a value that was recorded.
191    #[must_use]
192    pub const fn is_recorded(&self) -> bool {
193        !matches!(self, Self::Inapplicable | Self::Unknown)
194    }
195}
196
197/// One item's values, in row order.
198#[derive(Clone, Debug, Default)]
199pub struct Column {
200    values: Vec<CifValue>,
201    quoting: Vec<Quoting>,
202}
203
204impl Column {
205    /// Reserves space for additional values and their original quoting.
206    pub fn reserve(&mut self, additional: usize) {
207        self.values.reserve(additional);
208        self.quoting.reserve(additional);
209    }
210
211    /// Appends a value and how it was written.
212    pub fn push(&mut self, value: CifValue, quoting: Quoting) {
213        self.values.push(value);
214        self.quoting.push(quoting);
215    }
216
217    /// The number of rows.
218    #[must_use]
219    pub fn len(&self) -> usize {
220        self.values.len()
221    }
222
223    /// Returns true when the column holds nothing.
224    #[must_use]
225    pub fn is_empty(&self) -> bool {
226        self.values.is_empty()
227    }
228
229    /// The value in one row.
230    #[must_use]
231    pub fn get(&self, row: usize) -> Option<&CifValue> {
232        self.values.get(row)
233    }
234
235    /// How the value in one row was written, so a preserving write can put it
236    /// back the way it found it.
237    #[must_use]
238    pub fn quoting(&self, row: usize) -> Option<Quoting> {
239        self.quoting.get(row).copied()
240    }
241
242    /// Every value, in row order.
243    pub fn iter(&self) -> impl Iterator<Item = &CifValue> {
244        self.values.iter()
245    }
246}
247
248/// One category and its items.
249#[derive(Clone, Debug)]
250pub struct Category {
251    name: Box<str>,
252    columns: IndexMap<Box<str>, Column>,
253    span: ByteSpan,
254}
255
256impl Category {
257    /// Creates an empty category.
258    #[must_use]
259    pub fn new(name: impl Into<Box<str>>, span: ByteSpan) -> Self {
260        Self {
261            name: name.into(),
262            columns: IndexMap::new(),
263            span,
264        }
265    }
266
267    /// The category's name, without a leading underscore.
268    #[must_use]
269    pub fn name(&self) -> &str {
270        &self.name
271    }
272
273    /// Where the category was found.
274    #[must_use]
275    pub const fn span(&self) -> ByteSpan {
276        self.span
277    }
278
279    /// The number of rows, taken from the longest item.
280    #[must_use]
281    pub fn row_count(&self) -> usize {
282        // A category with no items has no rows, rather than an unknown number.
283        let Some(rows) = self.columns.values().map(Column::len).max() else {
284            return 0;
285        };
286        rows
287    }
288
289    /// The number of items.
290    #[must_use]
291    pub fn len(&self) -> usize {
292        self.columns.len()
293    }
294
295    /// Returns true when the category holds no items.
296    #[must_use]
297    pub fn is_empty(&self) -> bool {
298        self.columns.is_empty()
299    }
300
301    /// One item's values.
302    #[must_use]
303    pub fn column(&self, item: &str) -> Option<&Column> {
304        self.columns.get(item)
305    }
306
307    /// One item's values, creating the item if it is new.
308    pub fn column_mut(&mut self, item: &str) -> &mut Column {
309        if let Some(index) = self.columns.get_index_of(item) {
310            return &mut self.columns[index];
311        }
312        let (index, _) = self.columns.insert_full(item.into(), Column::default());
313        &mut self.columns[index]
314    }
315
316    /// The item names, in the order the file gave them.
317    pub fn items(&self) -> impl Iterator<Item = &str> {
318        self.columns.keys().map(Box::as_ref)
319    }
320
321    /// One value, by item and row.
322    #[must_use]
323    pub fn value(&self, item: &str, row: usize) -> Option<&CifValue> {
324        self.column(item)?.get(row)
325    }
326
327    /// One value as text, by item and row.
328    #[must_use]
329    pub fn text(&self, item: &str, row: usize) -> Option<&str> {
330        self.value(item, row)?.as_str()
331    }
332
333    /// One value as an identifier, by item and row.
334    #[must_use]
335    pub fn identifier(&self, item: &str, row: usize) -> Option<Cow<'_, str>> {
336        self.value(item, row)?.as_identifier()
337    }
338}
339
340/// One data block.
341#[derive(Clone, Debug)]
342pub struct DataBlock {
343    name: Box<str>,
344    categories: IndexMap<Box<str>, Category>,
345}
346
347impl DataBlock {
348    /// Creates an empty block.
349    #[must_use]
350    pub fn new(name: impl Into<Box<str>>) -> Self {
351        Self {
352            name: name.into(),
353            categories: IndexMap::new(),
354        }
355    }
356
357    /// The block's name.
358    #[must_use]
359    pub fn name(&self) -> &str {
360        &self.name
361    }
362
363    /// One category.
364    #[must_use]
365    pub fn category(&self, name: &str) -> Option<&Category> {
366        self.categories.get(name)
367    }
368
369    /// One category, creating it if it is new.
370    pub fn category_mut(&mut self, name: &str, span: ByteSpan) -> &mut Category {
371        if let Some(index) = self.categories.get_index_of(name) {
372            return &mut self.categories[index];
373        }
374        let (index, _) = self
375            .categories
376            .insert_full(name.into(), Category::new(name, span));
377        &mut self.categories[index]
378    }
379
380    /// The categories, in the order the file gave them.
381    pub fn categories(&self) -> impl Iterator<Item = &Category> {
382        self.categories.values()
383    }
384
385    /// The number of categories.
386    #[must_use]
387    pub fn len(&self) -> usize {
388        self.categories.len()
389    }
390
391    /// Returns true when the block holds no categories.
392    #[must_use]
393    pub fn is_empty(&self) -> bool {
394        self.categories.is_empty()
395    }
396}
397
398/// A whole file, losslessly.
399///
400/// # Examples
401///
402/// ```
403/// use molframe_cif::{Document, parse};
404/// use molframe_core::io::InputBuffer;
405///
406/// let input = InputBuffer::from_bytes(b"data_test\n_entry.id 1ABC\n".to_vec());
407/// let (document, _) = parse(&input)?;
408///
409/// let id = document.first_block().and_then(|block| block.category("entry"))
410///     .and_then(|entry| entry.text("id", 0));
411/// assert_eq!(id, Some("1ABC"));
412/// # Ok::<(), Vec<molframe_core::diagnostic::Diagnostic>>(())
413/// ```
414#[derive(Clone, Debug, Default)]
415pub struct Document {
416    blocks: Vec<DataBlock>,
417}
418
419impl Document {
420    /// Creates an empty document.
421    #[must_use]
422    pub const fn new() -> Self {
423        Self { blocks: Vec::new() }
424    }
425
426    /// Appends a block.
427    pub fn push(&mut self, block: DataBlock) {
428        self.blocks.push(block);
429    }
430
431    /// The first block, which is the one a single-entry file has.
432    #[must_use]
433    pub fn first_block(&self) -> Option<&DataBlock> {
434        self.blocks.first()
435    }
436
437    /// The last block, which is the one still being filled while parsing.
438    pub fn last_block_mut(&mut self) -> Option<&mut DataBlock> {
439        self.blocks.last_mut()
440    }
441
442    /// Every block, in order.
443    pub fn blocks(&self) -> impl Iterator<Item = &DataBlock> {
444        self.blocks.iter()
445    }
446
447    /// The number of blocks.
448    #[must_use]
449    pub fn len(&self) -> usize {
450        self.blocks.len()
451    }
452
453    /// Returns true when the document holds no blocks.
454    #[must_use]
455    pub fn is_empty(&self) -> bool {
456        self.blocks.is_empty()
457    }
458}
459
460#[cfg(test)]
461#[path = "model_tests.rs"]
462mod tests;