Skip to main content

rust_roll/notation/
modifier.rs

1//! Modifiers applied to the total of a roll, such as the `+3` in `2d6+3`.
2
3use super::{MAX_VALUE, NotationError};
4use std::fmt;
5use std::str::FromStr;
6
7/// An arithmetic operator in a modifier.
8///
9/// Parses from `+`, `-`, `*` or `/`.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum Operator {
12    /// Adds the value to the total (`+`).
13    Add,
14    /// Subtracts the value from the total, stopping at zero (`-`).
15    Sub,
16    /// Multiplies the total by the value (`*`).
17    Mul,
18    /// Divides the total by the value, rounding down (`/`).
19    Div,
20}
21
22impl FromStr for Operator {
23    type Err = NotationError;
24
25    fn from_str(s: &str) -> Result<Self, Self::Err> {
26        let operator = match s {
27            "+" => Operator::Add,
28            "-" => Operator::Sub,
29            "*" => Operator::Mul,
30            "/" => Operator::Div,
31            _ => return Err(NotationError::InvalidOperator),
32        };
33
34        Ok(operator)
35    }
36}
37
38impl fmt::Display for Operator {
39    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40        match self {
41            Operator::Add => write!(f, "+")?,
42            Operator::Sub => write!(f, "-")?,
43            Operator::Mul => write!(f, "*")?,
44            Operator::Div => write!(f, "/")?,
45        }
46
47        Ok(())
48    }
49}
50
51/// A validated operator and value applied to a roll's total.
52///
53/// Create one with [`Modifier::try_new`].
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub struct Modifier {
56    operator: Operator,
57    value: u32,
58}
59
60impl Modifier {
61    /// Creates a modifier, checking the value is usable with the operator.
62    ///
63    /// # Errors
64    ///
65    /// Returns [`NotationError::Zero`] if the operator is [`Operator::Div`]
66    /// and the value is zero, or [`NotationError::NumberTooLarge`] if the
67    /// value exceeds [`MAX_VALUE`].
68    ///
69    /// # Examples
70    ///
71    /// ```
72    /// use rust_roll::notation::{Modifier, Operator};
73    ///
74    /// let modifier = Modifier::try_new(Operator::Add, 3).unwrap();
75    /// assert_eq!(modifier.apply(10), 13);
76    ///
77    /// assert!(Modifier::try_new(Operator::Div, 0).is_err());
78    /// ```
79    pub fn try_new(operator: Operator, value: u32) -> Result<Self, NotationError> {
80        match (operator, value) {
81            (Operator::Div, 0) => Err(NotationError::Zero("divisor")),
82            (_, 0..=MAX_VALUE) => Ok(Modifier { operator, value }),
83            _ => Err(NotationError::NumberTooLarge(MAX_VALUE)),
84        }
85    }
86
87    /// The modifier's operator.
88    #[must_use]
89    pub fn operator(self) -> Operator {
90        self.operator
91    }
92
93    /// The modifier's value.
94    #[must_use]
95    pub fn value(self) -> u32 {
96        self.value
97    }
98
99    /// Applies the modifier to a total.
100    ///
101    /// The result never goes below zero or above [`u32::MAX`]: subtraction
102    /// stops at zero and addition and multiplication saturate. Division rounds
103    /// down, following the D&D 5e (2014) rules.
104    ///
105    /// # Examples
106    ///
107    /// ```
108    /// use rust_roll::notation::{Modifier, Operator};
109    ///
110    /// let halve = Modifier::try_new(Operator::Div, 2).unwrap();
111    /// assert_eq!(halve.apply(7), 3);
112    ///
113    /// let minus_five = Modifier::try_new(Operator::Sub, 5).unwrap();
114    /// assert_eq!(minus_five.apply(3), 0);
115    /// ```
116    #[must_use]
117    pub fn apply(self, total: u32) -> u32 {
118        match self.operator {
119            Operator::Add => total.saturating_add(self.value),
120            Operator::Sub => total.saturating_sub(self.value),
121            Operator::Mul => total.saturating_mul(self.value),
122            Operator::Div => total / self.value,
123        }
124    }
125}
126
127impl fmt::Display for Modifier {
128    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
129        write!(f, "{}{}", self.operator, self.value)
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use rstest::rstest;
136
137    use super::*;
138
139    mod operator {
140        use super::*;
141
142        mod from_str {
143            use super::*;
144
145            #[rstest]
146            #[case::add("+", Operator::Add)]
147            #[case::sub("-", Operator::Sub)]
148            #[case::mul("*", Operator::Mul)]
149            #[case::div("/", Operator::Div)]
150            fn accepts(#[case] input: &str, #[case] expected: Operator) {
151                let result: Operator = input.parse().unwrap();
152
153                assert_eq!(result, expected);
154            }
155
156            #[rstest]
157            #[case::empty("")]
158            #[case::one_char_string("%")]
159            #[case::duplicated_operator("++")]
160            fn rejects(#[case] input: &str) {
161                let result = input.parse::<Operator>();
162
163                assert!(matches!(result, Err(NotationError::InvalidOperator)));
164            }
165        }
166
167        mod display {
168            use super::*;
169
170            #[rstest]
171            #[case::add(Operator::Add, String::from("+"))]
172            #[case::sub(Operator::Sub, String::from("-"))]
173            #[case::mul(Operator::Mul, String::from("*"))]
174            #[case::div(Operator::Div, String::from("/"))]
175            fn returns_formatted_string(#[case] operator: Operator, #[case] expected: String) {
176                assert_eq!(operator.to_string(), expected);
177            }
178        }
179    }
180
181    mod modifier {
182        use super::*;
183
184        mod display {
185            use super::*;
186
187            #[test]
188            fn returns_formatted_string() {
189                let modifier = Modifier {
190                    operator: Operator::Add,
191                    value: 10,
192                };
193
194                let result = modifier.to_string();
195
196                assert_eq!(result, "+10");
197            }
198        }
199
200        mod try_new {
201            use super::*;
202
203            #[rstest]
204            #[case::add(Operator::Add)]
205            #[case::sub(Operator::Sub)]
206            #[case::mul(Operator::Mul)]
207            fn accepts_zero_for_non_division_operators(#[case] operator: Operator) {
208                let value = 0;
209
210                let result = Modifier::try_new(operator, value);
211
212                assert_eq!(result, Ok(Modifier { operator, value }));
213            }
214
215            #[rstest]
216            #[case::add(Operator::Add)]
217            #[case::sub(Operator::Sub)]
218            #[case::mul(Operator::Mul)]
219            #[case::div(Operator::Div)]
220            fn accepts_maximum_value(#[case] operator: Operator) {
221                let value = MAX_VALUE;
222
223                let result = Modifier::try_new(operator, value);
224
225                assert_eq!(result, Ok(Modifier { operator, value }));
226            }
227
228            #[rstest]
229            #[case::add(Operator::Add)]
230            #[case::sub(Operator::Sub)]
231            #[case::mul(Operator::Mul)]
232            #[case::div(Operator::Div)]
233            fn rejects_above_the_maximum(#[case] operator: Operator) {
234                let value = MAX_VALUE + 1;
235
236                let result = Modifier::try_new(operator, value);
237
238                assert!(matches!(
239                    result,
240                    Err(NotationError::NumberTooLarge(MAX_VALUE))
241                ));
242            }
243
244            #[test]
245            fn rejects_division_by_zero() {
246                let operator = Operator::Div;
247                let value = 0;
248
249                let result = Modifier::try_new(operator, value);
250
251                assert!(matches!(result, Err(NotationError::Zero("divisor"))));
252            }
253        }
254
255        mod apply {
256            use super::*;
257
258            #[rstest]
259            #[case::add(4, Operator::Add, 2, 6)]
260            #[case::add_saturates(u32::MAX, Operator::Add, 1, u32::MAX)]
261            #[case::sub(4, Operator::Sub, 2, 2)]
262            #[case::sub_saturates(1, Operator::Sub, 2, u32::MIN)]
263            #[case::mul(4, Operator::Mul, 3, 12)]
264            #[case::mul_saturates(4, Operator::Mul, u32::MAX / 2 + 1, u32::MAX)]
265            #[case::div(8, Operator::Div, 2, 4)]
266            #[case::div_rounds_down(7, Operator::Div, 2, 3)]
267            fn returns_expected_value(
268                #[case] total: u32,
269                #[case] operator: Operator,
270                #[case] value: u32,
271                #[case] expected: u32,
272            ) {
273                let modifier = Modifier { operator, value };
274
275                let result = modifier.apply(total);
276
277                assert_eq!(result, expected);
278            }
279        }
280    }
281}