use crate::error::{DiceError, Result};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParsedRoll {
pub dice: Vec<String>,
pub modifiers: Vec<i64>,
}
pub fn parse_roll_exprs(tokens: &[&str]) -> Result<ParsedRoll> {
if tokens.is_empty() {
return Err(DiceError::InvalidArguments(
"expected at least one roll token".to_string(),
));
}
let mut dice = Vec::new();
let mut modifiers = Vec::new();
for token in tokens {
if let Ok(value) = token.parse::<i64>() {
modifiers.push(value);
continue;
}
dice.extend(parse_die_token(token)?);
}
if dice.is_empty() && modifiers.is_empty() {
return Err(DiceError::InvalidExpression(
"expression did not contain dice or modifiers".to_string(),
));
}
Ok(ParsedRoll { dice, modifiers })
}
pub fn parse_dice_only_exprs(tokens: &[&str]) -> Result<Vec<String>> {
if tokens.is_empty() {
return Err(DiceError::InvalidArguments(
"expected at least one dice expression".to_string(),
));
}
let mut dice = Vec::new();
for token in tokens {
if token.parse::<i64>().is_ok() {
return Err(DiceError::InvalidExpression(format!(
"modifiers are not allowed in dice-only expressions: {token}"
)));
}
dice.extend(parse_die_token(token)?);
}
Ok(dice)
}
fn parse_die_token(token: &str) -> Result<Vec<String>> {
let digit_end = token
.char_indices()
.find(|(_, c)| !c.is_ascii_digit())
.map(|(index, _)| index)
.unwrap_or(token.len());
if digit_end == token.len() {
return Err(DiceError::InvalidExpression(format!(
"expected a die expression like d6 or 2custom, got {token}"
)));
}
let count = if digit_end == 0 {
1
} else {
token[..digit_end]
.parse::<usize>()
.map_err(|_| DiceError::InvalidExpression(format!("invalid dice count in {token}")))?
};
if count == 0 {
return Err(DiceError::InvalidFaceCount);
}
let die_name = &token[digit_end..];
if die_name.trim().is_empty() {
return Err(DiceError::InvalidExpression(format!(
"missing die name in expression {token}"
)));
}
Ok((0..count).map(|_| die_name.to_string()).collect())
}
#[cfg(test)]
mod tests {
use super::{parse_dice_only_exprs, parse_roll_exprs};
use crate::error::DiceError;
#[test]
fn parse_roll_exprs_expands_dice_and_modifiers() {
let parsed = parse_roll_exprs(&["3d6", "2d20", "d100", "2custom", "5", "-3"]).unwrap();
assert_eq!(
parsed.dice,
vec!["d6", "d6", "d6", "d20", "d20", "d100", "custom", "custom"]
);
assert_eq!(parsed.modifiers, vec![5, -3]);
}
#[test]
fn parse_dice_only_rejects_modifiers() {
let err = parse_dice_only_exprs(&["2d6", "-3"]).unwrap_err();
assert!(matches!(err, DiceError::InvalidExpression(message) if message.contains("-3")));
}
}