Skip to content

Instantly share code, notes, and snippets.

@huytd
Created May 8, 2022 08:57
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save huytd/2ca7d9b25f539cbcf549f4bd2a19bcca to your computer and use it in GitHub Desktop.
Save huytd/2ca7d9b25f539cbcf549f4bd2a19bcca to your computer and use it in GitHub Desktop.
A simple recursive descent parser in Rust
use std::fmt::Display;
/*
money = currency_symbol number ;
currency_symbol = '$' | '£' | '€' ;
number = INTEGER ;
*/
#[derive(Debug, PartialEq, Clone, Copy)]
enum TokenType {
CurrencySymbol,
Number
}
#[derive(Debug)]
struct Token<'a> {
token_type: TokenType,
content: &'a str
}
impl<'a> Token<'a> {
pub fn new(token_type: TokenType, content: &'a str) -> Self {
Self {
token_type,
content
}
}
}
#[derive(Debug, PartialEq)]
enum Currency {
USD,
GBP,
EUR
}
#[derive(Debug, PartialEq)]
struct MoneyNode {
currency: Currency,
amount: i32
}
#[derive(Debug, PartialEq)]
enum ParseError {
UnexpectedToken(TokenType, TokenType),
InvalidAmount
}
impl Display for ParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::UnexpectedToken(expected, found) =>
write!(f, "Unexpected Token: Expected {:?}. Found {:?}.", expected, found),
Self::InvalidAmount =>
write!(f, "Invalid Amount!"),
}
}
}
type ParseResult<T> = Result<T, ParseError>;
struct Parser<'a> {
tokens: Vec<Token<'a>>,
pos: usize
}
impl<'a> Parser<'a> {
pub fn new(tokens: Vec<Token<'a>>) -> Self {
Self {
tokens,
pos: 0
}
}
fn is_eof(&self) -> bool {
self.pos >= self.tokens.len()
}
fn peek(&self) -> &Token {
&self.tokens[self.pos]
}
fn is_match(&self, token_type: TokenType) -> bool {
!self.is_eof() && self.peek().token_type == token_type
}
fn advance(&mut self) {
self.pos += 1;
}
}
impl<'a> Parser<'a> {
fn parse_amount(&mut self) -> ParseResult<i32> {
let token = self.peek();
if self.is_match(TokenType::Number) {
let result = token.content.parse::<i32>().map_err(|_| ParseError::InvalidAmount);
self.advance();
return result;
}
Err(ParseError::UnexpectedToken(TokenType::Number, token.token_type))
}
fn parse_currency_symbol(&mut self) -> ParseResult<Currency> {
let token = self.peek();
if self.is_match(TokenType::CurrencySymbol) {
let currency_symbol = match token.content {
"$" => Currency::USD,
"£" => Currency::GBP,
_ => Currency::EUR
};
self.advance();
return Ok(currency_symbol);
}
Err(ParseError::UnexpectedToken(TokenType::CurrencySymbol, token.token_type))
}
fn parse_money(&mut self) -> ParseResult<MoneyNode> {
let currency = self.parse_currency_symbol()?;
let amount = self.parse_amount()?;
return Ok(MoneyNode {
currency,
amount
});
}
pub fn parse(&mut self) -> ParseResult<MoneyNode> {
self.parse_money()
}
}
#[test]
fn test_parse_usd() {
let tokens = vec![
Token::new(TokenType::CurrencySymbol, "$"),
Token::new(TokenType::Number, "512")
];
let mut parser = Parser::new(tokens);
assert_eq!(parser.parse(), Ok(MoneyNode {
currency: Currency::USD,
amount: 512
}))
}
#[test]
fn test_parse_eur() {
let tokens = vec![
Token::new(TokenType::CurrencySymbol, "€"),
Token::new(TokenType::Number, "9372")
];
let mut parser = Parser::new(tokens);
assert_eq!(parser.parse(), Ok(MoneyNode {
currency: Currency::EUR,
amount: 9372
}))
}
#[test]
fn test_parse_gbp() {
let tokens = vec![
Token::new(TokenType::CurrencySymbol, "£"),
Token::new(TokenType::Number, "128")
];
let mut parser = Parser::new(tokens);
assert_eq!(parser.parse(), Ok(MoneyNode {
currency: Currency::GBP,
amount: 128
}))
}
#[test]
fn test_parse_unexpected_token() {
let tokens = vec![
Token::new(TokenType::Number, "512"),
Token::new(TokenType::CurrencySymbol, "$"),
];
let mut parser = Parser::new(tokens);
assert_eq!(parser.parse(), Err(ParseError::UnexpectedToken(TokenType::CurrencySymbol, TokenType::Number)))
}
#[test]
fn test_parse_invalid_amount() {
let tokens = vec![
Token::new(TokenType::CurrencySymbol, "$"),
Token::new(TokenType::Number, "3rr0r"),
];
let mut parser = Parser::new(tokens);
assert_eq!(parser.parse(), Err(ParseError::InvalidAmount))
}
fn main() {}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment