1use regex::{Captures, Regex};
12use std::fmt;
13use std::{str::FromStr, sync::LazyLock};
14
15mod error;
16mod modifier;
17
18pub use error::NotationError;
19pub use modifier::{Modifier, Operator};
20
21pub const MAX_VALUE: u32 = 1_000;
24
25static NOTATION_RE: LazyLock<Regex> = LazyLock::new(|| {
26 Regex::new(
27 r"(?x) # verbose mode, literal whitespace ignored
28 ^ \s*
29 (?<count>[0-9]+)? \s* # optional number of dice
30 [dD] \s*
31 (?<sides>[0-9]+) # faces per die
32 (?: \s* (?<op>[-+*/]) \s* (?<value>[0-9]+) )? # optional modifier
33 \s* $
34 ",
35 )
36 .expect("Notation regex should be valid")
37});
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub struct Notation {
56 count: u32,
57 sides: u32,
58 modifier: Option<Modifier>,
59}
60
61impl Notation {
62 pub fn try_new(
78 count: u32,
79 sides: u32,
80 modifier: Option<Modifier>,
81 ) -> Result<Self, NotationError> {
82 Ok(Notation {
83 count: check_bounds(count, "count")?,
84 sides: check_bounds(sides, "sides")?,
85 modifier,
86 })
87 }
88
89 #[must_use]
91 pub fn count(self) -> u32 {
92 self.count
93 }
94
95 #[must_use]
97 pub fn sides(self) -> u32 {
98 self.sides
99 }
100
101 #[must_use]
103 pub fn modifier(self) -> Option<Modifier> {
104 self.modifier
105 }
106
107 #[must_use]
119 pub fn d20(modifier: Option<Modifier>) -> Notation {
120 Notation {
121 count: 1,
122 sides: 20,
123 modifier,
124 }
125 }
126
127 #[must_use]
146 pub fn critical(self) -> Notation {
147 Notation {
148 count: self.count * 2,
149 ..self
150 }
151 }
152}
153
154impl FromStr for Notation {
155 type Err = NotationError;
156
157 fn from_str(s: &str) -> Result<Self, Self::Err> {
158 let captures = NOTATION_RE
159 .captures(s)
160 .ok_or(NotationError::InvalidFormat)?;
161
162 Notation::try_new(
163 parse_count(&captures)?,
164 parse_sides(&captures)?,
165 parse_modifier(&captures)?,
166 )
167 }
168}
169
170impl fmt::Display for Notation {
171 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
172 if self.count != 1 {
173 write!(f, "{}", self.count)?;
174 }
175
176 write!(f, "d{}", self.sides)?;
177
178 if let Some(modifier) = self.modifier {
179 write!(f, "{modifier}")?;
180 }
181
182 Ok(())
183 }
184}
185
186fn parse_count(captures: &Captures) -> Result<u32, NotationError> {
187 let count = captures
188 .name("count")
189 .map(|m| m.as_str().parse::<u32>())
190 .transpose()?
191 .unwrap_or(1);
192
193 Ok(count)
194}
195
196fn parse_sides(captures: &Captures) -> Result<u32, NotationError> {
197 let sides = captures["sides"].parse()?;
198
199 Ok(sides)
200}
201
202fn parse_modifier(captures: &Captures) -> Result<Option<Modifier>, NotationError> {
203 match (captures.name("op"), captures.name("value")) {
204 (Some(op), Some(value)) => {
205 let operator = op.as_str().parse()?;
206 let value = value.as_str().parse()?;
207
208 Modifier::try_new(operator, value).map(Some)
209 }
210 (None, None) => Ok(None),
211 _ => Err(NotationError::InvalidFormat),
212 }
213}
214
215fn check_bounds(value: u32, name: &'static str) -> Result<u32, NotationError> {
216 match value {
217 0 => Err(NotationError::Zero(name)),
218 1..=MAX_VALUE => Ok(value),
219 _ => Err(NotationError::NumberTooLarge(MAX_VALUE)),
220 }
221}
222
223#[cfg(test)]
224mod tests {
225 use super::*;
226 use crate::test_helpers::modifier;
227 use ::rstest::rstest;
228
229 mod try_new {
230 use super::*;
231
232 #[rstest]
233 #[case::typical(2, 6, None)]
234 #[case::with_modifier(1, 20, Some(modifier(Operator::Add, 5)))]
235 #[case::max_count(MAX_VALUE, 100, None)]
236 #[case::max_sides(5, MAX_VALUE, None)]
237 fn accepts_valid_values(
238 #[case] count: u32,
239 #[case] sides: u32,
240 #[case] modifier: Option<Modifier>,
241 ) {
242 let result = Notation::try_new(count, sides, modifier);
243
244 assert_eq!(
245 result,
246 Ok(Notation {
247 count,
248 sides,
249 modifier
250 })
251 );
252 }
253
254 #[rstest]
255 #[case::zero_count(0, 6, NotationError::Zero("count"))]
256 #[case::zero_sides(1, 0, NotationError::Zero("sides"))]
257 #[case::count_too_large(MAX_VALUE + 1, 6, NotationError::NumberTooLarge(MAX_VALUE))]
258 #[case::sides_too_large(1, MAX_VALUE + 1, NotationError::NumberTooLarge(MAX_VALUE))]
259 fn rejects_invalid_values(
260 #[case] count: u32,
261 #[case] sides: u32,
262 #[case] expected: NotationError,
263 ) {
264 let result = Notation::try_new(count, sides, None);
265
266 assert_eq!(result, Err(expected));
267 }
268 }
269
270 mod from_str {
271 use super::*;
272
273 #[test]
274 fn defaults_to_one() {
275 let input = "d20";
276
277 let result: Notation = input.parse().unwrap();
278
279 assert_eq!(
280 result,
281 Notation {
282 count: 1,
283 sides: 20,
284 modifier: None
285 }
286 );
287 }
288
289 #[test]
290 fn ignores_whitespace() {
291 let input = " 3 d 4 + 5 ";
292
293 let result: Notation = input.parse().unwrap();
294
295 assert_eq!(
296 result,
297 Notation {
298 count: 3,
299 sides: 4,
300 modifier: Some(modifier(Operator::Add, 5))
301 }
302 );
303 }
304
305 #[test]
306 fn accepts_uppercase_d() {
307 let input = "D100";
308
309 let result: Notation = input.parse().unwrap();
310
311 assert_eq!(
312 result,
313 Notation {
314 count: 1,
315 sides: 100,
316 modifier: None
317 }
318 );
319 }
320
321 #[rstest]
322 #[case::word("banana")]
323 #[case::whitespace_between_digits("2d1 0")]
324 #[case::operator_without_value("d10+")]
325 fn rejects_invalid_format(#[case] input: &str) {
326 let result = input.parse::<Notation>();
327
328 assert!(matches!(result, Err(NotationError::InvalidFormat)));
329 }
330
331 #[rstest]
332 #[case::count("99999999999d6")]
333 #[case::sides("d99999999999")]
334 #[case::modifier("d10+99999999999")]
335 fn rejects_overflow(#[case] input: &str) {
336 let result = input.parse::<Notation>();
337
338 assert!(matches!(result, Err(NotationError::InvalidNumber(_))));
339 }
340
341 #[rstest]
342 #[case::add("+", Operator::Add)]
343 #[case::sub("-", Operator::Sub)]
344 #[case::mul("*", Operator::Mul)]
345 #[case::div("/", Operator::Div)]
346 fn accepts_modifier(#[case] input: &str, #[case] expected: Operator) {
347 let input = format!("1d8{input}2");
348
349 let result: Notation = input.parse().unwrap();
350
351 assert_eq!(
352 result,
353 Notation {
354 count: 1,
355 sides: 8,
356 modifier: Some(modifier(expected, 2))
357 }
358 );
359 }
360
361 #[rstest]
362 #[case::zero_count("0d6", NotationError::Zero("count"))]
363 #[case::number_too_large("d1001", NotationError::NumberTooLarge(MAX_VALUE))]
364 #[case::zero_divisor("d6/0", NotationError::Zero("divisor"))]
365 fn passes_on_validation(#[case] input: &str, #[case] expected: NotationError) {
366 let result = input.parse::<Notation>();
367
368 assert_eq!(result, Err(expected));
369 }
370 }
371
372 mod display {
373 use super::*;
374
375 #[rstest]
376 #[case::one_die(Notation { count: 1, sides: 100, modifier: None}, "d100")]
377 #[case::many_dice(Notation {count: 5, sides: 6, modifier: None}, "5d6")]
378 #[case::with_modifier(Notation { count: 10, sides: 4, modifier: Some(modifier(Operator::Sub, 5))}, "10d4-5")]
379 fn returns_formatted_string(#[case] notation: Notation, #[case] expected: &str) {
380 let result = notation.to_string();
381
382 assert_eq!(result, expected);
383 }
384 }
385
386 mod d20 {
387 use super::*;
388
389 #[rstest]
390 #[case::plain(None)]
391 #[case::with_modifier(Some(modifier(Operator::Add, 5)))]
392 fn builds_correct_notation(#[case] modifier: Option<Modifier>) {
393 let result = Notation::d20(modifier);
394
395 assert_eq!(
396 result,
397 Notation {
398 count: 1,
399 sides: 20,
400 modifier
401 }
402 );
403 }
404 }
405
406 mod critical {
407 use super::*;
408
409 #[test]
410 fn doubles_count() {
411 let modifier = modifier(Operator::Add, 2);
412 let notation = Notation {
413 count: 1,
414 sides: 8,
415 modifier: Some(modifier),
416 };
417
418 let result = notation.critical();
419
420 assert_eq!(
421 result,
422 Notation {
423 count: 2,
424 sides: 8,
425 modifier: Some(modifier)
426 }
427 );
428 }
429 }
430}