Semver Checker — Swift source
Parse, compare, and validate Semantic Versioning 2.0.0 strings. Check which of two versions is greater (with full prerelease precedence), test whether a version satisfies an npm-style range (^, ~, comparators, hyphen, ||), and bump major/minor/patch/prerelease. Runs 100% client-side.
This is the Swift implementation — the same logic the interactive tool runs, in a shareable, citable form.
// semver — Semantic Versioning 2.0.0: strict parse, precedence compare (incl. prerelease ordering), npm-style ranges (^ ~ comparators partials * hyphen AND ||) and bumping. Language: Swift (5.9, Foundation only). Port of src/lib/semver.ts — same logic as this dir's python.py; build metadata is parsed but ignored (it never affects precedence).
import Foundation
/// A parsed version: major.minor.patch plus dot-separated prerelease identifiers.
struct SemVer: CustomStringConvertible {
let major: Int, minor: Int, patch: Int
var pre: [String] = []
static let rx = try! NSRegularExpression(pattern: "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)" +
"(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?" +
"(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$")
/// Parse strict "M.m.p[-pre][+build]"; a leading v/V is tolerated. Nil on invalid.
static func parse(_ input: String) -> SemVer? {
var t = input.trimmingCharacters(in: .whitespaces)
if let f = t.first, f == "v" || f == "V" { t.removeFirst() } // tolerate the common "v1.2.3" form
let range = NSRange(t.startIndex..., in: t)
guard let m = rx.firstMatch(in: t, range: range) else { return nil }
func g(_ i: Int) -> String? { // capture group -> substring
guard let r = Range(m.range(at: i), in: t) else { return nil }
return String(t[r]) }
return SemVer(major: Int(g(1)!)!, minor: Int(g(2)!)!, patch: Int(g(3)!)!,
pre: g(4).map { $0.split(separator: ".", omittingEmptySubsequences: false).map(String.init) } ?? [])
}
var description: String { "\(major).\(minor).\(patch)" + (pre.isEmpty ? "" : "-" + pre.joined(separator: ".")) }
}
/// Partial range version: nil field = absent/wildcard (fields cascade down).
struct Partial { let major: Int?, minor: Int?, patch: Int? }
/// One primitive test: op in {">=", ">", "<", "<=", "="} against a full version.
struct Test { let op: String; let v: Partial }
func digitsOnly(_ s: String) -> Bool { !s.isEmpty && s.allSatisfy { $0.isASCII && $0.isNumber } }
func cmp(_ a: Int, _ b: Int) -> Int { (a > b) - (a < b) }
func identCmp(_ x: String, _ y: String) -> Int {
let (xd, yd) = (digitsOnly(x), digitsOnly(y))
if xd && yd { return cmp(Int(x)!, Int(y)!) } // numeric ids compare numerically
if xd { return -1 } // numeric ids rank below alphanumeric
if yd { return 1 }
return x < y ? -1 : x > y ? 1 : 0
}
/// Compare by precedence: -1 a<b, 0 equal, 1 a>b. Build metadata is ignored.
func compare(_ a: SemVer, _ b: SemVer) -> Int {
var c = cmp(a.major, b.major); if c != 0 { return c }
c = cmp(a.minor, b.minor); if c != 0 { return c }
c = cmp(a.patch, b.patch); if c != 0 { return c }
if a.pre.isEmpty || b.pre.isEmpty { return cmp(b.pre.count, a.pre.count) } // release > prerelease
for i in 0..<min(a.pre.count, b.pre.count) { c = identCmp(a.pre[i], b.pre[i]); if c != 0 { return c } }
return cmp(a.pre.count, b.pre.count) // equal shared ids: larger set wins
}
/// Parse a range partial: "1", "1.2", "1.2.3", "1.x", "*" — wildcards cascade down.
func parsePartial(_ s: String) -> Partial? {
var t = s; if let f = t.first, f == "v" || f == "V" { t.removeFirst() }
if t.isEmpty || ["*", "x", "X"].contains(t) { return Partial(major: nil, minor: nil, patch: nil) }
let f = t.split(separator: ".", omittingEmptySubsequences: false).map(String.init)
if f.count > 3 { return nil }
var v: [Int?] = [nil, nil, nil]
for (i, part) in f.enumerated() {
if ["*", "x", "X"].contains(part) { continue } // wildcard: trailing fields drop
guard digitsOnly(part) else { return nil }
v[i] = Int(part)! }
if v[0] == nil { return Partial(major: nil, minor: nil, patch: nil) }
if v[1] == nil { return Partial(major: v[0], minor: nil, patch: nil) }
return Partial(major: v[0], minor: v[1], patch: v[2])
}
/// Desugar one comparator (op + partial) into primitive >=/< tests.
func desugar(_ op: String, _ t: Partial) -> [Test] {
guard let M = t.major else { return [] } // wildcard matches everything
let m = t.minor ?? 0, p = t.patch ?? 0
func T(_ a: Int, _ b: Int, _ c: Int) -> Partial { Partial(major: a, minor: b, patch: c) }
switch op {
case "^": // never break the left-most non-zero part
let hi: Test
if M > 0 || t.minor == nil { hi = Test(op: "<", v: T(M + 1, 0, 0)) }
else if t.minor! > 0 || t.patch == nil { hi = Test(op: "<", v: T(0, t.minor! + 1, 0)) }
else { hi = Test(op: "<", v: T(0, 0, t.patch! + 1)) }
return [Test(op: ">=", v: T(M, m, p)), hi]
case "~":
return [Test(op: ">=", v: T(M, m, p)),
Test(op: "<", v: t.minor == nil ? T(M + 1, 0, 0) : T(M, t.minor! + 1, 0))]
case "bare", "=": // bare "1.2" acts as >=1.2.0 <1.3.0
if t.minor == nil { return [Test(op: ">=", v: T(M, 0, 0)), Test(op: "<", v: T(M + 1, 0, 0))] }
if t.patch == nil { return [Test(op: ">=", v: T(M, t.minor!, 0)), Test(op: "<", v: T(M, t.minor! + 1, 0))] }
return [Test(op: "=", v: t)]
case ">=": return [Test(op: ">=", v: T(M, m, p))]
case ">": // >1 desugars to >=2.0.0 (partial floors up)
if t.minor == nil { return [Test(op: ">=", v: T(M + 1, 0, 0))] }
if t.patch == nil { return [Test(op: ">=", v: T(M, t.minor! + 1, 0))] }
return [Test(op: ">", v: t)]
case "<=":
if t.minor == nil { return [Test(op: "<", v: T(M + 1, 0, 0))] }
if t.patch == nil { return [Test(op: "<", v: T(M, t.minor! + 1, 0))] }
return [Test(op: "<=", v: t)]
default: // "<"
if t.minor == nil { return [Test(op: "<", v: T(M, 0, 0))] }
if t.patch == nil { return [Test(op: "<", v: T(M, t.minor!, 0))] }
return [Test(op: "<", v: t)]
}
}
func check(_ t: Test, _ v: SemVer) -> Bool {
let c = compare(v, SemVer(major: t.v.major ?? 0, minor: t.v.minor ?? 0, patch: t.v.patch ?? 0))
switch t.op { case ">": return c > 0; case "<": return c < 0; case "<=": return c <= 0; case "=": return c == 0; default: return c >= 0 }
}
/// Evaluate one AND-clause (already split from "||"): hyphen ranges, ^ ~ comparators, partials.
func clauseMatches(_ v: SemVer, _ clause: String) -> Bool {
let toks = clause.split(whereSeparator: { $0 == " " || $0 == "\t" }).map(String.init)
var tests: [Test]
if toks.count == 3 && toks[1] == "-" { // hyphen range: >=lo <=hi, partials floor/ceiling
guard let lo = parsePartial(toks[0]), let hi = parsePartial(toks[2]) else { return false }
tests = desugar(">=", lo) + desugar("<=", hi)
} else {
tests = []
for tok in toks {
var op = "bare", r = tok
if let f = r.first, f == "^" || f == "~" { op = String(f); r.removeFirst() }
else if r.hasPrefix(">=") || r.hasPrefix("<=") { op = String(r.prefix(2)); r.removeFirst(2) }
else if let f = r.first, f == ">" || f == "<" || f == "=" { op = String(f); r.removeFirst() }
guard let part = parsePartial(r) else { return false } // invalid comparator -> unsatisfiable
tests += desugar(op, part) }
}
return tests.allSatisfy { check($0, v) } // no tests ("*" clause) -> true
}
/// Does `version` satisfy the npm-style `range`? An invalid version yields false.
func satisfies(_ version: String, _ range: String) -> Bool {
guard let v = SemVer.parse(version) else { return false }
// NB: splitting on "||" — Swift has no String.split(separator: String) pre-5.9's components.
return range.components(separatedBy: "||").contains { clauseMatches(v, $0) }
}
/// Bump by kind; the prerelease bump increments a trailing numeric id (or appends -0 / .1). Invalid input passes through.
func bump(_ input: String, _ kind: String) -> String {
guard let s = SemVer.parse(input) else { return input }
switch kind {
case "major": return "\(s.major + 1).0.0"
case "minor": return "\(s.major).\(s.minor + 1).0"
case "patch": return "\(s.major).\(s.minor).\(s.patch + 1)"
default:
if s.pre.isEmpty { return "\(s.major).\(s.minor).\(s.patch + 1)-0" }
let last = s.pre[s.pre.count - 1]
let head = s.pre.count > 1 ? s.pre.dropLast().joined(separator: ".") + "." : ""
return "\(s.major).\(s.minor).\(s.patch)-\(head)\(digitsOnly(last) ? String(Int(last)! + 1) : last + ".1")"
}
}
let a = SemVer.parse("1.2.3-alpha.1+build.5")!
print("parse 1.2.3-alpha.1+build.5 -> \(a)")
let chain = ["1.0.0-alpha", "1.0.0-alpha.1", "1.0.0-beta", "1.0.0-beta.11", "1.0.0-rc.1", "1.0.0"]
for i in 0..<(chain.count - 1) {
print("compare \(chain[i]) \(compare(SemVer.parse(chain[i])!, SemVer.parse(chain[i + 1])!) < 0 ? "<" : "!") \(chain[i + 1])") }
let vr = [("1.2.3", "^1.2.0"), ("1.9.0", "~1.2.0"), ("0.2.5", "^0.2.3"), ("0.3.0", "^0.2.3"),
("3.1.0", ">=1.0.0 <2.0.0 || >=3.0.0"), ("1.5.0", "1.2.3 - 1.9.0"), ("1.2.3", "*")]
for (v, r) in vr { print(String(format: "satisfies %-7s %-26s -> %@", v, r, satisfies(v, r) ? "true" : "false")) }
print("bump 1.2.3 minor -> \(bump("1.2.3", "minor"))")
print("bump 1.2.3-beta.2 pre -> \(bump("1.2.3-beta.2", "prerelease"))")
Also available in 13 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →