Skip to content

Commit 1b50859

Browse files
committed
feat: BTreeMap::merge implemented with into iterators only (similar to BTreeMap::append)
1 parent b7fb220 commit 1b50859

4 files changed

Lines changed: 223 additions & 1 deletion

File tree

library/alloc/src/collections/btree/append.rs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,36 @@ impl<K, V> Root<K, V> {
3333
self.bulk_push(iter, length, alloc)
3434
}
3535

36+
/// Merges all key-value pairs from the union of two ascending iterators,
37+
/// incrementing a `length` variable along the way. The latter makes it
38+
/// easier for the caller to avoid a leak when a drop handler panicks.
39+
///
40+
/// If both iterators produce the same key, this method constructs a pair using the
41+
/// key from the left iterator and calls on a closure `f` to return a value given
42+
/// the conflicting key and value from left and right iterators.
43+
///
44+
/// If you want the tree to end up in a strictly ascending order, like for
45+
/// a `BTreeMap`, both iterators should produce keys in strictly ascending
46+
/// order, each greater than all keys in the tree, including any keys
47+
/// already in the tree upon entry.
48+
pub(super) fn merge_from_sorted_iters_with<I, A: Allocator + Clone>(
49+
&mut self,
50+
left: I,
51+
right: I,
52+
length: &mut usize,
53+
alloc: A,
54+
f: impl FnMut(&K, V, V) -> V,
55+
) where
56+
K: Ord,
57+
I: Iterator<Item = (K, V)> + FusedIterator,
58+
{
59+
// We prepare to merge `left` and `right` into a sorted sequence in linear time.
60+
let iter = MergeIterWith { inner: MergeIterInner::new(left, right), f };
61+
62+
// Meanwhile, we build a tree from the sorted sequence in linear time.
63+
self.bulk_push(iter, length, alloc)
64+
}
65+
3666
/// Pushes all key-value pairs to the end of the tree, incrementing a
3767
/// `length` variable along the way. The latter makes it easier for the
3868
/// caller to avoid a leak when the iterator panicks.
@@ -115,3 +145,33 @@ where
115145
}
116146
}
117147
}
148+
149+
/// An iterator for merging two sorted sequences into one with
150+
/// a callback function to return a value on conflicting keys
151+
struct MergeIterWith<F, K, V, I: Iterator<Item = (K, V)>> {
152+
inner: MergeIterInner<I>,
153+
f: F,
154+
}
155+
156+
impl<F, K: Ord, V, I> Iterator for MergeIterWith<F, K, V, I>
157+
where
158+
F: FnMut(&K, V, V) -> V,
159+
I: Iterator<Item = (K, V)> + FusedIterator,
160+
{
161+
type Item = (K, V);
162+
163+
/// If two keys are equal, returns the key from the left and uses `f` to return
164+
/// a value given the conflicting key and values from left and right
165+
fn next(&mut self) -> Option<(K, V)> {
166+
let (a_next, b_next) = self.inner.nexts(|a: &(K, V), b: &(K, V)| K::cmp(&a.0, &b.0));
167+
match (a_next, b_next) {
168+
(Some((a_k, a_v)), Some((_, b_v))) => Some({
169+
let next_val = (self.f)(&a_k, a_v, b_v);
170+
(a_k, next_val)
171+
}),
172+
(Some(a), None) => Some(a),
173+
(None, Some(b)) => Some(b),
174+
(None, None) => None,
175+
}
176+
}
177+
}

library/alloc/src/collections/btree/map.rs

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1240,6 +1240,81 @@ impl<K, V, A: Allocator + Clone> BTreeMap<K, V, A> {
12401240
)
12411241
}
12421242

1243+
/// Moves all elements from `other` into `self`, leaving `other` empty.
1244+
///
1245+
/// If a key from `other` is already present in `self`, then the `conflict`
1246+
/// closure is used to return a value to `self`. The `conflict`
1247+
/// closure takes in a borrow of `self`'s key, `self`'s value, and `other`'s value
1248+
/// in that order.
1249+
///
1250+
/// An example of why one might use this method over [`append`]
1251+
/// is to combine `self`'s value with `other`'s value when their keys conflict.
1252+
///
1253+
/// Similar to [`insert`], though, the key is not overwritten,
1254+
/// which matters for types that can be `==` without being identical.
1255+
///
1256+
///
1257+
/// [`insert`]: BTreeMap::insert
1258+
/// [`append`]: BTreeMap::append
1259+
///
1260+
/// # Examples
1261+
///
1262+
/// ```
1263+
/// #![feature(btree_merge)]
1264+
/// use std::collections::BTreeMap;
1265+
///
1266+
/// let mut a = BTreeMap::new();
1267+
/// a.insert(1, String::from("a"));
1268+
/// a.insert(2, String::from("b"));
1269+
/// a.insert(3, String::from("c")); // Note: Key (3) also present in b.
1270+
///
1271+
/// let mut b = BTreeMap::new();
1272+
/// b.insert(3, String::from("d")); // Note: Key (3) also present in a.
1273+
/// b.insert(4, String::from("e"));
1274+
/// b.insert(5, String::from("f"));
1275+
///
1276+
/// // concatenate a's value and b's value
1277+
/// a.merge(b, |_, a_val, b_val| {
1278+
/// format!("{a_val}{b_val}")
1279+
/// });
1280+
///
1281+
/// assert_eq!(a.len(), 5); // all of b's keys in a
1282+
///
1283+
/// assert_eq!(a[&1], "a");
1284+
/// assert_eq!(a[&2], "b");
1285+
/// assert_eq!(a[&3], "cd"); // Note: "c" has been combined with "d".
1286+
/// assert_eq!(a[&4], "e");
1287+
/// assert_eq!(a[&5], "f");
1288+
/// ```
1289+
#[unstable(feature = "btree_merge", issue = "152152")]
1290+
pub fn merge(&mut self, mut other: Self, conflict: impl FnMut(&K, V, V) -> V)
1291+
where
1292+
K: Ord,
1293+
A: Clone,
1294+
{
1295+
// Do we have to append anything at all?
1296+
if other.is_empty() {
1297+
return;
1298+
}
1299+
1300+
// We can just swap `self` and `other` if `self` is empty.
1301+
if self.is_empty() {
1302+
mem::swap(self, &mut other);
1303+
return;
1304+
}
1305+
1306+
let self_iter = mem::replace(self, Self::new_in((*self.alloc).clone())).into_iter();
1307+
let other_iter = mem::replace(&mut other, Self::new_in((*self.alloc).clone())).into_iter();
1308+
let root = self.root.get_or_insert_with(|| Root::new((*self.alloc).clone()));
1309+
root.merge_from_sorted_iters_with(
1310+
self_iter,
1311+
other_iter,
1312+
&mut self.length,
1313+
(*self.alloc).clone(),
1314+
conflict,
1315+
)
1316+
}
1317+
12431318
/// Constructs a double-ended iterator over a sub-range of elements in the map.
12441319
/// The simplest way is to use the range syntax `min..max`, thus `range(min..max)` will
12451320
/// yield elements from min (inclusive) to max (exclusive).

library/alloc/src/collections/btree/map/tests.rs

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
use core::assert_matches;
2-
use std::iter;
32
use std::ops::Bound::{Excluded, Included, Unbounded};
43
use std::panic::{AssertUnwindSafe, catch_unwind};
54
use std::sync::atomic::AtomicUsize;
65
use std::sync::atomic::Ordering::SeqCst;
6+
use std::{cmp, iter};
77

88
use super::*;
99
use crate::boxed::Box;
@@ -2128,6 +2128,76 @@ create_append_test!(test_append_239, 239);
21282128
#[cfg(not(miri))] // Miri is too slow
21292129
create_append_test!(test_append_1700, 1700);
21302130

2131+
macro_rules! create_merge_test {
2132+
($name:ident, $len:expr) => {
2133+
#[test]
2134+
fn $name() {
2135+
let mut a = BTreeMap::new();
2136+
for i in 0..8 {
2137+
a.insert(i, i);
2138+
}
2139+
2140+
let mut b = BTreeMap::new();
2141+
for i in 5..$len {
2142+
b.insert(i, 2 * i);
2143+
}
2144+
2145+
a.merge(b, |_, a_val, b_val| a_val + b_val);
2146+
2147+
assert_eq!(a.len(), cmp::max($len, 8));
2148+
2149+
for i in 0..cmp::max($len, 8) {
2150+
if i < 5 {
2151+
assert_eq!(a[&i], i);
2152+
} else {
2153+
if i < cmp::min($len, 8) {
2154+
assert_eq!(a[&i], i + 2 * i);
2155+
} else if i >= $len {
2156+
assert_eq!(a[&i], i);
2157+
} else {
2158+
assert_eq!(a[&i], 2 * i);
2159+
}
2160+
}
2161+
}
2162+
2163+
a.check();
2164+
assert_eq!(
2165+
a.remove(&($len - 1)),
2166+
if $len >= 5 && $len < 8 {
2167+
Some(($len - 1) + 2 * ($len - 1))
2168+
} else {
2169+
Some(2 * ($len - 1))
2170+
}
2171+
);
2172+
assert_eq!(a.insert($len - 1, 20), None);
2173+
a.check();
2174+
}
2175+
};
2176+
}
2177+
2178+
// These are mostly for testing the algorithm that "fixes" the right edge after insertion.
2179+
// Single node, merge conflicting key values.
2180+
create_merge_test!(test_merge_7, 7);
2181+
// Single node.
2182+
create_merge_test!(test_merge_9, 9);
2183+
// Two leafs that don't need fixing.
2184+
create_merge_test!(test_merge_17, 17);
2185+
// Two leafs where the second one ends up underfull and needs stealing at the end.
2186+
create_merge_test!(test_merge_14, 14);
2187+
// Two leafs where the second one ends up empty because the insertion finished at the root.
2188+
create_merge_test!(test_merge_12, 12);
2189+
// Three levels; insertion finished at the root.
2190+
create_merge_test!(test_merge_144, 144);
2191+
// Three levels; insertion finished at leaf while there is an empty node on the second level.
2192+
create_merge_test!(test_merge_145, 145);
2193+
// Tests for several randomly chosen sizes.
2194+
create_merge_test!(test_merge_170, 170);
2195+
create_merge_test!(test_merge_181, 181);
2196+
#[cfg(not(miri))] // Miri is too slow
2197+
create_merge_test!(test_merge_239, 239);
2198+
#[cfg(not(miri))] // Miri is too slow
2199+
create_merge_test!(test_merge_1700, 1700);
2200+
21312201
#[test]
21322202
#[cfg_attr(not(panic = "unwind"), ignore = "test requires unwinding support")]
21332203
fn test_append_drop_leak() {
@@ -2615,3 +2685,19 @@ fn test_id_based_append() {
26152685

26162686
assert_eq!(lhs.pop_first().unwrap().0.name, "lhs_k".to_string());
26172687
}
2688+
2689+
#[test]
2690+
fn test_id_based_merge() {
2691+
let mut lhs = BTreeMap::new();
2692+
let mut rhs = BTreeMap::new();
2693+
2694+
lhs.insert(IdBased { id: 0, name: "lhs_k".to_string() }, "1".to_string());
2695+
rhs.insert(IdBased { id: 0, name: "rhs_k".to_string() }, "2".to_string());
2696+
2697+
lhs.merge(rhs, |_, mut lhs_val, rhs_val| {
2698+
lhs_val.push_str(&rhs_val);
2699+
lhs_val
2700+
});
2701+
2702+
assert_eq!(lhs.pop_first().unwrap().0.name, "lhs_k".to_string());
2703+
}

library/alloctests/tests/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#![feature(allocator_api)]
22
#![feature(binary_heap_pop_if)]
3+
#![feature(btree_merge)]
34
#![feature(const_heap)]
45
#![feature(deque_extend_front)]
56
#![feature(iter_array_chunks)]

0 commit comments

Comments
 (0)