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 }