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}