ca_formats/apgcode.rs
1//! Parsers for [apgcode](https://www.conwaylife.com/wiki/Apgcode) format
2//! and [Extended Wechsler format](https://www.conwaylife.com/wiki/Apgcode#Extended_Wechsler_Format).
3
4use crate::Coordinates;
5use displaydoc::Display;
6use std::str::Bytes;
7use thiserror::Error;
8
9/// Errors that can be returned when parsing a apgcode string.
10#[derive(Clone, Debug, Eq, Error, Display, PartialEq)]
11pub enum Error {
12 /// Unexpected character: {0}.
13 UnexpectedChar(char),
14 /// Pattern not encoded in extended Wechsler format
15 Unencodable,
16}
17
18/// A parser for [Extended Wechsler format](https://www.conwaylife.com/wiki/Apgcode#Extended_Wechsler_Format).
19///
20/// Extended Wechsler format is the part of apgcode that encodes the cells in the pattern,
21/// e.g. `153` in `xq4_153`.
22///
23/// As an iterator, it iterates over the living cells.
24///
25/// Reading from files is not supported, since the format is usually short and not stored in a file.
26///
27/// # Example
28///
29/// ```rust
30/// use ca_formats::apgcode::Wechsler;
31///
32/// const GLIDER: &str = "153";
33///
34/// let glider = Wechsler::new(GLIDER);
35///
36/// let cells = glider.map(|cell| cell.unwrap()).collect::<Vec<_>>();
37/// assert_eq!(cells, vec![(0, 0), (1, 0), (1, 2), (2, 0), (2, 1)]);
38/// ```
39#[must_use]
40#[derive(Clone, Debug)]
41pub struct Wechsler<'a> {
42 /// An iterator over bytes of the string.
43 bytes: Bytes<'a>,
44
45 /// Coordinates of the current cell.
46 position: Coordinates,
47
48 /// The current strip of 5 cells, represented by 1 character in Extended Wechsler format.
49 current_strip: u8,
50
51 /// Index of the current cell in the current strip.
52 index: u8,
53}
54
55impl<'a> Wechsler<'a> {
56 /// Creates a new parser instance from a string.
57 pub fn new(string: &'a str) -> Self {
58 Wechsler {
59 bytes: string.bytes(),
60 position: (0, 0),
61 current_strip: 0,
62 index: 5,
63 }
64 }
65}
66
67/// An iterator over living cells in a string in Extended Wechsler format.
68impl<'a> Iterator for Wechsler<'a> {
69 type Item = Result<Coordinates, Error>;
70
71 fn next(&mut self) -> Option<Self::Item> {
72 loop {
73 if self.index < 5 {
74 loop {
75 if self.current_strip & 1 << self.index == 0 {
76 self.index += 1;
77 if self.index == 5 {
78 self.position.0 += 1;
79 break;
80 }
81 } else {
82 let cell = (self.position.0, self.position.1 + self.index as i64);
83 self.index += 1;
84 if self.index == 5 {
85 self.position.0 += 1;
86 }
87 return Some(Ok(cell));
88 }
89 }
90 } else if let Some(c) = self.bytes.next() {
91 match c {
92 b'0' => self.position.0 += 1,
93 b'1'..=b'9' => {
94 self.current_strip = c - b'0';
95 self.index = 0;
96 }
97 b'a'..=b'v' => {
98 self.current_strip = c - b'a' + 10;
99 self.index = 0;
100 }
101 b'w' => self.position.0 += 2,
102 b'x' => self.position.0 += 3,
103 b'y' => {
104 if let Some(c) = self.bytes.next() {
105 let n = match c {
106 b'0'..=b'9' => c - b'0',
107 b'a'..=b'z' => c - b'a' + 10,
108 _ => return Some(Err(Error::UnexpectedChar(char::from(c)))),
109 };
110 self.position.0 += 4 + n as i64
111 } else {
112 return Some(Err(Error::UnexpectedChar('y')));
113 }
114 }
115 b'z' => {
116 self.position.0 = 0;
117 self.position.1 += 5;
118 }
119 _ => return Some(Err(Error::UnexpectedChar(char::from(c)))),
120 }
121 } else {
122 return None;
123 }
124 }
125 }
126}
127
128/// Type of a pattern.
129#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
130pub enum PatternType {
131 /// [Still life](https://conwaylife.com/wiki/Still_life).
132 StillLife,
133 /// [Oscillator](https://conwaylife.com/wiki/Oscillator).
134 Oscillator,
135 /// [Spaceship](https://conwaylife.com/wiki/Spaceship).
136 Spaceship,
137}
138
139/// A parser for [apgcode](https://www.conwaylife.com/wiki/Apgcode) format.
140///
141/// Only supports patterns that are encoded in Extended Wechsler format,
142/// i.e., still lifes, oscillators, spaceships.
143/// Rules with more than 2 states are not yet supported.
144///
145/// As an iterator, it iterates over the living cells.
146///
147/// Reading from files is not supported, since apgcode is usually short and not stored in a file.
148///
149/// # Example
150///
151/// ```rust
152/// use ca_formats::apgcode::{ApgCode, PatternType};
153///
154/// const GLIDER: &str = "xq4_153";
155///
156/// let glider = ApgCode::new(GLIDER).unwrap();
157/// assert_eq!(glider.pattern_type(), PatternType::Spaceship);
158/// assert_eq!(glider.period(), 4);
159///
160/// let cells = glider.map(|cell| cell.unwrap()).collect::<Vec<_>>();
161/// assert_eq!(cells, vec![(0, 0), (1, 0), (1, 2), (2, 0), (2, 1)]);
162/// ```
163#[must_use]
164#[derive(Clone, Debug)]
165pub struct ApgCode<'a> {
166 pattern_type: PatternType,
167 period: u64,
168 wechsler: Wechsler<'a>,
169}
170
171impl<'a> ApgCode<'a> {
172 /// Creates a new parser instance from a string.
173 pub fn new(string: &'a str) -> Result<Self, Error> {
174 let mut split = string.split('_');
175 let prefix = split.next().ok_or(Error::Unencodable)?;
176 if prefix[2..].bytes().any(|c| !c.is_ascii_digit()) {
177 return Err(Error::Unencodable);
178 }
179 let pattern_type = match &prefix[..2] {
180 "xs" => PatternType::StillLife,
181 "xp" => PatternType::Oscillator,
182 "xq" => PatternType::Spaceship,
183 _ => return Err(Error::Unencodable),
184 };
185 let period = if pattern_type == PatternType::StillLife {
186 1
187 } else {
188 prefix[2..].parse().map_err(|_| Error::Unencodable)?
189 };
190 let wechsler_string = split.next().ok_or(Error::Unencodable)?;
191 let wechsler = Wechsler::new(wechsler_string);
192 Ok(ApgCode {
193 pattern_type,
194 period,
195 wechsler,
196 })
197 }
198
199 /// Period of the pattern.
200 pub const fn period(&self) -> u64 {
201 self.period
202 }
203
204 /// Type of the pattern.
205 pub const fn pattern_type(&self) -> PatternType {
206 self.pattern_type
207 }
208}
209
210/// An iterator over living cells in an apgcode string.
211impl<'a> Iterator for ApgCode<'a> {
212 type Item = Result<Coordinates, Error>;
213
214 fn next(&mut self) -> Option<Self::Item> {
215 self.wechsler.next()
216 }
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222
223 #[test]
224 fn wechsler_glider() -> Result<(), Error> {
225 const GLIDER: &str = "153";
226 let glider = Wechsler::new(GLIDER);
227
228 let _ = glider.clone();
229
230 let cells = glider.collect::<Result<Vec<_>, _>>()?;
231 assert_eq!(cells, vec![(0, 0), (1, 0), (1, 2), (2, 0), (2, 1)]);
232 Ok(())
233 }
234
235 #[test]
236 fn wechsler_twin_bees_shuttle() -> Result<(), Error> {
237 const TWIN_BEE_SHUTTLE: &str = "033y133zzzckgsxsgkczz0cc";
238 let twin_bees_shuttle = Wechsler::new(TWIN_BEE_SHUTTLE);
239
240 let cells = twin_bees_shuttle.collect::<Result<Vec<_>, _>>()?;
241 assert_eq!(
242 cells,
243 vec![
244 (1, 0),
245 (1, 1),
246 (2, 0),
247 (2, 1),
248 (8, 0),
249 (8, 1),
250 (9, 0),
251 (9, 1),
252 (0, 17),
253 (0, 18),
254 (1, 17),
255 (1, 19),
256 (2, 19),
257 (3, 17),
258 (3, 18),
259 (3, 19),
260 (7, 17),
261 (7, 18),
262 (7, 19),
263 (8, 19),
264 (9, 17),
265 (9, 19),
266 (10, 17),
267 (10, 18),
268 (1, 27),
269 (1, 28),
270 (2, 27),
271 (2, 28)
272 ]
273 );
274 Ok(())
275 }
276
277 #[test]
278 fn apgcode_glider() -> Result<(), Error> {
279 const GLIDER: &str = "xq4_153";
280 let glider = ApgCode::new(GLIDER)?;
281
282 let _ = glider.clone();
283
284 assert_eq!(glider.pattern_type(), PatternType::Spaceship);
285 assert_eq!(glider.period(), 4);
286
287 let cells = glider.collect::<Result<Vec<_>, _>>()?;
288 assert_eq!(cells, vec![(0, 0), (1, 0), (1, 2), (2, 0), (2, 1)]);
289 Ok(())
290 }
291}