rusty_systems/interpretation/abop/
parser.rs

1//! Parsing grammars from *the Algorithmic Beauty of Plants*
2//!
3//! # The format
4//!
5//! ```plant
6//! # Comments start with a hash.
7//! # This describes plant 5 in fig 1.24 of ABOP (pg 25)
8//! n = 6           # Number of derivation iterations. This is one more iteration than in ABOP
9//! delta = 22.5    # The angle that the + and - tokens turn the "turtle" by.
10//!
11//! initial: X      # The starting string
12//!
13//! # And now the productions
14//! Forward -> Forward Forward
15//! X -> Forward + [ [ X ] - X ] - Forward [ - Forward X ] + X
16//! ```
17//!
18//! # Parsing
19//!
20//! If we have a string in the format given above, you can parse it like so:
21//!
22//! ```
23//! use rusty_systems::interpretation::abop::parser::parse;
24//! # let plant_string = "initial: X\nX -> F F";
25//!
26//! let (interpretation, system, initial_string) = parse(plant_string).unwrap();
27//!
28//! ```
29//!
30//! The [`parse`] function returns the following:
31//!
32//! * `interpretation`, being a [`AbopTurtleInterpretation`]. If you want to output SVG from
33//!    this, see [`SvgPathInterpretation`].
34//! * `system`, being a [`System`] ready to run.
35//! * `initial_string`, being the [`ProductionString`] the file specifies as the initial string.
36
37use crate::error::ErrorKind;
38use crate::interpretation::abop::*;
39use crate::parser::parse_prod_string;
40
41/// A tuple containing information parsed from a string or file containing an L-System
42/// specified in this library's "plant" format.
43///
44/// * `interpretation`, being a [`AbopTurtleInterpretation`]. If you want to output SVG from
45///    this, see [`SvgPathInterpretation`].
46/// * `system`, being a [`System`] ready to run.
47/// * `initial_string`, being the [`ProductionString`] the file specifies as the initial string.
48///
49/// You can use destructuring to easily access the tuple members:
50///
51/// ```
52/// use rusty_systems::interpretation::abop::parser::parse;
53/// # let plant_string = "initial: X\nX -> F F";
54///
55/// let (interpretation, system, initial_string) = parse(plant_string).unwrap();
56/// ```
57type ParsedAbop = (AbopTurtleInterpretation, System, ProductionString);
58
59/// Parses a string in a bespoke "plant" format. See the [namespace](crate::interpretation::abop::parser)
60/// namespace documentation for more information.
61///
62/// See [`parse_file`] to parse a file containing a string in this format.
63pub fn parse(string: &str) -> crate::Result<ParsedAbop> {
64    let string = string.trim();
65    if string.is_empty() {
66        return Err(Error::new(ErrorKind::Parse, "String should not be empty"));
67    }
68
69    let mut lines = string.lines().peekable();
70
71    let mut n = 2_usize;
72    let mut delta = 5.0_f32;
73
74    let system = AbopTurtleInterpretation::system()?;
75    let mut prod_count = 0_usize;
76    let mut initial : Option<&str> = None;
77
78    #[allow(clippy::while_let_on_iterator)]
79    while let Some(line) = lines.next() {
80        let line = remove_comment(line);
81        if line.is_empty() {
82            continue;
83        }
84
85        if is_equality_line(line) {
86            let equality = parse_equality(line)?;
87            match equality.name {
88                "n" | "N" => {
89                    n = equality.value.parse()?;
90                }
91                "d" | "D" | "delta" | "∂" => {
92                    delta = equality.value.parse()?;
93                }
94                _ => return Err(Error::new(ErrorKind::Parse, format!("Unrecognised line {}", line)))
95            }
96
97            continue;
98        }
99
100        if is_initial(line) {
101            initial = Some(parse_initial(line));
102            continue;
103        }
104
105        prod_count += 1;
106        system.parse_production(line)?;
107    }
108
109    if prod_count == 0 {
110        return Err(Error::new(ErrorKind::Parse, "No productions have been supplied"));
111    }
112
113    if initial.is_none() {
114        return Err(Error::new(ErrorKind::Parse, "No initial axiom has been supplied"));
115    }
116
117    let initial = parse_prod_string(initial.unwrap())?;
118
119    let interpretation = AbopTurtleInterpretation::new(n, delta);
120    Ok((interpretation, system, initial))
121}
122
123/// Reads a file containing an L-System written in the library's bespoke "plant" format.
124///
125/// See the [module documentation](crate::interpretation::abop::parser) for more information,
126/// as well as the [`parser`] function.
127pub fn parse_file<P: AsRef<std::path::Path>>(name: P) -> crate::Result<ParsedAbop> {
128    let contents = std::fs::read_to_string(name)?;
129    parse(&contents)
130}
131
132
133struct EqualityLine<'a> {
134    pub name: &'a str,
135    pub value: &'a str
136}
137
138fn is_equality_line(line: &str) -> bool {
139    line.contains('=')
140}
141
142fn is_initial(line: &str) -> bool {
143    line.trim().starts_with("initial:")
144}
145
146fn parse_initial(line: &str) -> &str {
147    let parts: Vec<_> = line.splitn(2, ':').collect();
148    parts[1].trim()
149}
150
151fn parse_equality(line: &str) -> crate::Result<EqualityLine> {
152    let parts: Vec<&str> = line.splitn(2, '=').collect();
153    if parts.len() != 2 {
154        return Err(Error::general("Invalid equality line"));
155    }
156    let name = parts[0].trim();
157    let value = parts[1].trim();
158    Ok(EqualityLine { name, value })
159}
160
161fn remove_comment(line: &str) -> &str {
162    line.split('#').next().unwrap().trim()
163}
164
165
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170
171    static GENERAL : &str = "# Totally for testing purposes
172n = 6
173delta = 22.5
174
175initial: X # Here we go
176# Start on a line
177
178Forward -> Forward Forward
179X -> Forward + [ [ X ] - X ] - Forward [ - Forward X ] + X
180
181# ENDED";
182
183
184    #[test]
185    fn test_parsing() {
186
187        let result = parse(GENERAL);
188        assert!(result.is_ok());
189
190        let (_, system, ..) = result.unwrap();
191        assert_eq!(system.production_len(), 2);
192
193        // The test data does not add any more tokens to the system than the family does.
194        assert_eq!(system.symbol_len(), AbopTurtleInterpretation::system().unwrap().symbol_len());
195
196    }
197}