Skip to content

Commit 98fef9f

Browse files
authored
Merge pull request #8 from pepelev/set-equality
Add SetEquality
2 parents be84f06 + 8ece06b commit 98fef9f

15 files changed

Lines changed: 351 additions & 46 deletions

.github/workflows/build-test-publish-nuget.yml

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,12 @@ jobs:
1616

1717
steps:
1818
- name: Checkout
19-
uses: actions/checkout@v2
19+
uses: actions/checkout@v4
2020

21-
- name: Setup .NET Core
22-
uses: actions/setup-dotnet@v1
21+
- name: Setup .NET 8
22+
uses: actions/setup-dotnet@v4
2323
with:
24-
dotnet-version: 5.0.103
24+
dotnet-version: 8.0.*
2525

2626
- name: Install dependencies
2727
run: dotnet restore
@@ -30,16 +30,30 @@ jobs:
3030
run: dotnet build --configuration Release --no-restore
3131

3232
- name: Test
33-
run: dotnet test --no-restore --verbosity normal
33+
run: dotnet test --no-restore --verbosity normal --framework net8
3434

3535
- name: Publish on version change
36-
id: publish_nuget
37-
uses: rohith/publish-nuget@v2
38-
with:
39-
PROJECT_FILE_PATH: ./src/Comparation/Comparation.csproj
40-
VERSION_REGEX: ^\s*<Version>(.*)<\/Version>\s*$
41-
TAG_COMMIT: true
42-
TAG_FORMAT: v*
43-
NUGET_KEY: ${{secrets.NUGETKEY}}
44-
NUGET_SOURCE: https://api.nuget.org
45-
INCLUDE_SYMBOLS: true
36+
shell: bash
37+
env:
38+
NUGETKEY: ${{ secrets.NUGETKEY }}
39+
run: |
40+
function publish_project() {
41+
ProjectName="$1"
42+
Version=$(grep --only-matching --perl-regex '<Version>[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z-]+)?</Version>' "$ProjectName/$ProjectName.csproj" | grep --only-matching --perl-regex '[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z-]+)?')
43+
echo "Version=$Version"
44+
if test -z "$Version"; then
45+
echo "Could not get version from $ProjectName.csproj"
46+
exit 1
47+
fi
48+
git fetch --tags
49+
if test -n "$(git tag --list "$ProjectName-$Version" | tr -d '[:space:]')"; then
50+
echo "Version $Version already exists for $ProjectName"
51+
else
52+
rm -rf nuget
53+
dotnet pack "$ProjectName" --configuration Release --no-build --include-symbols --nologo --output nuget &&\
54+
dotnet nuget push nuget/*.nupkg --source 'https://api.nuget.org/v3/index.json' --api-key "$NUGETKEY" --skip-duplicate &&\
55+
git tag "$ProjectName-$Version" && git push origin "$ProjectName-$Version" || { echo "Error occurred"; exit 1; }
56+
fi
57+
}
58+
59+
publish_project Comparation

.github/workflows/build-test.yml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,12 @@ jobs:
1616

1717
steps:
1818
- name: Checkout
19-
uses: actions/checkout@v2
19+
uses: actions/checkout@v4
2020

21-
- name: Setup .NET Core
22-
uses: actions/setup-dotnet@v1
21+
- name: Setup .NET 8
22+
uses: actions/setup-dotnet@v4
2323
with:
24-
dotnet-version: 5.0.103
24+
dotnet-version: 8.0.*
2525

2626
- name: Install dependencies
2727
run: dotnet restore
@@ -30,4 +30,4 @@ jobs:
3030
run: dotnet build --configuration Release --no-restore
3131

3232
- name: Test
33-
run: dotnet test --no-restore --verbosity normal
33+
run: dotnet test --no-restore --verbosity normal --framework net8

src/Comparation.Tests/Comparation.Tests.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
4-
<TargetFrameworks>net48;net5.0</TargetFrameworks>
4+
<TargetFrameworks>net48;net8</TargetFrameworks>
55
<OutputPath>..\..\bin\Comparation.Tests</OutputPath>
66
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
77
<LangVersion>9</LangVersion>
@@ -13,7 +13,7 @@
1313
<Nullable>annotations</Nullable>
1414
</PropertyGroup>
1515

16-
<PropertyGroup Condition="$(TargetFramework) == 'net5.0'">
16+
<PropertyGroup Condition="$(TargetFramework) == 'net8'">
1717
<Nullable>enable</Nullable>
1818
</PropertyGroup>
1919

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
using System;
2+
using System.Collections;
3+
using System.Collections.Generic;
4+
using System.Linq;
5+
using Comparation.Tests.Equality.Aspects;
6+
7+
namespace Comparation.Tests.Core
8+
{
9+
public sealed class EqualitySuite<T> : IEnumerable<Named<Test>>
10+
{
11+
private readonly string name;
12+
private readonly IEqualityComparer<T> sut;
13+
private readonly Named<T?>[][] equalGroups;
14+
15+
public EqualitySuite(string name, IEqualityComparer<T> sut, Named<T?>[][] equalGroups)
16+
{
17+
this.name = name;
18+
this.sut = sut;
19+
this.equalGroups = equalGroups;
20+
}
21+
22+
private IEnumerable<Named<Test>> Yield()
23+
{
24+
var allItems = equalGroups.SelectMany(group => group).ToList();
25+
var uniqueNames = allItems.Select(item => item.Name).Distinct().ToList();
26+
if (uniqueNames.Count != allItems.Count)
27+
{
28+
throw new Exception("Duplicate items");
29+
}
30+
31+
return Cases();
32+
33+
IEnumerable<Named<Test>> Cases()
34+
{
35+
for (var i = 0; i < equalGroups.Length; i++)
36+
{
37+
for (var j = 0; j < equalGroups.Length; j++)
38+
{
39+
for (var k = 0; k < equalGroups[i].Length; k++)
40+
{
41+
for (var m = 0; m < equalGroups[j].Length; m++)
42+
{
43+
var a = equalGroups[i][k];
44+
var b = equalGroups[j][m];
45+
var expectation = i == j;
46+
var sign = expectation
47+
? "=="
48+
: "!=";
49+
yield return (
50+
$"{name}: {a.Name} {sign} {b.Name}",
51+
new EqualityShouldWorkAsExpected<T>(
52+
sut,
53+
a.Value,
54+
b.Value,
55+
expectation: expectation
56+
)
57+
);
58+
59+
if (expectation)
60+
{
61+
yield return (
62+
$"{name}: HashCode({a.Name}) {sign} HashCode({b.Name})",
63+
new EqualityShouldGiveSameHashCodeForEqualObjects<T>(
64+
sut,
65+
a.Value,
66+
b.Value
67+
)
68+
);
69+
}
70+
}
71+
}
72+
}
73+
}
74+
}
75+
}
76+
77+
public IEnumerator<Named<Test>> GetEnumerator() => Yield().GetEnumerator();
78+
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
79+
}
80+
}

src/Comparation.Tests/Equality/Aspects/EqualityShouldGiveSameHashCodeForEqualObjects.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ namespace Comparation.Tests.Equality.Aspects
77
public sealed class EqualityShouldGiveSameHashCodeForEqualObjects<T> : Test
88
{
99
private readonly IEqualityComparer<T> equality;
10-
private readonly T a;
11-
private readonly T b;
10+
private readonly T? a;
11+
private readonly T? b;
1212

13-
public EqualityShouldGiveSameHashCodeForEqualObjects(IEqualityComparer<T> equality, T a, T b)
13+
public EqualityShouldGiveSameHashCodeForEqualObjects(IEqualityComparer<T> equality, T? a, T? b)
1414
{
1515
this.equality = equality;
1616
this.a = a;

src/Comparation.Tests/Equality/Cases.cs

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System.Collections;
1+
using System;
2+
using System.Collections;
23
using System.Collections.Generic;
34
using System.Linq;
45
using Comparation.Tests.Core;
@@ -11,6 +12,41 @@ public sealed class Cases : IEnumerable<TestCaseData>
1112
{
1213
private const string Category = "Equality";
1314

15+
private static EqualitySuite<IReadOnlyCollection<string?>> StringSetSuite()
16+
{
17+
// ReSharper disable InconsistentNaming
18+
Named<IReadOnlyCollection<string?>?> nullSet = (nameof(nullSet), null);
19+
Named<IReadOnlyCollection<string?>?> emptySet = (nameof(emptySet), Array.Empty<string>());
20+
Named<IReadOnlyCollection<string?>?> singleNull = (nameof(singleNull), new string?[] { null });
21+
Named<IReadOnlyCollection<string?>?> singleA = (nameof(singleA), new[] { "a" });
22+
Named<IReadOnlyCollection<string?>?> singleBigA = (nameof(singleBigA), new[] { "A" });
23+
Named<IReadOnlyCollection<string?>?> twoAs = (nameof(twoAs), new[] { "a", "A" });
24+
Named<IReadOnlyCollection<string?>?> ab = (nameof(ab), new[] { "a", "b" });
25+
Named<IReadOnlyCollection<string?>?> ba = (nameof(ba), new[] { "b", "a" });
26+
Named<IReadOnlyCollection<string?>?> abc = (nameof(abc), new[] { "a", "b", "c" });
27+
Named<IReadOnlyCollection<string?>?> ab2c = (nameof(ab2c), new[] { "a", "b", "c", "b" });
28+
Named<IReadOnlyCollection<string?>?> abnull = (nameof(abnull), new[] { "a", "b", null });
29+
Named<IReadOnlyCollection<string?>?> nullba = (nameof(nullba), new[] { null, "b", "a" });
30+
// ReSharper restore InconsistentNaming
31+
32+
return new EqualitySuite<IReadOnlyCollection<string?>>(
33+
nameof(SetEquality<string>),
34+
new SetEquality<string>(
35+
Comparation.Equality.Of<string>().By(@string => @string.ToLowerInvariant())
36+
),
37+
new[]
38+
{
39+
new[] { nullSet },
40+
new[] { emptySet },
41+
new[] { singleNull },
42+
new[] { singleA, singleBigA, twoAs },
43+
new[] { ab, ba },
44+
new[] { abc, ab2c },
45+
new[] { abnull, nullba }
46+
}
47+
);
48+
}
49+
1450
public IEnumerator<TestCaseData> GetEnumerator() => Sequence.Concat(
1551
new WorkAsExpected(),
1652
new Examples(),
@@ -19,7 +55,8 @@ public IEnumerator<TestCaseData> GetEnumerator() => Sequence.Concat(
1955
new GiveSameHashCodeForEqualObjects(),
2056
new TreatTreatsNullNotEqualToObject(),
2157
new Transitive(),
22-
new Equal()
58+
new Equal(),
59+
StringSetSuite()
2360
)
2461
.Select(@case => new NamePrefixing<Test>(Category, @case))
2562
.Select(@case => new TestCaseData(@case.Value).SetName(@case.Name).SetCategory(Category))

src/Comparation.Tests/TestsEntryPoint.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
using Comparation.Tests.Core;
22
using NUnit.Framework;
33

4+
[assembly: LevelOfParallelism(8)]
5+
46
namespace Comparation.Tests
57
{
8+
[Parallelizable(ParallelScope.All)]
69
public sealed class TestsEntryPoint
710
{
811
[Test]
@@ -12,5 +15,11 @@ public void Run(Test test)
1215
{
1316
test.Run();
1417
}
18+
19+
[Test]
20+
public void FakeTest()
21+
{
22+
Assert.Pass();
23+
}
1524
}
1625
}

src/Comparation/Box.cs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,20 @@ public Equality(IEqualityComparer<T> equality)
2424
this.equality = equality;
2525
}
2626

27-
public bool Equals(Box<T?> x, Box<T?> y) => (x.value, y.value) switch
28-
{
29-
({ } a, { } b) => equality.Equals(a, b),
30-
(null, null) => true,
31-
_ => false
32-
};
27+
#if NETSTANDARD2_0 || NETSTANDARD2_1
28+
public bool Equals(Box<T?> x, Box<T?> y) => equality.Equals(x.value!, y.value!);
29+
#else
30+
public bool Equals(Box<T?> x, Box<T?> y) => equality.Equals(x.value, y.value);
31+
#endif
3332

3433
public int GetHashCode(Box<T?> obj) => obj.value is { } value
3534
? equality.GetHashCode(value)
3635
: 0;
3736
}
3837
}
38+
39+
internal static class Box
40+
{
41+
public static Box<T> Wrap<T>(T item) => new(item);
42+
}
3943
}

src/Comparation/CollectionEquality.cs

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
using System;
22
using System.Collections.Generic;
3+
#if NET6_0_OR_GREATER
4+
using System.Runtime.CompilerServices;
5+
using System.Runtime.InteropServices;
6+
#endif
37

48
namespace Comparation
59
{
@@ -77,10 +81,14 @@ public int GetHashCode(IReadOnlyCollection<T?> collection)
7781
foreach (var item in x)
7882
{
7983
var box = new Box<T?>(item);
84+
#if NET6_0_OR_GREATER
85+
CollectionsMarshal.GetValueRefOrAddDefault(counts, box, out _)++;
86+
#else
8087
var newCount = counts.TryGetValue(box, out var count)
8188
? count + 1
8289
: 1;
8390
counts[box] = newCount;
91+
#endif
8492
}
8593

8694
return counts;
@@ -91,21 +99,33 @@ private static bool Equals(IEnumerable<T?> y, Dictionary<Box<T?>, int> counts)
9199
foreach (var item in y)
92100
{
93101
var box = new Box<T?>(item);
102+
#if NET6_0_OR_GREATER
103+
ref var count = ref CollectionsMarshal.GetValueRefOrNullRef(counts, box);
104+
if (Unsafe.IsNullRef(ref count))
105+
{
106+
return false;
107+
}
108+
109+
count--;
110+
if (count < 0)
111+
{
112+
return false;
113+
}
114+
#else
94115
if (counts.TryGetValue(box, out var count))
95116
{
96-
if (count == 1)
117+
if (count <= 0)
97118
{
98-
counts.Remove(box);
99-
}
100-
else
101-
{
102-
counts[box] = count - 1;
119+
return false;
103120
}
121+
122+
counts[box] = count - 1;
104123
}
105124
else
106125
{
107126
return false;
108127
}
128+
#endif
109129
}
110130

111131
return true;

src/Comparation/Comparation.csproj

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,19 @@
33
<PropertyGroup>
44
<OutputPath>..\..\bin\Comparation</OutputPath>
55
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
6+
<TargetFrameworks>netstandard2.0;netstandard2.1;net6</TargetFrameworks>
67
<LangVersion>9</LangVersion>
78
<Nullable>enable</Nullable>
89
</PropertyGroup>
910

1011
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
11-
<TargetFrameworks>netstandard2.0;netstandard2.1;net5.0</TargetFrameworks>
1212
<DebugSymbols>true</DebugSymbols>
1313
<DebugType>full</DebugType>
1414
<Optimize>false</Optimize>
1515
<DefineConstants>DEBUG;TRACE</DefineConstants>
1616
</PropertyGroup>
1717

1818
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
19-
<TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks>
2019
<DebugType>pdbonly</DebugType>
2120
<Optimize>true</Optimize>
2221
</PropertyGroup>

0 commit comments

Comments
 (0)