From a0f759c20b88f87bd7802102a1626be6b01a838f Mon Sep 17 00:00:00 2001 From: Tomohisa Tanaka Date: Sun, 22 May 2022 17:47:49 +0900 Subject: [PATCH] Imported sources --- .github/workflows/dotnet6.yml | 19 + .gitignore | 10 + COPYRIGHT.md | 23 + Collection.Test/.editorconfig | 22 + Collection.Test/Collection.Test.csproj | 25 + .../Collection/SimpleLinkedHashSet.cs | 213 +++ ...tractImmutableDictionaryConformanceTest.cs | 387 +++++ .../Test/AbstractMicrosoftExampleSetTest.cs | 154 ++ .../Test/AbstractSetConformanceTest.cs | 646 ++++++++ .../Test/HashSet/SetConformanceTest.cs | 10 + .../Collection/Test/HashTableConstantsTest.cs | 53 + .../ImmutableDictionaryConformanceTest.cs | 13 + .../ImmutableLinkedHashMapConformanceTest.cs | 13 + .../ImmutableLinkedHashMapTest.cs | 320 ++++ .../Collection/Test/InternMapTest.cs | 51 + .../Test/LinkedHashSet/BenchmarkTest.cs | 274 ++++ .../Test/LinkedHashSet/FreeHashString.cs | 34 + .../Test/LinkedHashSet/HugeElementsTest.cs | 85 + .../Test/LinkedHashSet/LinkedHashSetTest.cs | 546 +++++++ .../LinkedHashSet/MicrosoftExampleSetTest.cs | 11 + .../Test/LinkedHashSet/SafeLinkedHashSet.cs | 94 ++ .../Test/LinkedHashSet/SameHashOrIndexTest.cs | 137 ++ .../Test/LinkedHashSet/SetConformanceTest.cs | 10 + .../SimpleLinkedHashSet/SetConformanceTest.cs | 10 + Collection.Test/Usings.cs | 2 + Collection.sln | 31 + Collection/.editorconfig | 116 ++ Collection/Collection.csproj | 63 + .../Maroontress/Collection/.namespace.xml | 9 + .../Collection/HashTableConstants.cs | 101 ++ .../Collection/ImmutableLinkedHashMap.cs | 217 +++ .../Maroontress/Collection/InternMap.cs | 105 ++ .../Maroontress/Collection/LinkedHashSet.cs | 1398 +++++++++++++++++ .../Maroontress/Collection/namespace.cs | 6 + Collection/nuget/COPYRIGHT.txt | 23 + Collection/nuget/LEGAL_NOTICES.txt | 4 + Collection/nuget/readme.txt | 3 + README.md | 64 + doc/ImmutableLinkedHashMap.md | 16 + doc/InternMap.md | 15 + doc/LinkedHashSet.md | 142 ++ 41 files changed, 5475 insertions(+) create mode 100644 .github/workflows/dotnet6.yml create mode 100644 .gitignore create mode 100644 COPYRIGHT.md create mode 100644 Collection.Test/.editorconfig create mode 100644 Collection.Test/Collection.Test.csproj create mode 100644 Collection.Test/Maroontress/Collection/SimpleLinkedHashSet.cs create mode 100644 Collection.Test/Maroontress/Collection/Test/AbstractImmutableDictionaryConformanceTest.cs create mode 100644 Collection.Test/Maroontress/Collection/Test/AbstractMicrosoftExampleSetTest.cs create mode 100644 Collection.Test/Maroontress/Collection/Test/AbstractSetConformanceTest.cs create mode 100644 Collection.Test/Maroontress/Collection/Test/HashSet/SetConformanceTest.cs create mode 100644 Collection.Test/Maroontress/Collection/Test/HashTableConstantsTest.cs create mode 100644 Collection.Test/Maroontress/Collection/Test/ImmutableDictionary/ImmutableDictionaryConformanceTest.cs create mode 100644 Collection.Test/Maroontress/Collection/Test/ImmutableLinkedHashMap/ImmutableLinkedHashMapConformanceTest.cs create mode 100644 Collection.Test/Maroontress/Collection/Test/ImmutableLinkedHashMap/ImmutableLinkedHashMapTest.cs create mode 100644 Collection.Test/Maroontress/Collection/Test/InternMapTest.cs create mode 100644 Collection.Test/Maroontress/Collection/Test/LinkedHashSet/BenchmarkTest.cs create mode 100644 Collection.Test/Maroontress/Collection/Test/LinkedHashSet/FreeHashString.cs create mode 100644 Collection.Test/Maroontress/Collection/Test/LinkedHashSet/HugeElementsTest.cs create mode 100644 Collection.Test/Maroontress/Collection/Test/LinkedHashSet/LinkedHashSetTest.cs create mode 100644 Collection.Test/Maroontress/Collection/Test/LinkedHashSet/MicrosoftExampleSetTest.cs create mode 100644 Collection.Test/Maroontress/Collection/Test/LinkedHashSet/SafeLinkedHashSet.cs create mode 100644 Collection.Test/Maroontress/Collection/Test/LinkedHashSet/SameHashOrIndexTest.cs create mode 100644 Collection.Test/Maroontress/Collection/Test/LinkedHashSet/SetConformanceTest.cs create mode 100644 Collection.Test/Maroontress/Collection/Test/SimpleLinkedHashSet/SetConformanceTest.cs create mode 100644 Collection.Test/Usings.cs create mode 100644 Collection.sln create mode 100644 Collection/.editorconfig create mode 100644 Collection/Collection.csproj create mode 100644 Collection/Maroontress/Collection/.namespace.xml create mode 100644 Collection/Maroontress/Collection/HashTableConstants.cs create mode 100644 Collection/Maroontress/Collection/ImmutableLinkedHashMap.cs create mode 100644 Collection/Maroontress/Collection/InternMap.cs create mode 100644 Collection/Maroontress/Collection/LinkedHashSet.cs create mode 100644 Collection/Maroontress/Collection/namespace.cs create mode 100644 Collection/nuget/COPYRIGHT.txt create mode 100644 Collection/nuget/LEGAL_NOTICES.txt create mode 100644 Collection/nuget/readme.txt create mode 100644 README.md create mode 100644 doc/ImmutableLinkedHashMap.md create mode 100644 doc/InternMap.md create mode 100644 doc/LinkedHashSet.md diff --git a/.github/workflows/dotnet6.yml b/.github/workflows/dotnet6.yml new file mode 100644 index 0000000..7c2bac3 --- /dev/null +++ b/.github/workflows/dotnet6.yml @@ -0,0 +1,19 @@ +name: .NET 6 CI + +on: [push] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Setup .NET SDK + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 6.0.300 + - name: Build + run: dotnet build --configuration Release + - name: Test + run: dotnet test --configuration Release --no-build --logger "console;verbosity=detailed" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..99bbfdb --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +/.vs/ +/*/bin/ +/*/obj/ +/Collection/dcx/ +/Collection.Test/TestResults/ +/Coverlet-Html/ +/Collection/html/ +/Collection/Properties/ +/Collection.Test/Properties/ +*.csproj.user diff --git a/COPYRIGHT.md b/COPYRIGHT.md new file mode 100644 index 0000000..17cf9e0 --- /dev/null +++ b/COPYRIGHT.md @@ -0,0 +1,23 @@ +Copyright (c) 2022 Maroontress Fast Software. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the + distribution. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS *AS IS* AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS +BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN +IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/Collection.Test/.editorconfig b/Collection.Test/.editorconfig new file mode 100644 index 0000000..81032d2 --- /dev/null +++ b/Collection.Test/.editorconfig @@ -0,0 +1,22 @@ +[*.cs] + +# SA1600: Elements should be documented +dotnet_diagnostic.SA1600.severity = none + +# SA1101: Prefix local calls with this +dotnet_diagnostic.SA1101.severity = none + +# SA1633: File should have header +dotnet_diagnostic.SA1633.severity = none + +# SA1513: Closing brace should be followed by blank line +dotnet_diagnostic.SA1513.severity = none + +# SA0001: XML comment analysis disabled +dotnet_diagnostic.SA0001.severity = none + +# SA1002: Semicolons should be spaced correctly +dotnet_diagnostic.SA1002.severity = none + +# SA1122: Use string.Empty for empty strings +dotnet_diagnostic.SA1122.severity = none diff --git a/Collection.Test/Collection.Test.csproj b/Collection.Test/Collection.Test.csproj new file mode 100644 index 0000000..3692e52 --- /dev/null +++ b/Collection.Test/Collection.Test.csproj @@ -0,0 +1,25 @@ + + + + net6.0 + enable + enable + + false + + + + + + + + + + + + + + + + + diff --git a/Collection.Test/Maroontress/Collection/SimpleLinkedHashSet.cs b/Collection.Test/Maroontress/Collection/SimpleLinkedHashSet.cs new file mode 100644 index 0000000..a18b749 --- /dev/null +++ b/Collection.Test/Maroontress/Collection/SimpleLinkedHashSet.cs @@ -0,0 +1,213 @@ +namespace Maroontress.Collection; + +using System; +using System.Collections; +using System.Collections.Generic; + +public sealed class SimpleLinkedHashSet : ISet + where T : notnull +{ + private Func> keySet; + + /// + /// Initializes a new instance of the + /// class. + /// + /// + /// The initial capacity. + /// + public SimpleLinkedHashSet(int initialCapacity) + { + Map = new(initialCapacity); + NewKeySet = () => + { + var set = new HashSet(Map.Keys); + keySet = () => set; + return set; + }; + keySet = NewKeySet; + } + + /// + public int Count => List.Count; + + /// + public bool IsReadOnly => false; + + private LinkedList List { get; } = new(); + + private Dictionary> Map { get; set; } + + private Func> NewKeySet { get; } + + /// + public bool Add(T item) + { + if (Map.ContainsKey(item)) + { + return false; + } + var listNode = List.AddLast(item); + Map[item] = listNode; + keySet = NewKeySet; + return true; + } + + /// + public void Clear() + { + Map.Clear(); + List.Clear(); + keySet = NewKeySet; + } + + /// + public bool Contains(T item) => Map.ContainsKey(item); + + /// + public void CopyTo(T[] array, int arrayIndex) + { + if (array is null) + { + throw new ArgumentNullException(nameof(array)); + } + if (arrayIndex < 0) + { + throw new ArgumentOutOfRangeException( + nameof(arrayIndex), + $"Illegal array index: {arrayIndex}"); + } + if (arrayIndex > array.Length + || array.Length < Count + || arrayIndex > array.Length - Count) + { + throw new ArgumentException( + "Too small array length"); + } + List.CopyTo(array, arrayIndex); + } + + /// + public void ExceptWith(IEnumerable other) + { + if (other is null) + { + throw new ArgumentNullException(nameof(other)); + } + foreach (var e in other) + { + Remove(e); + } + keySet = NewKeySet; + } + + /// + public IEnumerator GetEnumerator() => List.GetEnumerator(); + + /// + public void IntersectWith(IEnumerable other) + { + if (other is null) + { + throw new ArgumentNullException(nameof(other)); + } + var newMap = new Dictionary>(); + foreach (var e in other) + { + if (!Map.Remove(e, out var node)) + { + continue; + } + newMap[e] = node; + } + foreach (var v in Map.Values) + { + List.Remove(v); + } + Map = newMap; + keySet = NewKeySet; + } + + /// + public bool IsProperSubsetOf(IEnumerable other) + => keySet().IsProperSubsetOf(other); + + /// + public bool IsProperSupersetOf(IEnumerable other) + => keySet().IsProperSupersetOf(other); + + /// + public bool IsSubsetOf(IEnumerable other) + => keySet().IsSubsetOf(other); + + /// + public bool IsSupersetOf(IEnumerable other) + => keySet().IsSupersetOf(other); + + /// + public bool Overlaps(IEnumerable other) + => keySet().Overlaps(other); + + /// + public bool Remove(T item) + { + if (!Map.Remove(item, out var listNode)) + { + return false; + } + List.Remove(listNode); + keySet = NewKeySet; + return true; + } + + /// + public bool SetEquals(IEnumerable other) + => keySet().SetEquals(other); + + /// + public void SymmetricExceptWith(IEnumerable other) + { + if (other is null) + { + throw new ArgumentNullException(nameof(other)); + } + if (ReferenceEquals(other, this)) + { + Clear(); + return; + } + var set = new HashSet(); + foreach (var e in other) + { + if (!set.Add(e)) + { + continue; + } + if (!Remove(e)) + { + Add(e); + } + } + keySet = NewKeySet; + } + + /// + public void UnionWith(IEnumerable other) + { + if (other is null) + { + throw new ArgumentNullException(nameof(other)); + } + foreach (var e in other) + { + Add(e); + } + keySet = NewKeySet; + } + + /// + void ICollection.Add(T item) => Add(item); + + /// + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +} diff --git a/Collection.Test/Maroontress/Collection/Test/AbstractImmutableDictionaryConformanceTest.cs b/Collection.Test/Maroontress/Collection/Test/AbstractImmutableDictionaryConformanceTest.cs new file mode 100644 index 0000000..e5bb0c7 --- /dev/null +++ b/Collection.Test/Maroontress/Collection/Test/AbstractImmutableDictionaryConformanceTest.cs @@ -0,0 +1,387 @@ +namespace Maroontress.Collection.Test; + +using System.Collections; +using System.Collections.Immutable; + +public abstract class AbstractImmutableDictionaryConformanceTest +{ + [TestMethod] + public void TryGetKey() + { + var empty = NewMap(); + var m = empty.AddRange( + ImmutableArray.Create(123, 456, 789) + .Select(NewPair)); + Assert.IsTrue(m.TryGetKey(123, out var k123)); + Assert.AreEqual(123, k123); + Assert.IsFalse(m.TryGetKey(12, out _)); + } + + [TestMethod] + public void TryGetValue() + { + var empty = NewMap(); + var m = empty.AddRange( + ImmutableArray.Create(123, 456, 789) + .Select(NewPair)); + Assert.IsTrue(m.TryGetValue(123, out var v123)); + Assert.AreEqual("123", v123); + Assert.IsFalse(m.TryGetValue(12, out _)); + } + + [TestMethod] + public void GetEnumerator() + { + var empty = NewMap(); + var m = empty.AddRange( + ImmutableArray.Create(123, 456, 789) + .Select(NewPair)); + var d = m.ToDictionary(p => p.Key, p => p.Value); + Assert.AreEqual(3, d.Count); + Assert.AreEqual("123", d[123]); + Assert.AreEqual("456", d[456]); + Assert.AreEqual("789", d[789]); + } + + [TestMethod] + public void GetEnumerator_IEnumerator() + { + var empty = NewMap(); + var m = empty.AddRange( + ImmutableArray.Create(123, 456, 789) + .Select(NewPair)); + var all = (IEnumerable)m; + var e = all.GetEnumerator(); + var d = new Dictionary(); + while (e.MoveNext()) + { + if (!(e.Current is KeyValuePair p)) + { + throw new AssertFailedException(); + } + d[p.Key] = p.Value; + } + Assert.AreEqual(3, d.Count); + Assert.AreEqual("123", d[123]); + Assert.AreEqual("456", d[456]); + Assert.AreEqual("789", d[789]); + } + + [TestMethod] + public void Contains() + { + var empty = NewMap(); + var m = empty.AddRange( + ImmutableArray.Create(123, 456, 789) + .Select(NewPair)); + Assert.IsTrue(m.Contains(NewPair(123))); + Assert.IsTrue(m.Contains(NewPair(456))); + Assert.IsTrue(m.Contains(NewPair(789))); + Assert.IsFalse(m.Contains(NewPair(12))); + Assert.IsFalse(m.Contains(NewPair(123, "12"))); + } + + [TestMethod] + public void ContainsKey() + { + var empty = NewMap(); + var m = empty.AddRange( + ImmutableArray.Create(123, 456, 789) + .Select(NewPair)); + Assert.IsTrue(m.ContainsKey(123)); + Assert.IsTrue(m.ContainsKey(456)); + Assert.IsTrue(m.ContainsKey(789)); + Assert.IsFalse(m.ContainsKey(1)); + } + + [TestMethod] + public void SetItems_Null() + { + var empty = NewMap(); + Assert.ThrowsException( + () => _ = empty.SetItems(null!)); + } + + [TestMethod] + public void SetItems_KeyDoesNotExist() + { + var empty = NewMap(); + var m1 = empty.AddRange( + ImmutableArray.Create(123, 456, 789) + .Select(NewPair)); + var m2 = m1.SetItems( + ImmutableArray.Create( + NewPair(12), + NewPair(45), + NewPair(12, "78"))); + Assert.AreEqual(5, m2.Count); + Assert.AreEqual("123", m2[123]); + Assert.AreEqual("456", m2[456]); + Assert.AreEqual("789", m2[789]); + Assert.AreEqual("78", m2[12]); + Assert.AreEqual("45", m2[45]); + } + + [TestMethod] + public void SetItems_Empty() + { + var empty = NewMap(); + var m1 = empty.AddRange( + ImmutableArray.Create(123, 456, 789) + .Select(NewPair)); + var m2 = m1.SetItems(empty); + Assert.AreSame(m1, m2); + } + + [TestMethod] + public void SetItems_Overwrite() + { + var empty = NewMap(); + var m1 = empty.AddRange( + ImmutableArray.Create(123, 456, 789) + .Select(NewPair)); + var m2 = m1.SetItems( + ImmutableArray.Create( + NewPair(789, "78"), + NewPair(456, "45"), + NewPair(123, "12"))); + Assert.AreEqual(3, m2.Count); + Assert.AreEqual("12", m2[123]); + Assert.AreEqual("45", m2[456]); + Assert.AreEqual("78", m2[789]); + } + + [TestMethod] + public void SetItems_ReturnThis() + { + var empty = NewMap(); + var m1 = empty.AddRange( + ImmutableArray.Create(123, 456, 789) + .Select(NewPair)); + var m2 = m1.SetItems( + ImmutableArray.Create(789, 456, 123) + .Select(NewPair)); + Assert.AreSame(m1, m2); + } + + [TestMethod] + public void SetItems() + { + var empty = NewMap(); + var m1 = empty.AddRange( + ImmutableArray.Create(123, 456, 789) + .Select(NewPair)); + var m2 = m1.SetItems( + ImmutableArray.Create( + NewPair(12), + NewPair(456), + NewPair(123, "23"))); + Assert.AreEqual(4, m2.Count); + Assert.AreEqual("23", m2[123]); + Assert.AreEqual("456", m2[456]); + Assert.AreEqual("789", m2[789]); + Assert.AreEqual("12", m2[12]); + } + + [TestMethod] + public void SetItems_AppendOnly() + { + var empty = NewMap(); + var m1 = empty.AddRange( + ImmutableArray.Create(123, 456, 789) + .Select(NewPair)); + var m2 = m1.SetItems( + ImmutableArray.Create( + NewPair(12), + NewPair(45))); + Assert.AreEqual(5, m2.Count); + Assert.AreEqual("123", m2[123]); + Assert.AreEqual("456", m2[456]); + Assert.AreEqual("789", m2[789]); + Assert.AreEqual("12", m2[12]); + Assert.AreEqual("45", m2[45]); + } + + [TestMethod] + public void SetItem_Overwrite() + { + var empty = NewMap(); + var m1 = empty.Add(123, "123"); + var m2 = m1.SetItem(123, "456"); + Assert.AreEqual(1, m2.Count); + Assert.AreEqual("456", m2[123]); + } + + [TestMethod] + public void SetItem_KeyDoesNotExist() + { + var empty = NewMap(); + var m1 = empty.Add(123, "123"); + var m2 = m1.SetItem(12, "12"); + Assert.AreEqual(2, m2.Count); + Assert.AreEqual("123", m2[123]); + Assert.AreEqual("12", m2[12]); + } + + [TestMethod] + public void SetItem_ReturnThis() + { + var empty = NewMap(); + var m1 = empty.Add(123, "123"); + var m2 = m1.SetItem(123, "123"); + Assert.AreSame(m1, m2); + } + + [TestMethod] + public void RemoveRange() + { + var empty = NewMap(); + var m1 = empty.AddRange( + ImmutableArray.Create(123, 456, 789, 12) + .Select(NewPair)); + var m2 = m1.RemoveRange( + ImmutableArray.Create(456, 789)); + Assert.AreEqual(2, m2.Count); + Assert.AreEqual("123", m2[123]); + Assert.AreEqual("12", m2[12]); + Assert.IsFalse(m2.ContainsKey(456)); + Assert.IsFalse(m2.ContainsKey(789)); + } + + [TestMethod] + public void RemoveRange_Null() + { + var empty = NewMap(); + Assert.ThrowsException( + () => _ = empty.RemoveRange(null!)); + } + + [TestMethod] + public void RemoveRange_Empty() + { + var empty = NewMap(); + var m1 = empty.AddRange( + ImmutableArray.Create(123, 456, 789) + .Select(NewPair)); + var m2 = m1.RemoveRange(Array.Empty()); + Assert.AreSame(m1, m2); + } + + [TestMethod] + public void RemoveRange_ReturnThis() + { + var empty = NewMap(); + var m1 = empty.AddRange( + ImmutableArray.Create(123, 456, 789) + .Select(NewPair)); + var m2 = m1.RemoveRange( + ImmutableArray.Create(12, 34, 56)); + Assert.AreSame(m1, m2); + } + + [TestMethod] + public void Remove_ReturnThis() + { + var empty = NewMap(); + var m1 = empty.Add(123, "123"); + var m2 = m1.Remove(456); + Assert.AreSame(m1, m2); + } + + [TestMethod] + public void Remove() + { + var empty = NewMap(); + var m1 = empty.AddRange( + ImmutableArray.Create(123, 456, 789) + .Select(NewPair)); + var m2 = m1.Remove(456); + Assert.AreEqual(2, m2.Count); + Assert.AreEqual("123", m2[123]); + Assert.AreEqual("789", m2[789]); + Assert.IsFalse(m2.ContainsKey(456)); + } + + [TestMethod] + public void AddRange_KeyExistsButHasDifferentValue() + { + var empty = NewMap(); + var m1 = empty.Add(123, "123"); + var delta = ImmutableArray.Create( + NewPair(456), + NewPair(789), + NewPair(123, "12")); + Assert.ThrowsException( + () => _ = m1.AddRange(delta)); + } + + [TestMethod] + public void AddRange_Null() + { + var empty = NewMap(); + Assert.ThrowsException( + () => _ = empty.AddRange(null!)); + } + + [TestMethod] + public void AddRange() + { + var empty = NewMap(); + var m = empty.AddRange( + ImmutableArray.Create(123, 456, 789, 789, 456, 123) + .Select(NewPair)); + Assert.AreEqual(3, m.Count); + Assert.AreEqual("123", m[123]); + Assert.AreEqual("456", m[456]); + Assert.AreEqual("789", m[789]); + } + + [TestMethod] + public void AddRange_ReturnThis() + { + var empty = NewMap(); + var m1 = empty.Add(123, "123"); + var m2 = m1.AddRange( + ImmutableArray.Create( + NewPair(123), + NewPair(123, "123"))); + Assert.AreSame(m1, m2); + } + + [TestMethod] + public void AddRange_Empty() + { + var empty = NewMap(); + var m1 = empty.Add(123, "123"); + var m2 = m1.AddRange(empty); + Assert.AreSame(m1, m2); + } + + [TestMethod] + public void Add_ReturnThis() + { + var empty = NewMap(); + var m1 = empty.Add(123, "123"); + var m2 = m1.Add(123, "123"); + var m3 = m1.Add(123, 123.ToString()); + Assert.AreSame(m1, m2); + Assert.AreSame(m1, m3); + } + + [TestMethod] + public void Add_KeyExistsButHasDifferentValue() + { + var empty = NewMap(); + var m1 = empty.Add(123, "123"); + Assert.ThrowsException( + () => _ = m1.Add(123, "456")); + } + + protected abstract IImmutableDictionary NewMap(); + + private static KeyValuePair NewPair(int i) + => new(i, i.ToString()); + + private static KeyValuePair NewPair(int i, string s) + => new(i, s); +} diff --git a/Collection.Test/Maroontress/Collection/Test/AbstractMicrosoftExampleSetTest.cs b/Collection.Test/Maroontress/Collection/Test/AbstractMicrosoftExampleSetTest.cs new file mode 100644 index 0000000..4587604 --- /dev/null +++ b/Collection.Test/Maroontress/Collection/Test/AbstractMicrosoftExampleSetTest.cs @@ -0,0 +1,154 @@ +namespace Maroontress.Collection.Test; + +public abstract class AbstractMicrosoftExampleSetTest +{ + [TestMethod] + public void SetEquals() + { + /* + https://docs.microsoft.com/en-us/dotnet/api/system.collections.generic.hashset-1.setequals?view=net-6.0#examples + */ + var lowNumbers = NewSet(); + var allNumbers = NewSet(); + + lowNumbers.UnionWith(Enumerable.Range(1, 4)); + allNumbers.UnionWith(Enumerable.Range(0, 10)); + Assert.IsFalse(allNumbers.SetEquals(lowNumbers)); + allNumbers.IntersectWith(lowNumbers); + Assert.IsTrue(allNumbers.SetEquals(lowNumbers)); + } + + [TestMethod] + public void UnionWith() + { + /* + https://docs.microsoft.com/en-us/dotnet/api/system.collections.generic.hashset-1.unionwith?view=net-6.0#examples + */ + var evenNumbers = NewSet(); + var oddNumbers = NewSet(); + + foreach (var i in Enumerable.Range(0, 5)) + { + var j = i * 2; + evenNumbers.Add(j); + oddNumbers.Add(j + 1); + } + var numbers = NewSet(); + numbers.UnionWith(evenNumbers); + Assert.IsTrue(numbers.SetEquals(evenNumbers)); + numbers.UnionWith(oddNumbers); + Assert.IsTrue(numbers.SetEquals(Enumerable.Range(0, 10))); + } + + [TestMethod] + public void ExceptWith() + { + /* + https://docs.microsoft.com/en-us/dotnet/api/system.collections.generic.hashset-1.exceptwith?view=net-6.0 + */ + var lowNumbers = NewSet(); + var highNumbers = NewSet(); + + lowNumbers.UnionWith(Enumerable.Range(0, 6)); + highNumbers.UnionWith(Enumerable.Range(3, 7)); + highNumbers.ExceptWith(lowNumbers); + Assert.IsTrue(highNumbers.SetEquals(Enumerable.Range(6, 4))); + } + + [TestMethod] + public void SymmetricExceptWith() + { + /* + https://docs.microsoft.com/en-us/dotnet/api/system.collections.generic.hashset-1.symmetricexceptwith?view=net-6.0 + */ + var lowNumbers = NewSet(); + var highNumbers = NewSet(); + + lowNumbers.UnionWith(Enumerable.Range(0, 6)); + highNumbers.UnionWith(Enumerable.Range(3, 7)); + lowNumbers.SymmetricExceptWith(highNumbers); + var numbers = Enumerable.Range(0, 3) + .Concat(Enumerable.Range(6, 4)); + Assert.IsTrue(lowNumbers.SetEquals(numbers)); + } + + [TestMethod] + public void Overlaps() + { + /* + https://docs.microsoft.com/en-us/dotnet/api/system.collections.generic.hashset-1.overlaps?view=net-6.0#examples + */ + var lowNumbers = NewSet(); + var allNumbers = NewSet(); + + lowNumbers.UnionWith(Enumerable.Range(1, 4)); + allNumbers.UnionWith(Enumerable.Range(0, 10)); + Assert.IsTrue(lowNumbers.Overlaps(allNumbers)); + } + + [TestMethod] + public void IsSubsetOf() + { + /* + https://docs.microsoft.com/en-us/dotnet/api/system.collections.generic.hashset-1.issubsetof?view=net-6.0#examples + */ + var lowNumbers = NewSet(); + var allNumbers = NewSet(); + + lowNumbers.UnionWith(Enumerable.Range(1, 4)); + allNumbers.UnionWith(Enumerable.Range(0, 10)); + Assert.IsTrue(lowNumbers.IsSubsetOf(allNumbers)); + allNumbers.IntersectWith(lowNumbers); + Assert.IsTrue(lowNumbers.IsSubsetOf(allNumbers)); + } + + [TestMethod] + public void IsProperSubsetOf() + { + /* + https://docs.microsoft.com/en-us/dotnet/api/system.collections.generic.hashset-1.ispropersubsetof?view=net-6.0#examples + */ + var lowNumbers = NewSet(); + var allNumbers = NewSet(); + + lowNumbers.UnionWith(Enumerable.Range(1, 4)); + allNumbers.UnionWith(Enumerable.Range(0, 10)); + Assert.IsTrue(lowNumbers.IsProperSubsetOf(allNumbers)); + allNumbers.IntersectWith(lowNumbers); + Assert.IsFalse(lowNumbers.IsProperSubsetOf(allNumbers)); + } + + [TestMethod] + public void IsSupersetOf() + { + /* + https://docs.microsoft.com/en-us/dotnet/api/system.collections.generic.hashset-1.issupersetof?view=net-6.0#examples + */ + var lowNumbers = NewSet(); + var allNumbers = NewSet(); + + lowNumbers.UnionWith(Enumerable.Range(1, 4)); + allNumbers.UnionWith(Enumerable.Range(0, 10)); + Assert.IsTrue(allNumbers.IsSupersetOf(lowNumbers)); + allNumbers.IntersectWith(lowNumbers); + Assert.IsTrue(allNumbers.IsSupersetOf(lowNumbers)); + } + + [TestMethod] + public void IsProperSupersetOf() + { + /* + https://docs.microsoft.com/en-us/dotnet/api/system.collections.generic.hashset-1.ispropersupersetof?view=net-6.0#examples + */ + var lowNumbers = NewSet(); + var allNumbers = NewSet(); + + lowNumbers.UnionWith(Enumerable.Range(1, 4)); + allNumbers.UnionWith(Enumerable.Range(0, 10)); + Assert.IsTrue(allNumbers.IsProperSupersetOf(lowNumbers)); + allNumbers.IntersectWith(lowNumbers); + Assert.IsFalse(allNumbers.IsProperSupersetOf(lowNumbers)); + } + + protected abstract ISet NewSet(); +} diff --git a/Collection.Test/Maroontress/Collection/Test/AbstractSetConformanceTest.cs b/Collection.Test/Maroontress/Collection/Test/AbstractSetConformanceTest.cs new file mode 100644 index 0000000..504bb15 --- /dev/null +++ b/Collection.Test/Maroontress/Collection/Test/AbstractSetConformanceTest.cs @@ -0,0 +1,646 @@ +namespace Maroontress.Collection.Test; + +using System.Collections; +using System.Collections.Immutable; +using System.Linq; + +public abstract class AbstractSetConformanceTest +{ + public static bool SortedSequenceEqual( + IEnumerable left, IEnumerable right) + { + var sortedLeft = left.OrderBy(i => i); + var sortedRight = right.OrderBy(i => i); + return sortedLeft.SequenceEqual(sortedRight); + } + + [TestMethod] + public void InitialState() + { + var s = NewSet(); + Assert.AreEqual(0, s.Count); + Assert.AreEqual(false, s.IsReadOnly); + Assert.IsFalse(s.Any()); + Assert.IsFalse(s.Contains("foo")); + var o = Array.Empty(); + s.CopyTo(o, 0); + Assert.IsFalse(s.Remove("foo")); + } + + [TestMethod] + public void CopyTo() + { + var s = NewSet(); + { + var empty = Array.Empty(); + Assert.ThrowsException( + () => s.CopyTo(empty, 1)); + } + s.Add("foo"); + { + var empty = Array.Empty(); + Assert.ThrowsException( + () => s.CopyTo(empty, 0)); + } + { + var one = new string[1]; + Assert.ThrowsException( + () => s.CopyTo(one, 1)); + } + s.Add("bar"); + { + var one = new string[1]; + Assert.ThrowsException( + () => s.CopyTo(one, 0)); + } + { + var two = new string[2]; + Assert.ThrowsException( + () => s.CopyTo(two, 1)); + } + } + + [TestMethod] + public void CopyTo_ArgumentOutOfRangeException() + { + var s = NewSet(); + { + var empty = Array.Empty(); + Assert.ThrowsException( + () => s.CopyTo(empty, -1)); + } + } + + [TestMethod] + public void CopyTo_Null() + { + var s = NewSet(); + { + Assert.ThrowsException( + () => s.CopyTo(null!, 0)); + } + } + + [TestMethod] + public void AddOne() + { + var s = NewSet(); + Assert.IsTrue(s.Add("foo")); + Assert.AreEqual(1, s.Count); + Assert.IsTrue(s.Any()); + Assert.IsTrue(s.Contains("foo")); + Assert.IsFalse(s.Contains("bar")); + var o = new string[1]; + s.CopyTo(o, 0); + Assert.AreEqual("foo", o[0]); + Assert.IsFalse(s.Remove("bar")); + Assert.IsTrue(s.Remove("foo")); + Assert.IsFalse(s.Remove("foo")); + Assert.AreEqual(0, s.Count); + } + + [TestMethod] + public void AddTwo() + { + var s = NewSet(); + Assert.IsTrue(s.Add("foo")); + Assert.IsFalse(s.Add("foo")); + Assert.AreEqual(1, s.Count); + Assert.IsTrue(s.Add("bar")); + Assert.IsFalse(s.Add("bar")); + Assert.AreEqual(2, s.Count); + Assert.IsTrue(s.Contains("foo")); + Assert.IsTrue(s.Contains("bar")); + Assert.IsFalse(s.Contains("baz")); + var o = new string[2]; + s.CopyTo(o, 0); + Array.Sort(o); + Assert.IsTrue(SortedSequenceEqual(Create("bar", "foo"), o)); + Assert.IsFalse(s.Remove("baz")); + Assert.IsTrue(s.Remove("foo")); + Assert.IsFalse(s.Remove("foo")); + Assert.AreEqual(1, s.Count); + Assert.IsTrue(s.Remove("bar")); + Assert.IsFalse(s.Remove("bar")); + Assert.AreEqual(0, s.Count); + } + + [TestMethod] + public void Clear() + { + var s = NewSet(); + Assert.IsTrue(s.Add("foo")); + Assert.IsTrue(s.Add("bar")); + Assert.IsTrue(s.Add("baz")); + Assert.AreEqual(3, s.Count); + Assert.IsTrue(s.Any()); + s.Clear(); + Assert.AreEqual(0, s.Count); + Assert.IsFalse(s.Any()); + } + + [TestMethod] + public void ExceptWith() + { + var s = NewSet(); + s.Add("foo"); + s.Add("bar"); + s.ExceptWith(Create("bar", "baz")); + Assert.IsTrue(SortedSequenceEqual(Create("foo"), s)); + } + + [TestMethod] + public void ExceptWith_Null() + { + var all = Create("foo", "bar"); + var s = NewSet(); + s.UnionWith(all); + Assert.ThrowsException( + () => s.ExceptWith(null!)); + } + + [TestMethod] + public void IntersectWith() + { + var all = Create("foo", "bar", "baz"); + var s = NewSet(); + s.UnionWith(all); + s.IntersectWith(Create("bar")); + Assert.IsTrue(SortedSequenceEqual(Create("bar"), s)); + } + + [TestMethod] + public void IntersectWith_DoubleAdd() + { + var all = Create("foo", "bar", "baz"); + var s = NewSet(); + s.UnionWith(all); + s.IntersectWith(Create("bar", "bar")); + Assert.IsTrue(SortedSequenceEqual(Create("bar"), s)); + } + + [TestMethod] + public void IntersectWith_4x2() + { + var all = Create("foo", "bar", "baz", "barBaz"); + var s = NewSet(); + s.UnionWith(all); + s.IntersectWith(Create("bar", "baz")); + Assert.IsTrue(SortedSequenceEqual(Create("bar", "baz"), s)); + } + + [TestMethod] + public void IntersectWith_RemoveTail() + { + var all = Create("foo", "bar"); + var s = NewSet(); + s.UnionWith(all); + s.IntersectWith(Create("foo", "baz")); + Assert.IsTrue(SortedSequenceEqual(Create("foo"), s)); + } + + [TestMethod] + public void IntersectWith_RemoveHead() + { + var all = Create("foo", "bar"); + var s = NewSet(); + s.UnionWith(all); + s.IntersectWith(Create("bar", "baz")); + Assert.IsTrue(SortedSequenceEqual(Create("bar"), s)); + } + + [TestMethod] + public void IntersectWith_Self() + { + var all = Create("foo", "bar", "baz"); + var s = NewSet(); + s.UnionWith(all); + s.IntersectWith(s); + Assert.IsTrue(SortedSequenceEqual(all, s)); + } + + [TestMethod] + public void IntersectWith_Null() + { + var all = Create("foo", "bar"); + var s = NewSet(); + s.UnionWith(all); + Assert.ThrowsException( + () => s.IntersectWith(null!)); + } + + [TestMethod] + public void IsProperSubsetOf() + { + static Func, bool> M(ISet set) + => a => set.IsProperSubsetOf(a); + + var s = NewSet(); + var m = M(s); + Assert.IsFalse(m(ImmutableArray.Empty)); + Assert.IsTrue(m(Create("foo"))); + s.Add("foo"); + s.Add("bar"); + Assert.IsFalse(m(ImmutableArray.Empty)); + Assert.IsFalse(m(Create("foo"))); + Assert.IsFalse(m(Create("baz"))); + Assert.IsFalse(m(Create("foo", "bar"))); + Assert.IsFalse(m(Create("foo", "baz"))); + Assert.IsTrue(m(Create("foo", "bar", "baz"))); + Assert.IsTrue(m(Create("foo", "bar", "foo", "baz"))); + } + + [TestMethod] + public void IsProperSubsetOf_Null() + { + var all = Create("foo", "bar"); + var s = NewSet(); + s.UnionWith(all); + Assert.ThrowsException( + () => s.IsProperSubsetOf(null!)); + } + + [TestMethod] + public void IsProperSubsetOf_Self() + { + var all = Create("foo", "bar"); + var s = NewSet(); + s.UnionWith(all); + Assert.IsFalse(s.IsProperSubsetOf(s)); + } + + [TestMethod] + public void IsProperSupersetOf() + { + static Func, bool> M(ISet set) + => a => set.IsProperSupersetOf(a); + + var s = NewSet(); + var m = M(s); + Assert.IsFalse(m(ImmutableArray.Empty)); + Assert.IsFalse(m(Create("foo"))); + s.Add("foo"); + s.Add("bar"); + Assert.IsTrue(m(ImmutableArray.Empty)); + Assert.IsTrue(m(Create("foo"))); + Assert.IsFalse(m(Create("baz"))); + Assert.IsFalse(m(Create("foo", "bar"))); + Assert.IsFalse(m(Create("foo", "baz"))); + Assert.IsFalse(m(Create("foo", "bar", "baz"))); + } + + [TestMethod] + public void IsProperSupersetOf_Null() + { + var all = Create("foo", "bar"); + var s = NewSet(); + s.UnionWith(all); + Assert.ThrowsException( + () => s.IsProperSupersetOf(null!)); + } + + [TestMethod] + public void IsProperSupersetOf_Self() + { + var all = Create("foo", "bar"); + var s = NewSet(); + s.UnionWith(all); + Assert.IsFalse(s.IsProperSupersetOf(s)); + } + + [TestMethod] + public void IsSubsetOf() + { + static Func, bool> M(ISet set) + => a => set.IsSubsetOf(a); + + var s = NewSet(); + var m = M(s); + Assert.IsTrue(m(ImmutableArray.Empty)); + Assert.IsTrue(m(Create("foo"))); + s.Add("foo"); + s.Add("bar"); + Assert.IsFalse(m(ImmutableArray.Empty)); + Assert.IsFalse(m(Create("foo"))); + Assert.IsFalse(m(Create("baz"))); + Assert.IsTrue(m(Create("foo", "bar"))); + Assert.IsFalse(m(Create("foo", "baz"))); + Assert.IsTrue(m(Create("foo", "bar", "baz"))); + Assert.IsTrue(m(Create("foo", "foo", "bar", "baz"))); + } + + [TestMethod] + public void IsSubsetOf_Null() + { + var all = Create("foo", "bar"); + var s = NewSet(); + s.UnionWith(all); + Assert.ThrowsException( + () => s.IsSubsetOf(null!)); + } + + [TestMethod] + public void IsSubsetOf_Self() + { + var all = Create("foo", "bar"); + var s = NewSet(); + s.UnionWith(all); + Assert.IsTrue(s.IsSubsetOf(s)); + } + + [TestMethod] + public void IsSupersetOf() + { + static Func, bool> M(ISet set) + => a => set.IsSupersetOf(a); + + var s = NewSet(); + var m = M(s); + Assert.IsTrue(m(ImmutableArray.Empty)); + Assert.IsFalse(m(Create("foo"))); + s.Add("foo"); + s.Add("bar"); + Assert.IsTrue(m(ImmutableArray.Empty)); + Assert.IsTrue(m(Create("foo"))); + Assert.IsTrue(m(Create("foo", "bar"))); + Assert.IsFalse(m(Create("foo", "bar", "baz"))); + } + + [TestMethod] + public void IsSupersetOf_Null() + { + var all = Create("foo", "bar"); + var s = NewSet(); + s.UnionWith(all); + Assert.ThrowsException( + () => s.IsSupersetOf(null!)); + } + + [TestMethod] + public void IsSupersetOf_Self() + { + var all = Create("foo", "bar"); + var s = NewSet(); + s.UnionWith(all); + Assert.IsTrue(s.IsSupersetOf(s)); + } + + [TestMethod] + public void Overlaps() + { + static Func, bool> M(ISet set) + => a => set.Overlaps(a); + + var s = NewSet(); + var m = M(s); + Assert.IsFalse(m(ImmutableArray.Empty)); + Assert.IsFalse(m(Create("foo"))); + s.Add("foo"); + s.Add("bar"); + Assert.IsFalse(m(ImmutableArray.Empty)); + Assert.IsTrue(m(Create("foo"))); + Assert.IsTrue(m(Create("foo", "bar"))); + Assert.IsTrue(m(Create("foo", "bar", "baz"))); + } + + [TestMethod] + public void Overlaps_Empty() + { + var s = NewSet(); + s.Overlaps(Create("foo", "bar")); + Assert.AreEqual(0, s.Count); + } + + [TestMethod] + public void Overlaps_Self() + { + var all = Create("foo", "bar"); + var s = NewSet(); + s.UnionWith(all); + Assert.IsTrue(s.Overlaps(s)); + } + + [TestMethod] + public void Overlaps_Null() + { + var all = Create("foo", "bar"); + var s = NewSet(); + s.UnionWith(all); + Assert.ThrowsException( + () => s.Overlaps(null!)); + } + + [TestMethod] + public void SetEquals() + { + static Func, bool> M(ISet set) + => a => set.SetEquals(a); + + var s = NewSet(); + var m = M(s); + Assert.IsTrue(m(ImmutableArray.Empty)); + Assert.IsFalse(m(Create("foo"))); + s.Add("foo"); + s.Add("bar"); + Assert.IsFalse(m(ImmutableArray.Empty)); + Assert.IsFalse(m(Create("foo"))); + Assert.IsFalse(m(Create("baz"))); + Assert.IsTrue(m(Create("foo", "bar"))); + Assert.IsTrue(m(Create("foo", "bar", "foo"))); + Assert.IsFalse(m(Create("foo", "baz"))); + Assert.IsFalse(m(Create("foo", "bar", "baz"))); + } + + [TestMethod] + public void SetEquals_Null() + { + var all = Create("foo", "bar"); + var s = NewSet(); + s.UnionWith(all); + Assert.ThrowsException( + () => s.SetEquals(null!)); + } + + [TestMethod] + public void SetEquals_Self() + { + var all = Create("foo", "bar"); + var s = NewSet(); + s.UnionWith(all); + Assert.IsTrue(s.SetEquals(s)); + } + + [TestMethod] + public void SymmetricExceptWith() + { + var all = Create("foo", "bar"); + var s = NewSet(); + s.UnionWith(all); + s.SymmetricExceptWith(Create("bar", "baz")); + Assert.IsTrue(SortedSequenceEqual(Create("baz", "foo"), s)); + } + + [TestMethod] + public void SymmetricExceptWith_AddOnly_2x1() + { + var all = Create("foo", "bar"); + var s = NewSet(); + s.UnionWith(all); + s.SymmetricExceptWith(Create("baz")); + Assert.IsTrue(SortedSequenceEqual(Create("bar", "baz", "foo"), s)); + } + + [TestMethod] + public void SymmetricExceptWith_AddOnly_1x2() + { + var all = Create("foo"); + var s = NewSet(); + s.UnionWith(all); + s.SymmetricExceptWith(Create("bar", "baz")); + Assert.IsTrue(SortedSequenceEqual(Create("bar", "baz", "foo"), s)); + } + + [TestMethod] + public void SymmetricExceptWith_RemoveOnly() + { + var all = Create("foo", "bar", "baz"); + var s = NewSet(); + s.UnionWith(all); + s.SymmetricExceptWith(Create("bar")); + Assert.IsTrue(SortedSequenceEqual(Create("baz", "foo"), s)); + } + + [TestMethod] + public void SymmetricExceptWith_RemoveOnly_Tail() + { + var all = Create("foo", "bar", "baz"); + var s = NewSet(); + s.UnionWith(all); + s.SymmetricExceptWith(Create("bar", "baz")); + Assert.IsTrue(SortedSequenceEqual(Create("foo"), s)); + } + + [TestMethod] + public void SymmetricExceptWith_RemoveOnly_Head() + { + var all = Create("foo", "bar", "baz"); + var s = NewSet(); + s.UnionWith(all); + s.SymmetricExceptWith(Create("foo", "bar")); + Assert.IsTrue(SortedSequenceEqual(Create("baz"), s)); + } + + [TestMethod] + public void SymmetricExceptWith_DoubleAdd() + { + var all = Create("foo", "bar"); + var s = NewSet(); + s.UnionWith(all); + s.SymmetricExceptWith(Create("bar", "baz", "baz")); + Assert.IsTrue(SortedSequenceEqual(Create("baz", "foo"), s)); + } + + [TestMethod] + public void SymmetricExceptWith_DoubleRemove() + { + var all = Create("foo", "bar"); + var s = NewSet(); + s.UnionWith(all); + s.SymmetricExceptWith(Create("bar", "bar", "baz")); + Assert.IsTrue(SortedSequenceEqual(Create("baz", "foo"), s)); + } + + [TestMethod] + public void SymmetricExceptWith_Equal() + { + var all = Create("foo", "bar"); + var s = NewSet(); + s.UnionWith(all); + s.SymmetricExceptWith(all); + Assert.AreEqual(0, s.Count); + } + + [TestMethod] + public void SymmetricExceptWith_Self() + { + var all = Create("foo", "bar"); + var s = NewSet(); + s.UnionWith(all); + s.SymmetricExceptWith(s); + Assert.AreEqual(0, s.Count); + } + + [TestMethod] + public void SymmetricExceptWith_Null() + { + var all = Create("foo", "bar"); + var s = NewSet(); + s.UnionWith(all); + Assert.ThrowsException( + () => s.SymmetricExceptWith(null!)); + } + + [TestMethod] + public void UnionWith() + { + var all = Create("foo", "bar"); + var s = NewSet(); + s.UnionWith(all); + s.UnionWith(Create("bar", "baz")); + Assert.IsTrue(SortedSequenceEqual(Create("bar", "baz", "foo"), s)); + } + + [TestMethod] + public void UnionWith_Self() + { + var all = Create("foo", "bar"); + var s = NewSet(); + s.UnionWith(all); + s.UnionWith(s); + Assert.IsTrue(SortedSequenceEqual(all, s)); + } + + [TestMethod] + public void UnionWith_Null() + { + var all = Create("foo", "bar"); + var s = NewSet(); + s.UnionWith(all); + Assert.ThrowsException( + () => s.UnionWith(null!)); + } + + [TestMethod] + public void ICollection_T_Add() + { + var s = NewSet(); + var c = (ICollection)s; + c.Add("foo"); + c.Add("bar"); + c.Add("foo"); + Assert.IsTrue(SortedSequenceEqual(Create("bar", "foo"), s)); + } + + [TestMethod] + public void IEnumerable_GetEnumerator() + { + var all = Create("foo", "bar"); + var s = NewSet(); + s.UnionWith(all); + var e = (IEnumerable)s; + + var list = new List(); + foreach (string i in e) + { + list.Add(i); + } + Assert.IsTrue(SortedSequenceEqual(list, s)); + } + + protected abstract ISet NewSet(); + + private static ImmutableArray Create(params T[] all) + { + return ImmutableArray.Create(all); + } +} diff --git a/Collection.Test/Maroontress/Collection/Test/HashSet/SetConformanceTest.cs b/Collection.Test/Maroontress/Collection/Test/HashSet/SetConformanceTest.cs new file mode 100644 index 0000000..d99e3ea --- /dev/null +++ b/Collection.Test/Maroontress/Collection/Test/HashSet/SetConformanceTest.cs @@ -0,0 +1,10 @@ +namespace Maroontress.Collection.Test.HashSet; + +[TestClass] +public sealed class SetConformanceTest : AbstractSetConformanceTest +{ + protected override ISet NewSet() + { + return new HashSet(); + } +} diff --git a/Collection.Test/Maroontress/Collection/Test/HashTableConstantsTest.cs b/Collection.Test/Maroontress/Collection/Test/HashTableConstantsTest.cs new file mode 100644 index 0000000..970d389 --- /dev/null +++ b/Collection.Test/Maroontress/Collection/Test/HashTableConstantsTest.cs @@ -0,0 +1,53 @@ +namespace Maroontress.Collection.Test; + +using Maroontress.Collection; + +[TestClass] +public sealed class HashTableConstantsTest +{ + [TestMethod] + public void GetCapacity() + { + var m = 0x100; + var c = new HashTableConstants(m, int.MaxValue, int.MaxValue); + Assert.AreEqual(1, c.GetCapacity(1)); + Assert.AreEqual(2, c.GetCapacity(2)); + Assert.AreEqual(4, c.GetCapacity(3)); + Assert.AreEqual(4, c.GetCapacity(4)); + Assert.AreEqual(16, c.GetCapacity(15)); + Assert.AreEqual(16, c.GetCapacity(16)); + Assert.AreEqual(32, c.GetCapacity(17)); + Assert.AreEqual(m, c.GetCapacity(m - 1)); + Assert.AreEqual(m, c.GetCapacity(m)); + Assert.AreEqual(m, c.GetCapacity(m + 1)); + } + + [TestMethod] + public void GetCapacity_Zero() + { + var c = new HashTableConstants(0x100, int.MaxValue, int.MaxValue); + Assert.ThrowsException( + () => c.GetCapacity(0)); + } + + [TestMethod] + public void MaxCapacityIsZero() + { + Assert.ThrowsException( + () => _ = new HashTableConstants(0, int.MaxValue, int.MaxValue)); + } + + [TestMethod] + public void MaxSizeIsZero() + { + Assert.ThrowsException( + () => _ = new HashTableConstants(0x100, 0, int.MaxValue)); + } + + [TestMethod] + public void MaxRoastIsZero() + { + Assert.ThrowsException( + () => _ = new HashTableConstants(0x100, int.MaxValue, 0)); + } +} diff --git a/Collection.Test/Maroontress/Collection/Test/ImmutableDictionary/ImmutableDictionaryConformanceTest.cs b/Collection.Test/Maroontress/Collection/Test/ImmutableDictionary/ImmutableDictionaryConformanceTest.cs new file mode 100644 index 0000000..9af174b --- /dev/null +++ b/Collection.Test/Maroontress/Collection/Test/ImmutableDictionary/ImmutableDictionaryConformanceTest.cs @@ -0,0 +1,13 @@ +namespace Maroontress.Collection.Test.ImmutableDictionary; + +using System.Collections.Immutable; + +[TestClass] +public sealed class ImmutableDictionaryConformanceTest + : AbstractImmutableDictionaryConformanceTest +{ + protected override IImmutableDictionary NewMap() + { + return ImmutableDictionary.Empty; + } +} diff --git a/Collection.Test/Maroontress/Collection/Test/ImmutableLinkedHashMap/ImmutableLinkedHashMapConformanceTest.cs b/Collection.Test/Maroontress/Collection/Test/ImmutableLinkedHashMap/ImmutableLinkedHashMapConformanceTest.cs new file mode 100644 index 0000000..3bdd2fa --- /dev/null +++ b/Collection.Test/Maroontress/Collection/Test/ImmutableLinkedHashMap/ImmutableLinkedHashMapConformanceTest.cs @@ -0,0 +1,13 @@ +namespace Maroontress.Collection.Test.ImmutableLinkedHashMap; + +using System.Collections.Immutable; + +[TestClass] +public sealed class ImmutableLinkedHashMapConformanceTest + : AbstractImmutableDictionaryConformanceTest +{ + protected override IImmutableDictionary NewMap() + { + return ImmutableLinkedHashMap.Empty; + } +} diff --git a/Collection.Test/Maroontress/Collection/Test/ImmutableLinkedHashMap/ImmutableLinkedHashMapTest.cs b/Collection.Test/Maroontress/Collection/Test/ImmutableLinkedHashMap/ImmutableLinkedHashMapTest.cs new file mode 100644 index 0000000..f75c1cd --- /dev/null +++ b/Collection.Test/Maroontress/Collection/Test/ImmutableLinkedHashMap/ImmutableLinkedHashMapTest.cs @@ -0,0 +1,320 @@ +namespace Maroontress.Collection.Test.ImmutableLinkedHashMap; + +using System.Collections.Immutable; + +[TestClass] +public sealed class ImmutableLinkedHashMapTest +{ + private static IImmutableDictionary Empty { get; } + = ImmutableLinkedHashMap.Empty; + + private static KeyValuePair Pair123 { get; } = NewPair(123); + + private static KeyValuePair Pair456 { get; } = NewPair(456); + + private static KeyValuePair Pair789 { get; } = NewPair(789); + + [TestMethod] + public void Clear() + { + var m = Empty.AddRange( + ImmutableArray.Create( + Pair123, + Pair456, + Pair789)); + Assert.AreSame(Empty, m.Clear()); + } + + [TestMethod] + public void Keys() + { + var m = Empty.AddRange( + ImmutableArray.Create( + Pair123, + Pair456, + Pair789)); + Assert.IsTrue( + Enumerable.SequenceEqual( + m.Keys, + ImmutableArray.Create(123, 456, 789))); + } + + [TestMethod] + public void Values() + { + var m = Empty.AddRange( + ImmutableArray.Create( + Pair123, + Pair456, + Pair789)); + AssertSameValues( + m.Values, + ImmutableArray.Create( + Pair123.Value, + Pair456.Value, + Pair789.Value)); + } + + [TestMethod] + public void Count_Empty() + { + Assert.AreEqual(0, Empty.Count); + } + + [TestMethod] + public void Count() + { + var m = Empty.AddRange( + ImmutableArray.Create( + Pair123, + Pair456, + Pair789)); + Assert.AreEqual(3, m.Count); + } + + [TestMethod] + public void Indexer() + { + var m = Empty.AddRange( + ImmutableArray.Create( + Pair123, + Pair456, + Pair789)); + Assert.AreSame(Pair123.Value, m[123]); + Assert.AreSame(Pair456.Value, m[456]); + Assert.AreSame(Pair789.Value, m[789]); + } + + [TestMethod] + public void AddRange() + { + var m = Empty.AddRange( + ImmutableArray.Create( + Pair123, + Pair456, + Pair789, + NewPair(789), + NewPair(456), + NewPair(123))); + AssertSamePairs( + ImmutableArray.Create( + Pair123, + Pair456, + Pair789), + m); + } + + [TestMethod] + public void AddRange_Double() + { + var m1 = Empty.AddRange( + ImmutableArray.Create( + Pair123, + Pair456, + Pair789, + NewPair(789), + NewPair(456), + NewPair(123))); + var m2 = Empty.AddRange( + ImmutableArray.Create( + Pair123, + Pair789)) + .AddRange(m1); + AssertSamePairs( + ImmutableArray.Create( + Pair123, + Pair789, + Pair456), + m2); + } + + [TestMethod] + public void AddRange_Empty() + { + var m1 = Empty.AddRange( + ImmutableArray.Create( + Pair123, + Pair456, + Pair789)); + var m2 = m1.AddRange(Empty); + Assert.AreSame(m1, m2); + } + + [TestMethod] + public void Remove_AndThen_Add() + { + var m0 = Empty.AddRange( + ImmutableArray.Create( + Pair123, + Pair456, + Pair789)); + var m1 = m0.Remove(456); + var m2 = Empty.AddRange( + ImmutableArray.Create( + Pair123, + Pair789)); + AssertSamePairs(m2, m1); + var m3 = m1.Add(456, Pair456.Value); + AssertSamePairs( + ImmutableArray.Create( + Pair123, + Pair789, + Pair456), + m3); + } + + [TestMethod] + public void RemoveRange() + { + var pair12 = NewPair(12); + var m0 = Empty.AddRange( + ImmutableArray.Create( + Pair123, + Pair456, + Pair789, + pair12)); + var m1 = m0.RemoveRange( + ImmutableArray.Create( + 456, + 789)); + AssertSamePairs( + ImmutableArray.Create( + Pair123, + pair12), + m1); + } + + [TestMethod] + public void SetItem_Append() + { + var pair12 = NewPair(12); + var m0 = Empty.AddRange( + ImmutableArray.Create( + Pair123, + Pair456, + Pair789)); + var m1 = m0.SetItem(pair12.Key, pair12.Value); + AssertSamePairs( + ImmutableArray.Create( + Pair123, + Pair456, + Pair789, + pair12), + m1); + } + + [TestMethod] + public void SetItem_Overwrite() + { + var newPair456 = NewPair(456, "45"); + var m0 = Empty.AddRange( + ImmutableArray.Create( + Pair123, + Pair456, + Pair789)); + var m1 = m0.SetItem(newPair456.Key, newPair456.Value); + AssertSamePairs( + ImmutableArray.Create( + Pair123, + newPair456, + Pair789), + m1); + } + + [TestMethod] + public void SetItem_Overwrite_EqualValue() + { + var newPair456 = NewPair(456); + var m0 = Empty.AddRange( + ImmutableArray.Create( + Pair123, + Pair456, + Pair789)); + var m1 = m0.SetItem(newPair456.Key, newPair456.Value); + Assert.AreSame(m0, m1); + } + + [TestMethod] + public void SetItems() + { + var pair12 = NewPair(12); + var newPair123 = NewPair(123, "23"); + var m0 = Empty.AddRange( + ImmutableArray.Create( + Pair123, + Pair456, + Pair789)); + var m1 = m0.SetItems( + ImmutableArray.Create( + pair12, + NewPair(456, "456"), + newPair123)); + AssertSamePairs( + ImmutableArray.Create( + newPair123, + Pair456, + Pair789, + pair12), + m1); + } + + [TestMethod] + public void SetItems_AppendOnly() + { + var m0 = Empty.AddRange( + ImmutableArray.Create( + Pair123, + Pair456, + Pair789)); + var pair12 = NewPair(12); + var pair34 = NewPair(34); + var m1 = m0.SetItems( + ImmutableArray.Create( + pair12, + pair34)); + AssertSamePairs( + ImmutableArray.Create( + Pair123, + Pair456, + Pair789, + pair12, + pair34), + m1); + } + + private static void AssertSameValues( + IEnumerable s1, IEnumerable s2) + { + var left = s1.ToArray(); + var right = s2.ToArray(); + var n = left.Length; + Assert.AreEqual(n, right.Length); + for (var k = 0; k < n; ++k) + { + Assert.AreSame(left[k], right[k]); + } + } + + private static void AssertSamePairs( + IEnumerable> s1, + IEnumerable> s2) + { + var left = s1.ToArray(); + var right = s2.ToArray(); + var n = left.Length; + Assert.AreEqual(n, right.Length); + for (var k = 0; k < n; ++k) + { + var leftOne = left[k]; + var rightOne = right[k]; + Assert.AreEqual(leftOne.Key, rightOne.Key); + Assert.AreSame(leftOne.Value, rightOne.Value); + } + } + + private static KeyValuePair NewPair(int i) + => new(i, i.ToString()); + + private static KeyValuePair NewPair(int i, string s) + => new(i, s); +} diff --git a/Collection.Test/Maroontress/Collection/Test/InternMapTest.cs b/Collection.Test/Maroontress/Collection/Test/InternMapTest.cs new file mode 100644 index 0000000..1334962 --- /dev/null +++ b/Collection.Test/Maroontress/Collection/Test/InternMapTest.cs @@ -0,0 +1,51 @@ +namespace Maroontress.Collection.Test; + +[TestClass] +public sealed class InternMapTest +{ + [TestMethod] + public void Intern() + { + var key = 12; + var map = new InternMap(k => k.ToString()); + var c1 = map.Intern(key); + var c2 = map.Intern(key); + Assert.AreSame(c1, c2); + Assert.AreEqual("12", c1); + } + + [TestMethod] + public void Ctor_InitialCapacity() + { + var map = new InternMap(k => $"{k}", 1000); + for (var k = 0; k < 1000; ++k) + { + _ = map.Intern(k); + } + var c1 = map.Intern(500); + var c2 = map.Intern(500); + Assert.AreEqual("500", c1); + Assert.AreSame(c1, c2); + } + + [TestMethod] + public void Ctor_Null() + { + Assert.ThrowsException( + () => _ = new InternMap(null!)); + } + + [TestMethod] + public void Ctor_NegativeCapacity() + { + Assert.ThrowsException( + () => _ = new InternMap(k => $"{k}", -1)); + } + + [TestMethod] + public void Ctor_ZeroConcurrencyLevel() + { + Assert.ThrowsException( + () => _ = new InternMap(k => $"{k}", 31, 0)); + } +} diff --git a/Collection.Test/Maroontress/Collection/Test/LinkedHashSet/BenchmarkTest.cs b/Collection.Test/Maroontress/Collection/Test/LinkedHashSet/BenchmarkTest.cs new file mode 100644 index 0000000..b58da18 --- /dev/null +++ b/Collection.Test/Maroontress/Collection/Test/LinkedHashSet/BenchmarkTest.cs @@ -0,0 +1,274 @@ +namespace Maroontress.Collection.Test.LinkedHashSet; + +using System.Diagnostics; + +[TestClass] +public sealed class BenchmarkTest +{ + private const int TryCount = 10; + + private const int Capacity = 100_000; + + private const int ArraySize = 50_000; + + private static readonly string[] Left = NewArray(0, ArraySize); + + private static readonly string[] SmallLeft = NewArray(1, ArraySize - 1); + + private static readonly string[] BigLeft = NewArray(0, ArraySize + 1); + + private static readonly string[] Right + = NewArray(ArraySize / 2, ArraySize); + + [TestMethod] + public void AddAndRehash() + { + static TimeSpan Trial(Func supply, int n) + => LapTime(() => _ = supply(Left, n)); + + TripleTrial( + n => Trial(NewHashSet, n), + n => Trial(NewSimpleLhs, n), + n => Trial(NewLhs, n), + 16); + } + + [TestMethod] + public void Add() + { + static TimeSpan Trial(Func supply, int n) + => LapTime(() => _ = supply(Left, n)); + + TripleTrial( + n => Trial(NewHashSet, n), + n => Trial(NewSimpleLhs, n), + n => Trial(NewLhs, n)); + } + + [TestMethod] + public void IsSupersetOf() + { + TimeSpan Trial(Func> newSet, int n) + { + var s = newSet(Left, n); + return LapTime(() => Assert.IsTrue(s.IsSupersetOf(SmallLeft))); + } + + TripleTrial( + n => Trial(NewHashSet, n), + n => Trial(NewSimpleLhs, n), + n => Trial(NewLhs, n)); + } + + [TestMethod] + public void IsSupersetOf_Set() + { + TimeSpan Trial( + Func newSet, int n, Func test) + { + var s = newSet(Left, n); + var t = newSet(SmallLeft, n); + return LapTime(() => Assert.IsTrue(test(s, t))); + } + + TripleTrial( + n => Trial(NewHashSet, n, (s, t) => s.IsSupersetOf(t)), + n => Trial(NewSimpleLhs, n, (s, t) => s.IsSupersetOf(t)), + n => Trial(NewLhs, n, (s, t) => s.IsSupersetOf(t))); + } + + [TestMethod] + public void IsSubsetOf() + { + TimeSpan Trial(Func> newSet, int n) + { + var s = newSet(Left, n); + return LapTime(() => Assert.IsTrue(s.IsSubsetOf(BigLeft))); + } + + TripleTrial( + n => Trial(NewHashSet, n), + n => Trial(NewSimpleLhs, n), + n => Trial(NewLhs, n)); + } + + [TestMethod] + public void IsSubsetOf_Set() + { + TimeSpan Trial( + Func newSet, int n, Func test) + { + var s = newSet(Left, n); + var t = newSet(BigLeft, n); + return LapTime(() => Assert.IsTrue(test(s, t))); + } + + TripleTrial( + n => Trial(NewHashSet, n, (s, t) => s.IsSubsetOf(t)), + n => Trial(NewSimpleLhs, n, (s, t) => s.IsSubsetOf(t)), + n => Trial(NewLhs, n, (s, t) => s.IsSubsetOf(t))); + } + + [TestMethod] + public void IsProperSubsetOf() + { + TimeSpan Trial(Func> newSet, int n) + { + var s = newSet(Left, n); + return LapTime(() => Assert.IsTrue(s.IsProperSubsetOf(BigLeft))); + } + + TripleTrial( + n => Trial(NewHashSet, n), + n => Trial(NewSimpleLhs, n), + n => Trial(NewLhs, n)); + } + + [TestMethod] + public void IsProperSubsetOf_Set() + { + TimeSpan Trial( + Func newSet, int n, Func test) + { + var s = newSet(Left, n); + var t = newSet(BigLeft, n); + return LapTime(() => Assert.IsTrue(test(s, t))); + } + + TripleTrial( + n => Trial(NewHashSet, n, (s, t) => s.IsProperSubsetOf(t)), + n => Trial(NewSimpleLhs, n, (s, t) => s.IsProperSubsetOf(t)), + n => Trial(NewLhs, n, (s, t) => s.IsProperSubsetOf(t))); + } + + [TestMethod] + public void IntersectWith() + { + TimeSpan Trial(Func> newSet, int n) + { + var s = newSet(Left, n); + return LapTime(() => s.IntersectWith(Right)); + } + + TripleTrial( + n => Trial(NewHashSet, n), + n => Trial(NewSimpleLhs, n), + n => Trial(NewLhs, n)); + } + + [TestMethod] + public void IntersectWith_Set() + { + TimeSpan Trial( + Func newSet, int n, Action apply) + { + var s = newSet(Left, n); + var t = newSet(Right, n); + return LapTime(() => apply(s, t)); + } + + TripleTrial( + n => Trial(NewHashSet, n, (s, t) => s.IntersectWith(t)), + n => Trial(NewSimpleLhs, n, (s, t) => s.IntersectWith(t)), + n => Trial(NewLhs, n, (s, t) => s.IntersectWith(t))); + } + + [TestMethod] + public void SymmetricExceptWith() + { + TimeSpan Trial(Func> newSet, int n) + { + var s = newSet(Left, n); + return LapTime(() => s.SymmetricExceptWith(Right)); + } + + TripleTrial( + n => Trial(NewHashSet, n), + n => Trial(NewSimpleLhs, n), + n => Trial(NewLhs, n)); + } + + [TestMethod] + public void SymmetricExceptWith_Set() + { + TimeSpan Trial( + Func newSet, int n, Action apply) + { + var s = newSet(Left, n); + var t = newSet(Right, n); + return LapTime(() => apply(s, t)); + } + + TripleTrial( + n => Trial(NewHashSet, n, (s, t) => s.SymmetricExceptWith(t)), + n => Trial(NewSimpleLhs, n, (s, t) => s.SymmetricExceptWith(t)), + n => Trial(NewLhs, n, (s, t) => s.SymmetricExceptWith(t))); + } + + private static void TripleTrial( + Func a, + Func b, + Func c, + int capacity = Capacity) + { + double GetResult(Func apply) + { + return Enumerable.Range(0, TryCount) + .Select(i => apply(capacity).Milliseconds) + .Average(); + } + + Console.WriteLine($"HashSet: {GetResult(a)}"); + Console.WriteLine($"Conventional: {GetResult(b)}"); + Console.WriteLine($"LinkedHashSet: {GetResult(c)}"); + } + + private static TimeSpan LapTime(Action action) + { + GC.Collect(2, GCCollectionMode.Forced, true, true); + var w = new Stopwatch(); + w.Start(); + action(); + w.Stop(); + return w.Elapsed; + } + + private static HashSet NewHashSet( + string[] array, int initialCapacity) + { + var s = new HashSet(initialCapacity); + foreach (var i in array) + { + s.Add(i); + } + return s; + } + + private static SimpleLinkedHashSet NewSimpleLhs( + string[] array, int initialCapacity) + { + var s = new SimpleLinkedHashSet(initialCapacity); + foreach (var i in array) + { + s.Add(i); + } + return s; + } + + private static LinkedHashSet NewLhs( + string[] array, int initialCapacity) + { + var s = new LinkedHashSet(initialCapacity); + foreach (var i in array) + { + s.Add(i); + } + return s; + } + + private static string[] NewArray(int n, int size) + => Enumerable.Range(n, size) + .Select(i => i.ToString()) + .Where(s => s.GetHashCode() is not 0) + .ToArray(); +} diff --git a/Collection.Test/Maroontress/Collection/Test/LinkedHashSet/FreeHashString.cs b/Collection.Test/Maroontress/Collection/Test/LinkedHashSet/FreeHashString.cs new file mode 100644 index 0000000..8831488 --- /dev/null +++ b/Collection.Test/Maroontress/Collection/Test/LinkedHashSet/FreeHashString.cs @@ -0,0 +1,34 @@ +namespace Maroontress.Collection.Test.LinkedHashSet; + +public sealed class FreeHashString : IComparable +{ + public FreeHashString(string s, int hash) + { + Value = s; + Hash = hash; + } + + private string Value { get; } + + private int Hash { get; } + + public int CompareTo(FreeHashString? other) + { + if (other is null) + { + throw new NullReferenceException(); + } + return Value.CompareTo(other.Value); + } + + public override bool Equals(object? o) + { + return o is FreeHashString s + && Value.Equals(s.Value); + } + + public override int GetHashCode() + { + return Hash; + } +} diff --git a/Collection.Test/Maroontress/Collection/Test/LinkedHashSet/HugeElementsTest.cs b/Collection.Test/Maroontress/Collection/Test/LinkedHashSet/HugeElementsTest.cs new file mode 100644 index 0000000..16854fa --- /dev/null +++ b/Collection.Test/Maroontress/Collection/Test/LinkedHashSet/HugeElementsTest.cs @@ -0,0 +1,85 @@ +namespace Maroontress.Collection.Test.LinkedHashSet; + +using System.Collections.Immutable; + +[TestClass] +public sealed class HugeElementsTest +{ + private const int MaxSize = 0x8000; + + [TestMethod] + public void RoastReset() + { + var s = new CustomLinkedHashSet(MaxSize); + s.UnionWith(Enumerable.Range(0, MaxSize)); + s.IntersectWith(Enumerable.Range(0x1000, 0x6000)); + s.SymmetricExceptWith(Enumerable.Range(0, MaxSize)); + var r = Enumerable.Range(0, 0x1000) + .Concat(Enumerable.Range(0x7000, 0x1000)); + Assert.IsTrue(s.SetEquals(r)); + Assert.IsTrue(s.IsProperSubsetOf(Enumerable.Range(0, MaxSize))); + Assert.IsTrue(s.IsProperSupersetOf(Enumerable.Range(0, 0x1000))); + } + + [TestMethod] + public void LoadFactorOne() + { + var s = new CustomLinkedHashSet(MaxSize, 1.0f); + s.UnionWith(Enumerable.Range(0, MaxSize)); + } + + [TestMethod] + public void AddInCriticalState() + { + var n = MaxSize; + + LinkedHashSet NewFullSet() + { + var s = new CustomLinkedHashSet(n); + s.UnionWith(Enumerable.Range(0, n)); + return s; + } + + { + var s = NewFullSet(); + Assert.ThrowsException( + () => _ = s.Add(-1)); + Assert.IsTrue(s.SetEquals(Enumerable.Range(0, n))); + } + + { + var right = ImmutableArray.Create(0, -1, -2); + var s = NewFullSet(); + Assert.ThrowsException( + () => s.SymmetricExceptWith(right)); + Assert.IsTrue(s.SetEquals(Enumerable.Range(1, n - 1).Append(-1))); + } + + { + var right = ImmutableArray.Create(0, -1, -2); + var s = NewFullSet(); + var t = new LinkedHashSet(); + t.UnionWith(right); + Assert.ThrowsException( + () => s.SymmetricExceptWith(t)); + Assert.IsTrue(s.SetEquals(Enumerable.Range(1, n - 1).Append(-1))); + } + } + + private class CustomLinkedHashSet : LinkedHashSet + where T : notnull + { + public CustomLinkedHashSet(int initialCapacity) + : base(Constants, initialCapacity, DefaultLoadFactor) + { + } + + public CustomLinkedHashSet(int initialCapacity, float loadFactor) + : base(Constants, initialCapacity, loadFactor) + { + } + + private static HashTableConstants Constants { get; } + = new(MaxSize, MaxSize, 2); + } +} diff --git a/Collection.Test/Maroontress/Collection/Test/LinkedHashSet/LinkedHashSetTest.cs b/Collection.Test/Maroontress/Collection/Test/LinkedHashSet/LinkedHashSetTest.cs new file mode 100644 index 0000000..0514a2c --- /dev/null +++ b/Collection.Test/Maroontress/Collection/Test/LinkedHashSet/LinkedHashSetTest.cs @@ -0,0 +1,546 @@ +namespace Maroontress.Collection.Test.LinkedHashSet; + +using System.Collections.Immutable; + +[TestClass] +public sealed class LinkedHashSetTest +{ + [TestMethod] + public void EnsureCapacity() + { + var n = 1000; + var all = Enumerable.Range(0, n); + var s = NewLinkedHashSet(); + s.UnionWith(all); + Assert.AreEqual(s.Count, n); + Assert.IsTrue(Enumerable.SequenceEqual(all, s)); + s.Validate(); + } + + [TestMethod] + public void InvalidInitialCapacity() + { + Assert.ThrowsException( + () => _ = NewLinkedHashSet(-1)); + } + + [TestMethod] + public void NonPositiveLoadFactor() + { + Assert.ThrowsException( + () => _ = NewLinkedHashSet(100, -0.5f)); + } + + [TestMethod] + public void NanLoadFactor() + { + Assert.ThrowsException( + () => _ = NewLinkedHashSet(100, float.NaN)); + } + + [TestMethod] + public void AddAndRemove() + { + var s = NewLinkedHashSet(); + s.UnionWith(Create("foo", "bar", "baz")); + Assert.IsTrue(s.SequenceEqual(Create("foo", "bar", "baz"))); + Assert.IsFalse(s.Add("bar")); + Assert.IsTrue(s.SequenceEqual(Create("foo", "bar", "baz"))); + Assert.IsTrue(s.Remove("bar")); + Assert.IsTrue(s.Add("bar")); + Assert.IsTrue(s.SequenceEqual(Create("foo", "baz", "bar"))); + s.Validate(); + } + + [TestMethod] + public void IntersectWith() + { + var s = NewLinkedHashSet(); + s.UnionWith(Create("foo", "bar", "baz")); + s.IntersectWith(Create("baz", "bar", "fooBar")); + Assert.IsTrue(s.SequenceEqual(Create("bar", "baz"))); + s.Validate(); + } + + [TestMethod] + public void IntersectWith_LeftEmpty() + { + var s = NewLinkedHashSet(); + s.IntersectWith(Create("baz", "bar", "fooBar")); + Assert.AreEqual(0, s.Count); + s.Validate(); + } + + [TestMethod] + public void IntersectWith_Self() + { + var all = Create("foo", "bar", "baz"); + var s = NewLinkedHashSet(); + s.UnionWith(all); + s.IntersectWith(s); + Assert.IsTrue(s.SequenceEqual(all)); + s.Validate(); + } + + [TestMethod] + public void IntersectWith_Null() + { + var n = (LinkedHashSet?)null; + var s = NewLinkedHashSet(); + Assert.ThrowsException( + () => s.IntersectWith(n!)); + } + + [TestMethod] + public void IntersectWith_LinkedHashSet() + { + var left = Create("foo", "bar", "baz"); + var right = Create("fooBar", "baz", "bar"); + var center = Create("bar", "baz"); + var s1 = NewLinkedHashSet(); + var s2 = NewLinkedHashSet(); + s1.UnionWith(left); + s2.UnionWith(right); + s1.IntersectWith(s2); + Assert.IsTrue(s1.SequenceEqual(center)); + Assert.IsTrue(s2.SequenceEqual(right)); + Validate(s1, s2); + } + + [TestMethod] + public void IntersectWith_LinkedHashSet_LeftEmpty() + { + var right = Create("fooBar", "baz", "bar"); + var s1 = NewLinkedHashSet(); + var s2 = NewLinkedHashSet(); + s2.UnionWith(right); + s1.IntersectWith(s2); + Assert.AreEqual(0, s1.Count); + Validate(s1, s2); + } + + [TestMethod] + public void IntersectWith_LinkedHashSet_RightEmpty() + { + var left = Create("foo", "bar", "baz"); + var s1 = NewLinkedHashSet(); + var s2 = NewLinkedHashSet(); + s1.UnionWith(left); + s1.IntersectWith(s2); + Assert.AreEqual(0, s1.Count); + Validate(s1, s2); + } + + [TestMethod] + public void ExceptWith() + { + var s = NewLinkedHashSet(); + s.UnionWith(Create("foo", "bar", "baz")); + s.ExceptWith(Create("barBaz", "bar", "fooBar")); + Assert.IsTrue(s.SequenceEqual(Create("foo", "baz"))); + s.Validate(); + } + + [TestMethod] + public void SymmetricExceptWith() + { + var s = NewLinkedHashSet(); + s.UnionWith(Create("foo", "bar", "baz")); + s.SymmetricExceptWith( + Create("barBaz", "bar", "fooBar", "bar", "barBaz")); + Assert.IsTrue(s.SequenceEqual( + Create("foo", "baz", "barBaz", "fooBar"))); + s.Validate(); + } + + [TestMethod] + public void SymmetricExceptWith_EmptyLeft() + { + var right = Create("barBaz", "bar", "fooBar", "bar", "barBaz"); + var unique = Create("barBaz", "bar", "fooBar"); + var s = NewLinkedHashSet(); + s.SymmetricExceptWith(right); + Assert.IsTrue(s.SequenceEqual(unique)); + s.Validate(); + } + + [TestMethod] + public void SymmetricExceptWith_Rehash() + { + var right = Enumerable.Range(5, 95) + .Select(i => i.ToString()); + var s = NewLinkedHashSet(); + s.UnionWith(Enumerable.Range(0, 10) + .Select(i => i.ToString())); + s.SymmetricExceptWith(right.Concat(right)); + Assert.IsTrue(s.SequenceEqual( + Enumerable.Range(0, 5) + .Concat(Enumerable.Range(10, 90)) + .Select(i => i.ToString()))); + s.Validate(); + } + + [TestMethod] + public void SymmetricExceptWith_Self() + { + var s = NewLinkedHashSet(); + s.UnionWith(Create("foo", "bar", "baz")); + s.SymmetricExceptWith(s); + Assert.AreEqual(0, s.Count); + s.Validate(); + } + + [TestMethod] + public void SymmetricExceptWith_Null() + { + var n = (LinkedHashSet?)null; + var s = NewLinkedHashSet(); + Assert.ThrowsException( + () => s.SymmetricExceptWith(n!)); + } + + [TestMethod] + public void SymmetricExceptWith_LinkedHashSet() + { + var left = Create("foo", "bar", "baz"); + var right = Create("fooBar", "baz", "bar"); + var outside = Create("foo", "fooBar"); + var s1 = NewLinkedHashSet(); + var s2 = NewLinkedHashSet(); + s1.UnionWith(left); + s2.UnionWith(right); + s1.SymmetricExceptWith(s2); + Assert.IsTrue(s1.SequenceEqual(outside)); + Assert.IsTrue(s2.SequenceEqual(right)); + Validate(s1, s2); + } + + [TestMethod] + public void SymmetricExceptWith_LinkedHashSet_Empty() + { + var right = Create("foo", "bar", "baz"); + var s1 = NewLinkedHashSet(); + var s2 = NewLinkedHashSet(); + s2.UnionWith(right); + s1.SymmetricExceptWith(s2); + Assert.IsTrue(s1.SequenceEqual(right)); + Assert.IsTrue(s2.SequenceEqual(right)); + Validate(s1, s2); + } + + [TestMethod] + public void UnionWith() + { + var s = NewLinkedHashSet(); + s.UnionWith(Create("foo", "bar", "baz")); + s.UnionWith(Create("barBaz", "bar", "fooBar")); + Assert.IsTrue(s.SequenceEqual( + Create("foo", "bar", "baz", "barBaz", "fooBar"))); + s.Validate(); + } + + [TestMethod] + public void UnionWith_LinkedHashSet_Null() + { + var right = (LinkedHashSet?)null; + var s = NewLinkedHashSet(); + s.UnionWith(Create("foo", "bar", "baz")); + Assert.ThrowsException( + () => s.UnionWith(right!)); + s.Validate(); + } + + [TestMethod] + public void UnionWith_LinkedHashSet() + { + var s1 = NewLinkedHashSet(); + var s2 = NewLinkedHashSet(); + s1.UnionWith(Create("foo", "bar", "baz")); + s2.UnionWith(Create("barBaz", "bar", "fooBar")); + s1.UnionWith(s2); + Assert.IsTrue(s1.SequenceEqual( + Create("foo", "bar", "baz", "barBaz", "fooBar"))); + s1.Validate(); + s2.Validate(); + } + + [TestMethod] + public void UnionWith_LinkedHashSet_BigRight() + { + var s1 = NewLinkedHashSet(); + var s2 = NewLinkedHashSet(); + s2.UnionWith(Enumerable.Range(0, 100) + .Select(i => i.ToString())); + s1.UnionWith(s2); + Assert.IsTrue(s1.SequenceEqual(s2)); + Assert.AreEqual(256, s1.GetCapacity()); + Assert.AreEqual(192, s1.GetLimit()); + Assert.AreEqual(s2.Count, s1.Count); + s1.Validate(); + s2.Validate(); + } + + [TestMethod] + public void Overlaps_LinkedHashSet() + { + static bool M(LinkedHashSet s1, LinkedHashSet s2) + where T : notnull + => s1.Overlaps(s2); + + var empty = NewLinkedHashSet(); + var s1 = NewLinkedHashSet(); + var s2 = NewLinkedHashSet(); + var s3 = NewLinkedHashSet(); + s1.UnionWith(Create("foo", "bar")); + s2.UnionWith(Create("fooBar", "bar", "barBaz")); + s3.UnionWith(Create("fooBar", "barBaz")); + Assert.IsTrue(M(s1, s2)); + Assert.IsFalse(M(s1, s3)); + Assert.IsTrue(M(s1, s1)); + Assert.IsFalse(M(s1, empty)); + Assert.IsFalse(M(empty, s1)); + Validate(s1, s2, s3, empty); + } + + [TestMethod] + public void Overlaps_Null() + { + var n = (LinkedHashSet?)null; + var s = NewLinkedHashSet(); + Assert.ThrowsException( + () => _ = s.Overlaps(n!)); + } + + [TestMethod] + public void IsSubsetOf_LinkedHashSet() + { + static bool M(LinkedHashSet s1, LinkedHashSet s2) + where T : notnull + => s1.IsSubsetOf(s2); + + var empty = NewLinkedHashSet(); + var s1 = NewLinkedHashSet(); + var s2 = NewLinkedHashSet(); + var s3 = NewLinkedHashSet(); + var s4 = NewLinkedHashSet(); + var s5 = NewLinkedHashSet(); + s1.UnionWith(Create("foo", "bar", "baz")); + s2.UnionWith(Create("foo", "bar", "baz", "fooBar")); + s3.UnionWith(Create("foo", "bar")); + s4.UnionWith(Create("fooBar", "barBaz", "fooBar")); + s5.UnionWith(Create("foo", "bar", "baz")); + Assert.IsTrue(M(s1, s2)); + Assert.IsFalse(M(s1, s3)); + Assert.IsFalse(M(s1, s4)); + Assert.IsTrue(M(s1, s5)); + + Assert.IsTrue(M(s1, s1)); + Assert.IsFalse(M(s1, empty)); + Assert.IsTrue(M(empty, s1)); + Assert.IsTrue(M(empty, NewLinkedHashSet())); + Validate(s1, s2, s3, s4, s5, empty); + } + + [TestMethod] + public void IsSubsetOf_Null() + { + var n = (LinkedHashSet?)null; + var s = NewLinkedHashSet(); + Assert.ThrowsException( + () => _ = s.IsSubsetOf(n!)); + } + + [TestMethod] + public void IsProperSubsetOf_LinkedHashSet() + { + static bool M(LinkedHashSet s1, LinkedHashSet s2) + where T : notnull + => s1.IsProperSubsetOf(s2); + + var empty = NewLinkedHashSet(); + var s1 = NewLinkedHashSet(); + var s2 = NewLinkedHashSet(); + var s3 = NewLinkedHashSet(); + var s4 = NewLinkedHashSet(); + var s5 = NewLinkedHashSet(); + s1.UnionWith(Create("foo", "bar", "baz")); + s2.UnionWith(Create("foo", "bar", "baz", "fooBar")); + s3.UnionWith(Create("foo", "bar")); + s4.UnionWith(Create("fooBar", "barBaz", "fooBar")); + s5.UnionWith(Create("foo", "bar", "baz")); + Assert.IsTrue(M(s1, s2)); + Assert.IsFalse(M(s1, s3)); + Assert.IsFalse(M(s1, s4)); + Assert.IsFalse(M(s1, s5)); + + Assert.IsFalse(M(s1, s1)); + Assert.IsFalse(M(s1, empty)); + Assert.IsTrue(M(empty, s1)); + Assert.IsFalse(M(empty, NewLinkedHashSet())); + Validate(s1, s2, s3, s4, s5, empty); + } + + [TestMethod] + public void IsProperSubsetOf_Null() + { + var n = (LinkedHashSet?)null; + var s = NewLinkedHashSet(); + Assert.ThrowsException( + () => _ = s.IsProperSubsetOf(n!)); + } + + [TestMethod] + public void IsSupersetOf_LinkedHashSet() + { + static bool M(LinkedHashSet s1, LinkedHashSet s2) + where T : notnull + => s1.IsSupersetOf(s2); + + var empty = NewLinkedHashSet(); + var s1 = NewLinkedHashSet(); + var s2 = NewLinkedHashSet(); + var s3 = NewLinkedHashSet(); + var s4 = NewLinkedHashSet(); + var s5 = NewLinkedHashSet(); + s1.UnionWith(Create("foo", "bar", "baz")); + s2.UnionWith(Create("foo", "bar", "baz", "fooBar")); + s3.UnionWith(Create("foo", "bar")); + s4.UnionWith(Create("fooBar", "barBaz", "fooBar")); + s5.UnionWith(Create("foo", "bar", "baz")); + Assert.IsFalse(M(s1, s2)); + Assert.IsTrue(M(s1, s3)); + Assert.IsFalse(M(s1, s4)); + Assert.IsTrue(M(s1, s5)); + + Assert.IsTrue(M(s1, s1)); + Assert.IsTrue(M(s1, empty)); + Assert.IsFalse(M(empty, s1)); + Assert.IsTrue(M(empty, NewLinkedHashSet())); + Validate(s1, s2, s3, s4, s5, empty); + } + + [TestMethod] + public void IsSupersetOf_Null() + { + var n = (LinkedHashSet?)null; + var s = NewLinkedHashSet(); + Assert.ThrowsException( + () => _ = s.IsSupersetOf(n!)); + } + + [TestMethod] + public void IsProperSupersetOf_LinkedHashSet() + { + static bool M(LinkedHashSet s1, LinkedHashSet s2) + where T : notnull + => s1.IsProperSupersetOf(s2); + + var empty = NewLinkedHashSet(); + var s1 = NewLinkedHashSet(); + var s2 = NewLinkedHashSet(); + var s3 = NewLinkedHashSet(); + var s4 = NewLinkedHashSet(); + var s5 = NewLinkedHashSet(); + s1.UnionWith(Create("foo", "bar", "baz")); + s2.UnionWith(Create("foo", "bar", "baz", "fooBar")); + s3.UnionWith(Create("foo", "bar")); + s4.UnionWith(Create("fooBar", "barBaz", "fooBar")); + s5.UnionWith(Create("foo", "bar", "baz")); + Assert.IsFalse(M(s1, s2)); + Assert.IsTrue(M(s1, s3)); + Assert.IsFalse(M(s1, s4)); + Assert.IsFalse(M(s1, s5)); + + Assert.IsFalse(M(s1, s1)); + Assert.IsTrue(M(s1, empty)); + Assert.IsFalse(M(empty, s1)); + Assert.IsFalse(M(empty, NewLinkedHashSet())); + Validate(s1, s2, s3, s4, s5, empty); + } + + [TestMethod] + public void IsProperSupersetOf_Null() + { + var n = (LinkedHashSet?)null; + var s = NewLinkedHashSet(); + Assert.ThrowsException( + () => _ = s.IsProperSupersetOf(n!)); + } + + [TestMethod] + public void SetEquals_LinkedHashSet() + { + static bool M(LinkedHashSet s1, LinkedHashSet s2) + where T : notnull + => s1.SetEquals(s2); + + var empty = NewLinkedHashSet(); + var s1 = NewLinkedHashSet(); + var s2 = NewLinkedHashSet(); + var s3 = NewLinkedHashSet(); + var s4 = NewLinkedHashSet(); + var s5 = NewLinkedHashSet(); + s1.UnionWith(Create("foo", "bar", "baz")); + s2.UnionWith(Create("foo", "bar", "baz", "fooBar")); + s3.UnionWith(Create("foo", "bar")); + s4.UnionWith(Create("fooBar", "barBaz", "fooBar")); + s5.UnionWith(Create("baz", "foo", "bar")); + Assert.IsFalse(M(s1, s2)); + Assert.IsFalse(M(s1, s3)); + Assert.IsFalse(M(s1, s4)); + Assert.IsTrue(M(s1, s5)); + + Assert.IsTrue(M(s1, s1)); + Assert.IsFalse(M(s1, empty)); + Assert.IsFalse(M(empty, s1)); + Assert.IsTrue(M(empty, NewLinkedHashSet())); + Validate(s1, s2, s3, s4, s5, empty); + } + + [TestMethod] + public void SetEquals_Null() + { + var n = (LinkedHashSet?)null; + var s = NewLinkedHashSet(); + Assert.ThrowsException( + () => _ = s.SetEquals(n!)); + } + + [TestMethod] + public void ConstantsIsNull() + { + Assert.ThrowsException( + () => _ = new ConstantsIsNullSet()); + } + + private static void Validate( + params SafeLinkedHashSet[] all) + { + foreach (var s in all) + { + s.Validate(); + } + } + + private static SafeLinkedHashSet NewLinkedHashSet( + int initialCapacity = LinkedHashSet.DefaultInitialCapacity, + float loadFactor = LinkedHashSet.DefaultLoadFactor) + where T : notnull + { + return new SafeLinkedHashSet(initialCapacity, loadFactor); + } + + private static ImmutableArray Create(params T[] all) + { + return ImmutableArray.Create(all); + } + + private class ConstantsIsNullSet : LinkedHashSet + where T : notnull + { + public ConstantsIsNullSet() + : base(null!, 16, 0.75f) + { + } + } +} diff --git a/Collection.Test/Maroontress/Collection/Test/LinkedHashSet/MicrosoftExampleSetTest.cs b/Collection.Test/Maroontress/Collection/Test/LinkedHashSet/MicrosoftExampleSetTest.cs new file mode 100644 index 0000000..0ae7eaf --- /dev/null +++ b/Collection.Test/Maroontress/Collection/Test/LinkedHashSet/MicrosoftExampleSetTest.cs @@ -0,0 +1,11 @@ +namespace Maroontress.Collection.Test.LinkedHashSet; + +[TestClass] +public sealed class MicrosoftExampleSetTest + : AbstractMicrosoftExampleSetTest +{ + protected override ISet NewSet() + { + return new LinkedHashSet(); + } +} diff --git a/Collection.Test/Maroontress/Collection/Test/LinkedHashSet/SafeLinkedHashSet.cs b/Collection.Test/Maroontress/Collection/Test/LinkedHashSet/SafeLinkedHashSet.cs new file mode 100644 index 0000000..5877dc9 --- /dev/null +++ b/Collection.Test/Maroontress/Collection/Test/LinkedHashSet/SafeLinkedHashSet.cs @@ -0,0 +1,94 @@ +namespace Maroontress.Collection.Test.LinkedHashSet; + +public sealed class SafeLinkedHashSet : LinkedHashSet + where T : notnull +{ + public SafeLinkedHashSet( + int initialCapacity = DefaultInitialCapacity, + float loadFactor = DefaultLoadFactor) + : base(initialCapacity, loadFactor) + { + } + + public int GetCapacity() => CapacityAndLimit.Capacity; + + public int GetLimit() => CapacityAndLimit.Limit; + + public void Validate() + { + if (Count is 0) + { + Validate0(); + return; + } + var (head, tail) = FirstAndLastNode; + var nodes = GetNodes().ToArray(); + Assert.IsNotNull(head); + Assert.IsNotNull(tail); + Assert.IsNull(head.PreviousNode); + Assert.IsNull(tail.NextNode); + { + var e = head; + while (e is not null) + { + var next = e.NextNode; + if (next is not null) + { + Assert.AreSame(e, next.PreviousNode); + } + else + { + Assert.AreSame(e, tail); + } + e = next; + } + } + { + var indexSet = new HashSet(); + var length = nodes.Length; + var n = 0; + var roast = head.Roast; + var e = head; + while (e is not null) + { + Assert.IsTrue(e.Roast <= roast); + var i = e.Hash & (length - 1); + indexSet.Add(i); + TraceChain(nodes[i]!, e); + e = e.NextNode; + ++n; + } + Assert.AreEqual(Count, n); + for (var k = 0; k < length; ++k) + { + if (indexSet.Contains(k)) + { + continue; + } + Assert.IsNull(nodes[k]); + } + } + } + + private static void TraceChain(ProtectedNode root, ProtectedNode e) + { + var node = root; + for (;;) + { + if (ReferenceEquals(node, e)) + { + return; + } + node = node!.ParentNode; + } + } + + private void Validate0() + { + var (head, tail) = FirstAndLastNode; + Assert.IsNull(head); + Assert.IsNull(tail); + var nodes = GetNodes(); + Assert.IsTrue(nodes.All(i => i is null)); + } +} diff --git a/Collection.Test/Maroontress/Collection/Test/LinkedHashSet/SameHashOrIndexTest.cs b/Collection.Test/Maroontress/Collection/Test/LinkedHashSet/SameHashOrIndexTest.cs new file mode 100644 index 0000000..b366ca7 --- /dev/null +++ b/Collection.Test/Maroontress/Collection/Test/LinkedHashSet/SameHashOrIndexTest.cs @@ -0,0 +1,137 @@ +namespace Maroontress.Collection.Test.LinkedHashSet; + +using System.Collections.Immutable; +using System.Linq; + +[TestClass] +public sealed class SameHashOrIndexTest +{ + private const int Capacity + = LinkedHashSet.DefaultInitialCapacity; + + [TestMethod] + public void Add() + { + var all = ImmutableArray.Create( + new FreeHashString("foo", 1), + new FreeHashString("bar", Capacity + 1), + new FreeHashString("baz", 1)); + var s = NewSet(); + s.UnionWith(all); + Assert.IsTrue(s.SequenceEqual(all)); + } + + [TestMethod] + public void AddAndThenRemove() + { + var all = ImmutableArray.Create( + new FreeHashString("foo", 1), + new FreeHashString("bar", Capacity + 1), + new FreeHashString("baz", 1)); + var s = NewSet(); + s.UnionWith(all); + foreach (var i in all) + { + Assert.IsTrue(s.Remove(i)); + } + Assert.AreEqual(s.Count, 0); + } + + [TestMethod] + public void AddAndThenRemove_ReverseOrder() + { + var all = ImmutableArray.Create( + new FreeHashString("foo", 1), + new FreeHashString("bar", Capacity + 1), + new FreeHashString("baz", 1)); + var s = NewSet(); + s.UnionWith(all); + foreach (var i in all.Reverse()) + { + Assert.IsTrue(s.Remove(i)); + } + Assert.AreEqual(s.Count, 0); + } + + [TestMethod] + public void AddAndThenRemoveOther() + { + var all = ImmutableArray.Create( + new FreeHashString("foo", 1), + new FreeHashString("bar", Capacity + 1), + new FreeHashString("baz", 1)); + var s = NewSet(); + s.UnionWith(all); + foreach (var i in ImmutableArray.Create( + new FreeHashString("fooBar", 1))) + { + Assert.IsFalse(s.Remove(i)); + } + Assert.AreEqual(s.Count, all.Length); + } + + [TestMethod] + public void AddAndThenRemoveInReverseOrder() + { + var all = ImmutableArray.Create( + new FreeHashString("foo", 1), + new FreeHashString("bar", Capacity + 1), + new FreeHashString("baz", 1)); + var s = NewSet(); + s.UnionWith(all); + foreach (var i in all.Reverse()) + { + s.Remove(i); + } + Assert.AreEqual(s.Count, 0); + } + + [TestMethod] + public void Contains() + { + var all = ImmutableArray.Create( + new FreeHashString("foo", 1), + new FreeHashString("bar", Capacity + 1), + new FreeHashString("baz", 1)); + var s = NewSet(); + s.UnionWith(all); + Assert.IsTrue(all.All(i => s.Contains(i))); + } + + [TestMethod] + public void IntersectWith() + { + var left = ImmutableArray.Create( + new FreeHashString("foo", 1), + new FreeHashString("bar", Capacity + 1), + new FreeHashString("baz", 1)); + var right = ImmutableArray.Create( + new FreeHashString("fooBaz", Capacity + 1)); + var s1 = NewSet(); + s1.UnionWith(left); + var s2 = NewSet(); + s2.UnionWith(right); + s1.IntersectWith(s2); + Assert.AreEqual(s1.Count, 0); + } + + [TestMethod] + public void SymmetryExceptWith() + { + var left = ImmutableArray.Create( + new FreeHashString("fooBaz", Capacity + 1)); + var right = ImmutableArray.Create( + new FreeHashString("foo", 1), + new FreeHashString("bar", Capacity + 1), + new FreeHashString("baz", 1)); + var s = NewSet(); + s.UnionWith(left); + s.SymmetricExceptWith(right); + Assert.IsTrue(s.SequenceEqual(left.Concat(right))); + } + + private static LinkedHashSet NewSet() + { + return new LinkedHashSet(); + } +} diff --git a/Collection.Test/Maroontress/Collection/Test/LinkedHashSet/SetConformanceTest.cs b/Collection.Test/Maroontress/Collection/Test/LinkedHashSet/SetConformanceTest.cs new file mode 100644 index 0000000..59cb0eb --- /dev/null +++ b/Collection.Test/Maroontress/Collection/Test/LinkedHashSet/SetConformanceTest.cs @@ -0,0 +1,10 @@ +namespace Maroontress.Collection.Test.LinkedHashSet; + +[TestClass] +public sealed class SetConformanceTest : AbstractSetConformanceTest +{ + protected override ISet NewSet() + { + return new LinkedHashSet(); + } +} diff --git a/Collection.Test/Maroontress/Collection/Test/SimpleLinkedHashSet/SetConformanceTest.cs b/Collection.Test/Maroontress/Collection/Test/SimpleLinkedHashSet/SetConformanceTest.cs new file mode 100644 index 0000000..3ff3510 --- /dev/null +++ b/Collection.Test/Maroontress/Collection/Test/SimpleLinkedHashSet/SetConformanceTest.cs @@ -0,0 +1,10 @@ +namespace Maroontress.Collection.Test.SimpleLinkedHashSet; + +[TestClass] +public sealed class SetConformanceTest : AbstractSetConformanceTest +{ + protected override ISet NewSet() + { + return new SimpleLinkedHashSet(16); + } +} diff --git a/Collection.Test/Usings.cs b/Collection.Test/Usings.cs new file mode 100644 index 0000000..2575d20 --- /dev/null +++ b/Collection.Test/Usings.cs @@ -0,0 +1,2 @@ +#pragma warning disable SA1200 // Using directives should be placed correctly +global using Microsoft.VisualStudio.TestTools.UnitTesting; diff --git a/Collection.sln b/Collection.sln new file mode 100644 index 0000000..64c82cb --- /dev/null +++ b/Collection.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.2.32516.85 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Collection", "Collection\Collection.csproj", "{63A3E05F-FA70-4885-BA40-31613A66AFD6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Collection.Test", "Collection.Test\Collection.Test.csproj", "{7B7F3318-F90D-4547-95D8-35533F75E420}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {63A3E05F-FA70-4885-BA40-31613A66AFD6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {63A3E05F-FA70-4885-BA40-31613A66AFD6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {63A3E05F-FA70-4885-BA40-31613A66AFD6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {63A3E05F-FA70-4885-BA40-31613A66AFD6}.Release|Any CPU.Build.0 = Release|Any CPU + {7B7F3318-F90D-4547-95D8-35533F75E420}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7B7F3318-F90D-4547-95D8-35533F75E420}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7B7F3318-F90D-4547-95D8-35533F75E420}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7B7F3318-F90D-4547-95D8-35533F75E420}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {EB42C2A9-F1C4-4663-A960-9E8193375415} + EndGlobalSection +EndGlobal diff --git a/Collection/.editorconfig b/Collection/.editorconfig new file mode 100644 index 0000000..b68f2a4 --- /dev/null +++ b/Collection/.editorconfig @@ -0,0 +1,116 @@ +[*.cs] + +# SA1633: File should have header +dotnet_diagnostic.SA1633.severity = none + +# CA1715: Identifiers should have correct prefix +dotnet_diagnostic.CA1715.severity = none + +# SA1314: Type parameter names should begin with T +dotnet_diagnostic.SA1314.severity = none + +# SA1200: Using directives should be placed correctly +dotnet_diagnostic.SA1200.severity = none + +# SA1513: Closing brace should be followed by blank line +dotnet_diagnostic.SA1513.severity = none + +# SA1101: Prefix local calls with this +dotnet_diagnostic.SA1101.severity = none + +# SA1302: Interface names should begin with I +dotnet_diagnostic.SA1302.severity = none + +# SA1012: Opening braces should be spaced correctly +dotnet_diagnostic.SA1012.severity = none + +# SA1013: Closing braces should be spaced correctly +dotnet_diagnostic.SA1013.severity = none + +# SA1002: Semicolons should be spaced correctly +dotnet_diagnostic.SA1002.severity = none + +csharp_indent_labels = one_less_than_current +csharp_using_directive_placement = outside_namespace:silent +csharp_prefer_simple_using_statement = true:suggestion +csharp_prefer_braces = true:silent +csharp_style_namespace_declarations = block_scoped:silent +csharp_style_prefer_method_group_conversion = true:silent +csharp_style_expression_bodied_methods = false:silent +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_operators = false:silent +csharp_style_expression_bodied_properties = true:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = false:silent +csharp_style_throw_expression = true:suggestion +csharp_style_prefer_null_check_over_type_check = true:suggestion + +[*.{cs,vb}] + +#### Naming styles #### + +# Naming rules + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +# Symbol specifications + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +# Naming styles + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case +dotnet_style_operator_placement_when_wrapping = beginning_of_line +tab_width = 4 +indent_size = 4 +end_of_line = crlf +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_auto_properties = true:silent +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_prefer_simplified_boolean_expressions = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion +dotnet_style_prefer_conditional_expression_over_return = true:suggestion +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_compound_assignment = true:suggestion +dotnet_style_prefer_simplified_interpolation = true:suggestion +dotnet_style_namespace_match_folder = true:suggestion diff --git a/Collection/Collection.csproj b/Collection/Collection.csproj new file mode 100644 index 0000000..e69a0b8 --- /dev/null +++ b/Collection/Collection.csproj @@ -0,0 +1,63 @@ + + + + netstandard2.1 + 10 + enable + True + True + latest-all + True + dcx/Maroontress.Collection.xml + Maroontress.Collection + Maroontress.Collection + + + + Maroontress.Collection + $(Version) + Tomohisa Tanaka + https://maroontress.github.io/Collection-CSharp/ + https://github.com/maroontress/Collection.CSharp + false + Maroontress.Collection is a C# class library containing some collection classes. + See https://maroontress.github.io/Collection-CSharp/releasenotes.html + Copyright (c) 2022 Maroontress Fast Software + + true + 1.0.0.0 + + Maroontress Fast Software + COPYRIGHT.txt + + + + + + + + + + + + + true + \ + + + true + \ + + + true + \ + + + + + + + + + + diff --git a/Collection/Maroontress/Collection/.namespace.xml b/Collection/Maroontress/Collection/.namespace.xml new file mode 100644 index 0000000..e4bcb4d --- /dev/null +++ b/Collection/Maroontress/Collection/.namespace.xml @@ -0,0 +1,9 @@ + + + + + This namespace provides the implementation for some collection classes, + which we don't need frequently but sometimes. + + + diff --git a/Collection/Maroontress/Collection/HashTableConstants.cs b/Collection/Maroontress/Collection/HashTableConstants.cs new file mode 100644 index 0000000..ee4f8ed --- /dev/null +++ b/Collection/Maroontress/Collection/HashTableConstants.cs @@ -0,0 +1,101 @@ +namespace Maroontress.Collection; + +using System; + +/// +/// This class provides constants for customizing the behavior of a instance. +/// +/// +/// Do not use this class for anything other than unit testing. +/// +public sealed class HashTableConstants +{ + /// + /// Initializes a new instance of the + /// class. + /// + /// + /// The maximum capacity that represents the maximum length of the array. + /// + /// + /// The maximum size. + /// + /// + /// The maximum roast. + /// + /// + /// If the or + /// is not positive. + /// + public HashTableConstants(int maxCapacity, int maxSize, int maxRoast) + { + if (maxCapacity <= 0) + { + throw new ArgumentException( + $"{nameof(maxCapacity)} must be positive"); + } + if (maxSize <= 0) + { + throw new ArgumentException( + $"{nameof(maxSize)} must be positive"); + } + if (maxRoast <= 0) + { + throw new ArgumentException( + $"{nameof(maxRoast)} must be positive"); + } + MaxCapacity = maxCapacity; + MaxSize = maxSize; + MaxRoast = maxRoast; + } + + /// + /// Gets the maximum capacity. + /// + public int MaxCapacity { get; } + + /// + /// Gets the maximum size. + /// + public int MaxSize { get; } + + /// + /// Gets the maximum roast. + /// + public int MaxRoast { get; } + + /// + /// Gets the initial capacity which is the smallest power-of-two number + /// equal to or greater then the specified value. If the value is greater + /// than , returns . + /// + /// + /// The preferred capacity. + /// + /// + /// The initial capacity. + /// + /// + /// If the is not positive. + /// + public int GetCapacity(int n) + { + if (n <= 0) + { + throw new ArgumentException($"{nameof(n)} must be positive"); + } + if (n >= MaxCapacity) + { + return MaxCapacity; + } + var m = n; + m |= m >> 1; + m |= m >> 2; + m |= m >> 4; + m |= m >> 8; + m |= m >> 16; + m -= m >> 1; + return (m == n) ? m : m << 1; + } +} diff --git a/Collection/Maroontress/Collection/ImmutableLinkedHashMap.cs b/Collection/Maroontress/Collection/ImmutableLinkedHashMap.cs new file mode 100644 index 0000000..d98e1a2 --- /dev/null +++ b/Collection/Maroontress/Collection/ImmutableLinkedHashMap.cs @@ -0,0 +1,217 @@ +namespace Maroontress.Collection; + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; + +/// +/// This class implements the +/// interface. It has and to maintain the elements so that it has +/// predictable iteration order, like that of the LinkedHashMap +/// class in Java. +/// +/// +/// The keeps the iteration order corresponding +/// to the insertion order in which you insert key-value pairs into +/// the map. Note that the insertion order is not affected when you re-inserted +/// any key into it (i.e., with the method). +/// +/// +/// The type of keys maintained by this dictionary. +/// +/// +/// The type of mapped values. +/// +public sealed class ImmutableLinkedHashMap : IImmutableDictionary + where K : notnull +{ + /// + /// Gets an empty . + /// + public static readonly ImmutableLinkedHashMap Empty = new(); + + private static readonly IEqualityComparer> + KeyCompare = new KeyComparer(); + + private ImmutableLinkedHashMap() + { + List = Enumerable.Empty>(); + Map = ImmutableDictionary.Empty; + } + + private ImmutableLinkedHashMap( + IEnumerable> newList, + ImmutableDictionary newMap) + { + List = newList; + Map = newMap; + } + + /// + public IEnumerable Keys => List.Select(p => p.Key); + + /// + public IEnumerable Values => List.Select(p => p.Value); + + /// + public int Count => Map.Count; + + private IEnumerable> List { get; } + + private ImmutableDictionary Map { get; } + + /// + public V this[K key] => Map[key]; + + /// + public IImmutableDictionary Add(K key, V value) + { + var newMap = Map.Add(key, value); + if (ReferenceEquals(newMap, Map)) + { + return this; + } + var pair = new KeyValuePair(key, value); + var newList = List.Append(pair); + return new ImmutableLinkedHashMap(newList, newMap); + } + + /// + public IImmutableDictionary AddRange( + IEnumerable> pairs) + { + var newMap = Map.AddRange(pairs); + if (ReferenceEquals(newMap, Map)) + { + return this; + } + var delta = pairs.Where(i => !Map.ContainsKey(i.Key)) + .Distinct(KeyCompare) + .ToImmutableArray(); + var newList = List.Concat(delta); + return new ImmutableLinkedHashMap(newList, newMap); + } + + /// + public IImmutableDictionary Clear() + => Empty; + + /// + public bool Contains(KeyValuePair pair) + { + return Map.TryGetValue(pair.Key, out var value) + && Equals(value, pair.Value); + } + + /// + public bool ContainsKey(K key) => Map.ContainsKey(key); + + /// + public IEnumerator> GetEnumerator() + => List.GetEnumerator(); + + /// + public IImmutableDictionary Remove(K key) + { + var newMap = Map.Remove(key); + if (ReferenceEquals(newMap, Map)) + { + return this; + } + var newList = List.Where(i => !i.Key.Equals(key)) + .ToImmutableArray(); + return new ImmutableLinkedHashMap(newList, newMap); + } + + /// + public IImmutableDictionary RemoveRange(IEnumerable keys) + { + var newMap = Map.RemoveRange(keys); + if (ReferenceEquals(newMap, Map)) + { + return this; + } + var newList = List.Where(i => newMap.ContainsKey(i.Key)) + .ToImmutableArray(); + return new ImmutableLinkedHashMap(newList, newMap); + } + + /// + public IImmutableDictionary SetItem(K key, V value) + { + var newMap = Map.SetItem(key, value); + if (ReferenceEquals(newMap, Map)) + { + return this; + } + var newPair = new KeyValuePair(key, value); + var newList = Map.ContainsKey(key) + ? List.Select(i => i.Key.Equals(key) ? newPair : i) + .ToImmutableArray() + : List.Append(newPair); + return new ImmutableLinkedHashMap(newList, newMap); + } + + /// + public IImmutableDictionary SetItems( + IEnumerable> items) + { + if (items is null) + { + throw new ArgumentNullException(nameof(items)); + } + var newMap = Map.SetItems(items); + if (ReferenceEquals(newMap, Map)) + { + return this; + } + var deltaList = new List>(); + var deltaMap = new Dictionary>(); + foreach (var i in items) + { + if (!Map.TryGetValue(i.Key, out var currentValue)) + { + deltaList.Add(i); + continue; + } + if (Equals(i.Value, currentValue)) + { + continue; + } + deltaMap[i.Key] = i; + } + var newList = List.Select( + i => deltaMap.TryGetValue(i.Key, out var v) ? v : i) + .Concat(deltaList.Distinct(KeyCompare)) + .ToImmutableArray(); + return new ImmutableLinkedHashMap(newList, newMap); + } + + /// + public bool TryGetKey(K equalKey, out K actualKey) + => Map.TryGetKey(equalKey, out actualKey); + + /// + public bool TryGetValue(K key, out V value) + => Map.TryGetValue(key, out value!); + + /// + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + private class KeyComparer : IEqualityComparer> + { + public bool Equals(KeyValuePair x, KeyValuePair y) + { + return x.Key.Equals(y.Key); + } + + public int GetHashCode(KeyValuePair o) + { + return o.Key.GetHashCode(); + } + } +} diff --git a/Collection/Maroontress/Collection/InternMap.cs b/Collection/Maroontress/Collection/InternMap.cs new file mode 100644 index 0000000..35049ba --- /dev/null +++ b/Collection/Maroontress/Collection/InternMap.cs @@ -0,0 +1,105 @@ +namespace Maroontress.Collection; + +using System; +using System.Collections.Concurrent; + +/// +/// The class provides the canonical value object +/// corresponding to the specified key. +/// +/// +/// The type of the key. +/// +/// +/// The type of the value. +/// +public sealed class InternMap + where K : notnull + where V : class +{ + private const int DefaultCapacity = 31; + + /// + /// Initializes a new instance of the class. + /// + /// + /// The function that returns a new value object corresponding to the + /// specified argument. + /// + public InternMap(Func newValue) + : this(newValue, DefaultCapacity, DefaultConcurrencyLevel) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The function that returns a new value object corresponding to the + /// specified argument. + /// + /// + /// The initial capacity. + /// + public InternMap(Func newValue, int initialCapacity) + : this(newValue, initialCapacity, DefaultConcurrencyLevel) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The function that returns a new value object corresponding to the + /// specified argument. + /// + /// + /// The initial capacity. + /// + /// + /// The concurrency level. + /// + public InternMap( + Func newValue, int initialCapacity, int concurrencyLevel) + { + if (newValue is null) + { + throw new ArgumentNullException(nameof(newValue)); + } + Map = new(concurrencyLevel, initialCapacity); + NewValue = newValue; + } + + private static int DefaultConcurrencyLevel { get; } + = Environment.ProcessorCount; + + /// + /// Gets the map from a key to the value. + /// + private ConcurrentDictionary Map { get; } + + private Func NewValue { get; } + + /// + /// Gets the canonical value object corresponding to the specified key + /// object. If the canonical value object does not exist in the internal + /// object pool, creates a new value object with the function specified + /// with the constructor. + /// + /// + /// The function newValue specified with the constructor can be + /// called concurrently with the equal keys if the multiple threads call + /// this method. Even so, this method returns only one canonical object + /// corresponding to the specified key. + /// + /// + /// The key object. + /// + /// + /// The canonical value object. + /// + public V Intern(K key) + { + return Map.GetOrAdd(key, NewValue); + } +} diff --git a/Collection/Maroontress/Collection/LinkedHashSet.cs b/Collection/Maroontress/Collection/LinkedHashSet.cs new file mode 100644 index 0000000..1ad0e02 --- /dev/null +++ b/Collection/Maroontress/Collection/LinkedHashSet.cs @@ -0,0 +1,1398 @@ +namespace Maroontress.Collection; + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +/// +/// The class implements the interface. Its instance has both the hash table and the +/// doubly-linked list to maintain the elements so that it has predictable +/// iteration order, like that of the LinkedHashSet class in Java. +/// +/// +/// +/// The linked list keeps the iteration order corresponding to the insertion +/// order in which you insert elements into the set. Note that the +/// insertion order is not affected when you re-inserted any into it (i.e., the +/// method returned false). +/// +/// +/// The hash table uses +/// +/// Separate Chaining for collision resolution and has the capacity, +/// size, and load factor. The capacity represents the number of +/// entries in the hash table. In this implementation, it is a power of two. +/// The size is the number of elements that the hash table contains. The set +/// rehashes the hash table when the size divided by the capacity is close to +/// the load factor.Rehashing makes the capacity of the hash table double. +/// +/// +/// To check whether two sets are equal, with iteration order taken into +/// account, use the +/// +/// method as follows: +/// +///
+/// public static bool AreEqualAndHaveSameInsertionOrder<T>(
+///     LinkedHashSet<T> s1, LinkedHashSet<T> s2)
+/// {
+///     return s1.SequenceEqual(s2);
+/// }
+/// +/// Note that the and methods ignore the iteration order and +/// return set equality; the method returns +/// reference equality. +/// +/// +/// The minimum and maximum capacities are +/// (16) and (0x40000000), +/// respectively. +/// +/// +/// As mentioned, if the number of elements in the set exceeds the product of +/// the capacity and the load factor, it rehashes its hash table with the +/// capacity updated to double. However, this implementation restricts the +/// maximum capacity to (0x40000000). +/// So, once the capacity reaches its maximum, rehashing will no longer occur. +/// Note that since the implementation uses Separate Chaining, it is possible +/// to add up to (0x7fffffff) elements to the +/// set even after the capacity reaches its maximumunless it throws an . +/// +/// +/// If the number of elements in the set reaches the maximum value (), any attempt to add elements throws an (e.g., with the +/// method). +/// +///
+/// +/// The type of elements maintained by this set. +/// +public class LinkedHashSet : ISet + where T : notnull +{ + /// + /// The default maximum capacity. + /// + public const int DefaultMaxCapacity = 0x4000_0000; + + /// + /// The default maximum size. + /// + public const int DefaultMaxSize = int.MaxValue; + + /// + /// The default initial capacity. This is also the minimum capacity. + /// + public const int DefaultInitialCapacity = 16; + + /// + /// The default load factor. + /// + public const float DefaultLoadFactor = 0.75f; + + private static readonly HashTableConstants DefaultHashConstants + = new(DefaultMaxCapacity, DefaultMaxSize, int.MaxValue); + + /// + /// Initializes a new instance of the + /// class. + /// + /// + /// The initial capacity. + /// + /// + /// The load factor. + /// + /// + /// The capacity is the smallest power-of-two number equal to or greater + /// than the specified . + /// + /// + /// If the initial capacity is negative or the load factor is nonpositive. + /// + public LinkedHashSet( + int initialCapacity = DefaultInitialCapacity, + float loadFactor = DefaultLoadFactor) + : this(DefaultHashConstants, initialCapacity, loadFactor) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The constants for the hash table. + /// + /// + /// The initial capacity. + /// + /// + /// The load factor. + /// + /// + /// If the is negative or the is nonpositive. + /// + /// + /// If the is null. + /// + protected LinkedHashSet( + HashTableConstants constants, int initialCapacity, float loadFactor) + { + if (constants is null) + { + throw new ArgumentNullException(nameof(constants)); + } + if (initialCapacity < 0) + { + throw new ArgumentException( + $"Illegal initial capacity: {initialCapacity}"); + } + if (loadFactor <= 0 || float.IsNaN(loadFactor)) + { + throw new ArgumentException( + $"Illegal load factor: {loadFactor}"); + } + var rawCapacity = Math.Max(initialCapacity, DefaultInitialCapacity); + var size = constants.GetCapacity(rawCapacity); + LoadFactor = Math.Max(Math.Min(loadFactor, 1.0f), 0.125f); + Constants = constants; + Limit = GetLimit(size); + Nodes = new Node[size]; + IndexMask = size - 1; + Head = null; + Tail = null; + Size = 0; + } + + /// + /// The read-only node in the hash table and the doubly-linked list. + /// + /// + /// Do not use this class for anything other than unit testing. + /// + protected interface ProtectedNode + { + /// + /// Gets the value that this node has. + /// + public T Value { get; } + + /// + /// Gets the hash code. + /// + public int Hash { get; } + + /// + /// Gets the roast. + /// + public int Roast { get; } + + /// + /// Gets the parent node in Separate Chaining. + /// + public ProtectedNode? ParentNode { get; } + + /// + /// Gets the previous node in the doubly-linked node. + /// + public ProtectedNode? PreviousNode { get; } + + /// + /// Gets the next node in the doubly-linked node. + /// + public ProtectedNode? NextNode { get; } + } + + /// + /// Gets the load factor. + /// + public float LoadFactor { get; } + + /// + public int Count => Size; + + /// + public bool IsReadOnly => false; + + /// + /// Gets the capacity and limit. + /// + /// + /// The tuple containing the capacity and the limit. + /// + protected (int Capacity, int Limit) CapacityAndLimit + { + get => (Nodes.Length, Limit); + } + + /// + /// Gets the first and last nodes, or null when this set is empty. + /// + /// + /// The tuple containing the first and last nodes. + /// + protected (ProtectedNode? First, ProtectedNode? Last) FirstAndLastNode + { + get => (Head, Tail); + } + + private static Func LessThan { get; } + = (i, j) => i < j; + + private static Func LessThanOrEqual { get; } + = (i, j) => i <= j; + + private static Func Equal { get; } + = (i, j) => i == j; + + private HashTableConstants Constants { get; } + + private int Limit { get; set; } + + private Node?[] Nodes { get; set; } + + private Node? Head { get; set; } + + private Node? Tail { get; set; } + + private int Size { get; set; } + + private int IndexMask { get; set; } + + /// + public bool Add(T item) + { + if (Append(item, item.GetHashCode())) + { + ++Size; + Extend(); + return true; + } + return false; + } + + /// + public void Clear() + { + /* + var e = Head; + while (e is not null) + { + var i = ToIndex(e.Hash, Nodes.Length); + Nodes[i] = null; + e = e.Next; + } + */ + Array.Clear(Nodes, 0, Nodes.Length); + Size = 0; + Head = null; + Tail = null; + } + + /// + public bool Contains(T item) + => Contains(item, item.GetHashCode()); + + /// + public void CopyTo(T[] array, int arrayIndex) + { + if (array is null) + { + throw new ArgumentNullException(nameof(array)); + } + if (arrayIndex < 0) + { + throw new ArgumentOutOfRangeException( + nameof(arrayIndex), + $"Illegal array index: {arrayIndex}"); + } + if (arrayIndex > array.Length + || array.Length < Size + || arrayIndex > array.Length - Size) + { + throw new ArgumentException( + "Too small array length"); + } + var i = arrayIndex; + for (var e = Head; e is not null; e = e.Next) + { + array[i] = e.Value; + ++i; + } + } + + /// + public void ExceptWith(IEnumerable other) + { + if (other is null) + { + throw new ArgumentNullException(nameof(other)); + } + foreach (var e in other) + { + Remove(e); + } + } + + /// + public IEnumerator GetEnumerator() + { + var e = Head; + while (e is not null) + { + var next = e.Next; + yield return e.Value; + e = next; + } + } + + /// + public void IntersectWith(IEnumerable other) + { + if (other is null) + { + throw new ArgumentNullException(nameof(other)); + } + if (ReferenceEquals(other, this)) + { + return; + } + if (Size is 0) + { + Clear(); + return; + } + var newSize = 0; + var newRoast = GetNewRoast(); + foreach (var item in other) + { + var hash = item.GetHashCode(); + if (!(FindNode(item, hash) is {} node)) + { + continue; + } + node.Roast = newRoast; + } + for (var e = Head; e is not null; e = e.Next) + { + if (e.Roast != newRoast) + { + DeleteNode(e); + continue; + } + ++newSize; + } + Size = newSize; + } + + /// + public bool IsProperSubsetOf(IEnumerable other) + { + if (other is null) + { + throw new ArgumentNullException(nameof(other)); + } + return !ReferenceEquals(other, this) + && ProperSubset(other); + } + + /// + public bool IsProperSupersetOf(IEnumerable other) + { + if (other is null) + { + throw new ArgumentNullException(nameof(other)); + } + return !ReferenceEquals(other, this) + && Size is not 0 + && IsSupersetOf(other, LessThan); + } + + /// + public bool IsSubsetOf(IEnumerable other) + { + if (other is null) + { + throw new ArgumentNullException(nameof(other)); + } + return ReferenceEquals(other, this) + || Subset(other); + } + + /// + public bool IsSupersetOf(IEnumerable other) + { + if (other is null) + { + throw new ArgumentNullException(nameof(other)); + } + return EqualToOrSupersetOf(other, LessThanOrEqual); + } + + /// + public bool Overlaps(IEnumerable other) + { + if (other is null) + { + throw new ArgumentNullException(nameof(other)); + } + return Size is not 0 + && (ReferenceEquals(other, this) + || other.Any(Contains)); + } + + /// + public bool Remove(T item) + { + var hash = item.GetHashCode(); + if (!(TakeNode(item, hash) is {} node)) + { + return false; + } + Uncouple(node); + --Size; + return true; + } + + /// + public bool SetEquals(IEnumerable other) + { + if (other is null) + { + throw new ArgumentNullException(nameof(other)); + } + return EqualToOrSupersetOf(other, Equal); + } + + /// + public void SymmetricExceptWith(IEnumerable other) + { + if (other is null) + { + throw new ArgumentNullException(nameof(other)); + } + if (ReferenceEquals(other, this)) + { + Clear(); + return; + } + if (Size is 0) + { + UnionWith(other); + return; + } + var removedList = new OneWayList(); + var addedList = new OneWayList(); + var newRoast = GetNewRoast(); + try + { + foreach (var item in other) + { + var lastTail = addedList.Tail; + if (!(FindOrAttachNode(item, addedList) is {} node)) + { + var newTail = addedList.Tail; + newTail!.Previous = lastTail; + newTail!.Roast = newRoast; + ++Size; + if (Size > Limit) + { + DoubleCapacity(); + Reparent(addedList.Head); + Reparent(removedList.Head); + } + continue; + } + if (node.Roast == newRoast) + { + continue; + } + --Size; + Uncouple(node); + removedList.Append(node); + node.Roast = newRoast; + } + } + finally + { + if (removedList.Tail is {} tail) + { + tail.Next = null; + for (var e = removedList.Head; e is not null; e = e.Next) + { + DetachNode(e); + } + } + var head = addedList.Head; + if (Tail is not null) + { + Tail.Next = head; + } + else + { + Head = head; + } + if (head is not null) + { + head.Previous = Tail; + Tail = addedList.Tail; + } + if (Head is not null) + { + Head.Roast = newRoast; + } + } + } + + /// + public void UnionWith(IEnumerable other) + { + if (other is null) + { + throw new ArgumentNullException(nameof(other)); + } + foreach (var e in other) + { + Add(e); + } + } + + /// + void ICollection.Add(T item) => Add(item); + + /// + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + /// + /// Modifies the current object to contain + /// only elements that are present in that object and in the specified + /// . + /// + /// + /// The set to compare to the current + /// object. + /// + /// + /// If the is null. + /// + public void IntersectWith(LinkedHashSet otherSet) + { + if (otherSet is null) + { + throw new ArgumentNullException(nameof(otherSet)); + } + if (ReferenceEquals(otherSet, this)) + { + return; + } + if (Size is 0 || otherSet.Size is 0) + { + Clear(); + return; + } + var count = 0; + for (var e = Head; e is not null; e = e.Next) + { + if (otherSet.Contains(e.Value, e.Hash)) + { + continue; + } + DeleteNode(e); + ++count; + } + Size -= count; + } + + /// + /// Modifies the current object to contain + /// all elements that are present in itself, the specified set, or both. + /// + /// + /// The collection to compare to the current + /// object. + /// + /// + /// If the is null. + /// + public void UnionWith(LinkedHashSet otherSet) + { + if (otherSet is null) + { + throw new ArgumentNullException(nameof(otherSet)); + } + if (Size is not 0) + { + for (var e = otherSet.Head; e is not null; e = e.Next) + { + if (Append(e.Value, e.Hash)) + { + ++Size; + Extend(); + } + } + return; + } + var s = otherSet.Size; + if (s > Limit) + { + var m = (double)Constants.MaxSize; + var n = (int)Math.Min((double)s / LoadFactor, m); + var nextLength = Constants.GetCapacity(n); + Nodes = new Node[nextLength]; + IndexMask = nextLength - 1; + Limit = GetLimit(nextLength); + } + for (var e = otherSet.Head; e is not null; e = e.Next) + { + var i = IndexMap(e.Hash); + ref var slot = ref Nodes[i]; + var root = slot; + var node = Node.Of(e.Value, e.Hash, root); + AppendNode(node); + slot = node; + } + Size = s; + } + + /// + /// Modifies the current object to contain + /// only elements that are present either in that object or in the + /// specified set, but not both. + /// + /// + /// The set to compare to the current + /// object. + /// + /// + /// If the is null. + /// + public void SymmetricExceptWith(LinkedHashSet otherSet) + { + if (otherSet is null) + { + throw new ArgumentNullException(nameof(otherSet)); + } + if (ReferenceEquals(otherSet, this)) + { + Clear(); + return; + } + if (Size is 0) + { + UnionWith(otherSet); + return; + } + for (var e = otherSet.Head; e is not null; e = e.Next) + { + var v = e.Value; + var hash = e.Hash; + if (DeleteOrAppendNode(v, hash) is {} node) + { + --Size; + continue; + } + ++Size; + } + } + + /// + /// Determines whether a object and the + /// specified set contain the same elements. + /// + /// + /// The collection to compare to the current + /// object. + /// + /// + /// true if the object is equal to + /// other; otherwise, false. + /// + /// + /// If the is null. + /// + public bool SetEquals(LinkedHashSet otherSet) + { + if (otherSet is null) + { + throw new ArgumentNullException(nameof(otherSet)); + } + return ReferenceEquals(otherSet, this) + || (otherSet.Size == Size && ContainsAll(otherSet)); + } + + /// + /// Determines whether the current object + /// and a specified set share common elements. + /// + /// + /// The set to compare to the current + /// object. + /// + /// + /// true if the object and + /// share at least one common element; + /// otherwise, false. + /// + /// + /// If the is null. + /// + public bool Overlaps(LinkedHashSet otherSet) + { + if (otherSet is null) + { + throw new ArgumentNullException(nameof(otherSet)); + } + if (Size is 0 || otherSet.Size is 0) + { + return false; + } + if (ReferenceEquals(otherSet, this)) + { + return true; + } + var (e, s) = (Size < otherSet.Size) + ? (Head, otherSet) + : (otherSet.Head, this); + for (; e is not null; e = e.Next) + { + if (s.Contains(e.Value, e.Hash)) + { + return true; + } + } + return false; + } + + /// + /// Determines whether a object is a + /// subset of the specified set. + /// + /// + /// The collection to compare to the current object. + /// + /// + /// true if the object is a subset + /// of ; otherwise, false. + /// + /// + /// If the is null. + /// + public bool IsSubsetOf(LinkedHashSet otherSet) + { + if (otherSet is null) + { + throw new ArgumentNullException(nameof(otherSet)); + } + return Size is 0 + || ReferenceEquals(otherSet, this) + || (otherSet.Size >= Size && otherSet.ContainsAll(this)); + } + + /// + /// Determines whether a object is a + /// superset of the specified set. + /// + /// + /// The set to compare to the current + /// object. + /// + /// + /// true if the object is a + /// superset of ; otherwise, false. + /// + /// + /// If the is null. + /// + public bool IsSupersetOf(LinkedHashSet otherSet) + { + if (otherSet is null) + { + throw new ArgumentNullException(nameof(otherSet)); + } + return otherSet.Size is 0 + || ReferenceEquals(otherSet, this) + || (otherSet.Size <= Size && ContainsAll(otherSet)); + } + + /// + /// Determines whether a object is a + /// proper subset of the specified set. + /// + /// + /// The set to compare to the current + /// object. + /// + /// + /// true if the object is a proper + /// subset of ; otherwise, false. + /// + /// + /// If the is null. + /// + public bool IsProperSubsetOf(LinkedHashSet otherSet) + { + if (otherSet is null) + { + throw new ArgumentNullException(nameof(otherSet)); + } + return (Size is 0 && otherSet.Size > 0) + || (!ReferenceEquals(otherSet, this) + && otherSet.Size > Size + && otherSet.ContainsAll(this)); + } + + /// + /// Determines whether a object is a + /// proper superset of the specified set. + /// + /// + /// The set to compare to the current + /// object. + /// + /// + /// true if the object is a proper + /// superset of ; otherwise, false. + /// + /// + /// If the is null. + /// + public bool IsProperSupersetOf(LinkedHashSet otherSet) + { + if (otherSet is null) + { + throw new ArgumentNullException(nameof(otherSet)); + } + return (otherSet.Size is 0 && Size > 0) + || (!ReferenceEquals(otherSet, this) + && otherSet.Size < Size + && ContainsAll(otherSet)); + } + + /// + /// Gets the backend array of the hash table. + /// + /// + /// The backend array. + /// + protected IEnumerable GetNodes() + => Nodes.AsEnumerable(); + + private int GetNewRoast() + { + var roast = Head!.Roast; + if (roast < Constants.MaxRoast) + { + return roast + 1; + } + for (var e = Head; e is not null; e = e.Next) + { + e.Roast = 0; + } + return 1; + } + + private int IndexMap(int hash) + { + return hash & IndexMask; + } + + private bool ProperSubset(IEnumerable other) + { + if (Head is null) + { + return other.Any(); + } + var newRoast = GetNewRoast(); + try + { + var includeSize = 0; + var excludes = false; + var containsAll = false; + foreach (var i in other) + { + var hash = i.GetHashCode(); + if (!(FindNode(i, hash) is {} node)) + { + excludes = true; + if (containsAll) + { + return true; + } + continue; + } + if (node.Roast == newRoast) + { + continue; + } + node.Roast = newRoast; + ++includeSize; + if (includeSize < Size) + { + continue; + } + containsAll = true; + if (excludes) + { + return true; + } + } + return false; + } + finally + { + Head.Roast = newRoast; + } + } + + private bool Subset(IEnumerable other) + { + if (Head is null) + { + return true; + } + var newRoast = GetNewRoast(); + try + { + var includeSize = 0; + foreach (var i in other) + { + var hash = i.GetHashCode(); + if (!(FindNode(i, hash) is {} node)) + { + continue; + } + if (node.Roast == newRoast) + { + continue; + } + node.Roast = newRoast; + ++includeSize; + if (includeSize < Size) + { + continue; + } + return true; + } + return false; + } + finally + { + Head.Roast = newRoast; + } + } + + private bool EqualToOrSupersetOf( + IEnumerable other, Func compare) + { + return ReferenceEquals(other, this) + || ((Size is 0) + ? !other.Any() + : IsSupersetOf(other, compare)); + } + + private bool IsSupersetOf( + IEnumerable other, Func compare) + { + var newRoast = GetNewRoast(); + var size = 0; + foreach (var i in other) + { + var hash = i.GetHashCode(); + if (!(FindNode(i, hash) is {} node)) + { + return false; + } + if (node.Roast == newRoast) + { + continue; + } + node.Roast = newRoast; + ++size; + } + return compare(size, Size); + } + + private bool ContainsAll(LinkedHashSet s) + { + for (var e = s.Head; e is not null; e = e.Next) + { + if (!Contains(e.Value, e.Hash)) + { + return false; + } + } + return true; + } + + private bool Contains(T item, int hash) + { + var i = IndexMap(hash); + for (var e = Nodes[i]; e is not null; e = e.Parent) + { + if (e.Hash == hash && e.Value.Equals(item)) + { + return true; + } + } + return false; + } + + private void Uncouple(Node e) + { + var previous = e.Previous; + var next = e.Next; + if (previous is null) + { + Head = next; + } + else + { + previous.Next = next; + } + if (next is null) + { + Tail = previous; + } + else + { + next.Previous = previous; + } + } + + private void DetachNode(Node node) + { + var hash = node.Hash; + var i = IndexMap(hash); + ref var slot = ref Nodes[i]; + var root = slot; + if (ReferenceEquals(root, node)) + { + slot = node.Parent; + return; + } + var child = root; + var e = root!.Parent; + for (;;) + { + var parent = e!.Parent; + if (ReferenceEquals(e, node)) + { + child!.Parent = parent; + return; + } + child = e; + e = parent; + } + } + + private void DeleteNode(Node node) + { + DetachNode(node); + Uncouple(node); + } + + private Node? TakeNode(T item, int hash) + { + var i = IndexMap(hash); + ref var slot = ref Nodes[i]; + var root = slot; + if (root is null) + { + return null; + } + if (root.Hash == hash && root.Value.Equals(item)) + { + slot = root.Parent; + return root; + } + var child = root; + var e = root.Parent; + while (e is not null) + { + var parent = e.Parent; + if (e.Hash == hash && e.Value.Equals(item)) + { + child.Parent = parent; + return e; + } + child = e; + e = parent; + } + return null; + } + + private Node? FindNode(T item, int hash) + { + var i = IndexMap(hash); + for (var e = Nodes[i]; e is not null; e = e.Parent) + { + if (e.Hash == hash && e.Value.Equals(item)) + { + return e; + } + } + return null; + } + + private Node? DeleteOrAppendNode(T item, int hash) + { + var i = IndexMap(hash); + ref var slot = ref Nodes[i]; + var root = slot; + if (root is not null) + { + if (root.Hash == hash && root.Value.Equals(item)) + { + slot = root.Parent; + Uncouple(root); + return root; + } + var child = root; + var e = root.Parent; + while (e is not null) + { + var parent = e.Parent; + if (e.Hash == hash && e.Value.Equals(item)) + { + child.Parent = parent; + Uncouple(e); + return e; + } + child = e; + e = parent; + } + } + if (Size == Constants.MaxSize) + { + throw new InsufficientMemoryException(); + } + var node = Node.Of(item, hash, root); + AppendNode(node); + slot = node; + return null; + } + + private void AppendNode(Node node) + { + if (Tail is null) + { + Head = node; + } + else + { + Tail.Next = node; + node.Previous = Tail; + } + Tail = node; + } + + private Node? FindOrAttachNode(T item, OneWayList nodeList) + { + var hash = item.GetHashCode(); + var i = IndexMap(hash); + ref var slot = ref Nodes[i]; + var root = slot; + for (var e = root; e is not null; e = e.Parent) + { + if (e.Hash == hash && e.Value.Equals(item)) + { + return e; + } + } + if (Size == Constants.MaxSize) + { + throw new InsufficientMemoryException(); + } + var node = Node.Of(item, hash, root); + slot = node; + nodeList.Append(node); + return null; + } + + private bool Append(T item, int hash) + { + var i = IndexMap(hash); + ref var slot = ref Nodes[i]; + var root = slot; + for (var e = root; e is not null; e = e.Parent) + { + if (e.Hash == hash && e.Value.Equals(item)) + { + return false; + } + } + if (Size == Constants.MaxSize) + { + throw new InsufficientMemoryException(); + } + var node = Node.Of(item, hash, root); + AppendNode(node); + slot = node; + return true; + } + + private void Extend() + { + if (Size <= Limit) + { + return; + } + DoubleCapacity(); + } + + private void DoubleCapacity() + { + var length = Nodes.Length; + var nextLength = (length > Constants.MaxCapacity / 2) + ? Constants.MaxCapacity + : length * 2; + Nodes = new Node[nextLength]; + IndexMask = nextLength - 1; + Reparent(Head); + Limit = (nextLength == Constants.MaxCapacity) + ? Constants.MaxSize + : GetLimit(nextLength); + } + + private int GetLimit(int length) + { + var limit = (double)length * LoadFactor; + return (limit >= Constants.MaxSize) + ? Constants.MaxSize + : (int)limit; + } + + private void Reparent(Node? head) + { + for (var e = head; e is not null; e = e.Next) + { + var i = e.Hash & IndexMask; + ref var slot = ref Nodes[i]; + e.Parent = slot; + slot = e; + } + } + + private sealed class OneWayList + { + public Node? Head { get; set; } + + public Node? Tail { get; set; } + + public int Size { get; private set; } + + public void Append(Node node) + { + if (Size is int.MaxValue) + { + throw new InsufficientMemoryException(); + } + if (Tail is {} tail) + { + tail.Next = node; + } + else + { + Head = node; + } + Tail = node; + ++Size; + } + } + + /// + /// The node in the hash table and the doubly-linked list. + /// + private sealed class Node : ProtectedNode + { + /// + /// Initializes a new instance of the class. + /// + /// + /// The value that this node has. + /// + /// + /// The hash code of . + /// + /// + /// The parent node in Separate Chaining. + /// + public Node(T value, int hash, Node? parent) + { + Value = value; + Hash = hash; + Parent = parent; + Previous = null; + Next = null; + } + + /// + /// Gets the value that this node has. + /// + public T Value { get; } + + /// + /// Gets the hash code of . + /// + public int Hash { get; } + + /// + /// Gets or sets the parent node in Separate Chaining. + /// + public Node? Parent { get; set; } + + /// + /// Gets or sets the previous node in the doubly-linked node. + /// + public Node? Previous { get; set; } + + /// + /// Gets or sets the next node in the doubly-linked node. + /// + public Node? Next { get; set; } + + /// + /// Gets or sets the roast. + /// + /// + /// The roast values that all the nodes in a set have must be equal to + /// or less than that of . + /// + public int Roast { get; set; } + + /// + public ProtectedNode? ParentNode => Parent; + + /// + public ProtectedNode? PreviousNode => Previous; + + /// + public ProtectedNode? NextNode => Next; + + /// + /// Gets a new node that has the specifie value, its hash value, + /// and the specified parent node. + /// + /// + /// The value. + /// + /// + /// The hash value. + /// + /// + /// The parent node or null. + /// + /// + /// The new node. + /// + public static Node Of(T value, int hash, Node? parent) + => new(value, hash, parent); + } +} diff --git a/Collection/Maroontress/Collection/namespace.cs b/Collection/Maroontress/Collection/namespace.cs new file mode 100644 index 0000000..fb605d2 --- /dev/null +++ b/Collection/Maroontress/Collection/namespace.cs @@ -0,0 +1,6 @@ +#pragma warning disable SA1516 // Elements should be separated by blank line + +using System; + +[assembly: CLSCompliant(true)] +namespace Maroontress.Collection; diff --git a/Collection/nuget/COPYRIGHT.txt b/Collection/nuget/COPYRIGHT.txt new file mode 100644 index 0000000..ed83e21 --- /dev/null +++ b/Collection/nuget/COPYRIGHT.txt @@ -0,0 +1,23 @@ +Copyright (c) 2022 Maroontress Fast Software. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the + distribution. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS +BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN +IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/Collection/nuget/LEGAL_NOTICES.txt b/Collection/nuget/LEGAL_NOTICES.txt new file mode 100644 index 0000000..fe2d7b9 --- /dev/null +++ b/Collection/nuget/LEGAL_NOTICES.txt @@ -0,0 +1,4 @@ +Acknowledgments: + +Portions of this software may utilize the following copyrighted materials, +the use of which is hereby acknowledged. diff --git a/Collection/nuget/readme.txt b/Collection/nuget/readme.txt new file mode 100644 index 0000000..6bc1e80 --- /dev/null +++ b/Collection/nuget/readme.txt @@ -0,0 +1,3 @@ +Maroontress.Collection NuGet README + +See https://maroontress.github.io/Collection-CSharp/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..31ad4a9 --- /dev/null +++ b/README.md @@ -0,0 +1,64 @@ +# Collection.CSharp + +Collection.CSharp is a C# class library containing some _collection_ classes, +which we don't need frequently but sometimes. It depends on .NET Standard 2.1. + +It contains the following classes: + +- [`ImmutableLinkedHashMap`](doc/ImmutableLinkedHashMap.md) +- [`InternMap`](doc/InternMap.md) +- [`LinkedHashSet`](doc/LinkedHashSet.md) + + + +## API Reference + +- [Maroontress.Collection][apiref-maroontress.collection] namespace + +## How to build + +### Requirements for build + +- Visual Studio 2022 (Version 17.2) + or [.NET 6.0 SDK (SDK 6.0.300)][dotnet-sdk] + +### Build + +```plaintext +git clone URL +cd Collection.CSharp +dotnet build +``` + +### Get the test coverage report with Coverlet + +Install [ReportGenerator][report-generator] as follows: + +```plaintext +dotnet tool install -g dotnet-reportgenerator-globaltool +``` + +Run all tests and get the report in the file `Coverlet-html/index.html`: + +```plaintext +rm -rf MsTestResults +dotnet test --collect:"XPlat Code Coverage" --results-directory MsTestResults \ + && reportgenerator -reports:MsTestResults/*/coverage.cobertura.xml \ + -targetdir:Coverlet-html +``` + +[report-generator]: + https://github.com/danielpalme/ReportGenerator +[dotnet-sdk]: + https://dotnet.microsoft.com/en-us/download +[apiref-maroontress.collection]: + https://maroontress.github.io/Collection-CSharp/api/latest/html/Maroontress.Collection.html +[nuget-maroontress.collection]: + https://www.nuget.org/packages/Maroontress.Collection/ +[nuget-logo]: + https://maroontress.github.io/images/NuGet-logo.png diff --git a/doc/ImmutableLinkedHashMap.md b/doc/ImmutableLinkedHashMap.md new file mode 100644 index 0000000..81b2c5f --- /dev/null +++ b/doc/ImmutableLinkedHashMap.md @@ -0,0 +1,16 @@ +# ImmutableLinkedHashMap + +## Summary + +The `ImmutableLinkedHashMap` class implements the +`IImmutableDictionary` interface. It has `ImmutableDictionary` and +`ImmutableArray` (where `T` stands for `KeyValuePair`) to maintain the +elements so that it has predictable iteration order, like that of the +`LinkedHashMap` class in Java. + +## Remarks + +The `ImmutableArray` keeps the iteration order corresponding to the +_insertion order_ in which you insert key-value pairs into the map. Note that +the insertion order is not affected when you re-inserted any key into it (i.e., +with the `SetItem(K, V)` method). diff --git a/doc/InternMap.md b/doc/InternMap.md new file mode 100644 index 0000000..d44e4c3 --- /dev/null +++ b/doc/InternMap.md @@ -0,0 +1,15 @@ +# InternMap + +The `InternMap` class provides the canonical value object corresponding to the +specified key. An example: + +```csharp +var map = new InternMap(i => i.ToString()); +var s1 = map.Intern(123); +var s2 = map.Intern(123); +``` + +where `s1` and `s2` refer the same object. + +This class has the `ConcurrentDictionary` class and just wraps its `GetOrAdd` +methods. diff --git a/doc/LinkedHashSet.md b/doc/LinkedHashSet.md new file mode 100644 index 0000000..25309ea --- /dev/null +++ b/doc/LinkedHashSet.md @@ -0,0 +1,142 @@ +# LinkedHashSet + +The `LinkedHashSet` class implements the `ISet` interface. Its instance +has both the hash table and the doubly-linked list to maintain the elements so +that it has predictable iteration order, like that of the `LinkedHashSet` class +in Java. + +The linked list keeps the iteration order corresponding to the _insertion +order_ in which you insert elements into the set. Note that the insertion +order is not affected when you re-inserted any into it (i.e., the +`Add(T)` method returned `false`). + +The hash table uses [Separate Chaining][separate-chainging] for collision +resolution and has the _capacity_, _size_, and _load factor_. The capacity +represents the number of entries in the hash table. In this implementation, it +is a power of two. The size is the number of elements that the hash table +contains. The set rehashes the hash table when the size divided by the capacity +is close to the load factor. Rehashing makes the capacity of the hash table +double. + +To check whether two sets are equal, with iteration order taken into account, +use the [`Enumerable.SequenceEqual(this IEnumerable, +IEnumerable)`][system.linq.enumerable.sequenceequal] method as follows: + +```cs +public static bool AreEqualAndHaveSameInsertionOrder( + LinkedHashSet s1, LinkedHashSet s2) +{ + return s1.SequenceEqual(s2); +} +``` + +Note that the +[`SetEquals(IEnumerable)`][system.collections.generic.hashset-1.setequals] +and `SetEquals(LinkedHashSet)` methods ignore the iteration order and return +set equality; the [`Equals(object)`][system.object.equals] method returns +reference equality. + +The minimum and maximum capacities are `DefaultInitialCapacity` (`16`) and +`DefaultMaxCapacity` (`0x40000000`), respectively. + +As mentioned, if the number of elements in the set exceeds the product of the +capacity and the load factor, it rehashes its hash table with the capacity +updated to double. However, this implementation restricts the maximum capacity +to `DefaultMaxCapacity` (`0x40000000`). So, once the capacity reaches its +maximum, rehashing will no longer occur. Note that since the implementation +uses Separate Chaining, it is possible to add up to +[`int.MaxValue`][system.int32.maxvalue] (`0x7fffffff`) elements to the set even +after the capacity reaches its maximum unless it throws an +[`OutOfMemoryException`][system.outofmemoryexception]. + +If the number of elements in the set reaches the maximum value +([`int.MaxValue`][system.int32.maxvalue]), any attempt to add elements throws +an [`InsufficientMemoryException`][system.insufficientmemoryexception] (e.g., +with the `Add(T)` method). + +## Benchmarks + +Let's show the performances of `HashSet`, `SimpleLinkedHashSet`, and +`LinkedHashSet` classes. We performed the following measurements on a laptop +with an Intel® Core™ i5-6200U CPU. The +[`SimpleLinkedHashSet`][simple-lhs] is just a reference, a simplified +implementation of the `ISet` interface, which has both +`HashMap>` and `LinkedList` to have predictable +iteration order, unlike `LinkedHashSet`. In most cases, `LinkedHashSet` is +slightly faster than `SimpleLinkedHashSet`, and even in the worst case, it +makes no difference. + +The following chart shows how long it took to add 50,000 elements to each set: + +![Add][chart-add] + +"With rehash" starts each set with a capacity of 16. "Without rehash" starts +each with that of 100,000 to avoid rehashing. Both results show that the +`SimpleLinkedHashSet` and `LinkedHashSet` classes are two or more times slower +than the `HashSet` class concerning the `Add` method because they create a +_node_ instance every time adding an element, but `HashSet` does not. + +The following chart shows how long it took to get the intersection of two sets +(where each of them contains 50,000 elements and the intersection does 25,000 +ones): + +![IntersectWith][chart-intersect-with] + +Concerning the `IntersectWith` methods, `LinkedHashSet` is about even with +`HashSet` since they only remove elements. + +The following chart shows how long it took to get the symmetric difference of +two sets containing 50,000 elements (and the symmetric difference contains +50,000 elements): + +![SymmetricExceptWith][chart-symmetric-except-with] + +Concerning the `SymmetricExceptWith` methods, `LinkedHashSet` is slower than +`HashSet` since they also add elements, unlike the `IntersectWith` methods. + +The following chart shows how long it took to get whether the smaller one of +the two sets is the subset of the bigger one, containing 50,000 and 50,001 +elements, respectively: + +![IsSubsetOf][chart-is-subset-of] + +Concerning the `IsSubsetOf(IEnumerable)` method, `LinkedHashSet` is about +even with `HashSet`. The `IsSubsetOf` method with two `LinkedHashSet`s is +slightly faster than with two `HashSet`s. + +The following chart shows how long it took to get whether the bigger one of the +two sets is the superset of the smaller one, containing 50,000 and 49,999 +elements, respectively: + +![IsSupersetOf][chart-is-superset-of] + +The `IsSupersetOf` methods show similar results as the `IsSubsetOf` methods. + +[separate-chainging]: + https://en.wikipedia.org/wiki/Hash_table#Separate_chaining +[open-addressing]: + https://en.wikipedia.org/wiki/Hash_table#Open_addressing +[system.linq.enumerable.sequenceequal]: + https://docs.microsoft.com/en-us/dotnet/api/system.linq.enumerable.sequenceequal?view=net-6.0 +[system.collections.generic.hashset-1.setequals]: + https://docs.microsoft.com/en-us/dotnet/api/system.collections.generic.hashset-1.setequals?view=net-6.0 +[system.object.equals]: + https://docs.microsoft.com/en-us/dotnet/api/system.object.equals?view=net-6.0#system-object-equals(system-object) +[system.int32.maxvalue]: + https://docs.microsoft.com/en-us/dotnet/api/system.int32.maxvalue?view=net-6.0 +[system.outofmemoryexception]: + https://docs.microsoft.com/en-us/dotnet/api/system.outofmemoryexception?view=net-6.0 +[system.insufficientmemoryexception]: + https://docs.microsoft.com/en-us/dotnet/api/system.insufficientmemoryexception?view=net-6.0 +[simple-lhs]: + ../Collection.Test/Maroontress/Collection/SimpleLinkedHashSet.cs +[chart-add]: + https://docs.google.com/spreadsheets/d/e/2PACX-1vRqsvt4rfe9OtsJxo_umIdgNllEqFjY6g6yeHqFKeY9Oq6eSzSRJ6_hx57AqBbtan_NL_vUk14O-Jyx/pubchart?oid=1349208269&format=image +[chart-intersect-with]: + https://docs.google.com/spreadsheets/d/e/2PACX-1vRqsvt4rfe9OtsJxo_umIdgNllEqFjY6g6yeHqFKeY9Oq6eSzSRJ6_hx57AqBbtan_NL_vUk14O-Jyx/pubchart?oid=780952512&format=image +[chart-symmetric-except-with]: + https://docs.google.com/spreadsheets/d/e/2PACX-1vRqsvt4rfe9OtsJxo_umIdgNllEqFjY6g6yeHqFKeY9Oq6eSzSRJ6_hx57AqBbtan_NL_vUk14O-Jyx/pubchart?oid=2046347697&format=image +[chart-is-subset-of]: + https://docs.google.com/spreadsheets/d/e/2PACX-1vRqsvt4rfe9OtsJxo_umIdgNllEqFjY6g6yeHqFKeY9Oq6eSzSRJ6_hx57AqBbtan_NL_vUk14O-Jyx/pubchart?oid=682726058&format=image +[chart-is-superset-of]: + https://docs.google.com/spreadsheets/d/e/2PACX-1vRqsvt4rfe9OtsJxo_umIdgNllEqFjY6g6yeHqFKeY9Oq6eSzSRJ6_hx57AqBbtan_NL_vUk14O-Jyx/pubchart?oid=1780540214&format=image