Skip to content

Commit

Permalink
Merge pull request #30 from RougeWare/feature/29-Fuzzy-comparison
Browse files Browse the repository at this point in the history
Added `.matches(_:ignoring:)`
  • Loading branch information
KyLeggiero committed Oct 25, 2021
2 parents 2c0d302 + ab7a8fe commit adcdc08
Show file tree
Hide file tree
Showing 11 changed files with 416 additions and 251 deletions.
1 change: 1 addition & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ let package = Package(
.macOS(.v10_10),
.iOS(.v11),
.tvOS(.v11),
.watchOS(.v4),
],
products: [
// Products define the executables and libraries produced by a package, and make them visible to other packages.
Expand Down
25 changes: 18 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
# Swift Semantic Versioning #
A small library that implements [SemVer 2.0.0](https://semver.org/spec/v2.0.0.html), which is copyrighted to Tom Preston-Werner CC BY 3.0. This is designed to be simple to use and to easily fit into any Swift codebase.
A small library that perfectly implements [SemVer 2.0.0](https://semver.org/spec/v2.0.0.html), which is copyrighted to Tom Preston-Werner CC BY 3.0. This is designed to be simple to use and to easily fit into any Swift codebase.

This is designed to be as easy as possible to use. You may use the extremely-verbose and explicit initializer which labels every parameter, or the one that does the same thing but excludes the labels, or the one that simply takes any valid SemVer string and gently fails to `nil` if that string is invalid. You are encouraged to use whichever one of these suits the needs at the time of use. Some examples are included in the unit tests.

Keep in mind that the pre-release and build extensions can be easily represented as string- and integer-literals. For instance, `SemVer(1,2,0, SemVer.Build(identifiers: ["123", "4"]))` has the same result as `SemVer(1,2,0, "123.4")` and `SemVer(1,2,0, [123,4])`. Again, this is done for ease-of-use. `SemVer` itself would also be expressible by a string literal, but it has too many resrictions so a failable initializer is presented instead.
Keep in mind that the pre-release and build extensions can be easily represented as string- and integer-literals. For instance, `SemVer(1,2,0, build: SemVer.Build(identifiers: ["123", "4"]))` has the same result as `SemVer(1,2,0, build: "123.4")` and `SemVer(1,2,0, build: [123,4])`. Again, this is done for ease-of-use. `SemVer` itself would also be expressible by a string literal, but it has too many resrictions so a failable initializer conforming to `LosslessStringConvertible` is presented instead, just like `Int`.

This also already conforms to `Comparable`, since comparison and precedence are a major part of the spec.
This also already conforms to `Comparable` since comparison and precedence are a major part of the spec, and to `Codable` since the encoding of semantic versions is clear and simple.


# Examples #

## Proven Strong ##

[`500` test assertions](./Tests/SemVerTests) prove that this library behaves precisely as described, conforming completely to the SemVer 2.0.0 spec.



## Examples ##

Let's say you have a release candidate of version 2.0.0 of your app. The following are all equivalent:

```swift
Expand All @@ -34,21 +42,24 @@ a `nil` object:
nil == SemVer("Obviously Bad")
nil == SemVer("1")
nil == SemVer("1.2")
nil == SemVer("-2.0")
nil == SemVer("-2.0.0")
nil == SemVer("2.0-β")
nil == SemVer("2.0-beta_1")
nil == SemVer("1.-2")
nil == SemVer("1.2.-3")
nil == SemVer("1.2.3.4")
nil == SemVer("1.2.3-😱")
```


# License #

## License ##

This is licensed under [BH-1-PS](https://github.com/BlueHuskyStudios/Licenses/blob/master/Licenses/BH-1-PS.txt).



# Requirements #
## Requirements ##

This package requires:
- Swift 5.1 or newer
Expand Down
1 change: 1 addition & 0 deletions Sources/SemVer/Semantic Version + Codable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// SemVer
//
// Created by Ky Leggiero on 2021-10-18.
// Copyright © 2021 Ben "Ky" Leggiero BH-1-PS.
//

import Foundation
Expand Down
127 changes: 127 additions & 0 deletions Sources/SemVer/Semantic Version + Comparable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
//
// Semantic Version + Comparable.swift
// SemVer
//
// Created by Ky Leggiero on 2021-10-25.
// Copyright © 2021 Ben "Ky" Leggiero BH-1-PS.
//

import Foundation



extension SemanticVersion: Comparable {

/// All of the fields of this `SemanticVersion` that should be used for comparison,
/// in order so that the first one takes the highest precedence
public var orderedComparableIdentifiers: [Identifier] {
var orderedComparableFields: [Identifier] = [major, minor, patch]
if let preRelease = preRelease {
orderedComparableFields.append(preRelease)
}
return orderedComparableFields
}


/// Implements Semantic Version precedence
///
/// https://semver.org/spec/v2.0.0.html#spec-item-11
///
/// - Parameters:
/// - lhs: The first semantic version to compare
/// - rhs: The second semantic version to compare
///
/// - Returns: `true` iff the left has lower precedence than the right
public static func <(lhs: SemanticVersion, rhs: SemanticVersion) -> Bool {
if lhs.major < rhs.major
|| (lhs.major == rhs.major && lhs.minor < rhs.minor)
|| (lhs.major == rhs.major && lhs.minor == rhs.minor && lhs.patch < rhs.patch)
{
return true
}

if let lhsPreRelease = lhs.preRelease {
if let rhsPreRelease = rhs.preRelease {
return lhsPreRelease < rhsPreRelease
}
else {
return true
}
}
else {
return false
}
}


/// Determines whether the given two semantic versions are equivalent. Equivalence is implied by the precedence
/// rules laid out in SemVer 2.0.0 paragraph 11: https://semver.org/spec/v2.0.0.html#spec-item-11
///
/// Remember that build metadata does not factor into equality. For example, `"1.2.3+45" == "1.2.3+67"`.
///
/// - Parameters:
/// - lhs: The first version to compare
/// - rhs: The second version to compare
///
/// - Returns: `true` if the two versions are equivalent
public static func ==(lhs: SemanticVersion, rhs: SemanticVersion) -> Bool {
return lhs.major == rhs.major
&& lhs.minor == rhs.minor
&& lhs.patch == rhs.patch
&& isEquivalent(lhs.preRelease, rhs.preRelease, isEquivalentToNil: { $0 == "" })
// According to https://semver.org/spec/v2.0.0.html#spec-item-11, "Build metadata does not figure into precedence"
}


/// Compares this semantic version to the other, ignoring the given set of identifiers
///
/// - Parameters:
/// - other: The other version to compare this one against
/// - ignoring: The identifiers of each version to be ignored in this comparison
///
/// - Returns: `true` iff this and the given versions match when ignoring the specified identifiers
public func matches(_ other: SemVer, ignoring ignoredIdentifiers: IgnorableIdentifiers) -> Bool {
self.removing(ignoredIdentifiers)
== other.removing(ignoredIdentifiers)
}



/// Returns a copy of this version where the specified identifiers are removed or set to `0`
///
/// - Parameter ignoredIdentifiers: The identifiers to remove or set to `0`
/// - Returns: A copy of this version without the specified identifiers
private func removing(_ ignoredIdentifiers: IgnorableIdentifiers) -> Self {
var copy = self

switch ignoredIdentifiers {
case .minorAndPatchAndPreRelease:
copy._minor = 0
fallthrough

case .patchAndPreRelease:
copy._patch = 0
fallthrough

case .preRelease:
copy.preRelease = nil
}

return copy
}



/// An identifier which can be ignored
public enum IgnorableIdentifiers {

/// Ignore the pre-release identifier (treat `1.2.3-Beta` as equal to `1.2.3`)
case preRelease

/// Ignore the patch and pre-release identifiers (treat `1.2.3` as equal to `1.2.99`; treat `1.2.3-Beta` as equal to `1.2.0`)
case patchAndPreRelease

/// Ignore the minor, patch, and pre-release identifiers (treat `1.2.3` as equal to `1.99.42`; treat `1.2.3` as equal to `1.2.99`; treat `1.2.3-Beta` as equal to `1.2.0`)
case minorAndPatchAndPreRelease
}
}
69 changes: 2 additions & 67 deletions Sources/SemVer/Semantic Version.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,10 @@ public struct SemanticVersion {
private var _major: Major

/// Holds the raw, unmanaged value for `minor`
private var _minor: Minor
internal var _minor: Minor

/// Holds the raw, unmanaged value for `patch`
private var _patch: Patch
internal var _patch: Patch


/// The MAJOR version; increment this when you make incompatible API changes.
Expand Down Expand Up @@ -441,71 +441,6 @@ extension SemanticVersion: LosslessStringConvertible {



extension SemanticVersion: Comparable {

/// All of the fields of this `SemanticVersion` that should be used for comparison,
/// in order so that the first one takes the highest precedence
public var orderedComparableIdentifiers: [Identifier] {
var orderedComparableFields: [Identifier] = [major, minor, patch]
if let preRelease = preRelease {
orderedComparableFields.append(preRelease)
}
return orderedComparableFields
}


/// Implements Semantic Version precedence
///
/// https://semver.org/spec/v2.0.0.html#spec-item-11
///
/// - Parameters:
/// - lhs: The first semantic version to compare
/// - rhs: The second semantic version to compare
///
/// - Returns: `true` iff the left has lower precedence than the right
public static func <(lhs: SemanticVersion, rhs: SemanticVersion) -> Bool {
if lhs.major < rhs.major
|| (lhs.major == rhs.major && lhs.minor < rhs.minor)
|| (lhs.major == rhs.major && lhs.minor == rhs.minor && lhs.patch < rhs.patch)
{
return true
}

if let lhsPreRelease = lhs.preRelease {
if let rhsPreRelease = rhs.preRelease {
return lhsPreRelease < rhsPreRelease
}
else {
return true
}
}
else {
return false
}
}


/// Determines whether the given two semantic versions are equivalent. Equivalence is implied by the precedence
/// rules laid out in SemVer 2.0.0 paragraph 11: https://semver.org/spec/v2.0.0.html#spec-item-11
///
/// Remember that build metadata does not factor into equality. For example, `"1.2.3+45" == "1.2.3+67"`.
///
/// - Parameters:
/// - lhs: The first version to compare
/// - rhs: The second version to compare
///
/// - Returns: `true` if the two versions are equivalent
public static func ==(lhs: SemanticVersion, rhs: SemanticVersion) -> Bool {
return lhs.major == rhs.major
&& lhs.minor == rhs.minor
&& lhs.patch == rhs.patch
&& isEquivalent(lhs.preRelease, rhs.preRelease, isEquivalentToNil: { $0 == "" })
// According to https://semver.org/spec/v2.0.0.html#spec-item-11, "Build metadata does not figure into precedence"
}
}



/// The PRE-RELEASE extension; this identifies versions that are available before being declared stable.
///
/// Identifiers MUST comprise only ASCII alphanumerics and hyphen `[0-9A-Za-z-]`.
Expand Down
4 changes: 3 additions & 1 deletion Tests/LinuxMain.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,6 @@ isTesting = true

XCTMain(SemVerTests.allTests
+ SemVerHashableTests.allTests
+ SemVerMutationTests.allTests)
+ SemVerMutationTests.allTests
+ SemVerCodableTests.allTests
+ SemVerComparableTests.allTests)
10 changes: 9 additions & 1 deletion Tests/SemVerTests/SemVer+Codable Tests.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//
// SemVer+Codable Tests.swift
//
// SemVerTests
//
// Created by Ky Leggiero on 2021-10-18.
//
Expand Down Expand Up @@ -89,6 +89,14 @@ class SemVerCodableTests: SemVerTestClass {
XCTAssertEqual(SemVer(01,2,3, preRelease: ["RC","4"], build: [567])!, try encodeDecode(SemVer(01,2,3, preRelease: ["RC","4"], build: [567])!))
XCTAssertEqual(SemVer("1.2.3-RC.4+567")!, try encodeDecode(SemVer("1.2.3-RC.4+567")!))
}



static let allTests = [
("testEncode", testEncode),
("testDecode", testDecode),
("testEncodeDecode", testEncodeDecode),
]
}


Expand Down

0 comments on commit adcdc08

Please sign in to comment.