Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions .github/workflows/build-test-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,13 @@ jobs:
- name: Build Debug
run: dotnet build -c Debug R4Utils
- name: Create Name
run: echo "SUFFIX=dev.$(tr /- .. <<<$GITHUB_REF).${GITHUB_RUN_ID}" >> $GITHUB_ENV
run: |
export RAW_SUFFIX="dev.$(tr /- .. <<<$GITHUB_REF)"
echo "SUFFIX=${RAW_SUFFIX:0:44}.${GITHUB_RUN_ID}" >> $GITHUB_ENV
- name: Pack
run: dotnet pack --no-build --no-dependencies -c Debug -p:PackageId=R4UtilsDev --version-suffix $SUFFIX
- name: Push
run: dotnet nuget push R4Utils/bin/Debug/R4UtilsDev.1.2.0-$SUFFIX.nupkg --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_API_KEY }} --skip-duplicate
run: dotnet nuget push R4Utils/bin/Debug/R4UtilsDev.1.2.1-$SUFFIX.nupkg --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_API_KEY }} --skip-duplicate

publish-pre:
runs-on: ubuntu-latest
Expand All @@ -73,7 +75,7 @@ jobs:
- name: Pack
run: dotnet pack -c Release --no-build --no-dependencies --version-suffix $SUFFIX
- name: Push
run: dotnet nuget push R4Utils/bin/Release/R4Utils.1.2.0-$SUFFIX.nupkg --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_API_KEY }} --skip-duplicate
run: dotnet nuget push R4Utils/bin/Release/R4Utils.1.2.1-$SUFFIX.nupkg --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_API_KEY }} --skip-duplicate

publish-production:
runs-on: ubuntu-latest
Expand All @@ -95,4 +97,4 @@ jobs:
- name: Pack
run: dotnet pack -c Release --no-build --no-dependencies
- name: Push
run: dotnet nuget push R4Utils/bin/Release/R4Utils.1.2.0.nupkg --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_API_KEY }} --skip-duplicate
run: dotnet nuget push R4Utils/bin/Release/R4Utils.1.2.1.nupkg --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_API_KEY }} --skip-duplicate
2 changes: 1 addition & 1 deletion R4Utils/R4Utils.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<PackageId>R4Utils</PackageId>
<VersionPrefix>1.2.0</VersionPrefix>
<VersionPrefix>1.2.1</VersionPrefix>
<Authors>Ronto4</Authors>
<LangVersion>13</LangVersion>
<RepositoryUrl>https://github.com/Ronto4/R4Utils</RepositoryUrl>
Expand Down
21 changes: 21 additions & 0 deletions R4Utils/ValueEqualityCollections/Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,25 @@ public static ValueEqualityCollection<T> AsOrderedValueEqualityCollection<T>(thi
/// </summary>
public static ValueEqualityCollection<T> AsValueEqualityCollection<T>(this ICollection<T> collection)
where T : IEquatable<T> => new(collection, ValueEqualityCollection<T>.OrderMode.Ignore);

/// <summary>
/// Transform this collection into a collection that performs value equality on its items for deducing collection equality.
/// <br/><br/>
/// The resulting will consider ordering whenever compared to another instance also considering ordering.
/// </summary>
public static ValueEqualityCollection<T, TCollection>
AsGenericOrderedValueEqualityCollection<T, TCollection>(this TCollection collection)
where T : IEquatable<T> where TCollection : IList<T> =>
new(collection, ValueEqualityCollection<T, TCollection>.OrderMode.Consider);

/// <summary>
/// Transform this collection into a collection that performs value equality on its items for deducing collection equality.
/// <br/><br/>
/// As the underlying <paramref name="collection"/> does not support ordering (does not inherit <see cref="IList{T}"/>),
/// the resulting <see cref="ValueEqualityCollection{T}"/> will ignore ordering whenever it is compared.
/// </summary>
public static ValueEqualityCollection<T, TCollection>
AsGenericValueEqualityCollection<T, TCollection>(this TCollection collection)
where T : IEquatable<T> where TCollection : ICollection<T> =>
new(collection, ValueEqualityCollection<T, TCollection>.OrderMode.Ignore);
}
165 changes: 134 additions & 31 deletions R4Utils/ValueEqualityCollections/ValueEqualityCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,37 @@

namespace R4Utils.ValueEqualityCollections;

file static class Helpers
{
// Source: https://stackoverflow.com/a/3670089/13849454
public static bool ScrambledEquals<T>(ICollection<T> collection1, ICollection<T> collection2) where T : notnull
{
var cnt = new Dictionary<T, int>();
// ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator
foreach (var s in collection1)
{
if (!cnt.TryAdd(s, 1))
{
cnt[s]++;
}
}

foreach (var s in collection2)
{
if (cnt.TryGetValue(s, out var value))
{
cnt[s] = --value;
}
else
{
return false;
}
}

return cnt.Values.All(c => c == 0);
}
}

/// <summary>
/// A wrapper around an <see cref="ICollection{T}"/> that uses value equality of its elements for equality of the collections.
/// </summary>
Expand Down Expand Up @@ -73,40 +104,11 @@ private bool EqualityDispatch(ValueEqualityCollection<T> other)
return OrderedEquality(list1, list2);
}

return ScrambledEquals(Collection, other.Collection);
return Helpers.ScrambledEquals(Collection, other.Collection);
}

private static bool OrderedEquality(IList<T> list1, IList<T> list2) => list1.SequenceEqual(list2);

// Source: https://stackoverflow.com/a/3670089/13849454
private static bool ScrambledEquals(ICollection<T> collection1, ICollection<T> collection2)
{
var cnt = new Dictionary<T, int>();
// ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator
foreach (var s in collection1)
{
if (!cnt.TryAdd(s, 1))
{
cnt[s]++;
}
}

foreach (var s in collection2)
{
if (cnt.TryGetValue(s, out var value))
{
cnt[s] = --value;
}
else
{
return false;
}
}

return cnt.Values.All(c => c == 0);
}


// ICollection<T> implementation

public IEnumerator<T> GetEnumerator() => Collection.GetEnumerator();
Expand All @@ -119,7 +121,7 @@ public override bool Equals(object? obj)
}

// TODO: Look into this, this might in fact be wrong.
public override int GetHashCode() => Collection.Aggregate(0, HashCode.Combine);
public override int GetHashCode() => HashCode.Combine(Collection.Aggregate(0, HashCode.Combine), Ordering);

public void Add(T item) => Collection.Add(item);

Expand All @@ -135,3 +137,104 @@ public override bool Equals(object? obj)

public bool IsReadOnly => Collection.IsReadOnly;
}

/// <summary>
/// A wrapper around a <typeparamref name="TCollection"/> that uses value equality of its elements for equality of the collections.
/// </summary>
public class ValueEqualityCollection<T, TCollection> : ICollection<T>,
IEquatable<ValueEqualityCollection<T, TCollection>>,
IEqualityOperators<ValueEqualityCollection<T, TCollection>, ValueEqualityCollection<T, TCollection>, bool>
where T : IEquatable<T> where TCollection : ICollection<T>
{
public override bool Equals(object? obj)
{
if (obj is null) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != GetType()) return false;
return Equals((ValueEqualityCollection<T, TCollection>)obj);
}

// TODO: Look into this, this might in fact be wrong.
public override int GetHashCode() => HashCode.Combine(Underlying.Aggregate(0, HashCode.Combine), Ordering);

/// <summary>
/// Defines strategies of dealing with ordering when comparing two instances.
/// </summary>
public enum OrderMode
{
/// <summary>
/// Consider ordering when comparing this instance with another one.
/// </summary>
Consider,

/// <summary>
/// Ignore ordering when comparing this instance with another one.
/// <br/><br/>
/// If any of the compared instances has this set, the ordering will be ignored.
/// </summary>
Ignore,
}

/// <summary>
/// The collection used to create this instance.
/// </summary>
public TCollection Underlying { get; }

/// <summary>
/// Whether to consider ordering when comparing this instance with another one.
/// <br/><br/>
/// This is always <see cref="OrderMode.Ignore"/> unless <typeparamref name="TCollection"/> inherits <see cref="IList{T}"/>.
/// </summary>
public OrderMode Ordering { get; }

internal ValueEqualityCollection(TCollection underlying, OrderMode ordering)
{
Underlying = underlying;
Ordering = underlying is IList<T> ? ordering : OrderMode.Ignore;
}

// Equality
public bool Equals(ValueEqualityCollection<T, TCollection>? other)
{
if (other is null) return false;
return ReferenceEquals(this, other) || EqualityDispatch(other);
}

private bool EqualityDispatch(ValueEqualityCollection<T, TCollection> other)
{
if (Ordering is OrderMode.Consider && other.Ordering is OrderMode.Consider && Underlying is IList<T> list1 &&
other.Underlying is IList<T> list2)
{
return OrderedEquality(list1, list2);
}

return Helpers.ScrambledEquals(Underlying, other.Underlying);
}

private static bool OrderedEquality(IList<T> list1, IList<T> list2) => list1.SequenceEqual(list2);

public static bool operator ==(ValueEqualityCollection<T, TCollection>? left,
ValueEqualityCollection<T, TCollection>? right) => right?.Equals(left) ?? left is null;

public static bool operator !=(ValueEqualityCollection<T, TCollection>? left,
ValueEqualityCollection<T, TCollection>? right) => !(left == right);

// Collection
public IEnumerator<T> GetEnumerator() => Underlying.GetEnumerator();

IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)Underlying).GetEnumerator();

public void Add(T item) => Underlying.Add(item);

public void Clear() => Underlying.Clear();

public bool Contains(T item) => Underlying.Contains(item);

public void CopyTo(T[] array, int arrayIndex) => Underlying.CopyTo(array, arrayIndex);

public bool Remove(T item) => Underlying.Remove(item);

public int Count => Underlying.Count;

public bool IsReadOnly => Underlying.IsReadOnly;
}
Loading