diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml new file mode 100644 index 0000000..a618bfd --- /dev/null +++ b/.github/workflows/ci-tests.yml @@ -0,0 +1,25 @@ +name: CI Tests + +on: + push: + branches: ['**'] # Test on push to all branches + pull_request: + branches: ['**'] # Test on PR to all branches (can remove type filter or adjust as needed) + +jobs: + test: # Renamed job for clarity + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 9.0.x + - name: Restore dependencies + run: dotnet restore + - name: Build + run: dotnet build --no-restore + - name: Test + run: dotnet test --no-build --verbosity normal diff --git a/.github/workflows/dotnetcore.yml b/.github/workflows/manual-publish.yml similarity index 72% rename from .github/workflows/dotnetcore.yml rename to .github/workflows/manual-publish.yml index 5990827..8e41d69 100644 --- a/.github/workflows/dotnetcore.yml +++ b/.github/workflows/manual-publish.yml @@ -1,24 +1,19 @@ -name: NuGet Generation +name: Manual NuGet Publication on: - push: - branches: [ master ] - pull_request: - types: [closed] - branches: [ master ] + workflow_dispatch: # Allows manual triggering from the GitHub Actions UI jobs: - build: - + publish: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - name: Setup .NET Core - uses: actions/setup-dotnet@v1 + - uses: actions/checkout@v4 + - name: Setup .NET + uses: actions/setup-dotnet@v4 with: - dotnet-version: 3.1.202 - - name: Install dependencies + dotnet-version: 9.0.x + - name: Restore dependencies run: dotnet restore - name: Pack solution run: dotnet pack --configuration Release -o out --no-restore diff --git a/IntSet.sln b/IntSet.sln index 84e0eff..fb24024 100644 --- a/IntSet.sln +++ b/IntSet.sln @@ -1,12 +1,18 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29613.14 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36109.1 d17.14 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IntSet", "src\IntSet\IntSet.csproj", "{AE63B664-F383-48F8-8EEE-70FCB2169AF7}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IntSet.Tests", "src\IntSet.Tests\IntSet.Tests.csproj", "{175DFCC8-9221-4D95-81D3-42C7252227D0}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Элементы решения", "Элементы решения", "{754FC069-D67B-A9D7-50A1-8D1CA196D8F1}" + ProjectSection(SolutionItems) = preProject + .github\workflows\ci-tests.yml = .github\workflows\ci-tests.yml + .github\workflows\manual-publish.yml = .github\workflows\manual-publish.yml + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU diff --git a/src/IntSet.Tests/IntSetTests.cs b/src/IntSet.Tests/IntSetTests.cs index 4c159a5..28062b1 100644 --- a/src/IntSet.Tests/IntSetTests.cs +++ b/src/IntSet.Tests/IntSetTests.cs @@ -1,6 +1,7 @@  using System; using System.Collections; +using System.Collections.Generic; using System.Linq; using Xunit; @@ -450,5 +451,159 @@ public void CopyTo_MatchExactSpace_CopiesAll() set.CopyTo(dst, 2); Assert.Equal(new[] { 0, 0, 1, 2, 3 }, dst); } + + #region GetElementsInRange Tests + + [Fact] + public void GetElementsInRange_EmptySet() + { + var set = new Kibnet.IntSet(); + Assert.Equal(new List(), set.GetElementsInRange(1, 10).ToList()); + Assert.Equal(new List(), set.GetElementsInRange(10, 1).ToList()); + } + + [Fact] + public void GetElementsInRange_PopulatedSet_Ascending() + { + var set = new Kibnet.IntSet(new[] { 1, 5, 10, 15, 20, 25 }); + Assert.Equal(new List { 1, 5, 10, 15, 20, 25 }, set.GetElementsInRange(1, 25).ToList()); + Assert.Equal(new List { 10, 15, 20 }, set.GetElementsInRange(10, 20).ToList()); + Assert.Equal(new List { 1, 5 }, set.GetElementsInRange(0, 5).ToList()); + Assert.Equal(new List { 20, 25 }, set.GetElementsInRange(20, 30).ToList()); + Assert.Equal(new List(), set.GetElementsInRange(11, 14).ToList()); + Assert.Equal(new List { 1, 5, 10, 15, 20, 25 }, set.GetElementsInRange(0, 30).ToList()); + Assert.Equal(new List { 5, 10, 15 }, set.GetElementsInRange(5, 15).ToList()); + } + + [Fact] + public void GetElementsInRange_PopulatedSet_Descending() + { + var set = new Kibnet.IntSet(new[] { 1, 5, 10, 15, 20, 25 }); + Assert.Equal(new List { 25, 20, 15, 10, 5, 1 }, set.GetElementsInRange(25, 1).ToList()); + Assert.Equal(new List { 20, 15, 10 }, set.GetElementsInRange(20, 10).ToList()); + Assert.Equal(new List { 25, 20 }, set.GetElementsInRange(30, 20).ToList()); + Assert.Equal(new List { 5, 1 }, set.GetElementsInRange(5, 0).ToList()); + Assert.Equal(new List(), set.GetElementsInRange(14, 11).ToList()); + Assert.Equal(new List { 25, 20, 15, 10, 5, 1 }, set.GetElementsInRange(30, 0).ToList()); + Assert.Equal(new List { 15, 10, 5 }, set.GetElementsInRange(15, 5).ToList()); + } + + [Fact] + public void GetElementsInRange_PopulatedSet_SingleElementRange() + { + var set = new Kibnet.IntSet(new[] { 1, 5, 10, 15, 20 }); + Assert.Equal(new List { 10 }, set.GetElementsInRange(10, 10).ToList()); + Assert.Equal(new List(), set.GetElementsInRange(11, 11).ToList()); + Assert.Equal(new List { 1 }, set.GetElementsInRange(1, 1).ToList()); + Assert.Equal(new List { 20 }, set.GetElementsInRange(20, 20).ToList()); + } + + [Fact] + public void GetElementsInRange_ComplexScenarios_Gaps() + { + var set = new Kibnet.IntSet(new[] { 1, 2, 3, 10, 11, 12, 20, 21, 22 }); + Assert.Equal(new List { 3, 10, 11 }, set.GetElementsInRange(3, 11).ToList()); + Assert.Equal(new List { 11, 10, 3 }, set.GetElementsInRange(11, 3).ToList()); + Assert.Equal(new List { 3, 10 }, set.GetElementsInRange(3, 10).ToList()); // Test case name was 'Asc across gap' + Assert.Equal(new List { 10, 3 }, set.GetElementsInRange(10, 3).ToList()); // Test case name was 'Desc across gap' + } + + [Fact] + public void GetElementsInRange_ComplexScenarios_NegativeNumbers() + { + var negSet = new Kibnet.IntSet(new[] { -10, -5, 0, 5, 10 }); + Assert.Equal(new List { -5, 0, 5 }, negSet.GetElementsInRange(-5, 5).ToList()); + Assert.Equal(new List { 5, 0, -5 }, negSet.GetElementsInRange(5, -5).ToList()); + Assert.Equal(new List { -10, -5 }, negSet.GetElementsInRange(-15, -5).ToList()); + Assert.Equal(new List { -5, -10 }, negSet.GetElementsInRange(-5, -15).ToList()); + Assert.Equal(new List { -10 }, negSet.GetElementsInRange(-10, -10).ToList()); + Assert.Equal(new List { 0 }, negSet.GetElementsInRange(0, 0).ToList()); + Assert.Equal(new List { 10 }, negSet.GetElementsInRange(10, 10).ToList()); + } + + [Fact] + public void GetElementsInRange_FullRange_MinMaxInt() + { + var set = new Kibnet.IntSet(new[] { int.MinValue, 0, int.MaxValue }); + Assert.Equal(new List { int.MinValue, 0, int.MaxValue }, set.GetElementsInRange(int.MinValue, int.MaxValue).ToList()); + Assert.Equal(new List { int.MaxValue, 0, int.MinValue }, set.GetElementsInRange(int.MaxValue, int.MinValue).ToList()); + } + + [Fact] + public void GetElementsInRange_LargeNumbers() + { + var set = new Kibnet.IntSet(new[] { 1000000, 2000000, 3000000, int.MaxValue - 5, int.MaxValue }); + Assert.Equal(new List { 1000000, 2000000 }, set.GetElementsInRange(1000000, 2500000).ToList()); + Assert.Equal(new List { 2000000, 1000000 }, set.GetElementsInRange(2500000, 1000000).ToList()); + Assert.Equal(new List { int.MaxValue - 5, int.MaxValue }, set.GetElementsInRange(int.MaxValue - 10, int.MaxValue).ToList()); + Assert.Equal(new List { int.MaxValue, int.MaxValue -5 }, set.GetElementsInRange(int.MaxValue, int.MaxValue - 10).ToList()); + } + + [Fact] + public void GetElementsInRange_RangeOutsidePopulatedData() + { + var set = new Kibnet.IntSet(new[] { 10, 20, 30 }); + Assert.Equal(new List(), set.GetElementsInRange(1, 5).ToList()); // Range before + Assert.Equal(new List(), set.GetElementsInRange(5, 1).ToList()); + Assert.Equal(new List(), set.GetElementsInRange(35, 40).ToList()); // Range after + Assert.Equal(new List(), set.GetElementsInRange(40, 35).ToList()); + Assert.Equal(new List(), set.GetElementsInRange(22, 28).ToList()); // Range between elements + Assert.Equal(new List(), set.GetElementsInRange(28, 22).ToList()); + } + + [Fact] + public void GetElementsInRange_SingleElementInSet_MatchingRange() + { + var set = new Kibnet.IntSet(new[] { 42 }); + Assert.Equal(new List { 42 }, set.GetElementsInRange(42, 42).ToList()); + Assert.Equal(new List { 42 }, set.GetElementsInRange(40, 45).ToList()); + Assert.Equal(new List { 42 }, set.GetElementsInRange(45, 40).ToList()); + } + + [Fact] + public void GetElementsInRange_SingleElementInSet_NonMatchingRange() + { + var set = new Kibnet.IntSet(new[] { 42 }); + Assert.Equal(new List(), set.GetElementsInRange(10, 20).ToList()); + Assert.Equal(new List(), set.GetElementsInRange(50, 60).ToList()); + } + + // Test with a set that forces traversal through multiple levels of cards + [Fact] + public void GetElementsInRange_DeepTraversal() + { + // These numbers are chosen to likely span different i0, i1, i2, i3 blocks + var data = new List { 0, 1, 2, + (1 << 8) + 5, (1 << 8) + 10, // Different i3 + (1 << 14) + 7, (1 << 14) + 15, // Different i2 + (1 << 20) + 3, (1 << 20) + 9, // Different i1 + (1 << 26) + 100, (1 << 26) + 200, // Different i0 + int.MaxValue - 10, int.MaxValue -1, int.MaxValue}; + var set = new Kibnet.IntSet(data); + + var expectedAsc = data.Where(x => x >= ((1 << 8) + 5) && x <= ((1 << 26) + 100)).OrderBy(x => x).ToList(); + Assert.Equal(expectedAsc, set.GetElementsInRange((1 << 8) + 5, (1 << 26) + 100).ToList()); + + var expectedDesc = data.Where(x => x >= ((1 << 14) + 7) && x <= ((1 << 20) + 9)).OrderByDescending(x => x).ToList(); + Assert.Equal(expectedDesc, set.GetElementsInRange((1 << 20) + 9, (1 << 14) + 7).ToList()); + + // Range including int.MaxValue + Assert.Equal(new List { (1 << 26) + 200, int.MaxValue - 10, int.MaxValue -1, int.MaxValue }, set.GetElementsInRange((1 << 26) + 150, int.MaxValue).ToList()); + Assert.Equal(new List { int.MaxValue, int.MaxValue -1, int.MaxValue - 10, (1 << 26) + 200 }, set.GetElementsInRange(int.MaxValue, (1 << 26) + 150).ToList()); + + // Range including int.MinValue (if 0 is considered MinValue for positive range) + // If IntSet can store negative numbers, this test would be more relevant with actual int.MinValue + var dataWithNeg = new List(data); + dataWithNeg.Add(int.MinValue); + dataWithNeg.Add(int.MinValue+1); + var setWithNeg = new Kibnet.IntSet(dataWithNeg); + dataWithNeg.Sort(); // For expected list + + Assert.Equal(dataWithNeg.Where(x => x >= int.MinValue && x <= 2).ToList(), setWithNeg.GetElementsInRange(int.MinValue, 2).ToList()); + Assert.Equal(dataWithNeg.Where(x => x >= int.MinValue && x <= 2).OrderByDescending(x=>x).ToList(), setWithNeg.GetElementsInRange(2, int.MinValue).ToList()); + + } + + #endregion } } diff --git a/src/IntSet.Tests/StrykerTests.cs b/src/IntSet.Tests/StrykerTests.cs index 4b9ea12..edd9bc7 100644 --- a/src/IntSet.Tests/StrykerTests.cs +++ b/src/IntSet.Tests/StrykerTests.cs @@ -195,6 +195,87 @@ public void SetEquals_DifferentOrderButSameElements_ReturnsTrue() Assert.True(a.SetEquals(b)); } + [Fact] + public void CheckUniqueAndUnfoundElements_EmptySet_ReturnsCorrectCounts() + { + var set = new IntSet(); + var result = set.IsSubsetOf(new[] { 1, 2 }); + Assert.Equal(true, result); + } + + [Fact] + public void CheckUniqueAndUnfoundElements_MixedElements_ReturnsCorrectCounts() + { + var set = new IntSet(new[] { 1, 2, 3 }); + var result = set.IsSubsetOf(new[] { 2, 3, 4, 5 }); + Assert.Equal(false, result); + } + + [Fact] + public void ContainsAllElements_EmptyEnumerable_ReturnsTrue() + { + var set = new IntSet(new[] { 1, 2, 3 }); + Assert.True(set.IsSupersetOf(Enumerable.Empty())); + } + + [Fact] + public void ContainsAllElements_MixedElements_ReturnsFalse() + { + var set = new IntSet(new[] { 1, 2, 3 }); + Assert.False(set.IsSupersetOf(new[] { 2, 3, 4 })); + } + + [Fact] + public void IntersectWithIntSet_EmptySet_DoesNotChange() + { + var set = new IntSet(new[] { 1, 2, 3 }); + var other = new IntSet(); + set.IntersectWith(other); + Assert.Empty(set); + } + + [Fact] + public void IntersectWithIntSet_PartialIntersection_RetainsCommonElements() + { + var set = new IntSet(new[] { 1, 2, 3 }); + var other = new IntSet(new[] { 2, 3, 4 }); + set.IntersectWith(other); + Assert.Equal(new[] { 2, 3 }, set.OrderBy(x => x)); + } + + [Fact] + public void IntersectWithEnumerable_EmptyEnumerable_DoesNotChange() + { + var set = new IntSet(new[] { 1, 2, 3 }); + set.IntersectWith(Enumerable.Empty()); + Assert.Equal(Enumerable.Empty(), set.OrderBy(x => x)); + } + + [Fact] + public void IntersectWithEnumerable_PartialIntersection_RetainsCommonElements() + { + var set = new IntSet(new[] { 1, 2, 3 }); + var other = new[] { 2, 3, 4 }; + set.IntersectWith(other); + Assert.Equal(new[] { 2, 3 }, set.OrderBy(x => x)); + } + + [Fact] + public void Constructor_FullSet_CreatesFullSet() + { + var set = new IntSet(false, true); + Assert.Equal(uint.MaxValue, set.LongCount); + Assert.True(set.root.Full); + Assert.Null(set.root.Cards); + } + + [Fact] + public void Constructor_EmptyCollection_DoesNotThrow() + { + var set = new IntSet(new int[0]); + Assert.Empty(set); + } + [Fact] public void SetEquals_DifferentCounts_ReturnsFalse() { @@ -328,6 +409,58 @@ public void CheckFull_NonFull_DoesNotClearBytes() Assert.NotNull(set.root.Cards[0].Cards[0].Cards[0].Cards[0].Bytes); } + [Fact] + public void Remove_CascadeRemoval_RemovesAllLevels() + { + var set = new IntSet(); + set.Add(1000000); // This will create a deep card structure + Assert.True(set.Contains(1000000)); + Assert.True(set.Remove(1000000)); + Assert.False(set.Contains(1000000)); + Assert.Equal(0, set.Count); + } + + [Fact] + public void LargeSetOperations_ReturnsCorrectCounts() + { + var set = new IntSet(); + for (int i = 0; i < 1000; i++) + { + set.Add(i * 2); // Add even numbers + } + + var otherSet = new IntSet(Enumerable.Range(0, 1000)); + Assert.Equal(1000, set.Count); // 1000 even numbers + Assert.Equal(1000, otherSet.Count); // 1000 numbers + + set.IntersectWith(otherSet); + Assert.Equal(500, set.Count); // 500 even numbers + } + + [Fact] + public void BoundaryValuesOperations_WorkCorrectly() + { + var set = new IntSet(); + + // Test with max int value + set.Add(int.MaxValue); + Assert.True(set.Contains(int.MaxValue)); + Assert.True(set.Remove(int.MaxValue)); + Assert.False(set.Contains(int.MaxValue)); + + // Test with min int value + set.Add(int.MinValue); + Assert.True(set.Contains(int.MinValue)); + Assert.True(set.Remove(int.MinValue)); + Assert.False(set.Contains(int.MinValue)); + + // Test with positive boundary + set.Add(16383); // Last index in first card + Assert.True(set.Contains(16383)); + Assert.True(set.Remove(16383)); + Assert.False(set.Contains(16383)); + } + [Fact] public void UnionWith_AddsAllElements() { diff --git a/src/IntSet/IntSet.cs b/src/IntSet/IntSet.cs index 462669f..e228efc 100644 --- a/src/IntSet/IntSet.cs +++ b/src/IntSet/IntSet.cs @@ -9,6 +9,12 @@ namespace Kibnet { + internal enum TraversalDirection + { + Ascending, + Descending + } + [Serializable] public class IntSet : ISet, IReadOnlyCollection { @@ -40,8 +46,14 @@ public IntSet(bool isFastest, bool isFull) protected internal long _count; + /// + /// The number of elements contained in the set + /// public long LongCount => _count; + /// + /// The flag is a setting indicating that actions need to be performed faster, sacrificing memory release + /// public bool IsFastest { get => _isFastest; @@ -516,15 +528,24 @@ public static void GetMask(Span span, Card arg) public IEnumerator GetEnumerator() { - foreach (var i0 in TraverseRootCards(root)) + foreach (var i0 in TraverseRootCardsDirectional(root, TraversalDirection.Ascending)) { - foreach (var i1 in TraverseCards(root, i0)) + var card0 = root.Full ? root : root.Cards?[i0]; + if (card0 == null) continue; + + foreach (var i1 in TraverseCardLevelDirectional(card0, TraversalDirection.Ascending)) { - foreach (var i2 in TraverseCards(root, i0, i1)) + var card1 = card0.Full ? card0 : card0.Cards?[i1]; + if (card1 == null) continue; + + foreach (var i2 in TraverseCardLevelDirectional(card1, TraversalDirection.Ascending)) { - foreach (var i3 in TraverseCards(root, i0, i1, i2)) + var card2 = card1.Full ? card1 : card1.Cards?[i2]; + if (card2 == null) continue; + + foreach (var i3 in TraverseCardLevelDirectional(card2, TraversalDirection.Ascending)) { - foreach (var value in ProcessBytes(root, i0, i1, i2, i3)) + foreach (var value in ProcessBytesDirectional(root, i0, i1, i2, i3, TraversalDirection.Ascending)) { yield return value; } @@ -538,14 +559,14 @@ private IEnumerable TraverseRootCards(Card root) { for (int i0 = 32; i0 < 64; i0++) { - if (root.Full || root.Cards[i0] != null) + if (root.Full || (root.Cards != null && root.Cards[i0] != null)) { yield return i0; } } for (int i0 = 0; i0 < 32; i0++) { - if (root.Full || root.Cards[i0] != null) + if (root.Full || (root.Cards != null && root.Cards[i0] != null)) { yield return i0; } @@ -554,28 +575,376 @@ private IEnumerable TraverseRootCards(Card root) private IEnumerable TraverseCards(Card node, params int[] indices) { - var current = indices.Aggregate(node, (n, index) => n.Full ? n : n.Cards[index]); - return current != null ? (IEnumerable)current : []; + var current = node; + foreach (var index in indices) + { + if (current.Full) break; + if (current.Cards == null || current.Cards[index] == null) return Enumerable.Empty(); + current = current.Cards[index]; + } + // At this point, 'current' is the card at the level specified by indices. + // This method, as used by the original GetEnumerator, expects to return the *indices* of the next level. + // Or, if it's the byte level, it returns the bytes themselves. + + if (current.Full) // If the target card is full, all its sub-indices/bytes are present. + { + if (indices.Length == 3) // This means 'current' is a level 3 card, next is byte level + { + return Enumerable.Range(0, 32).Select(i => (int)byte.MaxValue); // Represent full bytes + } + else // Next is another card level + { + return Enumerable.Range(0, 64); + } + } + else if (indices.Length == 3) // Target is byte level, and not full + { + if (current.Bytes != null) + { + return current.Bytes.Select(b => (int)b); + } + return Enumerable.Empty(); + } + else // Target is another card level, and not full + { + if (current.Cards != null) + { + List availableIndices = new List(); + for (int i = 0; i < 64; i++) + { + if (current.Cards[i] != null) + { + availableIndices.Add(i); + } + } + return availableIndices; + } + return Enumerable.Empty(); + } } private IEnumerable ProcessBytes(Card root, int i0, int i1, int i2, int i3) { - var bytes = TraverseCards(root, i0, i1, i2, i3); - int byteCount = 0; + // Navigate to the leaf card (level 4 card) + Card leafCard = root; + int[] path = { i0, i1, i2, i3 }; + foreach (var index in path) + { + if (leafCard.Full) break; // All sub-paths exist + if (leafCard.Cards == null || leafCard.Cards[index] == null) yield break; // Path does not exist + leafCard = leafCard.Cards[index]; + } - foreach (var i4 in bytes.Cast()) + IEnumerable bytesToProcess; + if (leafCard.Full) + { + bytesToProcess = Enumerable.Repeat(byte.MaxValue, 32); + } + else if (leafCard.Bytes != null) + { + bytesToProcess = leafCard.Bytes; + } + else { - for (int j = 0; j < 8; j++) + yield break; // No bytes to process + } + + int byteIndexInArray = 0; + foreach (var byteValue in bytesToProcess) + { + for (int bitIdx = 0; bitIdx < 8; bitIdx++) { - if ((i4 & (1 << j)) != 0) + if ((byteValue & (1 << bitIdx)) != 0) { - yield return (i0 << 26) | (i1 << 20) | (i2 << 14) | (i3 << 8) | (byteCount << 3) | j; + yield return (i0 << 26) | (i1 << 20) | (i2 << 14) | (i3 << 8) | (byteIndexInArray << 3) | bitIdx; } } - byteCount++; + byteIndexInArray++; } } + // New Bidirectional Traversal Methods + + private IEnumerable TraverseRootCardsDirectional(Card rootNode, TraversalDirection direction) + { + if (direction == TraversalDirection.Ascending) + { + for (int i0 = 32; i0 < 64; i0++) + { + if (rootNode.Full || (rootNode.Cards != null && rootNode.Cards[i0] != null)) + { + yield return i0; + } + } + for (int i0 = 0; i0 < 32; i0++) + { + if (rootNode.Full || (rootNode.Cards != null && rootNode.Cards[i0] != null)) + { + yield return i0; + } + } + } + else // Descending + { + for (int i0 = 31; i0 >= 0; i0--) + { + if (rootNode.Full || (rootNode.Cards != null && rootNode.Cards[i0] != null)) + { + yield return i0; + } + } + for (int i0 = 63; i0 >= 32; i0--) + { + if (rootNode.Full || (rootNode.Cards != null && rootNode.Cards[i0] != null)) + { + yield return i0; + } + } + } + } + + private IEnumerable TraverseCardLevelDirectional(Card currentCard, TraversalDirection direction) + { + if (direction == TraversalDirection.Ascending) + { + if (currentCard.Cards != null) + { + for (int idx = 0; idx < 64; idx++) + { + if (currentCard.Cards[idx] != null) + { + yield return idx; + } + } + } + else if (currentCard.Full) + { + for (int idx = 0; idx < 64; idx++) + { + yield return idx; + } + } + } + else // Descending + { + if (currentCard.Cards != null) + { + for (int idx = 63; idx >= 0; idx--) + { + if (currentCard.Cards[idx] != null) + { + yield return idx; + } + } + } + else if (currentCard.Full) + { + for (int idx = 63; idx >= 0; idx--) + { + yield return idx; + } + } + } + } + + private IEnumerable TraverseByteLevelDirectional(Card leafCard, TraversalDirection direction) + { + if (direction == TraversalDirection.Ascending) + { + if (leafCard.Bytes != null) + { + for (int byteIdx = 0; byteIdx < 32; byteIdx++) + { + yield return leafCard.Bytes[byteIdx]; + } + } + else if (leafCard.Full) + { + for (int byteIdx = 0; byteIdx < 32; byteIdx++) + { + yield return byte.MaxValue; + } + } + } + else // Descending + { + if (leafCard.Bytes != null) + { + for (int byteIdx = 31; byteIdx >= 0; byteIdx--) + { + yield return leafCard.Bytes[byteIdx]; + } + } + else if (leafCard.Full) + { + for (int byteIdx = 31; byteIdx >= 0; byteIdx--) + { + yield return byte.MaxValue; + } + } + } + } + + private IEnumerable ProcessBytesDirectional(Card rootNode, int i0, int i1, int i2, int i3, TraversalDirection direction) + { + var current = rootNode; + int[] indices = { i0, i1, i2, i3 }; + foreach (var index in indices) + { + if (current.Full) break; + if (current.Cards == null || current.Cards[index] == null) yield break; + current = current.Cards[index]; + } + Card leafCard = current; + + if (direction == TraversalDirection.Ascending) + { + int byteIndexInArray = 0; + foreach (var byteValue in TraverseByteLevelDirectional(leafCard, TraversalDirection.Ascending)) + { + for (int bitIdx = 0; bitIdx < 8; bitIdx++) + { + if ((byteValue & (1 << bitIdx)) != 0) + { + yield return (i0 << 26) | (i1 << 20) | (i2 << 14) | (i3 << 8) | (byteIndexInArray << 3) | bitIdx; + } + } + byteIndexInArray++; + } + } + else // Descending + { + int byteIndexInArray = 31; + foreach (var byteValue in TraverseByteLevelDirectional(leafCard, TraversalDirection.Descending)) + { + for (int bitIdx = 7; bitIdx >= 0; bitIdx--) + { + if ((byteValue & (1 << bitIdx)) != 0) + { + yield return (i0 << 26) | (i1 << 20) | (i2 << 14) | (i3 << 8) | (byteIndexInArray << 3) | bitIdx; + } + } + byteIndexInArray--; + } + } + } + + #endregion + + #region Public API + + /// + /// Get the elements in a given range sorted from first to last + /// + /// First element of range + /// Last element of range + /// + public IEnumerable GetElementsInRange(int firstElement, int lastElement) + { + int minValue = Math.Min(firstElement, lastElement); + int maxValue = Math.Max(firstElement, lastElement); + TraversalDirection direction = (firstElement <= lastElement) ? TraversalDirection.Ascending : TraversalDirection.Descending; + + Card c0Node, c1Node, c2Node; + + foreach (int i0 in TraverseRootCardsDirectional(root, direction)) + { + var (blockMin_i0, blockMax_i0) = GetBlockRange(i0); + if (direction == TraversalDirection.Ascending) { + if (blockMin_i0 > maxValue) break; + if (blockMax_i0 < minValue) continue; + } else { // Descending + if (blockMax_i0 < minValue) break; + if (blockMin_i0 > maxValue) continue; + } + + c0Node = (root.Full || root.Cards == null) ? root : root.Cards[i0]; + if (c0Node == null) continue; + + foreach (int i1 in TraverseCardLevelDirectional(c0Node, direction)) + { + var (blockMin_i1, blockMax_i1) = GetBlockRange(i0, i1); + if (direction == TraversalDirection.Ascending) { + if (blockMin_i1 > maxValue) break; + if (blockMax_i1 < minValue) continue; + } else { // Descending + if (blockMax_i1 < minValue) break; + if (blockMin_i1 > maxValue) continue; + } + + c1Node = (c0Node.Full || c0Node.Cards == null) ? c0Node : c0Node.Cards[i1]; + if (c1Node == null) continue; + + foreach (int i2 in TraverseCardLevelDirectional(c1Node, direction)) + { + var (blockMin_i2, blockMax_i2) = GetBlockRange(i0, i1, i2); + if (direction == TraversalDirection.Ascending) { + if (blockMin_i2 > maxValue) break; + if (blockMax_i2 < minValue) continue; + } else { // Descending + if (blockMax_i2 < minValue) break; + if (blockMin_i2 > maxValue) continue; + } + + c2Node = (c1Node.Full || c1Node.Cards == null) ? c1Node : c1Node.Cards[i2]; + if (c2Node == null) continue; + + foreach (int i3 in TraverseCardLevelDirectional(c2Node, direction)) + { + var (blockMin_i3, blockMax_i3) = GetBlockRange(i0, i1, i2, i3); + if (direction == TraversalDirection.Ascending) { + if (blockMin_i3 > maxValue) break; + if (blockMax_i3 < minValue) continue; + } else { // Descending + if (blockMax_i3 < minValue) break; + if (blockMin_i3 > maxValue) continue; + } + + foreach (int value in ProcessBytesDirectional(root, i0, i1, i2, i3, direction)) + { + if (value >= minValue && value <= maxValue) + { + yield return value; + } + // Optimization: if ProcessBytesDirectional guarantees sorted output for its block: + if (direction == TraversalDirection.Ascending && value > maxValue && value >= blockMin_i3) break; + if (direction == TraversalDirection.Descending && value < minValue && value <= blockMax_i3) break; + } + } + } + } + } + } + + private static (int Min, int Max) GetBlockRange(int i0 = -1, int i1 = -1, int i2 = -1, int i3 = -1) + { + int minBase = 0; + int maxBase = 0; + uint currentMaxMask = 0xFFFFFFFF; + + if (i0 != -1) { + minBase |= (i0 << 26); + maxBase |= (i0 << 26); + currentMaxMask = 0x03FFFFFF; + } + if (i1 != -1) { + minBase |= (i1 << 20); + maxBase |= (i1 << 20); + currentMaxMask = 0x000FFFFF; + } + if (i2 != -1) { + minBase |= (i2 << 14); + maxBase |= (i2 << 14); + currentMaxMask = 0x00003FFF; + } + if (i3 != -1) { + minBase |= (i3 << 8); + maxBase |= (i3 << 8); + currentMaxMask = 0x000000FF; + } + return (minBase, maxBase | (int)currentMaxMask); + } + #endregion #region ICollection