Skip to content

Instantly share code, notes, and snippets.

@rjstelling
Created June 4, 2020 09:26
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rjstelling/dfa3e6ac491806a63e359259a9a06dc3 to your computer and use it in GitHub Desktop.
Save rjstelling/dfa3e6ac491806a63e359259a9a06dc3 to your computer and use it in GitHub Desktop.
//
// SemanticVersioning.swift
//
//
// Created by Richard Stelling on 28/05/2020.
//
// The MIT License (MIT)
//
// Copyright (c) 2016-17 Richard Stelling (http://twitter.com/rjstelling)
//
// SemanticVersioning.swift: https://richardstelling.com/Swift-Gists/
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import Foundation
struct Version {
enum Error: Swift.Error {
case parse(String) // Hint
}
// MAJOR version when you make incompatible API changes,
// MINOR version when you add functionality in a backwards
// compatible manner, and
// PATCH version when you make backwards compatible bug fixes.
let major: UInt64, minor: UInt64, patch: UInt64
// The string representation of the the version portion
// of the Semantic Versioning data
var versionString: String { [major, minor, patch].map(String.init).joined(separator: ".") }
// Additional labels for pre-release and build metadata
// are available as extensions to the MAJOR.MINOR.PATCH
// format.
var labelString: String? { identifiers?.joined(separator: ".") }
// Identifiers MUST comprise only ASCII alphanumerics and
// hyphen [0-9A-Za-z-]. Identifiers MUST NOT be empty.
// Numeric identifiers MUST NOT include leading zeroes
let identifiers: [String]?
init(_ versionString: String) throws {
// Split string at the firts hyphen, lhs will be the MAJOR.MINOR.PATCH
// rhs will be the label, comprised of any number of identifiers.
let versionLabel = versionString.split(separator: "-", maxSplits: 1, omittingEmptySubsequences: true)
guard let version = versionLabel.first else { throw Error.parse("Empty version string in: \(versionString)") }
// If we have a lable extract it
if versionLabel.indices.contains(1), let labelComponent = versionLabel.last {
// A pre-release version MAY be denoted by appending a hyphen and
// a series of dot separated identifiers immediately following
// the patch version. Identifiers MUST comprise only ASCII
// alphanumerics and hyphen [0-9A-Za-z-]. Identifiers MUST NOT be
// empty. Numeric identifiers MUST NOT include leading zeroes.
// Pre-release versions have a lower precedence than the
// associated normal version. A pre-release version indicates that
// the version is unstable and might not satisfy the intended
// compatibility requirements as denoted by its associated normal
// version. Examples: 1.0.0-alpha, 1.0.0-alpha.1, 1.0.0-0.3.7,
// 1.0.0-x.7.z.92.
self.identifiers = labelComponent.split(separator: ".").map(String.init)
}
else {
self.identifiers = nil
}
// Continue with version extraction...
// A normal version number MUST take the form X.Y.Z where X, Y,
// and Z are non-negative integers, and MUST NOT contain leading
// zeroes. X is the major version, Y is the minor version, and Z
// is the patch version. Each element MUST increase numerically.
// For instance: 1.9.0 -> 1.10.0 -> 1.11.0.
let versionElements: [UInt64] = try version.split(separator: ".").map(String.init).map {
guard let ele = UInt64($0) else { throw Error.parse("Version element is invalid: \($0)") }
return ele
}
guard 1...3 ~= versionElements.count else { throw Error.parse("Version elements are the wrong size, \(versionElements.count), it shoudl be >=1, <=3") }
self.major = versionElements[0] // this is guaranteed becase we check above
// Default minor value is zero
self.minor = versionElements.indices.contains(1) ? versionElements[1] : 0
// Default patch value is zero
self.patch = versionElements.indices.contains(2) ? versionElements[2] : 0
}
func isCompatible(_ version: Version) -> Bool {
self.major == version.major
}
}
extension Version: CustomStringConvertible {
var description: String { [versionString, labelString].compactMap { $0 }.joined(separator: "-") }
}
extension Version: ExpressibleByStringLiteral {
typealias StringLiteralType = String
init(stringLiteral value: Self.StringLiteralType) {
// We use asserts here because be can't propergate an error
// at runtime or validate a string at compile time.
do {
try self.init(value)
}
catch {
fatalError("String Literal failed to parse: \(value)")
}
}
}
extension Version: Comparable {
static func == (lhs: Version, rhs: Version) -> Bool {
lhs.major == rhs.major &&
lhs.minor == rhs.minor &&
lhs.patch == rhs.patch &&
lhs.identifiers?.isEmpty == rhs.identifiers?.isEmpty
}
static func < (lhs: Version, rhs: Version) -> Bool {
guard lhs != rhs else { return false }
if lhs.major < rhs.major { return true }
else if lhs.major == rhs.major, lhs.minor < rhs.minor { return true }
else if lhs.major == rhs.major, lhs.minor == rhs.minor, lhs.patch < rhs.patch { return true }
else if lhs.major == rhs.major, lhs.minor == rhs.minor, lhs.patch == rhs.patch,
lhs.identifiers != nil, rhs.identifiers == nil { return true }
else { return false }
}
static func <= (lhs: Version, rhs: Version) -> Bool {
lhs == rhs || lhs < rhs
}
static func > (lhs: Version, rhs: Version) -> Bool {
guard lhs != rhs else { return false }
if lhs.major > rhs.major { return true }
else if lhs.major == rhs.major, lhs.minor > rhs.minor { return true }
else if lhs.major == rhs.major, lhs.minor == rhs.minor, lhs.patch > rhs.patch { return true }
else if lhs.major == rhs.major, lhs.minor == rhs.minor, lhs.patch == rhs.patch,
lhs.identifiers == nil, rhs.identifiers != nil { return true }
else { return false }
}
static func >= (lhs: Version, rhs: Version) -> Bool {
lhs == rhs || lhs > rhs
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment