1use crate::error::{DiceError, Result};
8
9#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct ParsedRoll {
12 pub dice: Vec<String>,
14 pub modifiers: Vec<i64>,
16}
17
18pub fn parse_roll_exprs(tokens: &[&str]) -> Result<ParsedRoll> {
42 if tokens.is_empty() {
43 return Err(DiceError::InvalidArguments(
44 "expected at least one roll token".to_string(),
45 ));
46 }
47
48 let mut dice = Vec::new();
49 let mut modifiers = Vec::new();
50
51 for token in tokens {
52 if let Ok(value) = token.parse::<i64>() {
53 modifiers.push(value);
54 continue;
55 }
56
57 dice.extend(parse_die_token(token)?);
58 }
59
60 if dice.is_empty() && modifiers.is_empty() {
61 return Err(DiceError::InvalidExpression(
62 "expression did not contain dice or modifiers".to_string(),
63 ));
64 }
65
66 Ok(ParsedRoll { dice, modifiers })
67}
68
69pub fn parse_dice_only_exprs(tokens: &[&str]) -> Result<Vec<String>> {
92 if tokens.is_empty() {
93 return Err(DiceError::InvalidArguments(
94 "expected at least one dice expression".to_string(),
95 ));
96 }
97
98 let mut dice = Vec::new();
99 for token in tokens {
100 if token.parse::<i64>().is_ok() {
101 return Err(DiceError::InvalidExpression(format!(
102 "modifiers are not allowed in dice-only expressions: {token}"
103 )));
104 }
105
106 dice.extend(parse_die_token(token)?);
107 }
108
109 Ok(dice)
110}
111
112fn parse_die_token(token: &str) -> Result<Vec<String>> {
113 let digit_end = token
114 .char_indices()
115 .find(|(_, c)| !c.is_ascii_digit())
116 .map(|(index, _)| index)
117 .unwrap_or(token.len());
118
119 if digit_end == token.len() {
120 return Err(DiceError::InvalidExpression(format!(
121 "expected a die expression like d6 or 2custom, got {token}"
122 )));
123 }
124
125 let count = if digit_end == 0 {
126 1
127 } else {
128 token[..digit_end]
129 .parse::<usize>()
130 .map_err(|_| DiceError::InvalidExpression(format!("invalid dice count in {token}")))?
131 };
132
133 if count == 0 {
134 return Err(DiceError::InvalidFaceCount);
135 }
136
137 let die_name = &token[digit_end..];
138 if die_name.trim().is_empty() {
139 return Err(DiceError::InvalidExpression(format!(
140 "missing die name in expression {token}"
141 )));
142 }
143
144 Ok((0..count).map(|_| die_name.to_string()).collect())
145}
146
147#[cfg(test)]
148mod tests {
149 use super::{parse_dice_only_exprs, parse_roll_exprs};
150 use crate::error::DiceError;
151
152 #[test]
153 fn parse_roll_exprs_expands_dice_and_modifiers() {
154 let parsed = parse_roll_exprs(&["3d6", "2d20", "d100", "2custom", "5", "-3"]).unwrap();
155 assert_eq!(
156 parsed.dice,
157 vec!["d6", "d6", "d6", "d20", "d20", "d100", "custom", "custom"]
158 );
159 assert_eq!(parsed.modifiers, vec![5, -3]);
160 }
161
162 #[test]
163 fn parse_dice_only_rejects_modifiers() {
164 let err = parse_dice_only_exprs(&["2d6", "-3"]).unwrap_err();
165 assert!(matches!(err, DiceError::InvalidExpression(message) if message.contains("-3")));
166 }
167}