ASOF join with multiple key columns. Groups both sides by key, then performs per-group ASOF join. `key_fn` extracts the grouping key from each row's data.
(
left: &[TimestampedRow<L>],
right: &[TimestampedRow<R>],
tolerance_ms: i64,
left_key: impl Fn(&L) -> K,
right_key: impl Fn(&R) -> K,
)
| 74 | /// Groups both sides by key, then performs per-group ASOF join. |
| 75 | /// `key_fn` extracts the grouping key from each row's data. |
| 76 | pub fn asof_join_keyed<L, R, K>( |
| 77 | left: &[TimestampedRow<L>], |
| 78 | right: &[TimestampedRow<R>], |
| 79 | tolerance_ms: i64, |
| 80 | left_key: impl Fn(&L) -> K, |
| 81 | right_key: impl Fn(&R) -> K, |
| 82 | ) -> Vec<AsofMatch<L, R>> |
| 83 | where |
| 84 | L: Clone, |
| 85 | R: Clone, |
| 86 | K: Eq + std::hash::Hash + Clone, |
| 87 | { |
| 88 | use std::collections::HashMap; |
| 89 | |
| 90 | // Group right side by key. |
| 91 | let mut right_groups: HashMap<K, Vec<&TimestampedRow<R>>> = HashMap::new(); |
| 92 | for row in right { |
| 93 | right_groups |
| 94 | .entry(right_key(&row.data)) |
| 95 | .or_default() |
| 96 | .push(row); |
| 97 | } |
| 98 | |
| 99 | let mut results = Vec::with_capacity(left.len()); |
| 100 | |
| 101 | for left_row in left { |
| 102 | let key = left_key(&left_row.data); |
| 103 | let matched = if let Some(group) = right_groups.get(&key) { |
| 104 | // Binary search for largest timestamp <= left timestamp. |
| 105 | let target = left_row.timestamp_ms; |
| 106 | let pos = group.partition_point(|r| r.timestamp_ms <= target); |
| 107 | if pos > 0 { |
| 108 | let candidate = group[pos - 1]; |
| 109 | let gap = target - candidate.timestamp_ms; |
| 110 | if tolerance_ms == i64::MAX || gap <= tolerance_ms { |
| 111 | Some(candidate.clone()) |
| 112 | } else { |
| 113 | None |
| 114 | } |
| 115 | } else { |
| 116 | None |
| 117 | } |
| 118 | } else { |
| 119 | None |
| 120 | }; |
| 121 | |
| 122 | results.push(AsofMatch { |
| 123 | left: left_row.clone(), |
| 124 | right: matched, |
| 125 | }); |
| 126 | } |
| 127 | |
| 128 | results |
| 129 | } |
| 130 | |
| 131 | #[cfg(test)] |
| 132 | mod tests { |