Skip to main content

rs_teststand/expression/operator/
arithmetic.rs

1//! Arithmetic operators.
2
3use super::Arity;
4
5/// An arithmetic operator.
6///
7/// Two behaviors differ from what the spelling suggests, both measured on a
8/// live engine:
9///
10/// * **Division always produces a real number.** `7/2` is `3.5`, never `3`.
11///   There is no integer division operator.
12/// * **The remainder takes the sign of the left operand.** `-7 % 3` is `-1`.
13///   That is truncated remainder, not the floored modulo some languages use, so
14///   a negative operand does not wrap into the positive range.
15///
16/// There is no exponentiation operator; `^` belongs to the bitwise family.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
18pub enum ArithmeticOperator {
19    /// Addition, `+`.
20    Add,
21    /// Subtraction, or negation when applied to one operand, `-`.
22    Subtract,
23    /// Multiplication, `*`.
24    Multiply,
25    /// Division, `/`. Always real-valued.
26    Divide,
27    /// Remainder, `%`.
28    Modulo,
29    /// Remainder, `MOD`, the word spelling of [`Self::Modulo`].
30    ModuloWord,
31    /// Increment, `++`.
32    Increment,
33    /// Decrement, `--`.
34    Decrement,
35}
36
37impl ArithmeticOperator {
38    /// Every arithmetic operator.
39    pub const ALL: [Self; 8] = [
40        Self::Add,
41        Self::Subtract,
42        Self::Multiply,
43        Self::Divide,
44        Self::Modulo,
45        Self::ModuloWord,
46        Self::Increment,
47        Self::Decrement,
48    ];
49
50    /// How the operator is written.
51    #[must_use]
52    pub const fn symbol(self) -> &'static str {
53        match self {
54            Self::Add => "+",
55            Self::Subtract => "-",
56            Self::Multiply => "*",
57            Self::Divide => "/",
58            Self::Modulo => "%",
59            Self::ModuloWord => "MOD",
60            Self::Increment => "++",
61            Self::Decrement => "--",
62        }
63    }
64
65    /// How many operands it takes.
66    #[must_use]
67    pub const fn arity(self) -> Arity {
68        match self {
69            Self::Increment | Self::Decrement => Arity::Unary,
70            _ => Arity::Binary,
71        }
72    }
73
74    /// Binding strength; lower binds tighter. Measured, see [`super`].
75    #[must_use]
76    pub const fn precedence(self) -> u8 {
77        match self {
78            Self::Increment | Self::Decrement => 1,
79            Self::Multiply | Self::Divide | Self::Modulo | Self::ModuloWord => 2,
80            Self::Add | Self::Subtract => 3,
81        }
82    }
83
84    /// The other spelling, for the operator that has one.
85    #[must_use]
86    pub const fn alternate_spelling(self) -> Option<Self> {
87        Some(match self {
88            Self::Modulo => Self::ModuloWord,
89            Self::ModuloWord => Self::Modulo,
90            _ => return None,
91        })
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::ArithmeticOperator;
98    use crate::expression::Arity;
99
100    #[test]
101    fn multiplication_binds_tighter_than_addition() {
102        // Measured: 2+3*4 evaluates to 14.
103        assert!(ArithmeticOperator::Multiply.precedence() < ArithmeticOperator::Add.precedence());
104    }
105
106    #[test]
107    fn the_two_remainder_spellings_agree() {
108        // Measured: 7 MOD 3 and 7 % 3 both yield 1.
109        let symbol = ArithmeticOperator::Modulo;
110        let word = ArithmeticOperator::ModuloWord;
111        assert_eq!(symbol.precedence(), word.precedence());
112        assert_eq!(symbol.arity(), word.arity());
113        assert_eq!(symbol.alternate_spelling(), Some(word));
114        assert_eq!(word.alternate_spelling(), Some(symbol));
115    }
116
117    #[test]
118    fn increment_and_decrement_take_one_operand() {
119        assert_eq!(ArithmeticOperator::Increment.arity(), Arity::Unary);
120        assert_eq!(ArithmeticOperator::Decrement.arity(), Arity::Unary);
121        assert_eq!(ArithmeticOperator::Add.arity(), Arity::Binary);
122    }
123
124    #[test]
125    fn no_operator_spells_exponentiation() {
126        // `^` is exclusive-or and lives in the bitwise family; nothing here
127        // should tempt a caller into reading it as a power operator.
128        assert!(ArithmeticOperator::ALL.iter().all(|op| op.symbol() != "^"));
129    }
130}