cmpchain/
lib.rs

1//! A Rust library containing a macro to chain comparison operators succintly.
2//! The syntax is similar to that found in Python and Julia.
3
4/// Succintly chain comparison operators like in Python and Julia.
5///
6/// `chain!` allows you to write comparisons that need to be simultaneously true
7/// more concisely. Instead of writing `a < b && b < c`, you can just write
8/// `chain!(a < b < c)`. `chain!` has the added benefit that each argument is
9/// only evaluated once, rather than being evaluated for both the left and right
10/// comparisons. Arguments are lazily evaluated from left to right so that any
11/// arguments after the first failing comparison are not evaluated. `chain!`
12/// supports the comparison operators `<`, `<=`, `>`, `>=`, `==`, `!=` in any
13/// order.
14///
15/// # Examples
16///
17/// ```
18/// # #[macro_use] extern crate cmpchain;
19/// // Check if a value falls between two bounds
20/// let x = 8;
21/// if chain!(4 < x <= 10) {
22///     assert!(true);
23///     // ...
24/// }
25/// # else { assert!(false); }
26/// ```
27///
28/// ```
29/// // Check for equality of multiple values
30/// # #[macro_use] extern crate cmpchain;
31/// assert!(chain!(4 == 2 * 2 == 12 / 3));
32/// ```
33#[macro_export]
34macro_rules! chain {
35    // @wrap acts somewhat like a function, iterating through the input tokens
36    // and placing parentheses around all the terms separated by the comparison
37    // operators and then passing these tokens to @op
38    // Thus for example it transforms 5 + 4 < 10 <= 20 * 2 into
39    // (5 + 4) < (10) <= (20 * 2)
40
41    // @wrap uses two square brackets containing tokens to save its current
42    // state as it processes tokens. The first contains everything that has
43    // been parsed so far, and the second contains the tokens that have
44    // appeared since the previous comparison operator. This means that when
45    // a new comparison operator is encountered, the tokens in the second
46    // bracket can be wrapped in parentheses and added to the first bracket.
47
48    // For example to call it for 5 + 4 < 10 + 5< 20 you would do
49    // chain!(@wrap [] [5] + 4 < 10 + 5 < 20)
50    // and part way through parsing the calls could be
51    // chain!(@wrap [(5 + 4)] [10 +] 5 < 20)
52    
53    (@wrap [$($prev:tt)*] [$($cur:tt)+] <  $next:tt $($rest:tt)*) => {
54        chain!(@wrap [$($prev)* ($($cur)*) <] [$next] $($rest)*)
55    };
56    (@wrap [$($prev:tt)*] [$($cur:tt)+] <= $next:tt $($rest:tt)*) => {
57        chain!(@wrap [$($prev)* ($($cur)*) <=] [$next] $($rest)*)
58    };
59    (@wrap [$($prev:tt)*] [$($cur:tt)+] >  $next:tt $($rest:tt)*) => {
60        chain!(@wrap [$($prev)* ($($cur)*) >] [$next] $($rest)*)
61    };
62    (@wrap [$($prev:tt)*] [$($cur:tt)+] >= $next:tt $($rest:tt)*) => {
63        chain!(@wrap [$($prev)* ($($cur)*) >=] [$next] $($rest)*)
64    };
65    (@wrap [$($prev:tt)*] [$($cur:tt)+] == $next:tt $($rest:tt)*) => {
66        chain!(@wrap [$($prev)* ($($cur)*) ==] [$next] $($rest)*)
67    };
68    (@wrap [$($prev:tt)*] [$($cur:tt)+] != $next:tt $($rest:tt)*) => {
69        chain!(@wrap [$($prev)* ($($cur)*) !=] [$next] $($rest)*)
70    };
71
72    (@arg_err $op:tt) => {
73        compile_error!(concat!(
74            "Expected two arguments for \"", stringify!($op), "\""
75        ));
76    };
77    // Match errors where a comparison operator is left trailing at the end of
78    // the input, and call error function
79    (@wrap [$($a:tt)*] [$($b:tt)*] < ) => { chain!(@arg_err <)  };
80    (@wrap [$($a:tt)*] [$($b:tt)*] <=) => { chain!(@arg_err <=) };
81    (@wrap [$($a:tt)*] [$($b:tt)*] > ) => { chain!(@arg_err >)  };
82    (@wrap [$($a:tt)*] [$($b:tt)*] >=) => { chain!(@arg_err >=) };
83    (@wrap [$($a:tt)*] [$($b:tt)*] ==) => { chain!(@arg_err ==) };
84    (@wrap [$($a:tt)*] [$($b:tt)*] !=) => { chain!(@arg_err !=) };
85
86    // Matches when all the tokens have been parsed. Then calls @op on the
87    // wrapped tokens
88    (@wrap [$($prev:tt)*] [$($cur:tt)+]) => { chain!(@op $($prev)* ($($cur)*)) };
89
90    // Matches when the next token to parse isnt a comparison operator, and just
91    // adds this next token to the current capture group
92    (@wrap [$($prev:tt)*] [$($cur:tt)+] $next:tt $($rest:tt)*) => {
93        chain!(@wrap [$($prev)*] [$($cur)* $next] $($rest)*)
94    };
95
96    // @op acts like a function that recursively expands chained comparison
97    // operators into a scope returning a boolean. This scope takes the left
98    // most comparison and then evaluates its arguments, saving the values.
99    // It then evaluates the comparison of these saved values, and if they
100    // are true recursively calls itself with the next comparison, using the
101    // second of the saved values as the first argument to the new comparison
102    // to prevent repeated evaluation
103    (@op $a:tt $op:tt $b:tt) => {{ $a $op $b }};
104    (@op $a:tt $op:tt $b:tt $($rest:tt)+) => {{
105        let a = $a;
106        let b = $b;
107        a $op b && chain!(@op b $($rest)*)
108    }};
109
110    // Error if for some reason the arguments to op cant be properly parsed as
111    // a conditional
112    (@op $($rest:tt)*) => {{
113        compile_error!("Expected comparison operator (<, <=, >, >=, ==, !=)");
114    }};
115    
116    // Throw errors if there is no left hand argument to the first comparison
117    (<  $($rest:tt)*) => { chain!(@arg_err <)  };
118    (<= $($rest:tt)*) => { chain!(@arg_err <=) };
119    (>  $($rest:tt)*) => { chain!(@arg_err >)  };
120    (>= $($rest:tt)*) => { chain!(@arg_err >=) };
121    (== $($rest:tt)*) => { chain!(@arg_err ==) };
122    (!= $($rest:tt)*) => { chain!(@arg_err !=) };
123
124    // Entrypoint
125    ($first:tt $($rest:tt)*) => {
126        chain!(@wrap [] [$first] $($rest)*)
127    };
128}
129
130#[cfg(test)]
131mod tests {
132    #[test]
133    fn no_chaining() {
134        // Check that basic comparisons without chaining still work
135        assert!(chain!(1 < 2));
136        assert!(chain!(1 <= 2));
137        assert!(chain!(1 != 2));
138    }
139
140    #[test]
141    fn three_args() {
142        assert!(chain!(1 < 3 > 2));
143        assert!(chain!(1 != 4 >= 2));
144        assert!(chain!(5 == 5 <= 5));
145    }
146
147    #[test]
148    fn side_effects() {
149        // Pass in parameters that have side effects and check they are only
150        // evaluated once and that arguments are evaluated left to right
151        let mut results: Vec<i32> = Vec::new();
152        let mut side_effect = |val: i32| {
153            results.push(val);
154            val
155        };
156        assert!(chain!(side_effect(1) < side_effect(2) != side_effect(3)));
157        assert_eq!(results, &[1, 2, 3]);
158
159        // Check that arguments are lazy evaluated so that if a comparison fails,
160        // arguments in comparisons to the right of it arent evaluated
161        let mut results: Vec<i32> = Vec::new();
162        let mut side_effect = |val: i32| {
163            results.push(val);
164            val
165        };
166        assert!(chain!(side_effect(1) == side_effect(2) < side_effect(3)) == false);
167        assert_eq!(results, &[1, 2]);
168    }
169
170    #[test]
171    fn other_operators() {
172        // Check that other operators like + are valid inbetween comparison
173        // operators without terms being encapsulated in parentheses
174        assert!(chain!(1 + 2 == 6 / 2 == 3));
175        assert!(chain!(4 < 4 * 2 <= 4 * 3));
176    }
177
178    #[test]
179    fn compile_fail_tests() {
180        let t = trybuild::TestCases::new();
181        t.compile_fail("tests/compile_fail/*.rs");
182    }
183}