Refactor dice amount parser to reusable parsers.

This follows the example in the Combine documentation to use impl
trait return types so we can isolate the parsers into their own
functions.
This commit is contained in:
projectmoon 2021-02-03 23:21:37 +00:00
parent b32b761f82
commit b7ccd4e7ad
1 changed files with 61 additions and 62 deletions

View File

@ -1,4 +1,6 @@
use combine::error::ParseError;
use combine::parser::char::{digit, letter, spaces}; use combine::parser::char::{digit, letter, spaces};
use combine::stream::Stream;
use combine::{many, many1, one_of, Parser}; use combine::{many, many1, one_of, Parser};
use thiserror::Error; use thiserror::Error;
@ -73,53 +75,71 @@ pub struct Amount {
pub element: Element, pub element: Element,
} }
/// Converts "+" or" -" into an Operator. No sign at all is an implied /// Parser that attempt to convert the text at the start of the dice
/// Plus. Part of the parser. /// parsing into an Amount instance.
fn map_operator(sign: char) -> Operator { fn first_amount_parser<Input>() -> impl Parser<Input, Output = ParseResult<Amount>>
match sign { where
Input: Stream<Token = char>,
Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
{
let map_first_amount = |value: String| {
if value.chars().all(char::is_numeric) {
let num = value.parse::<i32>()?;
Ok(Amount {
operator: Operator::Plus,
element: Element::Number(num),
})
} else {
Ok(Amount {
operator: Operator::Plus,
element: Element::Variable(value),
})
}
};
many1(letter())
.or(many1(digit()))
.skip(spaces().silent()) //Consume any space after first amount
.map(map_first_amount)
}
/// Attempt to convert some text in the middle or end of the dice roll
/// string into an Amount.
fn amount_parser<Input>() -> impl Parser<Input, Output = ParseResult<Amount>>
where
Input: Stream<Token = char>,
Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
{
let plus_or_minus = one_of("+-".chars());
let parse_operator = plus_or_minus.map(|sign: char| match sign {
'+' => Operator::Plus, '+' => Operator::Plus,
'-' => Operator::Minus, '-' => Operator::Minus,
_ => Operator::Plus, _ => Operator::Plus,
} });
}
/// Attempt to convert the text at the start of the expression, // Element must either be a proper i32, or a variable name.
/// extracted by the Combine parser, into an Amount instance. Part of let map_element = |value: String| -> ParseResult<Element> {
/// the parser. if value.chars().all(char::is_numeric) {
fn map_first_amount(value: String) -> ParseResult<Amount> { let num = value.parse::<i32>()?;
if value.chars().all(char::is_numeric) { Ok(Element::Number(num))
let num = value.parse::<i32>()?; } else {
Ok(Amount { Ok(Element::Variable(value))
operator: Operator::Plus, }
element: Element::Number(num), };
})
} else {
Ok(Amount {
operator: Operator::Plus,
element: Element::Variable(value),
})
}
}
/// Attempt to convert some text in the middle or end of the string, let parse_element = many1(letter()).or(many1(digit())).map(map_element);
/// extracted by the Combine parser, into an Element. Part of the
/// parser.
fn map_element(value: String) -> ParseResult<Element> {
if value.chars().all(char::is_numeric) {
let num = value.parse::<i32>()?;
Ok(Element::Number(num))
} else {
Ok(Element::Variable(value))
}
}
/// Collect an Operator and Element into an Amount, but only if the let element_parser = parse_operator
/// Element was successfully parsed. Part of the parser. .skip(spaces().silent())
fn map_amount((operator, element_result): (Operator, ParseResult<Element>)) -> ParseResult<Amount> { .and(parse_element)
match element_result { .skip(spaces().silent());
let convert_to_amount = |(operator, element_result)| match element_result {
Ok(element) => Ok(Amount { operator, element }), Ok(element) => Ok(Amount { operator, element }),
Err(e) => Err(e), Err(e) => Err(e),
} };
element_parser.map(convert_to_amount)
} }
/// Parse an expression of numbers and/or variables into elements /// Parse an expression of numbers and/or variables into elements
@ -135,29 +155,8 @@ fn map_amount((operator, element_result): (Operator, ParseResult<Element>)) -> P
pub fn parse_amounts(input: &str) -> ParseResult<Vec<Amount>> { pub fn parse_amounts(input: &str) -> ParseResult<Vec<Amount>> {
let input = input.trim(); let input = input.trim();
// Single sub-parser for the first amount expression, because it's let remaining_amounts = many(amount_parser()).map(|amounts: Vec<ParseResult<Amount>>| amounts);
// easier. Wraps into ParseResult. let mut parser = first_amount_parser().and(remaining_amounts);
let first_amount = many1(letter())
.or(many1(digit()))
.skip(spaces().silent()) //Consume any space after first amount
.map(map_first_amount);
// All of this, down to amount_parser, will convert expressions
// after the first into Amounts (wrapped in a ParseResult).
let plus_or_minus = one_of("+-".chars());
let maybe_sign = plus_or_minus.map(map_operator);
let variable_or_number = many1(letter()).or(many1(digit())).map(map_element);
let element_parser = maybe_sign
.skip(spaces().silent())
.and(variable_or_number)
.skip(spaces().silent());
let amount_parser = element_parser.map(map_amount);
let remaining_amounts = many(amount_parser).map(|amounts: Vec<ParseResult<Amount>>| amounts);
let mut parser = first_amount.and(remaining_amounts);
// Collapses first amount + remaining amounts into a single Vec, // Collapses first amount + remaining amounts into a single Vec,
// while collecting extraneous input. // while collecting extraneous input.