ca_formats/
plaintext.rs

1//! A parser for [Plaintext](https://www.conwaylife.com/wiki/Plaintext) format.
2
3use crate::{Coordinates, Input};
4use displaydoc::Display;
5use std::io::{BufReader, Error as IoError, Read};
6use thiserror::Error;
7
8/// Errors that can be returned when parsing a Plaintext file.
9#[derive(Debug, Error, Display)]
10pub enum Error {
11    /// Unexpected character: {0}.
12    UnexpectedChar(char),
13    /// Error when reading from input: {0}.
14    IoError(#[from] IoError),
15}
16
17/// A parser for [Plaintext](https://www.conwaylife.com/wiki/Plaintext) format.
18///
19/// As an iterator, it iterates over the living cells.
20///
21/// # Examples
22///
23/// ## Reading from a string:
24///
25/// ```rust
26/// use ca_formats::plaintext::Plaintext;
27///
28/// const GLIDER: &str = r"! Glider
29/// !
30/// .O.
31/// ..O
32/// OOO";
33///
34/// let glider = Plaintext::new(GLIDER).unwrap();
35///
36/// let cells = glider.map(|cell| cell.unwrap()).collect::<Vec<_>>();
37/// assert_eq!(cells, vec![(1, 0), (2, 1), (0, 2), (1, 2), (2, 2)]);
38/// ```
39///
40/// ## Reading from a file:
41///
42/// ``` rust
43/// use std::fs::File;
44/// use ca_formats::plaintext::Plaintext;
45///
46/// let file = File::open("tests/sirrobin.cells").unwrap();
47/// let sirrobin = Plaintext::new_from_file(file).unwrap();
48///
49/// assert_eq!(sirrobin.count(), 282);
50/// ```
51#[must_use]
52#[derive(Debug)]
53pub struct Plaintext<I: Input> {
54    /// An iterator over lines of a Plaintext file.
55    lines: I::Lines,
56
57    /// An iterator over bytes of the current line.
58    current_line: Option<I::Bytes>,
59
60    /// Coordinates of the current cell.
61    position: Coordinates,
62}
63
64impl<I: Input> Plaintext<I> {
65    /// Creates a new parser instance from input.
66    pub fn new(input: I) -> Result<Self, Error> {
67        let mut lines = input.lines();
68        let mut current_line = None;
69        for item in &mut lines {
70            let line = I::line(item)?;
71            if !line.as_ref().starts_with('!') {
72                current_line = Some(I::bytes(line));
73                break;
74            }
75        }
76        Ok(Self {
77            lines,
78            current_line,
79            position: (0, 0),
80        })
81    }
82}
83
84impl<I, L> Plaintext<I>
85where
86    I: Input<Lines = L>,
87    L: Input,
88{
89    /// Parse the remaining unparsed lines as a new Plaintext.
90    pub fn remains(self) -> Result<Plaintext<L>, Error> {
91        Plaintext::new(self.lines)
92    }
93}
94
95impl<R: Read> Plaintext<BufReader<R>> {
96    /// Creates a new parser instance from something that implements [`Read`] trait, e.g., a [`File`](std::fs::File).
97    pub fn new_from_file(file: R) -> Result<Self, Error> {
98        Self::new(BufReader::new(file))
99    }
100}
101
102impl<I: Input> Clone for Plaintext<I>
103where
104    I::Lines: Clone,
105    I::Bytes: Clone,
106{
107    fn clone(&self) -> Self {
108        Self {
109            lines: self.lines.clone(),
110            current_line: self.current_line.clone(),
111            position: self.position,
112        }
113    }
114}
115
116/// An iterator over living cells in a Plaintext file.
117impl<I: Input> Iterator for Plaintext<I> {
118    type Item = Result<Coordinates, Error>;
119
120    fn next(&mut self) -> Option<Self::Item> {
121        loop {
122            if let Some(c) = self.current_line.as_mut().and_then(Iterator::next) {
123                match c {
124                    b'O' | b'*' => {
125                        let cell = self.position;
126                        self.position.0 += 1;
127                        return Some(Ok(cell));
128                    }
129                    b'.' => self.position.0 += 1,
130                    _ if c.is_ascii_whitespace() => continue,
131                    _ => return Some(Err(Error::UnexpectedChar(char::from(c)))),
132                }
133            } else if let Some(item) = self.lines.next() {
134                match I::line(item) {
135                    Ok(line) => {
136                        if line.as_ref().starts_with('!') {
137                            continue;
138                        } else {
139                            self.position.0 = 0;
140                            self.position.1 += 1;
141                            self.current_line = Some(I::bytes(line));
142                        }
143                    }
144                    Err(e) => {
145                        return Some(Err(Error::IoError(e)));
146                    }
147                }
148            } else {
149                return None;
150            }
151        }
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158
159    #[test]
160    fn plaintext_glider() -> Result<(), Error> {
161        const GLIDER: &str = r"!Name: Glider
162!
163.O.
164..O
165OOO";
166
167        let glider = Plaintext::new(GLIDER)?;
168
169        let _ = glider.clone();
170
171        let cells = glider.collect::<Result<Vec<_>, _>>()?;
172        assert_eq!(cells, vec![(1, 0), (2, 1), (0, 2), (1, 2), (2, 2)]);
173        Ok(())
174    }
175}