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}