readcon_core/
iterators.rs

1//=============================================================================
2// The Public API - A clean iterator for users of our library
3//=============================================================================
4
5use crate::parser::parse_single_frame;
6use crate::{error, types};
7use std::iter::Peekable;
8
9/// An iterator that lazily parses simulation frames from a `.con` file's contents.
10///
11/// This struct wraps an iterator over the lines of a string and, upon each iteration,
12/// attempts to parse a complete `ConFrame`. This is the primary interface for reading
13/// data from a `.con` file.
14///
15/// The iterator yields items of type `Result<ConFrame, ParseError>`, allowing for
16/// robust error handling for each frame.
17pub struct ConFrameIterator<'a> {
18    lines: Peekable<std::str::Lines<'a>>,
19}
20
21impl<'a> ConFrameIterator<'a> {
22    /// Creates a new `ConFrameIterator` from a string slice of the entire file.
23    ///
24    /// # Arguments
25    ///
26    /// * `file_contents` - A string slice containing the text of one or more `.con` frames.
27    pub fn new(file_contents: &'a str) -> Self {
28        ConFrameIterator {
29            lines: file_contents.lines().peekable(),
30        }
31    }
32
33    /// Skips the next frame without fully parsing its atomic data.
34    ///
35    /// This is more efficient than `next()` if you only need to advance the
36    /// iterator. It reads the frame's header to determine how many lines to skip.
37    ///
38    /// # Returns
39    ///
40    /// * `Some(Ok(()))` on a successful skip.
41    /// * `Some(Err(ParseError::...))` if there's an error parsing the header.
42    /// * `None` if the iterator is already at the end.
43    pub fn forward(&mut self) -> Option<Result<(), error::ParseError>> {
44        // Skip frame by parsing only required header fields to avoid full parsing overhead
45        if self.lines.peek().is_none() {
46            return None;
47        }
48
49        // Manually consume the first 6 lines of the header, which we don't need for skipping.
50        for _ in 0..6 {
51            if self.lines.next().is_none() {
52                return Some(Err(error::ParseError::IncompleteHeader));
53            }
54        }
55
56        // Line 7: natm_types. We need to parse this.
57        let natm_types: usize = match self.lines.next() {
58            Some(line) => match crate::parser::parse_line_of_n::<usize>(line, 1) {
59                Ok(v) => v[0],
60                Err(e) => return Some(Err(e)),
61            },
62            None => return Some(Err(error::ParseError::IncompleteHeader)),
63        };
64
65        // Line 8: natms_per_type. We need this to sum the total number of atoms.
66        let natms_per_type: Vec<usize> = match self.lines.next() {
67            Some(line) => match crate::parser::parse_line_of_n(line, natm_types) {
68                Ok(v) => v,
69                Err(e) => return Some(Err(e)),
70            },
71            None => return Some(Err(error::ParseError::IncompleteHeader)),
72        };
73
74        // Line 9: masses_per_type. We just need to consume this line.
75        if self.lines.next().is_none() {
76            return Some(Err(error::ParseError::IncompleteHeader));
77        }
78
79        // Calculate how many more lines to skip.
80        let total_atoms: usize = natms_per_type.iter().sum();
81        // For each atom type, there is a symbol line and a "Coordinates..." line.
82        let non_atom_lines = natm_types * 2;
83        let lines_to_skip = total_atoms + non_atom_lines;
84
85        // Advance the iterator by skipping the remaining lines of the frame.
86        for _ in 0..lines_to_skip {
87            if self.lines.next().is_none() {
88                // The file ended before the header's promise was fulfilled.
89                return Some(Err(error::ParseError::IncompleteFrame));
90            }
91        }
92
93        Some(Ok(()))
94    }
95}
96
97impl<'a> Iterator for ConFrameIterator<'a> {
98    /// The type of item yielded by the iterator.
99    ///
100    /// Each item is a `Result` that contains a successfully parsed `ConFrame` or a
101    /// `ParseError` if the frame's data is malformed.
102    type Item = Result<types::ConFrame, error::ParseError>;
103
104    /// Advances the iterator and attempts to parse the next frame.
105    ///
106    /// This method will return `None` only when there are no more lines to consume.
107    /// If there are lines but they do not form a complete frame, it will return
108    /// `Some(Err(ParseError::...))`.
109    fn next(&mut self) -> Option<Self::Item> {
110        // If there are no more lines at all, the iterator is exhausted.
111        if self.lines.peek().is_none() {
112            return None;
113        }
114        // Otherwise, attempt to parse the next frame from the available lines.
115        Some(parse_single_frame(&mut self.lines))
116    }
117}