diff --git a/PolyShim.Tests/Net100/CollectionExtensionsTests.cs b/PolyShim.Tests/Net100/CollectionExtensionsTests.cs new file mode 100644 index 00000000..a514313e --- /dev/null +++ b/PolyShim.Tests/Net100/CollectionExtensionsTests.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using FluentAssertions; +using Xunit; + +namespace PolyShim.Tests.Net100; + +public class CollectionExtensionsTests +{ + [Fact] + public void AsReadOnly_ISet_Test() + { + // Arrange + var set = (ISet)new HashSet { 1, 2, 3 }; + + // Act + var readOnly = set.AsReadOnly(); + + // Assert + readOnly.Should().BeOfType>(); + readOnly.Count.Should().Be(3); + readOnly.Contains(1).Should().BeTrue(); + readOnly.Contains(4).Should().BeFalse(); + readOnly.IsSubsetOf(new[] { 1, 2, 3 }).Should().BeTrue(); + readOnly.IsSupersetOf(new[] { 1 }).Should().BeTrue(); + } + + [Fact] + public void AsReadOnly_ISet_ReflectsChanges_Test() + { + // Arrange + var inner = new HashSet { 1, 2, 3 }; + var set = (ISet)inner; + + // Act + var readOnly = set.AsReadOnly(); + inner.Add(4); + + // Assert + readOnly.Count.Should().Be(4); + readOnly.Contains(4).Should().BeTrue(); + } +} diff --git a/PolyShim.Tests/Net70/CollectionExtensionsTests.cs b/PolyShim.Tests/Net70/CollectionExtensionsTests.cs new file mode 100644 index 00000000..28073652 --- /dev/null +++ b/PolyShim.Tests/Net70/CollectionExtensionsTests.cs @@ -0,0 +1,54 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using FluentAssertions; +using Xunit; + +namespace PolyShim.Tests.Net70; + +public class CollectionExtensionsTests +{ + [Fact] + public void AsReadOnly_IList_Test() + { + // Arrange + var list = (IList)new List { "a", "b", "c" }; + + // Act + var readOnly = list.AsReadOnly(); + + // Assert + readOnly.Should().BeOfType>(); + readOnly.Should().Equal("a", "b", "c"); + } + + [Fact] + public void AsReadOnly_IList_ReflectsChanges_Test() + { + // Arrange + var inner = new List { "a", "b" }; + var list = (IList)inner; + + // Act + var readOnly = list.AsReadOnly(); + inner.Add("c"); + + // Assert + readOnly.Should().Equal("a", "b", "c"); + } + + [Fact] + public void AsReadOnly_IDictionary_Test() + { + // Arrange + var dictionary = + (IDictionary)new Dictionary { ["one"] = 1, ["two"] = 2 }; + + // Act + var readOnly = dictionary.AsReadOnly(); + + // Assert + readOnly.Should().BeOfType>(); + readOnly["one"].Should().Be(1); + readOnly["two"].Should().Be(2); + } +} diff --git a/PolyShim.Tests/Net80/CollectionExtensionsTests.cs b/PolyShim.Tests/Net80/CollectionExtensionsTests.cs new file mode 100644 index 00000000..418efa53 --- /dev/null +++ b/PolyShim.Tests/Net80/CollectionExtensionsTests.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using FluentAssertions; +using Xunit; + +namespace PolyShim.Tests.Net80; + +public class CollectionExtensionsTests +{ + [Fact] + public void AddRange_Test() + { + // Arrange + var list = new List { 1, 2, 3 }; + var items = (ReadOnlySpan)new[] { 4, 5, 6 }; + + // Act + list.AddRange(items); + + // Assert + list.Should().Equal(1, 2, 3, 4, 5, 6); + } + + [Fact] + public void AddRange_Empty_Test() + { + // Arrange + var list = new List { 1, 2, 3 }; + var items = (ReadOnlySpan)[]; + + // Act + list.AddRange(items); + + // Assert + list.Should().Equal(1, 2, 3); + } + + [Fact] + public void InsertRange_Test() + { + // Arrange + var list = new List { 1, 2, 6 }; + var items = (ReadOnlySpan)new[] { 3, 4, 5 }; + + // Act + list.InsertRange(2, items); + + // Assert + list.Should().Equal(1, 2, 3, 4, 5, 6); + } + + [Fact] + public void CopyTo_Test() + { + // Arrange + var list = new List { 1, 2, 3 }; + var destination = new int[5]; + + // Act + list.CopyTo(destination.AsSpan()); + + // Assert + destination[0].Should().Be(1); + destination[1].Should().Be(2); + destination[2].Should().Be(3); + } + + [Fact] + public void CopyTo_TooSmall_Test() + { + // Arrange + var list = new List { 1, 2, 3 }; + var destination = new int[2]; + + // Act + var act = () => list.CopyTo(destination.AsSpan()); + + // Assert + act.Should().Throw(); + } +} diff --git a/PolyShim.Tests/NetCore20/CollectionExtensionsTests.cs b/PolyShim.Tests/NetCore20/CollectionExtensionsTests.cs index 8be5b39b..a68c4770 100644 --- a/PolyShim.Tests/NetCore20/CollectionExtensionsTests.cs +++ b/PolyShim.Tests/NetCore20/CollectionExtensionsTests.cs @@ -21,4 +21,69 @@ public void GetValueOrDefault_Test() dictionary.GetValueOrDefault("b").Should().Be("B"); dictionary.GetValueOrDefault("d").Should().BeNull(); } + + [Fact] + public void TryAdd_IDictionary_Test() + { + // Arrange + var dictionary = + (IDictionary)new Dictionary { ["apple"] = 1, ["banana"] = 2 }; + + // Act + var result = dictionary.TryAdd("cherry", 3); + + // Assert + result.Should().BeTrue(); + dictionary.Should().ContainKey("cherry"); + dictionary["cherry"].Should().Be(3); + dictionary.Should().HaveCount(3); + } + + [Fact] + public void TryAdd_IDictionary_Exists_Test() + { + // Arrange + var dictionary = + (IDictionary)new Dictionary { ["apple"] = 1, ["banana"] = 2 }; + + // Act + var result = dictionary.TryAdd("apple", 99); + + // Assert + result.Should().BeFalse(); + dictionary["apple"].Should().Be(1); + dictionary.Should().HaveCount(2); + } + + [Fact] + public void Remove_IDictionary_Test() + { + // Arrange + var dictionary = + (IDictionary)new Dictionary { ["apple"] = 1, ["banana"] = 2 }; + + // Act + var result = dictionary.Remove("apple", out var value); + + // Assert + result.Should().BeTrue(); + value.Should().Be(1); + dictionary.Should().NotContainKey("apple"); + dictionary.Should().HaveCount(1); + } + + [Fact] + public void Remove_IDictionary_NonExistent_Test() + { + // Arrange + var dictionary = (IDictionary)new Dictionary { ["apple"] = 1 }; + + // Act + var result = dictionary.Remove("cherry", out var value); + + // Assert + result.Should().BeFalse(); + value.Should().Be(default); + dictionary.Should().HaveCount(1); + } } diff --git a/PolyShim/Net100/CollectionExtensions.cs b/PolyShim/Net100/CollectionExtensions.cs new file mode 100644 index 00000000..886dda67 --- /dev/null +++ b/PolyShim/Net100/CollectionExtensions.cs @@ -0,0 +1,23 @@ +#if (NETCOREAPP && !NET10_0_OR_GREATER) || (NETSTANDARD) || (NETFRAMEWORK) +#nullable enable +#pragma warning disable CS0436 + +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; + +#if !POLYSHIM_INCLUDE_COVERAGE +[ExcludeFromCodeCoverage] +#endif +internal static class MemberPolyfills_Net100_CollectionExtensions +{ +#if !NETFRAMEWORK || NET45_OR_GREATER + // ReadOnlySet is not available in .NET Framework below 4.5 + extension(ISet set) + { + // https://learn.microsoft.com/dotnet/api/system.collections.generic.collectionextensions.asreadonly#system-collections-generic-collectionextensions-asreadonly-1(system-collections-generic-iset((-0))) + public ReadOnlySet AsReadOnly() => new(set); + } +#endif +} +#endif diff --git a/PolyShim/Net50/IReadOnlySet.cs b/PolyShim/Net50/IReadOnlySet.cs new file mode 100644 index 00000000..656309de --- /dev/null +++ b/PolyShim/Net50/IReadOnlySet.cs @@ -0,0 +1,27 @@ +#if (NETCOREAPP && !NET5_0_OR_GREATER) || (NETSTANDARD) || (NETFRAMEWORK) +#nullable enable +#pragma warning disable CS0436 + +namespace System.Collections.Generic; + +#if !NETFRAMEWORK || NET45_OR_GREATER +// IReadOnlyCollection (base interface) is not available in .NET Framework below 4.5 +// https://learn.microsoft.com/dotnet/api/system.collections.generic.ireadonlyset-1 +internal interface IReadOnlySet : IReadOnlyCollection +{ + bool Contains(T item); + + bool IsProperSubsetOf(IEnumerable other); + + bool IsProperSupersetOf(IEnumerable other); + + bool IsSubsetOf(IEnumerable other); + + bool IsSupersetOf(IEnumerable other); + + bool Overlaps(IEnumerable other); + + bool SetEquals(IEnumerable other); +} +#endif +#endif diff --git a/PolyShim/Net70/CollectionExtensions.cs b/PolyShim/Net70/CollectionExtensions.cs new file mode 100644 index 00000000..e38c9cd5 --- /dev/null +++ b/PolyShim/Net70/CollectionExtensions.cs @@ -0,0 +1,30 @@ +#if (NETCOREAPP && !NET7_0_OR_GREATER) || (NETFRAMEWORK) || (NETSTANDARD) +#nullable enable +#pragma warning disable CS0436 + +using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; + +namespace System.Collections.Generic; + +#if !POLYSHIM_INCLUDE_COVERAGE +[ExcludeFromCodeCoverage] +#endif +internal static class MemberPolyfills_Net70_CollectionExtensions +{ + extension(IList list) + { + // https://learn.microsoft.com/dotnet/api/system.collections.generic.collectionextensions.asreadonly#system-collections-generic-collectionextensions-asreadonly-1(system-collections-generic-ilist((-0))) + public ReadOnlyCollection AsReadOnly() => new(list); + } + +#if !NETFRAMEWORK || NET45_OR_GREATER + extension(IDictionary dictionary) + where TKey : notnull + { + // https://learn.microsoft.com/dotnet/api/system.collections.generic.collectionextensions.asreadonly#system-collections-generic-collectionextensions-asreadonly-2(system-collections-generic-idictionary((-0-1))) + public ReadOnlyDictionary AsReadOnly() => new(dictionary); + } +#endif +} +#endif diff --git a/PolyShim/Net80/CollectionExtensions.cs b/PolyShim/Net80/CollectionExtensions.cs new file mode 100644 index 00000000..60c06fca --- /dev/null +++ b/PolyShim/Net80/CollectionExtensions.cs @@ -0,0 +1,38 @@ +#if (NETCOREAPP && !NET8_0_OR_GREATER) || (NETFRAMEWORK) || (NETSTANDARD) +#nullable enable +#pragma warning disable CS0436 + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +#if !POLYSHIM_INCLUDE_COVERAGE +[ExcludeFromCodeCoverage] +#endif +internal static class MemberPolyfills_Net80_CollectionExtensions +{ + extension(List list) + { + // https://learn.microsoft.com/dotnet/api/system.collections.generic.collectionextensions.addrange + public void AddRange(ReadOnlySpan source) + { + foreach (var item in source) + list.Add(item); + } + + // https://learn.microsoft.com/dotnet/api/system.collections.generic.collectionextensions.insertrange + public void InsertRange(int index, ReadOnlySpan source) => + list.InsertRange(index, source.ToArray()); + + // https://learn.microsoft.com/dotnet/api/system.collections.generic.collectionextensions.copyto + public void CopyTo(Span destination) + { + if (destination.Length < list.Count) + throw new ArgumentException("Destination is too short.", nameof(destination)); + + for (var i = 0; i < list.Count; i++) + destination[i] = list[i]; + } + } +} +#endif diff --git a/PolyShim/Net90/ReadOnlySet.cs b/PolyShim/Net90/ReadOnlySet.cs new file mode 100644 index 00000000..1caf8c07 --- /dev/null +++ b/PolyShim/Net90/ReadOnlySet.cs @@ -0,0 +1,44 @@ +#if (NETCOREAPP && !NET9_0_OR_GREATER) || (NETSTANDARD) || (NETFRAMEWORK) +#nullable enable +#pragma warning disable CS0436 + +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace System.Collections.ObjectModel; + +#if !NETFRAMEWORK || NET45_OR_GREATER +// IReadOnlySet is not available in .NET Framework below 4.5 +// https://learn.microsoft.com/dotnet/api/system.collections.objectmodel.readonlyset-1 +#if !POLYSHIM_INCLUDE_COVERAGE +[ExcludeFromCodeCoverage] +#endif +internal sealed class ReadOnlySet : IReadOnlySet +{ + private readonly ISet _set; + + public ReadOnlySet(ISet set) => _set = set; + + public int Count => _set.Count; + + public bool Contains(T item) => _set.Contains(item); + + public IEnumerator GetEnumerator() => _set.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_set).GetEnumerator(); + + public bool IsProperSubsetOf(IEnumerable other) => _set.IsProperSubsetOf(other); + + public bool IsProperSupersetOf(IEnumerable other) => _set.IsProperSupersetOf(other); + + public bool IsSubsetOf(IEnumerable other) => _set.IsSubsetOf(other); + + public bool IsSupersetOf(IEnumerable other) => _set.IsSupersetOf(other); + + public bool Overlaps(IEnumerable other) => _set.Overlaps(other); + + public bool SetEquals(IEnumerable other) => _set.SetEquals(other); +} +#endif +#endif diff --git a/PolyShim/NetCore20/CollectionExtensions.cs b/PolyShim/NetCore20/CollectionExtensions.cs index 7999fb11..0031164f 100644 --- a/PolyShim/NetCore20/CollectionExtensions.cs +++ b/PolyShim/NetCore20/CollectionExtensions.cs @@ -25,5 +25,32 @@ IDictionary dictionary // https://learn.microsoft.com/dotnet/api/system.collections.generic.collectionextensions.getvalueordefault#system-collections-generic-collectionextensions-getvalueordefault-2(system-collections-generic-ireadonlydictionary((-0-1))-0) public TValue? GetValueOrDefault(TKey key) => dictionary.GetValueOrDefault(key, default); } + + extension(IDictionary dictionary) + where TKey : notnull + { + // https://learn.microsoft.com/dotnet/api/system.collections.generic.collectionextensions.tryadd + public bool TryAdd(TKey key, TValue value) + { + if (dictionary.ContainsKey(key)) + return false; + + dictionary.Add(key, value); + return true; + } + + // https://learn.microsoft.com/dotnet/api/system.collections.generic.collectionextensions.remove + public bool Remove(TKey key, out TValue value) + { + if (dictionary.TryGetValue(key, out value!)) + { + dictionary.Remove(key); + return true; + } + + value = default!; + return false; + } + } } #endif