|
| 1 | +//! This module defines the [`binary_search_by_key_with_bounds`] utility method. This can be used to |
| 2 | +//! search over sorted slices of values to find the greatest lower or least upper bounds. |
| 3 | +use std::cmp::Ordering; |
| 4 | +use std::error::Error; |
| 5 | +use std::fmt::Debug; |
| 6 | + |
| 7 | +/// Defines the type of bound to return from a binary search operation. |
| 8 | +/// |
| 9 | +/// For a search operation over `values` using key `key`: |
| 10 | +/// * [`Bound::LeastUpper`] - Finds the smallest index `i` such that `values[i] >= key`. |
| 11 | +/// This represents the first element greater than or equal to the search key. |
| 12 | +/// |
| 13 | +/// * [`Bound::GreatestLower`] - Finds the largest index `i` such that `values[i] <= key`. |
| 14 | +/// This represents the last element less than or equal to the search key. |
| 15 | +#[allow(unused)] |
| 16 | +#[derive(Debug, Clone, Copy)] |
| 17 | +pub(crate) enum Bound { |
| 18 | + LeastUpper, |
| 19 | + GreatestLower, |
| 20 | +} |
| 21 | + |
| 22 | +/// Represents the errors that can occur when performing binary search using |
| 23 | +/// [`binary_search_by_key_with_bounds`]. |
| 24 | +#[allow(unused)] |
| 25 | +#[derive(Debug)] |
| 26 | +pub(crate) enum SearchError<T: Error> { |
| 27 | + /// Error that occurs when a search goes out of range. The meaning of "out of range" depends on |
| 28 | + /// the search [`Bound`]: |
| 29 | + /// - If the search bound is [`Bound::LeastUpper`], then no element was greater than or equal |
| 30 | + /// to the provided key. |
| 31 | + /// - If the search bound is [`Bound::GreatestLower`], then no element was less than or equal |
| 32 | + /// to the provided key. |
| 33 | + OutOfRange, |
| 34 | + /// Error that occurs when the `key_fn` fails when retrieving the key in |
| 35 | + /// [`binary_search_by_key_with_bounds`]. The error that occurred in the `key_fn` is returned in |
| 36 | + /// this error variant. |
| 37 | + KeyFunctionError(T), |
| 38 | +} |
| 39 | + |
| 40 | +/// Performs a binary search on a slice to find the greatest lower or least upper bound. |
| 41 | +/// This function performs a binary search with the following properties: |
| 42 | +/// |
| 43 | +/// 1. For [`Bound::LeastUpper`], find the smallest index `i` such that `values[i] >= key` |
| 44 | +/// 2. For [`Bound::GreatestLower`], find the largest index `i` such that `values[i] <= key` |
| 45 | +/// |
| 46 | +/// # Arguments |
| 47 | +/// |
| 48 | +/// * values - The slice to search in |
| 49 | +/// * key - The key value to search for |
| 50 | +/// * key_fn - A fallible function that extracts a comparable key from slice elements |
| 51 | +/// * bound - The type of bound to search for (least upper bound or greatest lower bound) |
| 52 | +/// |
| 53 | +/// # Returns |
| 54 | +/// |
| 55 | +/// * Ok(usize) - The index of the found element |
| 56 | +/// * [`SearchError::OutOfRange`] - If no suitable bound exists in the slice |
| 57 | +/// * [`SearchError::KeyFunctionError`] - If the key function fails. This contains the error |
| 58 | +/// emitted by the `key_fn`. |
| 59 | +/// |
| 60 | +/// #Safety |
| 61 | +/// Assumes that the provided input is sorted by key using the `key_fn`. If the input is not |
| 62 | +/// sorted by key, then the output is unspecified and meaningless. |
| 63 | +/// |
| 64 | +/// # Examples |
| 65 | +/// |
| 66 | +/// ## Finding the Least Upper Bound |
| 67 | +/// ```rust,ignore |
| 68 | +/// # use delta_kernel::history_manager::search::*; |
| 69 | +/// # use delta_kernel::DeltaResult; |
| 70 | +/// let values = [10, 20, 30, 40, 50]; |
| 71 | +/// // Simple key function that just returns the value |
| 72 | +/// let key_fn = |&val| -> DeltaResult<_> { Ok(val) }; |
| 73 | +/// |
| 74 | +/// // Find the least upper bound for 25 (element that is ≥ 25) |
| 75 | +/// let result = binary_search_by_key_with_bounds( |
| 76 | +/// &values, |
| 77 | +/// 25, |
| 78 | +/// key_fn, |
| 79 | +/// Bound::LeastUpper |
| 80 | +/// ); |
| 81 | +/// assert_eq!(result.unwrap(), 2); // Index of 30 |
| 82 | +/// ``` |
| 83 | +/// |
| 84 | +/// ## Finding the Greatest Lower Bound |
| 85 | +/// ```rust,ignore |
| 86 | +/// # use delta_kernel::history_manager::search::*; |
| 87 | +/// # use delta_kernel::DeltaResult; |
| 88 | +/// let values = [10, 20, 30, 40, 50]; |
| 89 | +/// let key_fn = |&val| -> DeltaResult<_> { Ok(val) }; |
| 90 | +/// |
| 91 | +/// // Find the greatest lower bound for 25 (element that is ≤ 25) |
| 92 | +/// let result = binary_search_by_key_with_bounds( |
| 93 | +/// &values, |
| 94 | +/// 25, |
| 95 | +/// key_fn, |
| 96 | +/// Bound::GreatestLower |
| 97 | +/// ); |
| 98 | +/// assert_eq!(result.unwrap(), 1); // Index of 20 |
| 99 | +/// ``` |
| 100 | +/// |
| 101 | +/// ## Handling Out of Range Values |
| 102 | +/// ```rust,ignore |
| 103 | +/// # use delta_kernel::history_manager::search::*; |
| 104 | +/// # use delta_kernel::DeltaResult; |
| 105 | +/// let values = [10, 20, 30, 40, 50]; |
| 106 | +/// let key_fn = |&val| -> DeltaResult<_> { Ok(val) }; |
| 107 | +/// |
| 108 | +/// // Finding a bound that's out of range |
| 109 | +/// let result = binary_search_by_key_with_bounds( |
| 110 | +/// &values, |
| 111 | +/// 5, |
| 112 | +/// key_fn, |
| 113 | +/// Bound::GreatestLower |
| 114 | +/// ); |
| 115 | +/// assert!(matches!(result, Err(SearchError::OutOfRange))); |
| 116 | +/// ``` |
| 117 | +/// |
| 118 | +/// ## Using a Fallible Key Function |
| 119 | +/// ```rust,ignore |
| 120 | +/// # use delta_kernel::history_manager::search::*; |
| 121 | +/// # use delta_kernel::DeltaResult; |
| 122 | +/// // Using a fallible key function |
| 123 | +/// let values = ["10", "20", "thirty", "40"]; |
| 124 | +/// let result = binary_search_by_key_with_bounds( |
| 125 | +/// &values, |
| 126 | +/// 25, |
| 127 | +/// |&s| s.parse::<i32>(), |
| 128 | +/// Bound::LeastUpper |
| 129 | +/// ); |
| 130 | +/// assert!(matches!(result, Err(SearchError::KeyFunctionError(_)))); |
| 131 | +/// ``` |
| 132 | +#[allow(unused)] |
| 133 | +pub(crate) fn binary_search_by_key_with_bounds<'a, T, K: Ord + Debug, E: Error>( |
| 134 | + values: &'a [T], |
| 135 | + key: K, |
| 136 | + key_fn: impl Fn(&'a T) -> Result<K, E>, |
| 137 | + bound: Bound, |
| 138 | +) -> Result<usize, SearchError<E>> { |
| 139 | + let (mut lo, mut hi) = (0, values.len()); |
| 140 | + while lo != hi { |
| 141 | + let mid = lo + (hi - lo) / 2; |
| 142 | + debug_assert!(lo <= mid && mid < hi); |
| 143 | + |
| 144 | + // Use the key function to get a key. Return an error otherwise. |
| 145 | + let mid_key = key_fn(&values[mid]).map_err(SearchError::KeyFunctionError)?; |
| 146 | + match (key.cmp(&mid_key), bound) { |
| 147 | + (Ordering::Less, _) => hi = mid, |
| 148 | + (Ordering::Equal, Bound::LeastUpper) => hi = mid, |
| 149 | + (Ordering::Equal, Bound::GreatestLower) => lo = mid + 1, |
| 150 | + (Ordering::Greater, _) => lo = mid + 1, |
| 151 | + } |
| 152 | + } |
| 153 | + |
| 154 | + // No exact match. We have a valid LUB (GLB) only if the upper (lower) |
| 155 | + // bound actually moved during the search. |
| 156 | + match bound { |
| 157 | + Bound::LeastUpper if hi < values.len() => Ok(hi), |
| 158 | + Bound::GreatestLower if lo > 0 => Ok(lo - 1), |
| 159 | + _ => Err(SearchError::OutOfRange), |
| 160 | + } |
| 161 | +} |
| 162 | + |
| 163 | +#[cfg(test)] |
| 164 | +mod tests { |
| 165 | + use super::*; |
| 166 | + use crate::{DeltaResult, Error}; |
| 167 | + |
| 168 | + // Simple key extraction function |
| 169 | + fn get_val(x: &i32) -> DeltaResult<i32> { |
| 170 | + Ok(*x) |
| 171 | + } |
| 172 | + |
| 173 | + #[test] |
| 174 | + fn test_exact_match() { |
| 175 | + let values = vec![1, 3, 5, 7, 9]; |
| 176 | + |
| 177 | + // LeastUpper bound with exact match |
| 178 | + let result = |
| 179 | + binary_search_by_key_with_bounds(&values, 5, get_val, Bound::LeastUpper).unwrap(); |
| 180 | + assert_eq!(result, 2); |
| 181 | + |
| 182 | + // GreatestLower bound with exact match |
| 183 | + let result = |
| 184 | + binary_search_by_key_with_bounds(&values, 5, get_val, Bound::GreatestLower).unwrap(); |
| 185 | + assert_eq!(result, 2); |
| 186 | + } |
| 187 | + |
| 188 | + #[test] |
| 189 | + fn test_no_exact_match() { |
| 190 | + let values = vec![1, 3, 5, 7, 9]; |
| 191 | + |
| 192 | + // LeastUpper bound (find element >= key) |
| 193 | + let result = |
| 194 | + binary_search_by_key_with_bounds(&values, 4, get_val, Bound::LeastUpper).unwrap(); |
| 195 | + assert_eq!(result, 2); // Index of 5 |
| 196 | + |
| 197 | + // GreatestLower bound (find element <= key) |
| 198 | + let result = |
| 199 | + binary_search_by_key_with_bounds(&values, 6, get_val, Bound::GreatestLower).unwrap(); |
| 200 | + assert_eq!(result, 2); // Index of 5 |
| 201 | + } |
| 202 | + |
| 203 | + #[test] |
| 204 | + fn test_duplicate_values() { |
| 205 | + let values = vec![1, 3, 5, 5, 5, 7, 9]; |
| 206 | + |
| 207 | + // LeastUpper should find first occurrence |
| 208 | + let result = |
| 209 | + binary_search_by_key_with_bounds(&values, 5, get_val, Bound::LeastUpper).unwrap(); |
| 210 | + assert_eq!(result, 2); // First index of 5 |
| 211 | + |
| 212 | + // GreatestLower should find last occurrence |
| 213 | + let result = |
| 214 | + binary_search_by_key_with_bounds(&values, 5, get_val, Bound::GreatestLower).unwrap(); |
| 215 | + assert_eq!(result, 4); // Last index of 5 |
| 216 | + } |
| 217 | + |
| 218 | + #[test] |
| 219 | + fn test_edge_cases() { |
| 220 | + // Empty array |
| 221 | + let empty: Vec<i32> = vec![]; |
| 222 | + let result = binary_search_by_key_with_bounds(&empty, 5, get_val, Bound::LeastUpper); |
| 223 | + assert!(result.is_err()); |
| 224 | + |
| 225 | + // Value less than all elements (LeastUpper) |
| 226 | + let values = vec![5, 7, 9]; |
| 227 | + let result = |
| 228 | + binary_search_by_key_with_bounds(&values, 3, get_val, Bound::LeastUpper).unwrap(); |
| 229 | + assert_eq!(result, 0); // Should return index of first element |
| 230 | + |
| 231 | + // Value less than all elements (GreatestLower) |
| 232 | + let result = binary_search_by_key_with_bounds(&values, 3, get_val, Bound::GreatestLower); |
| 233 | + assert!(matches!(result, Err(SearchError::OutOfRange))); // No lower bound exists |
| 234 | + |
| 235 | + // Value greater than all elements (LeastUpper) |
| 236 | + let result = binary_search_by_key_with_bounds(&values, 10, get_val, Bound::LeastUpper); |
| 237 | + assert!(matches!(result, Err(SearchError::OutOfRange))); // No upper bound exists |
| 238 | + |
| 239 | + // Value greater than all elements (GreatestLower) |
| 240 | + let result = |
| 241 | + binary_search_by_key_with_bounds(&values, 10, get_val, Bound::GreatestLower).unwrap(); |
| 242 | + assert_eq!(result, 2); // Should return index of last element |
| 243 | + } |
| 244 | + #[test] |
| 245 | + fn test_error_propagation() { |
| 246 | + let values = vec![1, 3, 5, 7, 9]; |
| 247 | + |
| 248 | + let failing_key_fn = |x: &i32| -> DeltaResult<i32> { |
| 249 | + if *x == 5 { |
| 250 | + Err(Error::generic("Error extracting key")) |
| 251 | + } else { |
| 252 | + Ok(*x) |
| 253 | + } |
| 254 | + }; |
| 255 | + |
| 256 | + let result = |
| 257 | + binary_search_by_key_with_bounds(&values, 7, failing_key_fn, Bound::LeastUpper); |
| 258 | + assert!(matches!( |
| 259 | + result, |
| 260 | + Err(SearchError::KeyFunctionError(crate::Error::Generic(msg))) if msg.contains("Error extracting key") |
| 261 | + )); |
| 262 | + } |
| 263 | +} |
0 commit comments