sipp 0.2.1

Simple parser package
Documentation
use std::{fs::File, io::Error, io::ErrorKind};

use sipp::{
    decoder::{ByteStreamCharDecoder, Utf8Decoder},
    parser::Parser,
};

fn main() -> Result<(), Error> {
    // Open the file which contains the Acornsoft Logo commands sequence.
    let file = File::open("examples/Acornsoft_Logo_command_sequence.txt")?;
    // We don't need to check the initial byte sequence for anything, so just wrap the file
    // in a decoder directly. The file was saved using UTF-8 encoding.
    let decoder = Utf8Decoder::wrap(file);
    // Now wrap the decoder in a Parser so that we can extract data from it.
    let mut parser = Parser::wrap(decoder);

    let mut turtle = Turtle::new();
    println!("Initial state of turtle:\n{:?}", turtle);

    // Keep parsing until no content remains.
    while let Some(c) = parser.peek()? {
        if c == '\n' {
            // End of current line, so skip past the newline character.
            parser.read()?;
            continue;
        }
        if c == '#' {
            // This line is just a comment, so skip to the end of the line.
            parser.skip_while(|c| c != '\n')?;
            continue;
        }
        // Any line not blank nor starting with a '#' we expect to be a command. This (very
        // much) simplified application only recognises four commands:
        // DRAW
        // FORWARD X
        // RIGHT D
        // LEFT D
        // where X is a number of pixels, and D is a number of degrees.
        // Because the four commands all start with a different character, this makes it much
        // easier to parse, as we can use this `peek` to see which command to expect.
        match c {
            'D' => parser.require_str("DRAW")?,
            'F' => parse_forward_command(&mut parser, &mut turtle)?,
            'R' => parse_right_command(&mut parser, &mut turtle)?,
            'L' => parse_left_command(&mut parser, &mut turtle)?,
            _ => {
                return Err(Error::new(ErrorKind::InvalidData, "Unrecognised command!"));
            }
        }
        // println!("Turtle status: {:?}", turtle);
    }

    println!("Final state of turtle:\n{:?}", turtle);
    Ok(())
}

fn require_whitespace(parser: &mut Parser<Utf8Decoder<File>, File>) -> Result<(), Error> {
    // Now skip past any whitespace, and check whether or not any whitespace was found.
    let found_space = parser.skip_while(|c| c == ' ')?;
    if !found_space {
        return Err(Error::new(
            std::io::ErrorKind::InvalidData,
            "Whitespace required after instruction!",
        ));
    }
    Ok(())
}

fn require_numeric_value(
    parser: &mut Parser<Utf8Decoder<File>, File>,
    command_name: &'static str,
) -> Result<u16, Error> {
    // Read up to (but not including) the newline character or the end of input (whichever comes
    // first). Note that the newline character will not be removed from the input stream.
    if let Some(variable) = parser.read_up_to('\n')? {
        if let Ok(degrees) = variable.parse::<u16>() {
            Ok(degrees)
        } else {
            Err(Error::new(
                std::io::ErrorKind::InvalidData,
                format!("Invalid number for {} command!", command_name),
            ))
        }
    } else {
        Err(Error::new(
            std::io::ErrorKind::InvalidData,
            format!("Number must follow {} command!", command_name),
        ))
    }
}

fn parse_forward_command(
    parser: &mut Parser<Utf8Decoder<File>, File>,
    turtle: &mut Turtle,
) -> Result<(), Error> {
    // Require that the input sequence contains "FORWARD" and not something else beginning
    // with 'F'.
    parser.require_str("FORWARD")?;
    require_whitespace(parser)?;
    let move_amount = require_numeric_value(parser, "FORWARD")?;
    println!("Turning turtle forward by {} pixels.", move_amount);
    turtle.forward(move_amount);
    Ok(())
}

fn parse_right_command(
    parser: &mut Parser<Utf8Decoder<File>, File>,
    turtle: &mut Turtle,
) -> Result<(), Error> {
    parser.require_str("RIGHT")?;
    require_whitespace(parser)?;
    let degrees = require_numeric_value(parser, "RIGHT")?;
    println!("Turning turtle right by {} degrees.", degrees);
    turtle.right(degrees);
    Ok(())
}

fn parse_left_command(
    parser: &mut Parser<Utf8Decoder<File>, File>,
    turtle: &mut Turtle,
) -> Result<(), Error> {
    parser.require_str("LEFT")?;
    require_whitespace(parser)?;
    let degrees = require_numeric_value(parser, "LEFT")?;
    println!("Turning turtle left by {} degrees.", degrees);
    turtle.left(degrees);
    Ok(())
}

#[derive(Debug)]
struct Turtle {
    heading: u16,
    x_pos: f64,
    y_pos: f64,
}

impl Turtle {
    fn new() -> Turtle {
        Turtle {
            heading: 0,
            x_pos: 0.0,
            y_pos: 0.0,
        }
    }

    fn forward(&mut self, move_amount: u16) {
        let alpha = (self.heading as f64) * std::f64::consts::TAU / 360.0;
        self.x_pos += (move_amount as f64) * alpha.sin();
        self.y_pos += (move_amount as f64) * alpha.cos();
    }

    fn right(&mut self, degrees: u16) {
        self.heading += degrees;
        self.heading %= 360;
    }

    fn left(&mut self, degrees: u16) {
        self.heading -= degrees;
        self.heading %= 360;
    }
}