use crate::die::{Die, Roll, Rollable};
use crate::expr::tokenizer::{Token, Tokenizer};
use anyhow::{Result, anyhow};
use std::cmp::Ordering;
use std::fmt::{self, Display, Formatter};
#[derive(Debug, Default)]
pub struct ExpressionParser {
ops: Vec<RollOperation>,
tokens: Vec<Token>,
cursor: usize,
}
impl ExpressionParser {
pub fn new(input: impl AsRef<str>) -> Self {
let tokens = Tokenizer::new(input.as_ref()).tokens().collect();
Self {
tokens,
..Self::default()
}
}
fn current(&self) -> &Token {
self.tokens.get(self.cursor).unwrap_or(&Token::End)
}
fn next(&mut self) {
self.cursor += 1;
}
fn peek(&mut self) -> &Token {
self.tokens.get(self.cursor + 1).unwrap_or(&Token::End)
}
pub fn parse(mut self) -> Result<Vec<RollOperation>> {
match self.current() {
Token::End => Err(anyhow!("No tokens to consume")),
token if token.is_empty() => Err(anyhow!("No tokens to consume")),
_ => self.parse_expression(),
}
.map(|()| {
if self.ops.is_empty() {
Err(anyhow!("Nothing returned while parsing expression"))
} else if !self.current().is_end() {
Err(anyhow!("Did not parse entire expression"))
} else {
Ok(self.ops)
}
})?
}
fn parse_expression(&mut self) -> Result<()> {
if self.current().is_die() {
self.parse_die_expression()
} else if self.current().is_int() && self.peek().is_die() {
self.parse_dice_expression()
} else {
self.parse_fixed_expression()
}
}
fn parse_die_expression(&mut self) -> Result<()> {
let r = self.parse_die()?;
self.ops.push(RollOperation(r, Op::Add));
self.next();
while self.current().is_plus() || self.current().is_minus() {
let op = Op::try_from(self.current())?;
self.next();
let r = self.parse_bare_rollable()?;
self.ops.push(RollOperation(r, op));
self.next();
}
Ok(())
}
fn parse_dice_expression(&mut self) -> Result<()> {
let r = self.parse_dice()?;
self.ops.push(RollOperation(r, Op::Add));
self.next();
while self.current().is_plus() || self.current().is_minus() {
let op = Op::try_from(self.current())?;
self.next();
let r = self.parse_bare_rollable()?;
self.ops.push(RollOperation(r, op));
self.next();
}
Ok(())
}
fn parse_fixed_expression(&mut self) -> Result<()> {
eprintln!("Parsing fixed expression with token: {}", self.current());
if self.current().is_plus() {
self.parse_plus_expression()
} else if self.current().is_minus() {
self.parse_minus_expression()
} else {
self.parse_inferred_plus_expression()
}
}
fn parse_plus_expression(&mut self) -> Result<()> {
self.next();
self.parse_arithmetic_expression(Op::Add)
}
fn parse_inferred_plus_expression(&mut self) -> Result<()> {
self.parse_arithmetic_expression(Op::Add)
}
fn parse_minus_expression(&mut self) -> Result<()> {
self.next();
self.parse_arithmetic_expression(Op::Sub)
}
fn parse_arithmetic_expression(&mut self, op: Op) -> Result<()> {
let r = if let Token::Int(n) = self.current() {
Ok(Rollable::from(*n))
} else {
Err(anyhow!("Expected integer, got {} instead", self.current()))
}?;
self.ops.push(RollOperation(r, op));
self.next();
while self.current().is_plus() || self.current().is_minus() {
let op = Op::try_from(self.current())?;
self.next();
let r = self.parse_bare_rollable()?;
self.ops.push(RollOperation(r, op));
self.next();
}
Ok(())
}
fn parse_die(&self) -> Result<Rollable> {
Rollable::try_from(self.current())
}
fn parse_dice(&mut self) -> Result<Rollable> {
let quantity = self.current().clone();
self.next();
let die = self.current().clone();
let dice = format!("{quantity}{die}");
Rollable::try_from(dice)
}
fn parse_fixed(&self) -> Result<Rollable> {
Rollable::try_from(self.current())
}
fn parse_bare_rollable(&mut self) -> Result<Rollable> {
if self.current().is_plus() || self.current().is_minus() {
Err(anyhow!("Unexpected token: {}", self.current()))
} else if self.current().is_die() {
self.parse_die()
} else if self.current().is_int() && self.peek().is_die() {
self.parse_dice()
} else {
self.parse_fixed()
}
}
}
#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
pub enum Op {
Add,
Sub,
}
impl Op {
pub fn operand(&self, n: &Roll) -> Roll {
assert!(*n >= 0, "n must be >= 0, not {n}");
match self {
Op::Add => *n,
Op::Sub => -n,
}
}
}
impl Display for Op {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::Add => write!(f, "+"),
Self::Sub => write!(f, "-"),
}
}
}
impl TryFrom<String> for Op {
type Error = anyhow::Error;
fn try_from(value: String) -> Result<Self, Self::Error> {
Self::try_from(value.as_str())
}
}
impl TryFrom<&str> for Op {
type Error = anyhow::Error;
fn try_from(value: &str) -> Result<Self, Self::Error> {
match value {
"+" => Ok(Op::Add),
"-" => Ok(Op::Sub),
_ => Err(anyhow!("Unknown value: {value}")),
}
}
}
impl TryFrom<Token> for Op {
type Error = anyhow::Error;
fn try_from(value: Token) -> Result<Self, Self::Error> {
Self::try_from(&value)
}
}
impl TryFrom<&Token> for Op {
type Error = anyhow::Error;
fn try_from(value: &Token) -> Result<Self, Self::Error> {
match value {
Token::Plus => Ok(Op::Add),
Token::Minus => Ok(Op::Sub),
_ => Err(anyhow!("Could not convert \"{value}\" to Op")),
}
}
}
impl TryFrom<Token> for Rollable {
type Error = anyhow::Error;
fn try_from(value: Token) -> Result<Self, Self::Error> {
Rollable::try_from(&value)
}
}
impl TryFrom<&Token> for Rollable {
type Error = anyhow::Error;
fn try_from(value: &Token) -> Result<Self, Self::Error> {
match value {
Token::Die(sides) => Ok(Self::Dice(Die::try_from(sides)?, 1)),
Token::Int(n) => Ok(Self::Fixed(*n)),
_ => Err(anyhow!("Cannot create rollable from {value}")),
}
}
}
#[derive(Debug, Eq)]
pub struct RollOperation(pub Rollable, pub Op);
impl PartialEq for RollOperation {
fn eq(&self, other: &Self) -> bool {
let RollOperation(lhs, _) = self;
let RollOperation(rhs, _) = other;
lhs == rhs
}
}
impl PartialOrd for RollOperation {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for RollOperation {
fn cmp(&self, other: &Self) -> Ordering {
let RollOperation(lhs, _) = self;
let RollOperation(rhs, _) = other;
lhs.cmp(rhs)
}
}
#[cfg(test)]
mod tests {
mod unspaced_expression_parsing {
use super::super::{ExpressionParser, Op, RollOperation};
use crate::die::{Die, Rollable};
fn operations(expr: impl AsRef<str>) -> Vec<RollOperation> {
ExpressionParser::new(expr.as_ref())
.parse()
.expect(&format!("could not parse expression: {}", expr.as_ref()))
}
macro_rules! die_add {
($die:ident) => {
RollOperation(Rollable::Dice(Die::$die, 1), Op::Add)
};
}
macro_rules! dice_add {
($die:ident, $quantity:expr) => {
RollOperation(Rollable::Dice(Die::$die, $quantity), Op::Add)
};
}
macro_rules! fixed_add {
($value:expr) => {
RollOperation(Rollable::Fixed($value), Op::Add)
};
}
macro_rules! fixed_sub {
($value:expr) => {
RollOperation(Rollable::Fixed($value), Op::Sub)
};
}
#[test]
fn it_spaces_an_expression_with_a_die_and_a_modifier() {
let actual = operations("d20+4");
assert_eq!(actual, vec![die_add!(D20), fixed_add!(4)]);
}
#[test]
fn it_spaces_an_expression_with_a_single_dice_and_a_modifier() {
let actual = operations("1d20+4");
assert_eq!(actual, vec![die_add!(D20), fixed_add!(4)]);
}
#[test]
fn it_spaces_an_expression_with_a_dice_and_a_modifier() {
let actual = operations("2d10+4");
assert_eq!(actual, vec![dice_add!(D10, 2), fixed_add!(4)]);
}
#[test]
fn it_spaces_an_expression_with_a_dice_and_a_negative_modifier() {
let actual = operations("2d10-4");
assert_eq!(actual, vec![dice_add!(D10, 2), fixed_sub!(4)]);
}
#[test]
fn it_spaces_an_expression_with_a_die_and_a_negative_modifier() {
let actual = operations("d20-4");
assert_eq!(actual, vec![die_add!(D20), fixed_sub!(4)]);
}
#[test]
fn it_spaces_an_expression_with_a_leading_modifier() {
let actual = operations("4+2d10");
assert_eq!(actual, vec![fixed_add!(4), dice_add!(D10, 2)]);
}
#[test]
fn it_spaces_an_expression_with_multiple_modifiers() {
let actual = operations("2d10+4+6+2");
assert_eq!(
actual,
vec![
dice_add!(D10, 2),
fixed_add!(4),
fixed_add!(6),
fixed_add!(2),
]
);
}
#[test]
fn it_spaces_an_expression_with_multiple_mixed_modifiers() {
let actual = operations("2d10-4+6+2");
assert_eq!(
actual,
vec![
dice_add!(D10, 2),
fixed_sub!(4),
fixed_add!(6),
fixed_add!(2),
]
);
}
#[test]
fn it_spaces_an_expression_with_multiple_negative_modifiers() {
let actual = operations("2d10-4-6-2");
assert_eq!(
actual,
vec![
dice_add!(D10, 2),
fixed_sub!(4),
fixed_sub!(6),
fixed_sub!(2),
]
);
}
#[test]
fn it_spaces_an_expression_with_a_leading_plus() {
let actual = operations("+4+2d10");
assert_eq!(actual, vec![fixed_add!(4), dice_add!(D10, 2)]);
}
#[test]
fn it_spaces_an_expression_with_a_leading_minus() {
let actual = operations("-4+2d10");
assert_eq!(actual, vec![fixed_sub!(4), dice_add!(D10, 2)]);
}
}
}