binary_search/lib.rs
1use std::ops::{Add, BitAnd, Shr};
2
3pub trait Betweenable
4where
5 Self: Copy,
6{
7 fn between(x: Self, y: Self) -> Option<Self>;
8}
9
10impl<X> Betweenable for X
11where
12 X: Copy,
13 X: Shr<i8, Output = X>,
14 X: Add<X, Output = X>,
15 X: BitAnd<X, Output = X>,
16 X: From<u8>,
17 X: PartialOrd,
18{
19 fn between(low: Self, high: Self) -> Option<Self> {
20 let one = X::from(1);
21 if high <= low + one {
22 None
23 } else {
24 let mid = (low >> 1) + (high >> 1) + (low & high & one);
25 Some(mid)
26 }
27 }
28}
29
30///
31/// Whether or not the current candidate transition point is lower or higher than the search target.
32///
33pub enum Direction<A, B> {
34 Low(A),
35 High(B),
36}
37
38///
39/// Perform a binary search within the specified bounds.
40///
41/// Given a monotone function, find the largest quantity that is too small and the smallest quantity that is too large.
42///
43/// A user-specified function will be called to determine whether or not an entry is considered lower or higher.
44///
45/// ## Generics
46///
47/// - `X` - the type of the search space. used to specify the bounds of the search and is used for the returned `largest_low` and `smallest_high` values.
48/// - `A` - the type for the low witness.
49/// - `B` - the type for the high witness.
50///
51/// ## Arguments
52/// ### `low`
53/// A tuple specifying the lower bound of the search.
54///
55/// **Expanded as `(lower_bound, witness)` here for brevity.**
56///
57/// - `lower_bound: X` - the lower bound of the search space.\
58/// - `witness: A` - a value passed to the provided function that can be used to verify whether or not a transition is valid.
59///
60/// ### `high`
61/// A tuple specifying the upper bound of the search.
62///
63/// **Expanded as `(upper_bound, witness)` here for brevity.**
64///
65/// - `upper_bound: X` - the upper bound of the search space.\
66/// - `witness: B` - a value passed to the provided function that can be used to verify whether or not a transition is valid.
67///
68/// ### `f`
69/// A `FnMut(X)` function that is called to evaluate each candidate transition point.
70///
71/// #### Arguments
72///
73/// - `X` - the current candidate transition point.
74///
75/// #### Returns
76///
77/// - `Direction` - whether the current candidate transition point is lower or higher than the search target.
78///
79/// ## Return value
80///
81/// Returns a tuple `((largest_low: X, low_witness: A), (smallest_high: X, high_witness: B))`.
82///
83/// - `largest_low: X` - the largest value that the provided function indicated was `Low`.
84/// - `low_witness: A` - the witness value that the provided function last returned inside the `Direction::Low`.
85///
86/// - `smallest_high: X` - the smallest value that the provided function indicated was `High`.
87/// - `high_witness: B` - the witness value that the provided function last returned inside the `Direction::High`.
88///
89/// ## Examples
90///
91/// ```
92/// use binary_search::{binary_search, Direction};
93/// let result = binary_search((1 as usize, ()), (100, ()), |x| {
94/// if x < 23 {
95/// Direction::Low(())
96/// } else {
97/// Direction::High(())
98/// }
99/// });
100/// assert_eq!(result, ((22, ()), (23, ())))
101/// ```
102///
103pub fn binary_search<X, A, B, F>(low: (X, A), high: (X, B), mut f: F) -> ((X, A), (X, B))
104where
105 X: Betweenable,
106 F: FnMut(X) -> Direction<A, B>,
107{
108 match X::between(low.0, high.0) {
109 None => (low, high),
110 Some(x) => match (f)(x) {
111 Direction::Low(low) => binary_search((x, low), high, f),
112 Direction::High(high) => binary_search(low, (x, high), f),
113 },
114 }
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120
121 #[test]
122 fn split_usize() {
123 assert_eq!(usize::between(1, 0), None);
124 assert_eq!(usize::between(1, 1), None);
125 assert_eq!(usize::between(1, 2), None);
126 assert_eq!(usize::between(1, 3), Some(2));
127 assert_eq!(
128 usize::between(usize::max_value() - 3, usize::max_value() - 1),
129 Some(usize::max_value() - 2),
130 );
131 assert_eq!(
132 usize::between(usize::max_value() - 2, usize::max_value()),
133 Some(usize::max_value() - 1),
134 );
135 }
136
137 #[test]
138 fn binary_search_test() {
139 let result = binary_search((1 as usize, ()), (100, ()), |x| {
140 if x < 23 {
141 Direction::Low(())
142 } else {
143 Direction::High(())
144 }
145 });
146 assert_eq!(result, ((22, ()), (23, ())))
147 }
148
149 #[test]
150 fn binary_search_simple_test() {
151 let values = [0, 4, 5, 6, 7, 9, 456];
152
153 let (largest_low, smallest_high) = binary_search((0, ()), (values.len(), ()), |i| {
154 if values[i] < 6 {
155 Direction::Low(())
156 } else {
157 Direction::High(())
158 }
159 });
160
161 dbg!(largest_low);
162 dbg!(smallest_high);
163 }
164
165 #[test]
166 fn binary_search_witness_test() {
167 let values = [Ok("foo"), Ok("bar"), Ok("baz"), Err(false), Err(true)];
168
169 let (largest_low, smallest_high) =
170 binary_search((0, "bar"), (values.len() - 1, true), |i| {
171 match dbg!(values[dbg!(i)]) {
172 Ok(x) => Direction::Low(x),
173 Err(x) => Direction::High(x),
174 }
175 });
176
177 dbg!(largest_low); // "baz"
178 dbg!(smallest_high); // false
179 }
180}