Skip to content

Commit d148d8e

Browse files
Merge pull request #11 from sonos/prerelease_comparisons
Fix comparison issues
2 parents 45e2ec8 + 8544443 commit d148d8e

File tree

2 files changed

+75
-12
lines changed

2 files changed

+75
-12
lines changed

Sources/SemanticVersion/SemanticVersion.swift

Lines changed: 70 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@ public struct SemanticVersion: Codable, Equatable, Hashable {
4343
self.preRelease = preRelease
4444
self.build = build
4545
}
46+
47+
public enum PreReleaseIdentifier: Equatable {
48+
case alphanumeric(String)
49+
case numeric(Int)
50+
}
4651
}
4752

4853

@@ -74,26 +79,81 @@ extension SemanticVersion: Comparable {
7479
if lhs.major != rhs.major { return lhs.major < rhs.major }
7580
if lhs.minor != rhs.minor { return lhs.minor < rhs.minor }
7681
if lhs.patch != rhs.patch { return lhs.patch < rhs.patch }
77-
if lhs.preRelease != rhs.preRelease {
78-
// Ensure stable versions sort after their betas ...
79-
if lhs.isStable { return false }
80-
if rhs.isStable { return true }
81-
// ... otherwise sort by preRelease
82-
return lhs.preRelease < rhs.preRelease
83-
}
84-
// ... and build
85-
return lhs.build < rhs.build
82+
83+
// A stable release takes precedence over a pre-release
84+
if lhs.isStable != rhs.isStable { return rhs.isStable }
85+
86+
// Otherwise compare the pre-releases per section 11.4
87+
// See: https://semver.org/#spec-item-11
88+
return lhs.preReleaseIdentifiers < rhs.preReleaseIdentifiers
89+
90+
// Note that per section 10, buildmetadata MUST not be
91+
// considered when determining precedence
92+
// See: https://semver.org/#spec-item-10
8693
}
8794
}
8895

8996

9097
extension SemanticVersion {
91-
public var isStable: Bool { return preRelease.isEmpty && build.isEmpty }
98+
public var isStable: Bool { return preRelease.isEmpty }
9299
public var isPreRelease: Bool { return !isStable }
93100
public var isMajorRelease: Bool { return isStable && (major > 0 && minor == 0 && patch == 0) }
94101
public var isMinorRelease: Bool { return isStable && (minor > 0 && patch == 0) }
95102
public var isPatchRelease: Bool { return isStable && patch > 0 }
96103
public var isInitialRelease: Bool { return self == .init(0, 0, 0) }
104+
105+
public var preReleaseIdentifiers: [PreReleaseIdentifier] {
106+
return preRelease
107+
.split(separator: ".")
108+
.map { PreReleaseIdentifier(String($0)) }
109+
}
110+
}
111+
112+
extension SemanticVersion.PreReleaseIdentifier {
113+
init(_ rawValue: String) {
114+
if let number = Int(rawValue) {
115+
self = .numeric(number)
116+
} else {
117+
self = .alphanumeric(rawValue)
118+
}
119+
}
120+
}
121+
122+
extension SemanticVersion.PreReleaseIdentifier: Comparable {
123+
public static func < (lhs: Self, rhs: Self) -> Bool {
124+
// These rules are laid out in section 11.4 of the semver spec
125+
// See: https://semver.org/#spec-item-11
126+
switch (lhs, rhs) {
127+
case (.numeric, .alphanumeric):
128+
// 11.4.3 - Numeric identifiers always have lower precedence than non-numeric identifiers
129+
return true
130+
case (.alphanumeric, .numeric):
131+
// 11.4.3 - Numeric identifiers always have lower precedence than non-numeric identifiers
132+
return false
133+
case (.numeric(let lhInt), .numeric(let rhInt)):
134+
// 11.4.1 - Identifiers consisting of only digits are compared numerically
135+
return lhInt < rhInt
136+
case (.alphanumeric(let lhString), .alphanumeric(let rhString)):
137+
// 11.4.2 - Identifiers with letters or hyphens are compared lexically in ASCII sort order
138+
return lhString < rhString
139+
}
140+
}
141+
}
142+
143+
144+
extension Array: Comparable where Element == SemanticVersion.PreReleaseIdentifier {
145+
public static func < (lhs: Self, rhs: Self) -> Bool {
146+
// Per section 11.4 of the semver spec, compare left to right until a
147+
// difference is found.
148+
// See: https://semver.org/#spec-item-11
149+
for (lhIdentifier, rhIdentifier) in zip(lhs, rhs) {
150+
if lhIdentifier != rhIdentifier { return lhIdentifier < rhIdentifier }
151+
}
152+
153+
// 11.4.4 - A larger set of identifiers will have a higher precendence
154+
// than a smaller set, if all the preceding identifiers are equal.
155+
return lhs.count < rhs.count
156+
}
97157
}
98158

99159
#if swift(>=5.5)

Tests/SemanticVersionTests/SemanticVersionTests.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ final class SemanticVersionTests: XCTestCase {
124124
XCTAssert(SemanticVersion(1, 0, 0) < SemanticVersion(1, 1, 0))
125125
XCTAssert(SemanticVersion(1, 0, 0) < SemanticVersion(1, 0, 1))
126126
XCTAssert(SemanticVersion(1, 0, 0, "a") < SemanticVersion(1, 0, 0, "b"))
127-
XCTAssert(SemanticVersion(1, 0, 0, "a", "a") < SemanticVersion(1, 0, 0, "a", "b"))
127+
XCTAssertFalse(SemanticVersion(1, 0, 0, "a", "a") < SemanticVersion(1, 0, 0, "a", "b"))
128128

129129
// ensure betas come before releases
130130
XCTAssert(SemanticVersion(1, 0, 0, "b1") < SemanticVersion(1, 0, 0))
@@ -133,14 +133,17 @@ final class SemanticVersionTests: XCTestCase {
133133
XCTAssert(SemanticVersion(1, 0, 0) < SemanticVersion(1, 0, 1, "b1"))
134134
// once the patch bumps up to the beta level again, it sorts higher
135135
XCTAssert(SemanticVersion(1, 0, 1) > SemanticVersion(1, 0, 1, "b1"))
136+
137+
// Ensure a release with build metadata sorts above a pre-release
138+
XCTAssert(SemanticVersion(1, 0, 1, "alpha") < SemanticVersion(1, 0, 1, "", "build.14"))
136139
}
137140

138141
func test_isStable() throws {
139142
XCTAssert(SemanticVersion(1, 0, 0).isStable)
140143
XCTAssert(SemanticVersion(1, 0, 0, "").isStable)
141144
XCTAssert(SemanticVersion(1, 0, 0, "", "").isStable)
142145
XCTAssertFalse(SemanticVersion(1, 0, 0, "a").isStable)
143-
XCTAssertFalse(SemanticVersion(1, 0, 0, "", "a").isStable)
146+
XCTAssertTrue(SemanticVersion(1, 0, 0, "", "a").isStable)
144147
}
145148

146149
func test_isMajorRelease() throws {

0 commit comments

Comments
 (0)