pgn4/
from_str.rs

1use fen4::{Position, PositionParseError};
2use std::str::FromStr;
3
4use crate::types::*;
5
6use thiserror::Error;
7
8#[derive(Error, PartialEq, Clone, Debug)]
9pub enum MoveError {
10    #[error("Basic move is malformed.")]
11    Other,
12    #[error("A move starts with O-O, but is not a correct type of move.")]
13    Castle,
14    #[error("Unable to parse basic move because {0}")]
15    PositionInvalid(#[from] PositionParseError),
16}
17impl FromStr for BasicMove {
18    type Err = MoveError;
19    fn from_str(string: &str) -> Result<Self, Self::Err> {
20        let mut iter = string.chars();
21        let start = iter.next().ok_or(MoveError::Other)?;
22        let (piece, pieceless) = if start.is_ascii_lowercase() {
23            ('P', string)
24        } else {
25            (start, iter.as_str())
26        };
27        let mateless = pieceless.trim_end_matches('#');
28        let checkless = mateless.trim_end_matches('+');
29
30        let mates = pieceless.len() - mateless.len();
31        let checks = mateless.len() - checkless.len();
32
33        let (two_pos, promotion) = if let Some(equals) = checkless.find('=') {
34            let (left_over, promote) = checkless.split_at(equals);
35            let mut iter = promote.chars();
36            if iter.next() != Some('=') {
37                return Err(MoveError::Other);
38            }
39            let p = iter.next().ok_or(MoveError::Other)?;
40            if iter.next().is_some() {
41                return Err(MoveError::Other);
42            }
43            (left_over, Some(p))
44        } else {
45            (checkless, None)
46        };
47
48        let loc = if let Some(dash) = two_pos.find('-') {
49            dash
50        } else if let Some(x) = two_pos.find('x') {
51            x
52        } else {
53            return Err(MoveError::Other);
54        };
55        let (left, tmp) = two_pos.split_at(loc);
56        let (mid, mut right) = tmp.split_at(1); // x and - are both ascii and therefore 1 byte
57        let from = left.parse::<Position>()?;
58        let captured = if mid == "x" {
59            let mut iter = right.chars();
60            let start = iter.next().ok_or(MoveError::Other)?;
61            Some(if start.is_ascii_lowercase() {
62                'P'
63            } else {
64                right = iter.as_str();
65                start
66            })
67        } else {
68            None
69        };
70        let to = right.parse::<Position>()?;
71        Ok(BasicMove {
72            piece,
73            from,
74            captured,
75            to,
76            promotion,
77            checks,
78            mates,
79        })
80    }
81}
82
83impl FromStr for Move {
84    type Err = MoveError;
85    fn from_str(string: &str) -> Result<Self, Self::Err> {
86        use Move::*;
87        Ok(match string {
88            "#" => Checkmate,
89            "S" => Stalemate,
90            "T" => Timeout,
91            "R" => Resign,
92            s if s.starts_with("O-O") => {
93                let mateless = s.trim_end_matches('#');
94                let mates = s.len() - mateless.len();
95                match mateless {
96                    "O-O-O" => QueenCastle(mates),
97                    "O-O" => KingCastle(mates),
98                    _ => return Err(MoveError::Castle),
99                }
100            }
101            _ => Normal(string.parse::<BasicMove>()?),
102        })
103    }
104}
105
106struct MovePair {
107    main: Move,
108    modifier: Option<Move>,
109    stalemate: bool,
110}
111
112impl FromStr for MovePair {
113    type Err = MoveError;
114    fn from_str(string: &str) -> Result<Self, Self::Err> {
115        let mut stalemate = false;
116        let break_index = if string.len() == 2 {
117            1 // No move is 2 bytes long
118        } else if string.len() > 2 {
119            if string.ends_with("RS") && !string.ends_with("=RS")
120                || string.ends_with("TS") && !string.ends_with("=TS")
121            {
122                stalemate = true;
123                string.len() - 2
124            } else if (string.ends_with('R') && !string.ends_with("=R"))
125                || (string.ends_with('S') && !string.ends_with("=S"))
126                || (string.ends_with('T') && !string.ends_with("=T"))
127            {
128                string.len() - 1
129            } else {
130                0
131            }
132        } else {
133            0
134        };
135        Ok(if break_index == 0 {
136            Self {
137                main: string.parse()?,
138                modifier: None,
139                stalemate,
140            }
141        } else {
142            Self {
143                main: string.get(..break_index).ok_or(MoveError::Other)?.parse()?,
144                modifier: Some(
145                    string
146                        .get(break_index..(break_index + 1))
147                        .ok_or(MoveError::Other)?
148                        .parse()?,
149                ),
150                stalemate,
151            }
152        })
153    }
154}
155
156#[derive(PartialEq, Clone, Debug)]
157enum IntermediateError {
158    Other(usize),
159    TurnNumber(usize),
160    TurnNumberParse(usize, String),
161    TurnTooLong(usize),
162    MoveErr(MoveError, String, usize),
163    Description(usize),
164}
165
166fn parse_quarter(string: &str) -> Result<(QuarterTurn, &str), IntermediateError> {
167    /// Generally the move is bounded by whitespace, but supporting pgns that don't
168    /// have all the neccessary whitespace is good. Notably, whitespace before a new
169    ///  line number is critical.
170    fn next_move(c: char) -> bool {
171        c.is_whitespace()
172            || match c {
173                '.' | '{' | '(' | ')' => true,
174                _ => false,
175            }
176    }
177    use IntermediateError::*;
178    let trimmed = string.trim_start();
179    if trimmed == "" {
180        return Err(Other(trimmed.len()));
181    }
182    let split = trimmed.find(next_move).unwrap_or(string.len() - 1);
183    let (main_str, mut rest) = trimmed.split_at(split);
184    let move_pair = main_str
185        .trim()
186        .parse::<MovePair>()
187        .map_err(|m| MoveErr(m, main_str.to_owned(), rest.len()))?;
188    let mut description = None;
189    let mut alternatives = Vec::new();
190    rest = rest.trim_start();
191
192    if let Some(c) = rest.chars().next() {
193        if c == '{' {
194            let desc_end = rest.find('}').ok_or(Description(rest.len()))?;
195            let (mut desc_str, rest_tmp) = rest.split_at(desc_end + 1);
196            desc_str = desc_str.strip_prefix("{ ").ok_or(Description(rest.len()))?;
197            desc_str = desc_str.strip_suffix(" }").ok_or(Description(rest.len()))?;
198            description = Some(desc_str.to_owned());
199            rest = rest_tmp;
200        }
201    } else {
202        return Ok((
203            QuarterTurn {
204                main: move_pair.main,
205                modifier: move_pair.modifier,
206                extra_stalemate: move_pair.stalemate,
207                description,
208                alternatives,
209            },
210            rest,
211        ));
212    };
213
214    rest = rest.trim_start();
215
216    while let Some(rest_tmp) = rest.strip_prefix('(') {
217        rest = rest_tmp;
218        let mut turns = Vec::new();
219        while rest.chars().next() != Some(')') {
220            let (turn, rest_tmp) = parse_turn(rest)?;
221            rest = rest_tmp;
222            turns.push(turn);
223        }
224        rest = rest.strip_prefix(')').unwrap().trim_start();
225        alternatives.push(turns);
226    }
227    Ok((
228        QuarterTurn {
229            main: move_pair.main,
230            modifier: move_pair.modifier,
231            extra_stalemate: move_pair.stalemate,
232            description,
233            alternatives,
234        },
235        rest,
236    ))
237}
238
239fn parse_turn(string: &str) -> Result<(Turn, &str), IntermediateError> {
240    use IntermediateError::*;
241    let trimmed = string.trim_start();
242    let dot_loc = trimmed.find('.').ok_or(TurnNumber(trimmed.len()))?;
243    let (number_str, dots) = trimmed.split_at(dot_loc);
244    let number = if number_str == "" {
245        0
246    } else {
247        number_str
248            .parse()
249            .map_err(|_| TurnNumberParse(trimmed.len(), number_str.to_string()))?
250    };
251    let dot = dots.strip_prefix('.').unwrap();
252    let (mut rest, double_dot) = if let Some(dotted) = dot.strip_prefix('.') {
253        (dotted, true)
254    } else {
255        (dot, false)
256    };
257    let mut turns = Vec::new();
258    let for_error = rest.len();
259    let (qturn, rest_tmp) = parse_quarter(rest)?;
260    rest = rest_tmp.trim_start();
261    turns.push(qturn);
262    while let Some(rest_tmp) = rest.strip_prefix("..") {
263        if turns.len() >= 4 {
264            return Err(TurnTooLong(for_error));
265        }
266        let (qturn, rest_tmp) = parse_quarter(rest_tmp)?;
267        rest = rest_tmp.trim_start();
268        turns.push(qturn);
269    }
270    Ok((
271        Turn {
272            number,
273            double_dot,
274            turns,
275        },
276        rest,
277    ))
278}
279
280#[derive(Error, PartialEq, Clone, Debug)]
281pub enum PGN4Error {
282    #[error("Some error occured at {0}")]
283    Other(ErrorLocation),
284    #[error("Expected a turn number starting at {0}, but there isn't a dot")]
285    TurnNumber(ErrorLocation),
286    #[error("Turn number at {0} is malformed \"{1}\" should be a number or \"\"")]
287    TurnNumberParse(ErrorLocation, String),
288    #[error("More than 4 quarter turns are present in the turn starting at {0}")]
289    TurnTooLong(ErrorLocation),
290    #[error("Tag starting at {0} is malformed")]
291    BadTagged(ErrorLocation),
292    #[error("Move \"{1}\" at {2} failed to parse. {0}")]
293    BadMove(MoveError, String, ErrorLocation),
294    #[error("Description starting at {0} is malformed")]
295    BadDescription(ErrorLocation),
296}
297
298#[derive(PartialEq, Clone, Debug)]
299pub struct ErrorLocation {
300    pub line: usize,
301    pub column: usize,
302    pub raw_offset: usize,
303}
304
305impl std::fmt::Display for ErrorLocation {
306    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
307        write!(f, "line {} column {}", self.line, self.column)
308    }
309}
310
311impl FromStr for PGN4 {
312    type Err = PGN4Error;
313    fn from_str(string: &str) -> Result<Self, Self::Err> {
314        let mut bracketed = Vec::new();
315        let mut rest = string;
316        while let Some(rest_tmp) = rest.strip_prefix('[') {
317            let label_end = rest_tmp.find(|c: char| c.is_whitespace()).unwrap_or(0);
318            let (label, middle) = rest_tmp.split_at(label_end);
319            rest = middle
320                .trim_start()
321                .strip_prefix('"')
322                .ok_or_else(|| make_tagged(rest_tmp, string))?;
323
324            let value_end = rest
325                .find('"')
326                .ok_or_else(|| make_tagged(rest_tmp, string))?;
327            let (value, end) = rest.split_at(value_end);
328            rest = end
329                .strip_prefix("\"]")
330                .ok_or_else(|| make_tagged(rest_tmp, string))?
331                .trim_start();
332
333            bracketed.push((label.to_owned(), value.to_owned()));
334        }
335        let mut turns = Vec::new();
336        while rest != "" {
337            let (turn, rest_tmp) = parse_turn(rest).map_err(|ie| add_details(ie, string))?;
338            rest = rest_tmp;
339            turns.push(turn);
340        }
341        Ok(PGN4 { bracketed, turns })
342    }
343}
344
345fn map_location(bytes_left: usize, base: &str) -> ErrorLocation {
346    let front = base.split_at(base.len() - bytes_left).0;
347    let from_last_newline = front.lines().last().unwrap();
348    let line = front.lines().count();
349    ErrorLocation {
350        line,
351        column: from_last_newline.chars().count(),
352        raw_offset: front.len(),
353    }
354}
355
356fn make_tagged(rest: &str, string: &str) -> PGN4Error {
357    PGN4Error::BadTagged(map_location(rest.len(), string))
358}
359
360fn add_details(ie: IntermediateError, string: &str) -> PGN4Error {
361    use IntermediateError::*;
362    match ie {
363        Other(r) => PGN4Error::Other(map_location(r, string)),
364        TurnNumber(r) => PGN4Error::TurnNumber(map_location(r, string)),
365        TurnNumberParse(r, num) => PGN4Error::TurnNumberParse(map_location(r, string), num),
366        TurnTooLong(r) => PGN4Error::TurnTooLong(map_location(r, string)),
367        MoveErr(m, e, r) => PGN4Error::BadMove(m, e, map_location(r, string)),
368        Description(r) => PGN4Error::BadDescription(map_location(r, string)),
369    }
370}