Skip to content

Instantly share code, notes, and snippets.

@bteapot
Last active July 27, 2020 14:51
Show Gist options
  • Save bteapot/b502eb00688d89cd44eab70e9c2cf401 to your computer and use it in GitHub Desktop.
Save bteapot/b502eb00688d89cd44eab70e9c2cf401 to your computer and use it in GitHub Desktop.
//
// SemanticVersion.swift
// library
//
// Created by Денис Либит on 27.07.2020.
//
import Foundation
/// Semantic versioning
///
/// https://semver.org
struct SemanticVersion {
let major: Int
let minor: Int
let patch: Int
let prereleases: [String]
let builds: [String]
init(_ major: Int, _ minor: Int, _ patch: Int, prereleases: [String] = [], builds: [String] = []) {
self.major = major
self.minor = minor
self.patch = patch
self.prereleases = prereleases
self.builds = builds
}
init?(_ value: String) {
let pattern =
#"^(?<major>0|[1-9]\d*)\.(?<minor>0|[1-9]\d*)\.(?<patch>0|[1-9]\d*)(?:-(?<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$"#
guard let regex = try? NSRegularExpression(pattern: pattern, options: []) else {
return nil
}
let nsrange = NSRange(value.startIndex..<value.endIndex, in: value)
var success: Bool = false
var major: Int = 0
var minor: Int = 0
var patch: Int = 0
var prereleases: String = ""
var builds: String = ""
regex.enumerateMatches(in: value, range: nsrange) { (match: NSTextCheckingResult?, flags: NSRegularExpression.MatchingFlags, stop: UnsafeMutablePointer<ObjCBool>) in
guard let match = match else {
return
}
let string = { (name: String) -> String? in
let nsrange = match.range(withName: name)
if nsrange.location != NSNotFound,
let range = Range(nsrange, in: value)
{
return String(value[range])
} else {
return nil
}
}
let int = { (name: String) -> Int? in
if let string = string(name),
let int = Int(string)
{
return int
} else {
return nil
}
}
guard
let _major = int("major"),
let _minor = int("minor"),
let _patch = int("patch")
else {
return
}
success = true
major = _major
minor = _minor
patch = _patch
prereleases = string("prerelease") ?? ""
builds = string("buildmetadata") ?? ""
}
if success == false {
return nil
}
self.major = major
self.minor = minor
self.patch = patch
self.prereleases = prereleases.split(separator: ".").map(String.init)
self.builds = builds.split(separator: ".").map(String.init)
}
var stringValue: String {
var string = "\(self.major).\(self.minor).\(self.patch)"
if self.prereleases.isEmpty == false {
string += "-" + self.prereleases.joined(separator: ".")
}
if self.builds.isEmpty == false {
string += "+" + self.builds.joined(separator: ".")
}
return string
}
}
extension SemanticVersion: Comparable {
static func == (lhs: SemanticVersion, rhs: SemanticVersion) -> Bool {
if lhs.major != rhs.major { return false }
if lhs.minor != rhs.minor { return false }
if lhs.patch != rhs.patch { return false }
if lhs.prereleases.count != rhs.prereleases.count { return false }
if zip(lhs.prereleases, rhs.prereleases).contains(where: { $0 != $1 }) { return false }
if lhs.builds.count != rhs.builds.count { return false }
if zip(lhs.builds, rhs.builds).contains(where: { $0 != $1 }) { return false }
return true
}
static func < (lhs: SemanticVersion, rhs: SemanticVersion) -> Bool {
if lhs.major < rhs.major { return true }
if lhs.minor < rhs.minor { return true }
if lhs.patch < rhs.patch { return true }
let numericCompare = { (lhs: String, rhs: String) -> Bool in
if let lhsInt = Int(lhs), let rhsInt = Int(rhs) {
return lhsInt < rhsInt
} else {
return lhs < rhs
}
}
if zip(lhs.prereleases, rhs.prereleases).contains(where: numericCompare) { return true }
if lhs.prereleases.count != rhs.prereleases.count { return lhs.prereleases.count < rhs.prereleases.count }
if zip(lhs.builds, rhs.builds).contains(where: numericCompare) { return true }
if lhs.builds.count != rhs.builds.count { return lhs.builds.count < rhs.builds.count }
return false
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment