Did some code cleanup,
added some extra thingies. switched to spans. Let google gemini do whatever it wanted..
This commit is contained in:
parent
978d0873dc
commit
7bea233edc
11 changed files with 944 additions and 248 deletions
|
|
@ -13,6 +13,9 @@ namespace PersistentMap
|
|||
public static bool TryGetValue<K, V, TStrategy>(Node<K> root, K key, TStrategy strategy, out V value)
|
||||
where TStrategy : IKeyStrategy<K>
|
||||
{
|
||||
// 1. Calculate ONCE
|
||||
long keyPrefix = strategy.UsesPrefixes ? strategy.GetPrefix(key) : 0;
|
||||
|
||||
Node<K> current = root;
|
||||
while (true)
|
||||
{
|
||||
|
|
@ -20,7 +23,7 @@ namespace PersistentMap
|
|||
{
|
||||
var leaf = current.AsLeaf<V>();
|
||||
// Leaf uses standard FindIndex (Lower Bound) to find exact match
|
||||
int index = FindIndex(leaf, key, strategy);
|
||||
int index = FindIndex(leaf, key, keyPrefix, strategy);
|
||||
if (index < leaf.Header.Count && strategy.Compare(leaf.Keys[index], key) == 0)
|
||||
{
|
||||
value = leaf.Values[index];
|
||||
|
|
@ -33,7 +36,7 @@ namespace PersistentMap
|
|||
{
|
||||
// FIX: Internal uses FindRoutingIndex (Upper Bound)
|
||||
var internalNode = current.AsInternal();
|
||||
int index = FindRoutingIndex(internalNode, key, strategy);
|
||||
int index = FindRoutingIndex(internalNode, key, keyPrefix, strategy);
|
||||
current = internalNode.Children[index]!;
|
||||
}
|
||||
}
|
||||
|
|
@ -65,10 +68,13 @@ public static Node<K> Set<K, V>(Node<K> root, K key, V value, IKeyStrategy<K> st
|
|||
// Recursive Helper
|
||||
private static SplitResult<K>? InsertRecursive<K, V>(Node<K> node, K key, V value, IKeyStrategy<K> strategy, OwnerId owner, out bool added)
|
||||
{
|
||||
// 1. Calculate ONCE
|
||||
long keyPrefix = strategy.UsesPrefixes ? strategy.GetPrefix(key) : 0;
|
||||
|
||||
if (node.IsLeaf)
|
||||
{
|
||||
var leaf = node.AsLeaf<V>();
|
||||
int index = FindIndex(leaf, key, strategy);
|
||||
int index = FindIndex(leaf, key, keyPrefix, strategy);
|
||||
|
||||
if (index < leaf.Header.Count && strategy.Compare(leaf.Keys[index], key) == 0)
|
||||
{
|
||||
|
|
@ -91,7 +97,7 @@ private static SplitResult<K>? InsertRecursive<K, V>(Node<K> node, K key, V valu
|
|||
else
|
||||
{
|
||||
var internalNode = node.AsInternal();
|
||||
int index = FindRoutingIndex(internalNode, key, strategy);
|
||||
int index = FindRoutingIndex(internalNode, key, keyPrefix, strategy);
|
||||
|
||||
var child = internalNode.Children[index]!.EnsureEditable(owner);
|
||||
internalNode.Children[index] = child;
|
||||
|
|
@ -141,10 +147,13 @@ where TStrategy : IKeyStrategy<K>
|
|||
private static bool RemoveRecursive<K, V, TStrategy>(Node<K> node, K key, TStrategy strategy, OwnerId owner, out bool removed)
|
||||
where TStrategy : IKeyStrategy<K>
|
||||
{
|
||||
// 1. Calculate ONCE
|
||||
long keyPrefix = strategy.UsesPrefixes ? strategy.GetPrefix(key) : 0;
|
||||
|
||||
if (node.IsLeaf)
|
||||
{
|
||||
var leaf = node.AsLeaf<V>();
|
||||
int index = FindIndex(leaf, key, strategy);
|
||||
int index = FindIndex(leaf, key, keyPrefix, strategy);
|
||||
|
||||
if (index < leaf.Header.Count && strategy.Compare(leaf.Keys[index], key) == 0)
|
||||
{
|
||||
|
|
@ -159,7 +168,7 @@ where TStrategy : IKeyStrategy<K>
|
|||
else
|
||||
{
|
||||
var internalNode = node.AsInternal();
|
||||
int index = FindRoutingIndex(internalNode, key, strategy);
|
||||
int index = FindRoutingIndex(internalNode, key, keyPrefix, strategy);
|
||||
|
||||
var child = internalNode.Children[index]!.EnsureEditable(owner);
|
||||
internalNode.Children[index] = child;
|
||||
|
|
@ -179,23 +188,17 @@ where TStrategy : IKeyStrategy<K>
|
|||
// ---------------------------------------------------------
|
||||
|
||||
// Used by Leaf Nodes: Finds the first key >= searchKey (Lower Bound)
|
||||
// 2. Propagate to Helpers
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal static int FindIndex<K, TStrategy>(Node<K> node, K key, TStrategy strategy)
|
||||
internal static int FindIndex<K, TStrategy>(Node<K> node, K key, long keyPrefix, TStrategy strategy)
|
||||
where TStrategy : IKeyStrategy<K>
|
||||
{
|
||||
if (strategy.UsesPrefixes)
|
||||
{
|
||||
long keyPrefix = strategy.GetPrefix(key);
|
||||
// Use the pre-calculated prefix here!
|
||||
int index = PrefixScanner.FindFirstGreaterOrEqual(node.Prefixes, keyPrefix);
|
||||
|
||||
// Pass strategy to Refine
|
||||
return RefineSearch(index, node.GetKeys(), key, strategy);
|
||||
}
|
||||
|
||||
Span<K> keys = node.GetKeys();
|
||||
|
||||
|
||||
return LinearSearchKeys(node.GetKeys(), key, strategy);
|
||||
}
|
||||
|
||||
|
|
@ -227,23 +230,18 @@ where TStrategy : IKeyStrategy<K>
|
|||
|
||||
// Used by Internal Nodes: Finds the child index to descend into.
|
||||
// If Key == Separator, we must go RIGHT (index + 1), so we need (Upper Bound).
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal static int FindRoutingIndex<K, TStrategy>(InternalNode<K> node, K key, TStrategy strategy)
|
||||
internal static int FindRoutingIndex<K, TStrategy>(InternalNode<K> node, K key, long keyPrefix, TStrategy strategy)
|
||||
where TStrategy : IKeyStrategy<K>
|
||||
{
|
||||
if (!strategy.UsesPrefixes)
|
||||
{
|
||||
// C. Fallback
|
||||
return LinearSearchRouting(node.GetKeys(), key, strategy);
|
||||
}
|
||||
|
||||
|
||||
long keyPrefix = strategy.GetPrefix(key);
|
||||
|
||||
// SIMD still finds >=.
|
||||
// Use the pre-calculated prefix here!
|
||||
int index = PrefixScanner.FindFirstGreaterOrEqual(node.Prefixes, keyPrefix);
|
||||
|
||||
// Refine using Upper Bound logic (<= 0 instead of < 0)
|
||||
return RefineRouting(index, node.Keys, node.Header.Count, key, strategy);
|
||||
}
|
||||
|
||||
|
|
@ -298,15 +296,17 @@ where TStrategy : IKeyStrategy<K>
|
|||
private static SplitResult<K>? InsertRecur2sive<K, V, TStrategy>(Node<K> node, K key, V value, TStrategy strategy, OwnerId owner)
|
||||
where TStrategy : IKeyStrategy<K>
|
||||
{
|
||||
// DELETE the single FindIndex call at the top.
|
||||
// int index = FindIndex(node, key, strategy); <-- REMOVE THIS
|
||||
|
||||
// 1. Calculate ONCE
|
||||
long keyPrefix = strategy.UsesPrefixes ? strategy.GetPrefix(key) : 0;
|
||||
|
||||
|
||||
// --- LEAF CASE ---
|
||||
if (node.IsLeaf)
|
||||
{
|
||||
var leaf = node.AsLeaf<V>();
|
||||
// Leaf uses FindIndex
|
||||
int index = FindIndex(leaf, key, strategy);
|
||||
int index = FindIndex(leaf, key, keyPrefix, strategy);
|
||||
|
||||
if (index < leaf.Header.Count && strategy.Compare(leaf.Keys[index], key) == 0)
|
||||
{
|
||||
|
|
@ -329,7 +329,7 @@ where TStrategy : IKeyStrategy<K>
|
|||
var internalNode = node.AsInternal();
|
||||
|
||||
// FIX: Internal uses FindRoutingIndex
|
||||
int childIndex = FindRoutingIndex(internalNode, key, strategy);
|
||||
int childIndex = FindRoutingIndex(internalNode, key, keyPrefix, strategy);
|
||||
|
||||
var child = internalNode.Children[childIndex]!.EnsureEditable(owner);
|
||||
internalNode.Children[childIndex] = child;
|
||||
|
|
@ -358,11 +358,10 @@ where TStrategy : IKeyStrategy<K>
|
|||
int count = leaf.Header.Count;
|
||||
if (index < count)
|
||||
{
|
||||
// Arrays allow access up to .Length (64), ignoring 'Count'
|
||||
Array.Copy(leaf.Keys, index, leaf.Keys, index + 1, count - index);
|
||||
|
||||
// This fails if leaf.Values is a Span<V> of length 'count'
|
||||
Array.Copy(leaf.Values, index, leaf.Values, index + 1, count - index);
|
||||
int moveCount = count - index;
|
||||
// Fast Span memory moves
|
||||
leaf.Keys.AsSpan(index, moveCount).CopyTo(leaf.Keys.AsSpan(index + 1));
|
||||
leaf.Values.AsSpan(index, moveCount).CopyTo(leaf.Values.AsSpan(index + 1));
|
||||
|
||||
if (strategy.UsesPrefixes)
|
||||
{
|
||||
|
|
@ -396,8 +395,9 @@ where TStrategy : IKeyStrategy<K>
|
|||
int moveCount = totalCount - splitPoint;
|
||||
if (moveCount > 0)
|
||||
{
|
||||
Array.Copy(left.Keys, splitPoint, right.Keys, 0, moveCount);
|
||||
Array.Copy(left.Values, splitPoint, right.Values, 0, moveCount);
|
||||
// Fast Span memory moves
|
||||
left.Keys.AsSpan(splitPoint, moveCount).CopyTo(right.Keys.AsSpan(0));
|
||||
left.Values.AsSpan(splitPoint, moveCount).CopyTo(right.Values.AsSpan(0));
|
||||
// Manually copy prefixes if needed or re-calculate
|
||||
if (strategy.UsesPrefixes)
|
||||
for (int i = 0; i < moveCount; i++) right.AllPrefixes[i] = left.AllPrefixes[splitPoint + i];
|
||||
|
|
@ -417,9 +417,6 @@ where TStrategy : IKeyStrategy<K>
|
|||
InsertIntoLeaf(right, insertIndex - splitPoint, key, value, strategy);
|
||||
}
|
||||
|
||||
// Linked List Maintenance
|
||||
right.Next = left.Next;
|
||||
left.Next = right;
|
||||
|
||||
// In B+ Tree, the separator is the first key of the right node
|
||||
return new SplitResult<K>(right, right.Keys[0]);
|
||||
|
|
@ -433,7 +430,11 @@ where TStrategy : IKeyStrategy<K>
|
|||
// Shift Keys and Prefixes
|
||||
if (index < count)
|
||||
{
|
||||
Array.Copy(node.Keys, index, node.Keys, index + 1, count - index);
|
||||
|
||||
int moveCount = count - index;
|
||||
|
||||
// Fast Span memory moves
|
||||
node.Keys.AsSpan(index, moveCount).CopyTo(node.Keys.AsSpan(index + 1));
|
||||
|
||||
// FIX: Shift raw prefix array
|
||||
if (strategy.UsesPrefixes)
|
||||
|
|
@ -476,11 +477,16 @@ where TStrategy : IKeyStrategy<K>
|
|||
|
||||
// Move Keys/Prefixes to Right
|
||||
int moveCount = count - splitPoint - 1; // -1 because splitPoint key goes up
|
||||
Array.Copy(left.Keys, splitPoint + 1, right.Keys, 0, moveCount);
|
||||
if (strategy.UsesPrefixes)
|
||||
for(int i=0; i<moveCount; i++) right.AllPrefixes[i] = left.AllPrefixes[splitPoint + 1 + i];
|
||||
// Fast Span memory moves
|
||||
if (moveCount > 0)
|
||||
{
|
||||
left.Keys.AsSpan(splitPoint + 1, moveCount).CopyTo(right.Keys.AsSpan(0));
|
||||
|
||||
// Move Children to Right
|
||||
if (strategy.UsesPrefixes)
|
||||
{
|
||||
left.AllPrefixes.Slice(splitPoint + 1, moveCount).CopyTo(right.AllPrefixes.Slice(0));
|
||||
}
|
||||
}
|
||||
// Left has children 0..splitPoint. Right has children splitPoint+1..End
|
||||
for (int i = 0; i <= moveCount; i++)
|
||||
{
|
||||
|
|
@ -529,13 +535,19 @@ where TStrategy : IKeyStrategy<K>
|
|||
where TStrategy : IKeyStrategy<K>
|
||||
{
|
||||
int count = leaf.Header.Count;
|
||||
Array.Copy(leaf.Keys, index + 1, leaf.Keys, index, count - index - 1);
|
||||
Array.Copy(leaf.Values, index + 1, leaf.Values, index, count - index - 1);
|
||||
int moveCount = count - index - 1;
|
||||
|
||||
if (moveCount > 0)
|
||||
{
|
||||
// Fast Span memory moves
|
||||
leaf.Keys.AsSpan(index + 1, moveCount).CopyTo(leaf.Keys.AsSpan(index));
|
||||
leaf.Values.AsSpan(index + 1, moveCount).CopyTo(leaf.Values.AsSpan(index));
|
||||
|
||||
if (strategy.UsesPrefixes)
|
||||
{
|
||||
var p = leaf.AllPrefixes;
|
||||
for (int i = index; i < count - 1; i++) p[i] = p[i + 1];
|
||||
// Replaced manual 'for' loop with native slice copy
|
||||
leaf.AllPrefixes.Slice(index + 1, moveCount).CopyTo(leaf.AllPrefixes.Slice(index));
|
||||
}
|
||||
}
|
||||
|
||||
leaf.SetCount(count - 1);
|
||||
|
|
@ -604,9 +616,8 @@ where TStrategy : IKeyStrategy<K>
|
|||
|
||||
int lCount = leftLeaf.Header.Count;
|
||||
int rCount = rightLeaf.Header.Count;
|
||||
|
||||
Array.Copy(rightLeaf.Keys, 0, leftLeaf.Keys, lCount, rCount);
|
||||
Array.Copy(rightLeaf.Values, 0, leftLeaf.Values, lCount, rCount);
|
||||
rightLeaf.Keys.AsSpan(0, rCount).CopyTo(leftLeaf.Keys.AsSpan(lCount));
|
||||
rightLeaf.Values.AsSpan(0, rCount).CopyTo(leftLeaf.Values.AsSpan(lCount));
|
||||
if (strategy.UsesPrefixes)
|
||||
{
|
||||
rightLeaf.AllPrefixes.Slice(0, rCount)
|
||||
|
|
@ -614,7 +625,6 @@ where TStrategy : IKeyStrategy<K>
|
|||
}
|
||||
|
||||
leftLeaf.SetCount(lCount + rCount);
|
||||
leftLeaf.Next = rightLeaf.Next;
|
||||
}
|
||||
// Case B: Merging Internal Nodes
|
||||
else
|
||||
|
|
@ -631,7 +641,7 @@ where TStrategy : IKeyStrategy<K>
|
|||
leftInternal.AllPrefixes[lCount] = strategy.GetPrefix(separator);
|
||||
|
||||
int rCount = rightInternal.Header.Count;
|
||||
Array.Copy(rightInternal.Keys, 0, leftInternal.Keys, lCount + 1, rCount);
|
||||
rightInternal.Keys.AsSpan(0, rCount).CopyTo(leftInternal.Keys.AsSpan(lCount + 1));
|
||||
if (strategy.UsesPrefixes)
|
||||
{
|
||||
rightInternal.AllPrefixes.Slice(0, rCount)
|
||||
|
|
@ -648,11 +658,17 @@ where TStrategy : IKeyStrategy<K>
|
|||
|
||||
// Remove Separator and Right Child from Parent
|
||||
int pCount = parent.Header.Count;
|
||||
Array.Copy(parent.Keys, separatorIndex + 1, parent.Keys, separatorIndex, pCount - separatorIndex - 1);
|
||||
int moveCount = pCount - separatorIndex - 1;
|
||||
|
||||
if (moveCount > 0)
|
||||
{
|
||||
parent.Keys.AsSpan(separatorIndex + 1, moveCount).CopyTo(parent.Keys.AsSpan(separatorIndex));
|
||||
|
||||
if (strategy.UsesPrefixes)
|
||||
{
|
||||
var pp = parent.AllPrefixes;
|
||||
for (int i = separatorIndex; i < pCount - 1; i++) pp[i] = pp[i + 1];
|
||||
// Replaced manual 'for' loop with native slice copy
|
||||
parent.AllPrefixes.Slice(separatorIndex + 1, moveCount).CopyTo(parent.AllPrefixes.Slice(separatorIndex));
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = separatorIndex + 2; i <= pCount; i++)
|
||||
|
|
@ -703,11 +719,16 @@ where TStrategy : IKeyStrategy<K>
|
|||
|
||||
// Shift children
|
||||
for (int i = 0; i < rCount; i++) rightInternal.Children[i] = rightInternal.Children[i + 1];
|
||||
if (rCount > 1)
|
||||
{
|
||||
// Fast Span memory moves (Replaces Array.Copy & manual loop)
|
||||
rightInternal.Keys.AsSpan(1, rCount - 1).CopyTo(rightInternal.Keys.AsSpan(0));
|
||||
|
||||
// Shift keys
|
||||
Array.Copy(rightInternal.Keys, 1, rightInternal.Keys, 0, rCount - 1);
|
||||
var rp = rightInternal.AllPrefixes;
|
||||
for(int i=0; i<rCount-1; i++) rp[i] = rp[i+1];
|
||||
if (strategy.UsesPrefixes)
|
||||
{
|
||||
rightInternal.AllPrefixes.Slice(1, rCount - 1).CopyTo(rightInternal.AllPrefixes.Slice(0));
|
||||
}
|
||||
}
|
||||
|
||||
rightInternal.SetCount(rCount - 1);
|
||||
}
|
||||
|
|
@ -750,5 +771,169 @@ where TStrategy : IKeyStrategy<K>
|
|||
leftInternal.SetCount(last);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static bool TryGetMin<K, V>(Node<K> root, out K key, out V value)
|
||||
{
|
||||
var current = root;
|
||||
while (!current.IsLeaf)
|
||||
{
|
||||
current = current.AsInternal().Children[0]!;
|
||||
}
|
||||
|
||||
var leaf = current.AsLeaf<V>();
|
||||
if (leaf.Header.Count == 0)
|
||||
{
|
||||
key = default!;
|
||||
value = default!;
|
||||
return false;
|
||||
}
|
||||
|
||||
key = leaf.Keys![0];
|
||||
value = leaf.Values[0];
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool TryGetMax<K, V>(Node<K> root, out K key, out V value)
|
||||
{
|
||||
var current = root;
|
||||
while (!current.IsLeaf)
|
||||
{
|
||||
var internalNode = current.AsInternal();
|
||||
current = internalNode.Children[internalNode.Header.Count]!;
|
||||
}
|
||||
|
||||
var leaf = current.AsLeaf<V>();
|
||||
if (leaf.Header.Count == 0)
|
||||
{
|
||||
key = default!;
|
||||
value = default!;
|
||||
return false;
|
||||
}
|
||||
|
||||
int last = leaf.Header.Count - 1;
|
||||
key = leaf.Keys![last];
|
||||
value = leaf.Values[last];
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool TryGetSuccessor<K, V, TStrategy>(Node<K> root, K key, TStrategy strategy, out K nextKey, out V nextValue)
|
||||
where TStrategy : IKeyStrategy<K>
|
||||
{
|
||||
InternalNode<K>[] path = new InternalNode<K>[32];
|
||||
int[] indices = new int[32];
|
||||
int depth = 0;
|
||||
long keyPrefix = strategy.UsesPrefixes ? strategy.GetPrefix(key) : 0;
|
||||
|
||||
|
||||
var current = root;
|
||||
while (!current.IsLeaf)
|
||||
{
|
||||
var internalNode = current.AsInternal();
|
||||
int idx = FindRoutingIndex(internalNode, key, keyPrefix, strategy);
|
||||
path[depth] = internalNode;
|
||||
indices[depth] = idx;
|
||||
depth++;
|
||||
current = internalNode.Children[idx]!;
|
||||
}
|
||||
|
||||
var leaf = current.AsLeaf<V>();
|
||||
int index = FindIndex(leaf, key, keyPrefix, strategy);
|
||||
|
||||
if (index < leaf.Header.Count && strategy.Compare(leaf.Keys![index], key) == 0) index++;
|
||||
|
||||
// 1. Successor is in the same leaf
|
||||
if (index < leaf.Header.Count)
|
||||
{
|
||||
nextKey = leaf.Keys![index];
|
||||
nextValue = leaf.Values[index];
|
||||
return true;
|
||||
}
|
||||
|
||||
// 2. Successor is in the next leaf (We must backtrack up the tree!)
|
||||
for (int i = depth - 1; i >= 0; i--)
|
||||
{
|
||||
// If we haven't reached the right-most child of this parent
|
||||
if (indices[i] < path[i].Header.Count)
|
||||
{
|
||||
// Take one step right, then go absolute left all the way down
|
||||
current = path[i].Children[indices[i] + 1]!;
|
||||
while (!current.IsLeaf)
|
||||
{
|
||||
current = current.AsInternal().Children[0]!;
|
||||
}
|
||||
|
||||
var targetLeaf = current.AsLeaf<V>();
|
||||
nextKey = targetLeaf.Keys![0];
|
||||
nextValue = targetLeaf.Values[0];
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
nextKey = default!;
|
||||
nextValue = default!;
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool TryGetPredecessor<K, V, TStrategy>(Node<K> root, K key, TStrategy strategy, out K prevKey, out V prevValue)
|
||||
where TStrategy : IKeyStrategy<K>
|
||||
{
|
||||
// Max depth of a B-Tree is small, preallocate a small array to track the descent path.
|
||||
InternalNode<K>[] path = new InternalNode<K>[32];
|
||||
int[] indices = new int[32];
|
||||
int depth = 0;
|
||||
long keyPrefix = strategy.UsesPrefixes ? strategy.GetPrefix(key) : 0;
|
||||
|
||||
|
||||
var current = root;
|
||||
while (!current.IsLeaf)
|
||||
{
|
||||
var internalNode = current.AsInternal();
|
||||
int idx = FindRoutingIndex(internalNode, key, keyPrefix, strategy);
|
||||
path[depth] = internalNode;
|
||||
indices[depth] = idx;
|
||||
depth++;
|
||||
current = internalNode.Children[idx]!;
|
||||
}
|
||||
|
||||
var leaf = current.AsLeaf<V>();
|
||||
int index = FindIndex(leaf, key, keyPrefix, strategy);
|
||||
|
||||
// Easy case: Predecessor is in the same leaf
|
||||
if (index > 0)
|
||||
{
|
||||
prevKey = leaf.Keys![index - 1];
|
||||
prevValue = leaf.Values[index - 1];
|
||||
return true;
|
||||
}
|
||||
|
||||
// Hard case: We need to backtrack to find the first left branch we ignored
|
||||
for (int i = depth - 1; i >= 0; i--)
|
||||
{
|
||||
if (indices[i] > 0)
|
||||
{
|
||||
// Jump to the left sibling branch, then take the absolute right-most path down
|
||||
current = path[i].Children[indices[i] - 1]!;
|
||||
while (!current.IsLeaf)
|
||||
{
|
||||
var internalNode = current.AsInternal();
|
||||
current = internalNode.Children[internalNode.Header.Count]!;
|
||||
}
|
||||
|
||||
var targetLeaf = current.AsLeaf<V>();
|
||||
int last = targetLeaf.Header.Count - 1;
|
||||
prevKey = targetLeaf.Keys![last];
|
||||
prevValue = targetLeaf.Values[last];
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
prevKey = default!;
|
||||
prevValue = default!;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -80,4 +80,118 @@ public abstract class BaseOrderedMap<K, V, TStrategy> : IEnumerable<KeyValuePair
|
|||
// 4. Until (Start at beginning)
|
||||
public BTreeEnumerable<K, V, TStrategy> Until(K max)
|
||||
=> new(_root, _strategy, false, default, true, max);
|
||||
|
||||
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// Navigation Operations
|
||||
// ---------------------------------------------------------
|
||||
|
||||
public bool TryGetMin(out K key, out V value) => BTreeFunctions.TryGetMin(_root, out key, out value);
|
||||
|
||||
public bool TryGetMax(out K key, out V value) => BTreeFunctions.TryGetMax(_root, out key, out value);
|
||||
|
||||
public bool TryGetSuccessor(K key, out K nextKey, out V nextValue) => BTreeFunctions.TryGetSuccessor(_root, key, _strategy, out nextKey, out nextValue);
|
||||
|
||||
public bool TryGetPredecessor(K key, out K prevKey, out V prevValue) => BTreeFunctions.TryGetPredecessor(_root, key, _strategy, out prevKey, out prevValue);
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// Set Operations (Linear Merge O(N+M))
|
||||
// ---------------------------------------------------------
|
||||
|
||||
public IEnumerable<KeyValuePair<K, V>> Intersect(BaseOrderedMap<K, V, TStrategy> other)
|
||||
{
|
||||
using var enum1 = this.GetEnumerator();
|
||||
using var enum2 = other.GetEnumerator();
|
||||
|
||||
bool has1 = enum1.MoveNext();
|
||||
bool has2 = enum2.MoveNext();
|
||||
|
||||
while (has1 && has2)
|
||||
{
|
||||
int cmp = _strategy.Compare(enum1.Current.Key, enum2.Current.Key);
|
||||
if (cmp == 0)
|
||||
{
|
||||
yield return enum1.Current;
|
||||
has1 = enum1.MoveNext();
|
||||
has2 = enum2.MoveNext();
|
||||
}
|
||||
else if (cmp < 0) has1 = enum1.MoveNext();
|
||||
else has2 = enum2.MoveNext();
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<KeyValuePair<K, V>> Except(BaseOrderedMap<K, V, TStrategy> other)
|
||||
{
|
||||
using var enum1 = this.GetEnumerator();
|
||||
using var enum2 = other.GetEnumerator();
|
||||
|
||||
bool has1 = enum1.MoveNext();
|
||||
bool has2 = enum2.MoveNext();
|
||||
|
||||
while (has1 && has2)
|
||||
{
|
||||
int cmp = _strategy.Compare(enum1.Current.Key, enum2.Current.Key);
|
||||
if (cmp == 0)
|
||||
{
|
||||
has1 = enum1.MoveNext();
|
||||
has2 = enum2.MoveNext();
|
||||
}
|
||||
else if (cmp < 0)
|
||||
{
|
||||
yield return enum1.Current;
|
||||
has1 = enum1.MoveNext();
|
||||
}
|
||||
else
|
||||
{
|
||||
has2 = enum2.MoveNext();
|
||||
}
|
||||
}
|
||||
|
||||
while (has1)
|
||||
{
|
||||
yield return enum1.Current;
|
||||
has1 = enum1.MoveNext();
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<KeyValuePair<K, V>> SymmetricExcept(BaseOrderedMap<K, V, TStrategy> other)
|
||||
{
|
||||
using var enum1 = this.GetEnumerator();
|
||||
using var enum2 = other.GetEnumerator();
|
||||
|
||||
bool has1 = enum1.MoveNext();
|
||||
bool has2 = enum2.MoveNext();
|
||||
|
||||
while (has1 && has2)
|
||||
{
|
||||
int cmp = _strategy.Compare(enum1.Current.Key, enum2.Current.Key);
|
||||
if (cmp == 0)
|
||||
{
|
||||
has1 = enum1.MoveNext();
|
||||
has2 = enum2.MoveNext();
|
||||
}
|
||||
else if (cmp < 0)
|
||||
{
|
||||
yield return enum1.Current;
|
||||
has1 = enum1.MoveNext();
|
||||
}
|
||||
else
|
||||
{
|
||||
yield return enum2.Current;
|
||||
has2 = enum2.MoveNext();
|
||||
}
|
||||
}
|
||||
|
||||
while (has1)
|
||||
{
|
||||
yield return enum1.Current;
|
||||
has1 = enum1.MoveNext();
|
||||
}
|
||||
while (has2)
|
||||
{
|
||||
yield return enum2.Current;
|
||||
has2 = enum2.MoveNext();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -120,12 +120,14 @@ public struct BTreeEnumerator<K, V, TStrategy> : IEnumerator<KeyValuePair<K, V>>
|
|||
{
|
||||
Node<K> node = _root;
|
||||
_depth = 0;
|
||||
long keyPrefix = _strategy.UsesPrefixes ? _strategy.GetPrefix(key) : 0;
|
||||
|
||||
|
||||
// Dive using Routing
|
||||
while (!node.IsLeaf)
|
||||
{
|
||||
var internalNode = node.AsInternal();
|
||||
int idx = BTreeFunctions.FindRoutingIndex<K, TStrategy>(internalNode, key, _strategy);
|
||||
int idx = BTreeFunctions.FindRoutingIndex<K, TStrategy>(internalNode, key, keyPrefix, _strategy);
|
||||
|
||||
_nodeStack[_depth] = internalNode;
|
||||
_indexStack[_depth] = idx;
|
||||
|
|
@ -136,7 +138,7 @@ public struct BTreeEnumerator<K, V, TStrategy> : IEnumerator<KeyValuePair<K, V>>
|
|||
|
||||
// Find index in Leaf
|
||||
_currentLeaf = node.AsLeaf<V>();
|
||||
int index = BTreeFunctions.FindIndex<K, TStrategy>(_currentLeaf, key, _strategy);
|
||||
int index = BTreeFunctions.FindIndex<K, TStrategy>(_currentLeaf, key, keyPrefix, _strategy);
|
||||
|
||||
// Set position to (index - 1) so that the first MoveNext() lands on 'index'
|
||||
_currentLeafIndex = index - 1;
|
||||
|
|
|
|||
|
|
@ -117,10 +117,10 @@ public static class PrefixScanner
|
|||
// If target is MinValue, any value in prefixes is >= target.
|
||||
// So the first element (index 0) is the match.
|
||||
// TODO: evaluate if this is needed.
|
||||
if (targetPrefix == long.MinValue)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
//if (targetPrefix == long.MinValue)
|
||||
//{
|
||||
// return 0;
|
||||
//}
|
||||
|
||||
// Fallback for short arrays or unsupported hardware
|
||||
if (!Avx2.IsSupported || prefixes.Length < 4)
|
||||
|
|
|
|||
11
PersistentMap/PersistentMap.csproj
Normal file
11
PersistentMap/PersistentMap.csproj
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<LangVersion>14</LangVersion>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
113
PersistentMap/Readme.org
Normal file
113
PersistentMap/Readme.org
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
* NiceBtree (PersistentMap)
|
||||
|
||||
A high-performance, persistent (Copy-on-Write) B+ Tree implemented in C#.
|
||||
|
||||
It is designed for zero-overhead reads, SIMD-accelerated key routing, and allocation-free range queries. It supports both fully immutable usage and "Transient" mode for high-throughput bulk mutations.
|
||||
|
||||
** Features
|
||||
- *Copy-on-Write Semantics*: Thread-safe, immutable tree states. Modifying the tree yields a new version while sharing unmodified nodes.
|
||||
- *Transient Mode*: Perform bulk mutations in-place with standard mutable performance, then freeze it into a =PersistentMap= in $O(1)$ time.
|
||||
- *SIMD Prefix Scanning*: Uses AVX2/AVX512 to vectorize B+ tree routing and binary searches via =long= key-prefixes.
|
||||
- *Linear Time Set Operations*: Sort-merge based =Intersect=, =Except=, and =SymmetricExcept= execute in $O(N+M)$ time using lazy evaluation.
|
||||
|
||||
|
||||
** When should I use this?
|
||||
Never, probably. This was just a fun little project. If you want a really fast immutable sorted map you should consider it. Despite this map being faster than LanguageExt.HashMap for some key types, you should definitely use that if you don't need a sorted collection. It is well tested and does not have any problems key collisions, which will slow this map down by a lot.
|
||||
|
||||
** Quick Start
|
||||
|
||||
*** 1. Basic Immutable Usage
|
||||
By default, the map is immutable. Every write operation returns a new, updated version of the map.
|
||||
|
||||
#+begin_src csharp
|
||||
// Create a map with a specific key strategy (e.g., Int, Unicode, Double)
|
||||
var map1 = BaseOrderedMap<int, string, IntStrategy>.Create(new IntStrategy());
|
||||
|
||||
// Set returns a new tree instance. map1 remains empty.
|
||||
var map2 = map1.Set(1, "Apple")
|
||||
.Set(2, "Banana")
|
||||
.Set(3, "Cherry");
|
||||
|
||||
if (map2.TryGetValue(2, out var value))
|
||||
{
|
||||
Console.WriteLine(value); // "Banana"
|
||||
}
|
||||
#+end_src
|
||||
|
||||
*** 2. Transient Mode (Bulk Mutations)
|
||||
If you need to insert thousands of elements, creating a new persistent tree on every insert is too slow. Use a =TransientMap= to mutate the tree in-place, then lock it into a persistent snapshot.
|
||||
|
||||
#+begin_src csharp
|
||||
var transientMap = BaseOrderedMap<int, string, IntStrategy>.CreateTransient(new IntStrategy());
|
||||
|
||||
// Mutates in-place. No allocations for unchanged tree paths.
|
||||
for (int i = 0; i < 10_000; i++)
|
||||
{
|
||||
transientMap.Set(i, $"Value_{i}");
|
||||
}
|
||||
|
||||
// O(1) freeze. Returns a thread-safe immutable PersistentMap.
|
||||
var persistentSnapshot = transientMap.ToPersistent();
|
||||
#+end_src
|
||||
|
||||
*** 3. Range Queries and Iteration
|
||||
Because it is a B+ tree, leaf nodes are linked. Range queries require zero allocations and simply walk the leaves.
|
||||
|
||||
#+begin_src csharp
|
||||
var map = GetPopulatedMap();
|
||||
|
||||
// Iterate exact bounds
|
||||
foreach (var kvp in map.Range(min: 10, max: 50))
|
||||
{
|
||||
Console.WriteLine($"{kvp.Key}: {kvp.Value}");
|
||||
}
|
||||
|
||||
// Open-ended queries
|
||||
var greaterThan100 = map.From(100);
|
||||
var lessThan50 = map.Until(50);
|
||||
var allElements = map.AsEnumerable();
|
||||
#+end_src
|
||||
|
||||
*** 4. Tree Navigation
|
||||
Find bounds and adjacent elements instantly. Missing keys will correctly resolve to the mathematical lower/upper bound.
|
||||
|
||||
#+begin_src csharp
|
||||
// Get extremes
|
||||
map.TryGetMin(out int minKey, out string minVal);
|
||||
map.TryGetMax(out int maxKey, out string maxVal);
|
||||
|
||||
// Get the immediate next/previous element (works even if '42' doesn't exist)
|
||||
if (map.TryGetSuccessor(42, out int nextKey, out string nextVal))
|
||||
{
|
||||
Console.WriteLine($"The key immediately after 42 is {nextKey}");
|
||||
}
|
||||
|
||||
if (map.TryGetPredecessor(42, out int prevKey, out string prevVal))
|
||||
{
|
||||
Console.WriteLine($"The key immediately before 42 is {prevKey}");
|
||||
}
|
||||
#+end_src
|
||||
|
||||
*** 5. Set Operations
|
||||
Set operations take advantage of the tree's underlying sorted linked-list structure to merge trees in linear $O(N+M)$ time.
|
||||
|
||||
#+begin_src csharp
|
||||
var mapA = CreateMap(1, 2, 3, 4);
|
||||
var mapB = CreateMap(3, 4, 5, 6);
|
||||
|
||||
// Returns { 3, 4 }
|
||||
var common = mapA.Intersect(mapB);
|
||||
|
||||
// Returns { 1, 2 }
|
||||
var onlyInA = mapA.Except(mapB);
|
||||
|
||||
// Returns { 1, 2, 5, 6 }
|
||||
var symmetricDiff = mapA.SymmetricExcept(mapB);
|
||||
#+end_src
|
||||
|
||||
** Architecture Notes: Key Strategies
|
||||
NiceBtree uses =IKeyStrategy<K>= to map generic keys (like =string= or =double=) into sortable =long= prefixes. This achieves two things:
|
||||
1. Enables AVX512/AVX2 vector instructions to search internal nodes simultaneously.
|
||||
2. Avoids expensive =IComparable<T>= interface calls or =string.Compare= during the initial descent of the tree, only falling back to exact comparisons when refining the search within a leaf.
|
||||
|
||||
This means that it will be fast for integers and anything you can pack in 8 bytes. If you use stings with high prefix entropy, this will be /very/ performant. If you don't, it is just another b+tree.
|
||||
|
|
@ -2,7 +2,7 @@ using System.Collections;
|
|||
|
||||
namespace PersistentMap;
|
||||
|
||||
public sealed class TransientMap<K, V, TStrategy> : BaseOrderedMap<K, V, TStrategy>, IEnumerable<KeyValuePair<K, V>> where TStrategy : IKeyStrategy<K>
|
||||
public sealed class TransientMap<K, V, TStrategy> : BaseOrderedMap<K, V, TStrategy> where TStrategy : IKeyStrategy<K>
|
||||
{
|
||||
// This is mutable, but we treat it as readonly for the ID generation logic usually.
|
||||
private OwnerId _transactionId;
|
||||
|
|
@ -27,17 +27,12 @@ public sealed class TransientMap<K, V, TStrategy> : BaseOrderedMap<K, V, TStrate
|
|||
|
||||
public PersistentMap<K, V, TStrategy> ToPersistent()
|
||||
{
|
||||
// 1. Create the snapshot.
|
||||
// The nodes currently have _transactionId.
|
||||
// The PersistentMap will read them fine (it reads anything).
|
||||
// BUT: If we write to PersistentMap, it uses OwnerId.None, so it COPIES. (Safe)
|
||||
// 1. Create the snapshot by copying all relevant information
|
||||
|
||||
var snapshot = new PersistentMap<K, V, TStrategy>(_root, _strategy, Count);
|
||||
|
||||
// 2. Protect the snapshot from THIS TransientMap.
|
||||
// If we Set() again on this map, we have the same _transactionId.
|
||||
// We would mutate the nodes we just gave to the snapshot.
|
||||
// FIX: "Seal" the current transaction by rolling to a new ID.
|
||||
// 2. Protect the snapshot from THIS TransientMap by getting a new ownerId
|
||||
// so that future edits will be done by CoW
|
||||
_transactionId = OwnerId.Next();
|
||||
|
||||
return snapshot;
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ public class BTreeFuzzTests
|
|||
// CONFIGURATION
|
||||
const int Iterations = 100_000; // High enough to trigger all splits/merges
|
||||
const int KeyRange = 5000; // Small enough to cause frequent collisions
|
||||
const bool showOps = true;
|
||||
const bool showOps = false;
|
||||
int Seed = 2135974; // Environment.TickCount;
|
||||
|
||||
// ORACLES
|
||||
|
|
|
|||
181
TestProject1/OrderedQueriesTest.cs
Normal file
181
TestProject1/OrderedQueriesTest.cs
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
using System.Linq;
|
||||
using Xunit;
|
||||
using PersistentMap;
|
||||
|
||||
namespace PersistentMap.Tests
|
||||
{
|
||||
public class BTreeExtendedOperationsTests
|
||||
{
|
||||
// Helper to quickly spin up a populated map
|
||||
private TransientMap<int, string, IntStrategy> CreateMap(params int[] keys)
|
||||
{
|
||||
var map = BaseOrderedMap<int, string, IntStrategy>.CreateTransient(new IntStrategy());
|
||||
foreach (var key in keys)
|
||||
{
|
||||
map.Set(key, $"val_{key}");
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinMax_OnEmptyTree_ReturnsFalse()
|
||||
{
|
||||
var map = CreateMap();
|
||||
|
||||
bool hasMin = map.TryGetMin(out int minKey, out string minVal);
|
||||
bool hasMax = map.TryGetMax(out int maxKey, out string maxVal);
|
||||
|
||||
Assert.False(hasMin);
|
||||
Assert.False(hasMax);
|
||||
Assert.Equal(default, minKey);
|
||||
Assert.Equal(default, maxKey);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinMax_OnPopulatedTree_ReturnsCorrectExtremes()
|
||||
{
|
||||
var map = CreateMap(50, 10, 40, 20, 30); // Insert out of order
|
||||
|
||||
bool hasMin = map.TryGetMin(out int minKey, out string minVal);
|
||||
bool hasMax = map.TryGetMax(out int maxKey, out string maxVal);
|
||||
|
||||
Assert.True(hasMin);
|
||||
Assert.Equal(10, minKey);
|
||||
Assert.Equal("val_10", minVal);
|
||||
|
||||
Assert.True(hasMax);
|
||||
Assert.Equal(50, maxKey);
|
||||
Assert.Equal("val_50", maxVal);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(20, true, 30)] // Exact match, get next
|
||||
[InlineData(25, true, 30)] // Missing key, gets first greater
|
||||
[InlineData(50, false, 0)] // Max element has no successor
|
||||
[InlineData(60, false, 0)] // Out of bounds high
|
||||
public void Successor_ReturnsCorrectNextKey(int searchKey, bool expectedSuccess, int expectedNextKey)
|
||||
{
|
||||
var map = CreateMap(10, 20, 30, 40, 50);
|
||||
|
||||
bool success = map.TryGetSuccessor(searchKey, out int nextKey, out string nextVal);
|
||||
|
||||
Assert.Equal(expectedSuccess, success);
|
||||
if (expectedSuccess)
|
||||
{
|
||||
Assert.Equal(expectedNextKey, nextKey);
|
||||
Assert.Equal($"val_{expectedNextKey}", nextVal);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(40, true, 30)] // Exact match, get previous
|
||||
[InlineData(35, true, 30)] // Missing key, gets largest smaller
|
||||
[InlineData(10, false, 0)] // Min element has no predecessor
|
||||
[InlineData(5, false, 0)] // Out of bounds low
|
||||
public void Predecessor_ReturnsCorrectPreviousKey(int searchKey, bool expectedSuccess, int expectedPrevKey)
|
||||
{
|
||||
var map = CreateMap(10, 20, 30, 40, 50);
|
||||
|
||||
bool success = map.TryGetPredecessor(searchKey, out int prevKey, out string prevVal);
|
||||
|
||||
Assert.Equal(expectedSuccess, success);
|
||||
if (expectedSuccess)
|
||||
{
|
||||
Assert.Equal(expectedPrevKey, prevKey);
|
||||
Assert.Equal($"val_{expectedPrevKey}", prevVal);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SuccessorPredecessor_CrossNodeBoundaries_WorksCorrectly()
|
||||
{
|
||||
// Insert 200 elements to guarantee leaf node splits (Capacity is 64)
|
||||
// and internal node creation.
|
||||
var keys = Enumerable.Range(1, 200).ToArray();
|
||||
var map = CreateMap(keys);
|
||||
|
||||
// Test boundaries between multiple leaves
|
||||
for (int i = 1; i < 200; i++)
|
||||
{
|
||||
// Successor
|
||||
bool sFound = map.TryGetSuccessor(i, out int next, out _);
|
||||
Assert.True(sFound);
|
||||
Assert.Equal(i + 1, next);
|
||||
|
||||
// Predecessor
|
||||
bool pFound = map.TryGetPredecessor(i + 1, out int prev, out _);
|
||||
Assert.True(pFound);
|
||||
Assert.Equal(i, prev);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetOperations_Intersect_ReturnsCommonElements()
|
||||
{
|
||||
var mapA = CreateMap(1, 2, 3, 4, 5);
|
||||
var mapB = CreateMap(4, 5, 6, 7, 8);
|
||||
|
||||
var intersect = mapA.Intersect(mapB).Select(kvp => kvp.Key).ToArray();
|
||||
|
||||
Assert.Equal(new[] { 4, 5 }, intersect);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetOperations_Except_ReturnsElementsOnlyInFirstMap()
|
||||
{
|
||||
var mapA = CreateMap(1, 2, 3, 4, 5);
|
||||
var mapB = CreateMap(4, 5, 6, 7, 8);
|
||||
|
||||
var aExceptB = mapA.Except(mapB).Select(kvp => kvp.Key).ToArray();
|
||||
var bExceptA = mapB.Except(mapA).Select(kvp => kvp.Key).ToArray();
|
||||
|
||||
Assert.Equal(new[] { 1, 2, 3 }, aExceptB);
|
||||
Assert.Equal(new[] { 6, 7, 8 }, bExceptA);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetOperations_SymmetricExcept_ReturnsNonOverlappingElements()
|
||||
{
|
||||
var mapA = CreateMap(1, 2, 3, 4, 5);
|
||||
var mapB = CreateMap(4, 5, 6, 7, 8);
|
||||
|
||||
var symmetric = mapA.SymmetricExcept(mapB).Select(kvp => kvp.Key).ToArray();
|
||||
|
||||
// Should return elements exclusively in A or B, but not both.
|
||||
// Expected sorted naturally: 1, 2, 3, 6, 7, 8
|
||||
Assert.Equal(new[] { 1, 2, 3, 6, 7, 8 }, symmetric);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetOperations_WithEmptyMaps_HandleGracefully()
|
||||
{
|
||||
var populatedMap = CreateMap(1, 2, 3);
|
||||
var emptyMap = CreateMap();
|
||||
|
||||
// Intersect with empty is empty
|
||||
Assert.Empty(populatedMap.Intersect(emptyMap));
|
||||
Assert.Empty(emptyMap.Intersect(populatedMap));
|
||||
|
||||
// Populated Except empty is Populated
|
||||
Assert.Equal(new[] { 1, 2, 3 }, populatedMap.Except(emptyMap).Select(k => k.Key));
|
||||
|
||||
// Empty Except Populated is empty
|
||||
Assert.Empty(emptyMap.Except(populatedMap));
|
||||
|
||||
// Symmetric Except with empty is just the populated map
|
||||
Assert.Equal(new[] { 1, 2, 3 }, populatedMap.SymmetricExcept(emptyMap).Select(k => k.Key));
|
||||
Assert.Equal(new[] { 1, 2, 3 }, emptyMap.SymmetricExcept(populatedMap).Select(k => k.Key));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetOperations_CompleteOverlap_HandlesCorrectly()
|
||||
{
|
||||
var mapA = CreateMap(1, 2, 3);
|
||||
var mapB = CreateMap(1, 2, 3);
|
||||
|
||||
Assert.Equal(new[] { 1, 2, 3 }, mapA.Intersect(mapB).Select(k => k.Key));
|
||||
Assert.Empty(mapA.Except(mapB));
|
||||
Assert.Empty(mapA.SymmetricExcept(mapB));
|
||||
}
|
||||
}
|
||||
}
|
||||
25
TestProject1/TestProject1.csproj
Normal file
25
TestProject1/TestProject1.csproj
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4"/>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1"/>
|
||||
<PackageReference Include="xunit" Version="2.9.3"/>
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\PersistentMap\PersistentMap.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
70
TestProject1/UnitTest1.cs
Normal file
70
TestProject1/UnitTest1.cs
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
namespace TestProject1;
|
||||
|
||||
using Xunit;
|
||||
using PersistentMap;
|
||||
|
||||
public class BasicTests
|
||||
{
|
||||
private readonly UnicodeStrategy _strategy = new UnicodeStrategy();
|
||||
|
||||
[Fact]
|
||||
public void Transient_InsertAndGet_Works()
|
||||
{
|
||||
var map = BaseOrderedMap<string, int, UnicodeStrategy>.CreateTransient(_strategy);
|
||||
|
||||
map.Set("Apple", 1);
|
||||
map.Set("Banana", 2);
|
||||
map.Set("Cherry", 3);
|
||||
|
||||
Assert.True(map.TryGetValue("Apple", out int v1));
|
||||
Assert.Equal(1, v1);
|
||||
|
||||
Assert.True(map.TryGetValue("Banana", out int v2));
|
||||
Assert.Equal(2, v2);
|
||||
|
||||
Assert.False(map.TryGetValue("Date", out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Transient_Update_Works()
|
||||
{
|
||||
var map = BaseOrderedMap<string, int, UnicodeStrategy>.CreateTransient(_strategy);
|
||||
map.Set("Key", 100);
|
||||
map.Set("Key", 200); // Overwrite
|
||||
|
||||
map.TryGetValue("Key", out int val);
|
||||
Assert.Equal(200, val);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Transient_Remove_Works()
|
||||
{
|
||||
var map = BaseOrderedMap<string, int, UnicodeStrategy>.CreateTransient(_strategy);
|
||||
map.Set("A", 1);
|
||||
map.Set("B", 2);
|
||||
map.Set("C", 3);
|
||||
|
||||
map.Remove("B");
|
||||
|
||||
Assert.True(map.ContainsKey("A"));
|
||||
Assert.False(map.ContainsKey("B"));
|
||||
Assert.True(map.ContainsKey("C"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Transient_PrefixCollision_HandlesCollision()
|
||||
{
|
||||
// UnicodeStrategy only packs the first 4 chars.
|
||||
// "Test1" and "Test2" have the same prefix "Test".
|
||||
var map = BaseOrderedMap<string, int, UnicodeStrategy>.CreateTransient(_strategy);
|
||||
|
||||
map.Set("Test1", 1);
|
||||
map.Set("Test2", 2);
|
||||
|
||||
Assert.True(map.TryGetValue("Test1", out var v1));
|
||||
Assert.Equal(1, v1);
|
||||
|
||||
Assert.True(map.TryGetValue("Test2", out var v2));
|
||||
Assert.Equal(2, v2);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue