dice_command_parser/
lib.rs

1#![warn(clippy::all, clippy::pedantic)]
2#![allow(clippy::pedantic::module_name_repetitions)]
3#![warn(missing_docs)]
4#![warn(missing_doc_code_examples)]
5
6//! This crate provides functionality for the basic parsing of dice roll commands e.g. `d100`, `d6 + 5`, `2d20 - 1`.
7//! Given some input it will produce a `DiceRollWithOp` struct which can be used to then calculate a result.
8
9use nom::{branch, bytes, character, combinator, sequence, Err};
10
11/// Provides access to the `DiceRoll` struct.
12pub mod dice_roll;
13/// Provides access to the `ParserError` struct.
14pub mod error;
15
16/// Provides access to the `DiceRollWithOp` struct.
17pub mod dice_roll_with_op;
18
19mod parse;
20
21use crate::dice_roll::{DiceRoll, Operation, RollType};
22use crate::dice_roll_with_op::DiceRollWithOp;
23use crate::error::ParserError;
24use crate::parse::terminated_spare;
25
26// + or - or end
27fn parse_end_of_input_or_modifier(i: &str) -> nom::IResult<&str, &str> {
28    branch::alt((
29        bytes::complete::tag("+"),
30        bytes::complete::tag("-"),
31        bytes::complete::tag(","),
32        combinator::eof,
33    ))(i)
34}
35
36// + or -
37fn parse_operator_as_value(i: &str) -> nom::IResult<&str, Operation> {
38    branch::alt((
39        combinator::value(Operation::Addition, character::complete::char('+')),
40        combinator::value(Operation::Subtraction, character::complete::char('-')),
41    ))(i)
42}
43
44// Returns RollType::Regular if no roll rolltype is observed
45fn parse_roll_type(i: &str) -> nom::IResult<&str, RollType> {
46    let result = combinator::opt(branch::alt((
47        combinator::value(
48            RollType::KeepHighest,
49            // Only parse advantage if preceding a operating separating dice strings or at the end of input
50            terminated_spare(
51                branch::alt((
52                    bytes::complete::tag_no_case("kh"),
53                    bytes::complete::tag_no_case("adv"),
54                    bytes::complete::tag_no_case("a"),
55                    bytes::complete::tag_no_case("bane"),
56                )),
57                parse_end_of_input_or_modifier,
58            ),
59        ),
60        combinator::value(
61            RollType::KeepLowest,
62            // Only parse advantage if preceding a operating separating dice strings or at the end of input
63            terminated_spare(
64                branch::alt((
65                    bytes::complete::tag_no_case("kl"),
66                    bytes::complete::tag_no_case("dadv"),
67                    bytes::complete::tag_no_case("d"),
68                    bytes::complete::tag_no_case("boon"),
69                )),
70                parse_end_of_input_or_modifier,
71            ),
72        ),
73    )))(i);
74    match result {
75        Ok((i, None)) => Ok((i, RollType::Regular)),
76        Ok((i, Some(roll_type))) => Ok((i, roll_type)),
77        Err(e) => Err(e),
78    }
79}
80
81// Matches: 3d6
82fn parse_dice_parts(i: &str) -> nom::IResult<&str, (Option<&str>, &str, &str)> {
83    sequence::tuple((
84        combinator::opt(character::complete::digit1),
85        bytes::complete::tag_no_case("d"),
86        character::complete::digit1,
87    ))(i)
88}
89
90// Matches: 3d6-1a, 3d6-1, 3d6
91fn parse_roll_as_value(i: &str) -> nom::IResult<&str, DiceRoll> {
92    // Order matters
93    branch::alt((parse_dice_with_operator, parse_dice_without_operator))(i)
94}
95
96// Matches: 3d6-1a or 3d6-1
97fn parse_dice_with_operator(i: &str) -> nom::IResult<&str, DiceRoll> {
98    let result = sequence::tuple((
99        parse_dice_parts,
100        parse_operator_as_value,
101        terminated_spare(
102            character::complete::digit1,
103            combinator::not(sequence::tuple((
104                bytes::complete::tag_no_case("d"),
105                character::complete::digit1,
106            ))),
107        ),
108        parse_roll_type,
109    ))(i);
110    match result {
111        Ok((remaining, ((number_of_dice, _, dice_sides), operation, modifier, roll_type))) => Ok((
112            remaining,
113            dice_roll_from_parsed_items(
114                number_of_dice,
115                dice_sides,
116                Some(operation),
117                Some(modifier),
118                roll_type,
119            ),
120        )),
121        Err(e) => Err(e),
122    }
123}
124
125// Matches: 2d8a or 2d8
126fn parse_dice_without_operator(i: &str) -> nom::IResult<&str, DiceRoll> {
127    let result = sequence::tuple((parse_dice_parts, parse_roll_type))(i);
128    match result {
129        Ok((remaining, ((number_of_dice, _, dice_sides), roll_type))) => Ok((
130            remaining,
131            dice_roll_from_parsed_items(number_of_dice, dice_sides, None, None, roll_type),
132        )),
133        Err(e) => Err(e),
134    }
135}
136
137fn parse_statement_with_leading_op(i: &str) -> nom::IResult<&str, Vec<DiceRollWithOp>> {
138    let (remaining, (operation, roll, later_rolls)) = sequence::tuple((
139        parse_operator_as_value,
140        parse_roll_as_value,
141        branch::alt((
142            parse_statement_with_leading_op, // Recursive call
143            combinator::value(Vec::new(), character::complete::space0), // Base case
144        )),
145    ))(i)?;
146    let mut rolls = Vec::with_capacity(later_rolls.len() + 1);
147    rolls.push(DiceRollWithOp::new(roll, operation));
148    for roll in later_rolls {
149        rolls.push(roll);
150    }
151    Ok((remaining, rolls))
152}
153
154// TODO: Refactor
155fn parse_initial_statement(i: &str) -> nom::IResult<&str, DiceRollWithOp> {
156    let (remaining, (operator, roll)) = sequence::tuple((
157        combinator::opt(parse_operator_as_value),
158        parse_roll_as_value,
159    ))(i)?;
160    Ok((
161        remaining,
162        DiceRollWithOp::new(roll, operator.unwrap_or(Operation::Addition)),
163    ))
164}
165
166fn parse_statements(i: &str) -> nom::IResult<&str, Vec<DiceRollWithOp>> {
167    // TODO: Make `later_rolls` an Option<>
168    let (remaining, (operation, roll, later_rolls)) = sequence::tuple((
169        parse_operator_as_value,
170        parse_roll_as_value,
171        branch::alt((
172            parse_statement_with_leading_op, // Recursive call
173            // TODO: Use combinator that consumes the 'empty string' i.e. doesn't consume input instead of value
174            combinator::value(Vec::new(), character::complete::space0), // Base case
175        )),
176    ))(i)?;
177    let mut rolls = Vec::with_capacity(later_rolls.len() + 1);
178    rolls.push(DiceRollWithOp::new(roll, operation));
179    for roll in later_rolls {
180        rolls.push(roll);
181    }
182    Ok((remaining, rolls))
183}
184
185fn parse_group(i: &str) -> nom::IResult<&str, Vec<DiceRollWithOp>> {
186    let (remaining, (initial_roll, additional_rolls)) = sequence::tuple((
187        parse_initial_statement,
188        // TODO: Try combinator::opt()
189        branch::alt((
190            parse_statements,
191            // TODO: Use combinator that consumes the 'empty string' i.e. doesn't consume input instead of value
192            combinator::value(Vec::new(), character::complete::space0),
193        )),
194    ))(i)?;
195
196    let mut rolls = Vec::with_capacity(additional_rolls.len() + 1);
197    rolls.push(initial_roll);
198    for roll in additional_rolls {
199        rolls.push(roll);
200    }
201    Ok((remaining, rolls))
202}
203
204fn parse_groups(i: &str) -> nom::IResult<&str, Vec<Vec<DiceRollWithOp>>> {
205    let (remaining, (group_rolls, other_groups)) = sequence::tuple((
206        parse_group,
207        combinator::opt(sequence::tuple((
208            character::complete::char(','),
209            parse_groups,
210        ))),
211    ))(i)?;
212
213    let other_groups_size = match &other_groups {
214        Some((_, rolls)) => rolls.len(),
215        None => 0,
216    };
217
218    let mut rolls: Vec<Vec<DiceRollWithOp>> = Vec::with_capacity(other_groups_size + 1);
219    rolls.push(group_rolls);
220    if other_groups.is_some() {
221        let (_, other_groups_rolls) = other_groups.unwrap();
222        rolls.extend(other_groups_rolls);
223    }
224    Ok((remaining, rolls))
225}
226
227fn dice_roll_from_parsed_items(
228    number_of_dice: Option<&str>,
229    dice_sides: &str,
230    modifier_operation: Option<Operation>,
231    modifier_value: Option<&str>,
232    roll_type: RollType,
233) -> DiceRoll {
234    let number_of_dice: u32 = number_of_dice.map_or(Ok(1), str::parse).unwrap();
235    let dice_sides: u32 = dice_sides.parse().unwrap();
236    if modifier_operation.is_some() && modifier_value.is_some() {
237        let modifier_operation = modifier_operation.unwrap();
238        let modifier_value: i32 = modifier_value.unwrap().parse().unwrap();
239        match modifier_operation {
240            Operation::Addition => {
241                return DiceRoll::new(dice_sides, Some(modifier_value), number_of_dice, roll_type)
242            }
243            Operation::Subtraction => {
244                let modifier = Some(-modifier_value);
245                return DiceRoll::new(dice_sides, modifier, number_of_dice, roll_type);
246            }
247        }
248    }
249    DiceRoll::new(dice_sides, None, number_of_dice, roll_type)
250}
251
252/// Takes a string of dice input and returns a `Result<DiceRoll, ParserError>`
253///
254/// The string will be consumed in the process and must strictly match the format of the parser.
255///
256/// # Examples
257///
258/// Standard usage:
259///
260/// ```
261/// use dice_command_parser::{parse_line, error::ParserError};
262///
263/// let input = "3d6 - 5";
264/// let dice_roll = parse_line(&input)?;
265/// # Ok::<(), ParserError>(())
266/// ```
267///
268/// # Errors
269/// This function can fail when one of the following occurs
270/// 1. The line failed to parse.
271/// 2. An error occurred parsing the numbers provided. This will likely be an overflow or underflow error.
272///
273/// For more information see `ParserError`.
274pub fn parse_line(i: &str) -> Result<Vec<Vec<DiceRollWithOp>>, ParserError> {
275    let whitespaceless: String = i.replace(" ", "");
276
277    match parse_groups(&whitespaceless) {
278        Ok((remaining, dice_rolls)) => {
279            if !remaining.trim().is_empty() {
280                return Err(ParserError::ParseError(format!(
281                    "Expected remaining input to be empty, found: {0}",
282                    remaining
283                )));
284            }
285            return Ok(dice_rolls);
286        }
287        Err(Err::Error(e)) | Err(Err::Failure(e)) => {
288            return Err(ParserError::ParseError(format!("{0}", e)));
289        }
290        Err(Err::Incomplete(_)) => {
291            return Err(ParserError::Unknown);
292        }
293    }
294}
295
296#[cfg(test)]
297mod tests {
298    use super::*;
299
300    #[test]
301    fn test_parse_dice_parts() {
302        assert_eq!(
303            parse_dice_parts("2d6 + 2"),
304            Ok((" + 2", (Some("2"), "d", "6")))
305        );
306        assert_eq!(parse_dice_parts("d8 + 3"), Ok((" + 3", (None, "d", "8"))));
307        assert_eq!(parse_dice_parts("d6 + 2"), Ok((" + 2", (None, "d", "6"))));
308        assert_eq!(parse_dice_parts("d6+2+"), Ok(("+2+", (None, "d", "6"))));
309        assert_eq!(parse_dice_parts("d1"), Ok(("", (None, "d", "1"))));
310        assert!(parse_dice_parts("6 + 2").is_err());
311    }
312
313    #[test]
314    fn test_parse_operator_as_value() {
315        assert_eq!(
316            parse_operator_as_value("+2"),
317            Ok(("2", Operation::Addition))
318        );
319        assert_eq!(
320            parse_operator_as_value("-1"),
321            Ok(("1", Operation::Subtraction))
322        );
323
324        assert_eq!(parse_operator_as_value("*1").is_err(), true);
325    }
326
327    #[test]
328    fn test_roll_type_parser() {
329        assert_eq!(parse_roll_type("a"), Ok(("", RollType::KeepHighest)));
330
331        assert_eq!(parse_roll_type("d"), Ok(("", RollType::KeepLowest)));
332
333        assert_eq!(parse_roll_type(""), Ok(("", RollType::Regular)));
334
335        assert_eq!(parse_roll_type("e"), Ok(("e", RollType::Regular)));
336        assert_eq!(parse_roll_type("+"), Ok(("+", RollType::Regular)));
337        assert_eq!(parse_roll_type("d+"), Ok(("+", RollType::KeepLowest)));
338    }
339
340    #[test]
341    fn test_parse_roll_as_value() {
342        assert_eq!(
343            parse_roll_as_value("d6+2"),
344            Ok(("", DiceRoll::new_regular_roll(6, Some(2), 1)))
345        );
346
347        assert_eq!(
348            parse_roll_as_value("3d6-2"),
349            Ok(("", DiceRoll::new_regular_roll(6, Some(-2), 3)))
350        );
351
352        assert_eq!(
353            parse_roll_as_value("2d20-2a"),
354            Ok(("", DiceRoll::new(20, Some(-2), 2, RollType::KeepHighest)))
355        );
356
357        assert_eq!(
358            parse_roll_as_value("2d20-2kh"),
359            Ok(("", DiceRoll::new(20, Some(-2), 2, RollType::KeepHighest)))
360        );
361
362        assert_eq!(
363            parse_roll_as_value("2d20-2adv"),
364            Ok(("", DiceRoll::new(20, Some(-2), 2, RollType::KeepHighest)))
365        );
366
367        assert_eq!(
368            parse_roll_as_value("2d20-2bane"),
369            Ok(("", DiceRoll::new(20, Some(-2), 2, RollType::KeepHighest)))
370        );
371
372        assert_eq!(
373            parse_roll_as_value("2d20-2a+d4"),
374            Ok(("+d4", DiceRoll::new(20, Some(-2), 2, RollType::KeepHighest)))
375        );
376
377        assert_eq!(
378            parse_roll_as_value("2d20-2+d4"),
379            Ok(("+d4", DiceRoll::new(20, Some(-2), 2, RollType::Regular)))
380        );
381
382        assert_eq!(
383            parse_roll_as_value("2d6+d4"),
384            Ok(("+d4", DiceRoll::new(6, None, 2, RollType::Regular)))
385        );
386        assert_eq!(
387            parse_roll_as_value("2d6+2d4"),
388            Ok(("+2d4", DiceRoll::new(6, None, 2, RollType::Regular)))
389        );
390
391        assert_eq!(
392            parse_roll_as_value("3d4+1d"),
393            Ok(("", DiceRoll::new(4, Some(1), 3, RollType::KeepLowest)))
394        );
395
396        assert_eq!(
397            parse_roll_as_value("3d4+1d"),
398            Ok(("", DiceRoll::new(4, Some(1), 3, RollType::KeepLowest)))
399        );
400
401        assert_eq!(
402            parse_roll_as_value("3d4+1dadv"),
403            Ok(("", DiceRoll::new(4, Some(1), 3, RollType::KeepLowest)))
404        );
405
406        assert_eq!(
407            parse_roll_as_value("3d4+1boon"),
408            Ok(("", DiceRoll::new(4, Some(1), 3, RollType::KeepLowest)))
409        );
410
411        assert_eq!(
412            parse_roll_as_value("3d4+1kl"),
413            Ok(("", DiceRoll::new(4, Some(1), 3, RollType::KeepLowest)))
414        );
415
416        assert_eq!(
417            parse_roll_as_value("d1d"),
418            Ok(("", DiceRoll::new(1, None, 1, RollType::KeepLowest)))
419        );
420        assert_eq!(
421            parse_roll_as_value("d20d,d4"),
422            Ok((",d4", DiceRoll::new(20, None, 1, RollType::KeepLowest)))
423        );
424    }
425
426    #[test]
427    fn test_parse_initial() {
428        assert_eq!(
429            parse_initial_statement("d6"),
430            Ok((
431                "",
432                DiceRollWithOp::new(
433                    DiceRoll::new(6, None, 1, RollType::Regular,),
434                    Operation::Addition
435                )
436            ))
437        );
438
439        assert_eq!(
440            parse_initial_statement("4d6+10a"),
441            Ok((
442                "",
443                DiceRollWithOp::new(
444                    DiceRoll::new(6, Some(10), 4, RollType::KeepHighest),
445                    Operation::Addition
446                )
447            ))
448        );
449
450        assert_eq!(
451            parse_initial_statement("d6+d4"),
452            Ok((
453                "+d4",
454                DiceRollWithOp::new(
455                    DiceRoll::new(6, None, 1, RollType::Regular,),
456                    Operation::Addition
457                )
458            ))
459        );
460
461        assert_eq!(
462            parse_initial_statement("4d6+10a-d4"),
463            Ok((
464                "-d4",
465                DiceRollWithOp::new(
466                    DiceRoll::new(6, Some(10), 4, RollType::KeepHighest),
467                    Operation::Addition
468                )
469            ))
470        );
471
472        assert_eq!(
473            parse_initial_statement("-d1d-4d4d"),
474            Ok((
475                "-4d4d",
476                DiceRollWithOp::new(
477                    DiceRoll::new(1, None, 1, RollType::KeepLowest),
478                    Operation::Subtraction
479                )
480            ))
481        );
482
483        assert_eq!(
484            parse_initial_statement("d20d,d4"),
485            Ok((
486                ",d4",
487                DiceRollWithOp::new(
488                    DiceRoll::new(20, None, 1, RollType::KeepLowest),
489                    Operation::Addition
490                )
491            ))
492        );
493    }
494
495    #[test]
496    fn test_parse_line() {
497        assert_eq!(
498            parse_line("d6"),
499            Ok(vec![vec![DiceRollWithOp::new(
500                DiceRoll::new(6, None, 1, RollType::Regular,),
501                Operation::Addition
502            )]])
503        );
504        assert_eq!(
505            parse_line("d20 +      5"),
506            Ok(vec![vec![DiceRollWithOp::new(
507                DiceRoll::new(20, Some(5), 1, RollType::Regular,),
508                Operation::Addition,
509            )]])
510        );
511        assert_eq!(
512            parse_line("2d10 - 5"),
513            Ok(vec![vec![DiceRollWithOp::new(
514                DiceRoll::new(10, Some(-5), 2, RollType::Regular,),
515                Operation::Addition
516            )]])
517        );
518        assert_eq!(
519            parse_line("3d6"),
520            Ok(vec![vec![DiceRollWithOp::new(
521                DiceRoll::new(6, None, 3, RollType::Regular,),
522                Operation::Addition,
523            )]])
524        );
525        assert_eq!(
526            parse_line("5d20 +      5"),
527            Ok(vec![vec![DiceRollWithOp::new(
528                DiceRoll::new(20, Some(5), 5, RollType::Regular,),
529                Operation::Addition
530            )]])
531        );
532
533        // TODO: Make this an error
534        assert_eq!(
535            parse_line("d0 - 5"),
536            Ok(vec![vec![DiceRollWithOp::new(
537                DiceRoll::new(0, Some(-5), 1, RollType::Regular,),
538                Operation::Addition
539            )]])
540        );
541
542        assert_eq!(
543            parse_line("d200a"),
544            Ok(vec![vec![DiceRollWithOp::new(
545                DiceRoll::new(200, None, 1, RollType::KeepHighest),
546                Operation::Addition
547            )]])
548        );
549
550        assert_eq!(
551            parse_line("d200 A"),
552            Ok(vec![vec![DiceRollWithOp::new(
553                DiceRoll::new(200, None, 1, RollType::KeepHighest),
554                Operation::Addition
555            )]])
556        );
557        assert_eq!(
558            parse_line("d200 d"),
559            Ok(vec![vec![DiceRollWithOp::new(
560                DiceRoll::new(200, None, 1, RollType::KeepLowest),
561                Operation::Addition
562            )]])
563        );
564
565        assert_eq!(
566            parse_line("d100 + d4"),
567            Ok(vec![vec![
568                DiceRollWithOp::new(
569                    DiceRoll::new(100, None, 1, RollType::Regular),
570                    Operation::Addition
571                ),
572                DiceRollWithOp::new(
573                    DiceRoll::new(4, None, 1, RollType::Regular),
574                    Operation::Addition
575                )
576            ]])
577        );
578
579        assert_eq!(
580            parse_line("d100 - d6"),
581            Ok(vec![vec![
582                DiceRollWithOp::new(
583                    DiceRoll::new(100, None, 1, RollType::Regular),
584                    Operation::Addition
585                ),
586                DiceRollWithOp::new(
587                    DiceRoll::new(6, None, 1, RollType::Regular),
588                    Operation::Subtraction
589                )
590            ]])
591        );
592
593        assert!(parse_line("cd20").is_err());
594
595        assert_eq!(
596            parse_line("2d6 + 2d4"),
597            Ok(vec![vec![
598                DiceRollWithOp::new(
599                    DiceRoll::new(6, None, 2, RollType::Regular),
600                    Operation::Addition
601                ),
602                DiceRollWithOp::new(
603                    DiceRoll::new(4, None, 2, RollType::Regular),
604                    Operation::Addition
605                )
606            ]])
607        );
608
609        assert_eq!(
610            parse_line("d20 + 2 + d4"),
611            Ok(vec![vec![
612                DiceRollWithOp::new(
613                    DiceRoll::new(20, Some(2), 1, RollType::Regular,),
614                    Operation::Addition
615                ),
616                DiceRollWithOp::new(
617                    DiceRoll::new(4, None, 1, RollType::Regular,),
618                    Operation::Addition
619                )
620            ]])
621        );
622
623        assert_eq!(
624            parse_line("d20 + d4 - 2d6"),
625            Ok(vec![vec![
626                DiceRollWithOp::new(
627                    DiceRoll::new(20, None, 1, RollType::Regular),
628                    Operation::Addition
629                ),
630                DiceRollWithOp::new(
631                    DiceRoll::new(4, None, 1, RollType::Regular,),
632                    Operation::Addition
633                ),
634                DiceRollWithOp::new(
635                    DiceRoll::new(6, None, 2, RollType::Regular,),
636                    Operation::Subtraction
637                ),
638            ]])
639        );
640
641        assert_eq!(
642            parse_line("d20 + 2 + d4 - 2d6"),
643            Ok(vec![vec![
644                DiceRollWithOp::new(
645                    DiceRoll::new(20, Some(2), 1, RollType::Regular,),
646                    Operation::Addition
647                ),
648                DiceRollWithOp::new(
649                    DiceRoll::new(4, None, 1, RollType::Regular,),
650                    Operation::Addition
651                ),
652                DiceRollWithOp::new(
653                    DiceRoll::new(6, None, 2, RollType::Regular,),
654                    Operation::Subtraction
655                ),
656            ]])
657        );
658
659        assert_eq!(
660            parse_line("d20 - 6 + d4 - 2d6"),
661            Ok(vec![vec![
662                DiceRollWithOp::new(
663                    DiceRoll::new(20, Some(-6), 1, RollType::Regular,),
664                    Operation::Addition
665                ),
666                DiceRollWithOp::new(
667                    DiceRoll::new(4, None, 1, RollType::Regular,),
668                    Operation::Addition
669                ),
670                DiceRollWithOp::new(
671                    DiceRoll::new(6, None, 2, RollType::Regular,),
672                    Operation::Subtraction
673                ),
674            ]])
675        );
676
677        assert_eq!(
678            parse_line("6d20 - 3d4+1kl"),
679            Ok(vec![vec![
680                DiceRollWithOp::new(
681                    DiceRoll::new(20, None, 6, RollType::Regular),
682                    Operation::Addition
683                ),
684                DiceRollWithOp::new(
685                    DiceRoll::new(4, Some(1), 3, RollType::KeepLowest),
686                    Operation::Subtraction
687                )
688            ]])
689        );
690
691        assert_eq!(
692            parse_line("-d1kl - 4d4kl"),
693            Ok(vec![vec![
694                DiceRollWithOp::new(
695                    DiceRoll::new(1, None, 1, RollType::KeepLowest),
696                    Operation::Subtraction
697                ),
698                DiceRollWithOp::new(
699                    DiceRoll::new(4, None, 4, RollType::KeepLowest),
700                    Operation::Subtraction
701                )
702            ]])
703        );
704        assert_eq!(
705            parse_line("d20, d4"),
706            Ok(vec![
707                vec![DiceRollWithOp::new(
708                    DiceRoll::new(20, None, 1, RollType::Regular),
709                    Operation::Addition
710                )],
711                vec![DiceRollWithOp::new(
712                    DiceRoll::new(4, None, 1, RollType::Regular),
713                    Operation::Addition
714                )]
715            ])
716        );
717        assert_eq!(
718            parse_line("d20, d4, d6, d100, 3d100"),
719            Ok(vec![
720                vec![DiceRollWithOp::new(
721                    DiceRoll::new(20, None, 1, RollType::Regular),
722                    Operation::Addition
723                )],
724                vec![DiceRollWithOp::new(
725                    DiceRoll::new(4, None, 1, RollType::Regular),
726                    Operation::Addition
727                )],
728                vec![DiceRollWithOp::new(
729                    DiceRoll::new(6, None, 1, RollType::Regular),
730                    Operation::Addition
731                )],
732                vec![DiceRollWithOp::new(
733                    DiceRoll::new(100, None, 1, RollType::Regular),
734                    Operation::Addition
735                )],
736                vec![DiceRollWithOp::new(
737                    DiceRoll::new(100, None, 3, RollType::Regular),
738                    Operation::Addition
739                )]
740            ])
741        );
742        assert_eq!(
743            parse_line("d20, -d4, -d6, -d100+2, -3d100-6kl"),
744            Ok(vec![
745                vec![DiceRollWithOp::new(
746                    DiceRoll::new(20, None, 1, RollType::Regular),
747                    Operation::Addition
748                )],
749                vec![DiceRollWithOp::new(
750                    DiceRoll::new(4, None, 1, RollType::Regular),
751                    Operation::Subtraction
752                )],
753                vec![DiceRollWithOp::new(
754                    DiceRoll::new(6, None, 1, RollType::Regular),
755                    Operation::Subtraction
756                )],
757                vec![DiceRollWithOp::new(
758                    DiceRoll::new(100, Some(2), 1, RollType::Regular),
759                    Operation::Subtraction
760                )],
761                vec![DiceRollWithOp::new(
762                    DiceRoll::new(100, Some(-6), 3, RollType::KeepLowest),
763                    Operation::Subtraction
764                )]
765            ])
766        );
767        assert_eq!(
768            parse_line("d20kl, d4"),
769            Ok(vec![
770                vec![DiceRollWithOp::new(
771                    DiceRoll::new(20, None, 1, RollType::KeepLowest),
772                    Operation::Addition
773                )],
774                vec![DiceRollWithOp::new(
775                    DiceRoll::new(4, None, 1, RollType::Regular),
776                    Operation::Addition
777                )]
778            ])
779        );
780    }
781
782    #[test]
783    fn test_statement_parser() {
784        assert_eq!(
785            parse_statement_with_leading_op("+2d4"),
786            Ok((
787                "",
788                vec![DiceRollWithOp::new(
789                    DiceRoll::new(4, None, 2, RollType::Regular),
790                    Operation::Addition
791                )]
792            ))
793        );
794
795        assert_eq!(
796            parse_statement_with_leading_op("-3d12-4a"),
797            Ok((
798                "",
799                vec![DiceRollWithOp::new(
800                    DiceRoll::new(12, Some(-4), 3, RollType::KeepHighest),
801                    Operation::Subtraction
802                )]
803            ))
804        );
805    }
806}