Skip to content

Commit

Permalink
Merge Soluto/RadixTree into Tweek.Engine (#2096)
Browse files Browse the repository at this point in the history
  • Loading branch information
ebickle committed Sep 28, 2023
1 parent d55987a commit 40614e7
Show file tree
Hide file tree
Showing 6 changed files with 673 additions and 2 deletions.
278 changes: 278 additions & 0 deletions core/Engine/Tweek.Engine.Tests/Collections/RadixTreeTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Xunit;

namespace Tweek.Engine.Collections.Tests
{
public class RadixTreeTests
{
[Fact]
public void Insert_ExistingKey_Replaces()
{
//Arrange
var expectedTree = new Dictionary<string, int>
{
[""] = 2,
["a"] = 1,
};
var radixTree = InitTree(expectedTree.Keys, 1);

//Act
var (oldValue, updated) = radixTree.Insert("", 2);

//Assert
Assert.True(updated, "key should be updated");
Assert.Equal(1, oldValue);
Assert.Equal(expectedTree.Count, radixTree.Count);
Assert.Equal(expectedTree.Keys, radixTree.Keys);
Assert.Equal(expectedTree, radixTree.ToDictionary());
}

[Theory]
[InlineData(1)]
[InlineData(100)]
[InlineData(10000)]
public void Insert_NotExistingKey_Adds(int count)
{
//Arrange
var expectedTree = Enumerable.Range(0, count).ToDictionary(_ => Guid.NewGuid().ToString(), x => x);
var radixTree = new RadixTree<int>();

foreach (var item in expectedTree)
{
// Act
var (_, updated) = radixTree.Insert(item.Key, item.Value);

// Assert
Assert.False(updated);
}

Assert.Equal(expectedTree.Count, radixTree.Count);
Assert.Equal(expectedTree.Keys, radixTree.Keys);
Assert.Equal(expectedTree, radixTree.ToDictionary());
}

[Theory]
[InlineData("")]
[InlineData("A")]
[InlineData("AB")]
public void Delete_ExistingKey_Deleted(string keyToDelete)
{
// Arrange
var keys = new[] { "", "A", "AB" };
var radixTree = InitTree(keys, 1);

// Act
var (val, ok) = radixTree.Delete(keyToDelete);

// Assert
Assert.True(ok, "Key should be removed");
Assert.Equal(1, val);
Assert.Equal(keys.Length - 1, radixTree.Count);
Assert.Equal(keys.Where(x => x != keyToDelete), radixTree.Keys.OrderBy(x => x));
}

[Theory]
[InlineData("C")]
[InlineData("AC")]
[InlineData("ABC")]
public void Delete_NotExistingKey_NoChange(string keyToDelete)
{
// Arrange
var keys = new[] { "", "A", "AB" };
var radixTree = InitTree(keys, 1);

// Act
var (_, ok) = radixTree.Delete(keyToDelete);

// Assert
Assert.False(ok, "Key should not be removed");
Assert.Equal(keys.Length, radixTree.Count);
Assert.Equal(keys, radixTree.Keys.OrderBy(x => x));
}

[Theory]
[InlineData("")]
[InlineData("A")]
public void Delete_EmptyTree_NoChange(string keyToDelete)
{
// Arrange
var radixTree = new RadixTree<int>();

// Act
var (_, ok) = radixTree.Delete(keyToDelete);

// Assert
Assert.False(ok, "Key should not be removed");
Assert.Equal(0, radixTree.Count);
}

[Theory]
[InlineData("")]
[InlineData("foo")]
public void TryGetValue_ExistingKey_ReturnsValue(string input)
{
// Arrange
var rand = new Random();
var keys = new[] {
"",
"foo",
};
var dictionary = keys.ToDictionary(x => x, _ => rand.Next());
var radixTree = new RadixTree<int>(dictionary);

// Act
var found = radixTree.TryGetValue(input, out int value);

// Assert
Assert.True(found, "should find value");
Assert.Equal(dictionary[input], value);
}

[Theory]
[InlineData("fo")]
[InlineData("b")]
[InlineData("foobar")]
[InlineData("barfoo")]
public void TryGetValue_NotExistingKey_NotFound(string input)
{
// Arrange
var radixTree = InitTree(new[] { "foo", "bar" }, 1);

// Act
var found = radixTree.TryGetValue(input, out int _);

// Assert
Assert.False(found, "should not find value");
}

[Theory]
[InlineData("")]
[InlineData("a")]
public void TryGetValue_EmptyTree_NotFound(string input)
{
// Arrange
var radixTree = new RadixTree<int>();

// Act
var found = radixTree.TryGetValue(input, out int _);

// Assert
Assert.False(found, "should not find value");
}

[Theory]
[InlineData("a", "")]
[InlineData("abc", "")]
[InlineData("fo", "")]
[InlineData("foo", "foo")]
[InlineData("foob", "foo")]
[InlineData("foobar", "foobar")]
[InlineData("foobarba", "foobar")]
[InlineData("foobarbaz", "foobarbaz")]
[InlineData("foobarbazzi", "foobarbaz")]
[InlineData("foobarbazzip", "foobarbazzip")]
[InlineData("foozi", "foo")]
[InlineData("foozip", "foozip")]
[InlineData("foozipzap", "foozip")]
public void LongestPrefix_ExistingPrefix_ReturnsValue(string input, string expected)
{
// Arrange
var rand = new Random();
var keys = new[] {
"",
"foo",
"foobar",
"foobarbaz",
"foobarbazzip",
"foozip",
};
var dictionary = keys.ToDictionary(x => x, _ => rand.Next());
var radixTree = new RadixTree<int>(dictionary);

// Act
var (key, value, found) = radixTree.LongestPrefix(input);

// Assert
Assert.True(found, "should find longest prefix match");
Assert.Equal(expected, key);
Assert.Equal(dictionary[expected], value);
}

[Theory]
[InlineData("a")]
[InlineData("abc")]
[InlineData("fo")]
[InlineData("oo")]
[InlineData("bar")]
public void LongestPrefix_NonExistingPrefix_NotFound(string input)
{
// Arrange
var keys = new[]
{
"foo",
"foobar",
"foobarbaz",
"foobarbazzip",
"foozip"
};
var radixTree = InitTree(keys, 1);

// Act
var (_, _, found) = radixTree.LongestPrefix(input);

// Assert
Assert.False(found, "should not find longest prefix match");

}

[Theory]
[InlineData("")]
[InlineData("a")]
public void LongestPrefix_EmptyTree_NotFound(string input)
{
// Arrange
var radixTree = new RadixTree<int>();

// Act
var (_, _, found) = radixTree.LongestPrefix(input);

// Assert
Assert.False(found, "should not find longest prefix match");
}

[Theory]
[InlineData("f", new[] { "foobar", "foo/bar/baz", "foo/baz/bar", "foo/zip/zap" })]
[InlineData("foo", new[] { "foobar", "foo/bar/baz", "foo/baz/bar", "foo/zip/zap" })]
[InlineData("foob", new[] { "foobar" })]
[InlineData("foo/", new[] { "foo/bar/baz", "foo/baz/bar", "foo/zip/zap" })]
[InlineData("foo/b", new[] { "foo/bar/baz", "foo/baz/bar" })]
[InlineData("foo/ba", new[] { "foo/bar/baz", "foo/baz/bar" })]
[InlineData("foo/bar", new[] { "foo/bar/baz" })]
[InlineData("foo/bar/baz", new[] { "foo/bar/baz" })]
[InlineData("foo/bar/bazoo", new string[] { })]
[InlineData("z", new[] { "zipzap" })]
public void ListPrefix(string input, string[] expected)
{
// Arrange
var keys = new[]
{
"foobar",
"foo/bar/baz",
"foo/zip/zap",
"zipzap",
"foo/baz/bar"
};
var r = InitTree(keys, 1);

// Act
var result = r.ListPrefix(input);

// Assert
Assert.Equal(expected.Select(x => (x, 1)).OrderBy(x => x).ToList(), result);
}

private static RadixTree<T> InitTree<T>(IEnumerable<string> keys, T initialValue) => new RadixTree<T>(keys.ToDictionary(x => x, _ => initialValue));
}
}
11 changes: 11 additions & 0 deletions core/Engine/Tweek.Engine/Collections/LeafNode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace Tweek.Engine.Collections
{
/// <summary>
/// Represents a value
/// </summary>
internal class LeafNode<TValue>
{
internal string Key = string.Empty;
internal TValue Value;
}
}
62 changes: 62 additions & 0 deletions core/Engine/Tweek.Engine/Collections/Node.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
using System.Collections.Generic;

namespace Tweek.Engine.Collections
{
internal class Node<TValue>
{
/// <summary>
/// Used to store possible leaf
/// </summary>
internal LeafNode<TValue> Leaf;

/// <summary>
/// The common prefix we ignore
/// </summary>
internal string Prefix = string.Empty;

/// <summary>
/// Edges should be stored in-order for iteration.
/// We avoid a fully materialized slice to save memory,
/// since in most cases we expect to be sparse
/// </summary>
internal SortedList<char, Node<TValue>> Edges = new SortedList<char, Node<TValue>>();

public bool IsLeaf => Leaf != null;

public void AddEdge(char label, Node<TValue> node)
{
Edges.Add(label, node);
}

public void SetEdge(char label, Node<TValue> node)
{
Edges[label] = node;
}

public bool TryGetEdge(char label, out Node<TValue> edge)
{
return Edges.TryGetValue(label, out edge);
}

public void RemoveEdge(char label)
{
Edges.Remove(label);
}

public void MergeChild()
{
var child = Edges.Values[0];

Prefix = Prefix + child.Prefix;
Leaf = child.Leaf;
Edges = child.Edges;
}

public void Clear()
{
Leaf = null;
Prefix = string.Empty;
Edges.Clear();
}
}
}

0 comments on commit 40614e7

Please sign in to comment.