diff --git a/CHANGELOG.md b/CHANGELOG.md index 5db6072..8837c0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,9 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ### Changed - `SemanticVersion` `==`/`Equals`/`GetHashCode` now ignore build metadata, matching precedence comparison rules. +- `SemanticVersionRange` equality (`==`, `Equals`, `GetHashCode`) is now structural and based + on the constraint sets, so ranges that parse to the same comparators compare as equal + (previously it was reference-sensitive on the backing array). ### Fixed diff --git a/src/semver.tests/SemanticVersionRangeTests.cs b/src/semver.tests/SemanticVersionRangeTests.cs index ea392e1..ec11063 100644 --- a/src/semver.tests/SemanticVersionRangeTests.cs +++ b/src/semver.tests/SemanticVersionRangeTests.cs @@ -282,4 +282,14 @@ internal sealed class SemanticVersionRangeTests await Assert.That(success).IsFalse(); await Assert.That(charsWritten).IsEqualTo(0); } + + [Test] + public async Task I_can_treat_structurally_equal_ranges_as_equal() + { + var a = SemanticVersionRange.Parse("^1.2.3"); + var b = SemanticVersionRange.Parse("[1.2.3,2.0.0)"); + + await Assert.That(a == b).IsTrue(); + await Assert.That(a.GetHashCode()).IsEqualTo(b.GetHashCode()); + } } diff --git a/src/semver/SemanticVersionRange.Formatting.cs b/src/semver/SemanticVersionRange.Formatting.cs index 8b1c570..d35d44e 100644 --- a/src/semver/SemanticVersionRange.Formatting.cs +++ b/src/semver/SemanticVersionRange.Formatting.cs @@ -5,7 +5,7 @@ using System.Runtime.CompilerServices; namespace Geekeey.SemVer; -public readonly partial record struct SemanticVersionRange : ISpanFormattable +public readonly partial struct SemanticVersionRange : ISpanFormattable { /// public override string ToString() diff --git a/src/semver/SemanticVersionRange.JsonConverter.cs b/src/semver/SemanticVersionRange.JsonConverter.cs index ff95e73..d94d305 100644 --- a/src/semver/SemanticVersionRange.JsonConverter.cs +++ b/src/semver/SemanticVersionRange.JsonConverter.cs @@ -7,7 +7,7 @@ using System.Text.Json.Serialization; namespace Geekeey.SemVer; [JsonConverter(typeof(SemanticVersionRangeJsonConverter))] -public readonly partial record struct SemanticVersionRange +public readonly partial struct SemanticVersionRange { internal sealed class SemanticVersionRangeJsonConverter : JsonConverter { diff --git a/src/semver/SemanticVersionRange.Parsing.cs b/src/semver/SemanticVersionRange.Parsing.cs index 098f1f4..8234747 100644 --- a/src/semver/SemanticVersionRange.Parsing.cs +++ b/src/semver/SemanticVersionRange.Parsing.cs @@ -5,7 +5,7 @@ using System.Diagnostics.CodeAnalysis; namespace Geekeey.SemVer; -public readonly partial record struct SemanticVersionRange : ISpanParsable +public readonly partial struct SemanticVersionRange : ISpanParsable { #region IParsable diff --git a/src/semver/SemanticVersionRange.cs b/src/semver/SemanticVersionRange.cs index fa1ee37..3a8432b 100644 --- a/src/semver/SemanticVersionRange.cs +++ b/src/semver/SemanticVersionRange.cs @@ -1,13 +1,15 @@ // Copyright (c) The Geekeey Authors // SPDX-License-Identifier: EUPL-1.2 +using System; + namespace Geekeey.SemVer; /// /// Represents a semantic version range, which is a set of version constraints /// used to match specific semantic versions based on defined ranges or patterns. /// -public readonly partial record struct SemanticVersionRange +public readonly partial struct SemanticVersionRange : IEquatable { // OR of AND-groups. null == empty range (matches nothing). private readonly ConstraintSet[]? _sets; @@ -31,6 +33,86 @@ public readonly partial record struct SemanticVersionRange return _sets.Any(set => set.Includes(version)); } + + /// + public bool Equals(SemanticVersionRange other) + { + if (_sets is null || other._sets is null) + { + return _sets is null && other._sets is null; + } + + if (_sets.Length != other._sets.Length) + { + return false; + } + + for (var i = 0; i < _sets.Length; i++) + { + if (!Equals(_sets[i], other._sets[i])) + { + return false; + } + } + + return true; + } + + private static bool Equals(ConstraintSet x, ConstraintSet y) + { + if (x.Constraints.Length != y.Constraints.Length) + { + return false; + } + + for (var i = 0; i < x.Constraints.Length; i++) + { + if (x.Constraints[i].Operation != y.Constraints[i].Operation || + x.Constraints[i].Version != y.Constraints[i].Version) + { + return false; + } + } + + return true; + } + + /// + public override bool Equals(object? obj) + => obj is SemanticVersionRange other && Equals(other); + + /// + public override int GetHashCode() + { + if (_sets is null) + { + return 0; + } + + var hash = new HashCode(); + foreach (var set in _sets) + { + foreach (var constraint in set.Constraints) + { + hash.Add(constraint.Operation); + hash.Add(constraint.Version); + } + } + + return hash.ToHashCode(); + } + + /// + /// Determines whether two semantic version ranges are equal. + /// + public static bool operator ==(SemanticVersionRange left, SemanticVersionRange right) + => left.Equals(right); + + /// + /// Determines whether two semantic version ranges differ. + /// + public static bool operator !=(SemanticVersionRange left, SemanticVersionRange right) + => !left.Equals(right); } internal enum Comparison { Eq, Neq, Lt, Lte, Gt, Gte }