# Copyright (C) Dominik Picheta. All rights reserved. # BSD License. Look at license.txt for more info. ## Module for handling versions and version ranges such as ``>= 1.0 & <= 1.5`` import strutils, tables, hashes, parseutils type TVersion* = distinct string TVersionRangeEnum* = enum verLater, # > V verEarlier, # < V verEqLater, # >= V -- Equal or later verEqEarlier, # <= V -- Equal or earlier verIntersect, # > V & < V verEq, # V verAny # * PVersionRange* = ref TVersionRange TVersionRange* = object case kind*: TVersionRangeEnum of verLater, verEarlier, verEqLater, verEqEarlier, verEq: ver*: TVersion of verIntersect: verILeft, verIRight: PVersionRange of verAny: nil EParseVersion* = object of EInvalidValue proc newVersion*(ver: string): TVersion = return TVersion(ver) proc `$`*(ver: TVersion): String {.borrow.} proc hash*(ver: TVersion): THash {.borrow.} proc `<`*(ver: TVersion, ver2: TVersion): Bool = var sVer = string(ver).split('.') var sVer2 = string(ver2).split('.') for i in 0..max(sVer.len, sVer2.len)-1: var sVerI = 0 if i < sVer.len: discard parseInt(sVer[i], sVerI) var sVerI2 = 0 if i < sVer2.len: discard parseInt(sVer2[i], sVerI2) if sVerI < sVerI2: return True elif sVerI == sVerI2: nil else: return False proc `==`*(ver: TVersion, ver2: TVersion): Bool = var sVer = string(ver).split('.') var sVer2 = string(ver2).split('.') for i in 0..max(sVer.len, sVer2.len)-1: var sVerI = 0 if i < sVer.len: discard parseInt(sVer[i], sVerI) var sVerI2 = 0 if i < sVer2.len: discard parseInt(sVer2[i], sVerI2) if sVerI == sVerI2: result = true else: return False proc `<=`*(ver: TVersion, ver2: TVersion): Bool = return (ver == ver2) or (ver < ver2) proc withinRange*(ver: TVersion, ran: PVersionRange): Bool = case ran.kind of verLater: return ver > ran.ver of verEarlier: return ver < ran.ver of verEqLater: return ver >= ran.ver of verEqEarlier: return ver <= ran.ver of verEq: return ver == ran.ver of verIntersect: return withinRange(ver, ran.verILeft) and withinRange(ver, ran.verIRight) of verAny: return True proc contains*(ran: PVersionRange, ver: TVersion): bool = return withinRange(ver, ran) proc makeRange*(version: string, op: string): PVersionRange = new(result) if version == "": raise newException(EParseVersion, "A version needs to accompany the operator.") case op of ">": result.kind = verLater of "<": result.kind = verEarlier of ">=": result.kind = verEqLater of "<=": result.kind = verEqEarlier of "": result.kind = verEq else: raise newException(EParseVersion, "Invalid operator: " & op) result.ver = TVersion(version) proc parseVersionRange*(s: string): PVersionRange = # >= 1.5 & <= 1.8 new(result) var i = 0 var op = "" var version = "" while True: case s[i] of '>', '<', '=': op.add(s[i]) of '&': result.kind = verIntersect result.verILeft = makeRange(version, op) # Parse everything after & # Recursion <3 result.verIRight = parseVersionRange(substr(s, i + 1)) # Disallow more than one verIntersect. It's pointless and could lead to # major unpredictable mistakes. if result.verIRight.kind == verIntersect: raise newException(EParseVersion, "Having more than one `&` in a version range is pointless") break of '0'..'9', '.': version.add(s[i]) of '\0': result = makeRange(version, op) break of ' ': # Make sure '0.9 8.03' is not allowed. if version != "" and i < s.len: if s[i+1] in {'0'..'9', '.'}: raise newException(EParseVersion, "Whitespace is not allowed in a version literal.") else: raise newException(EParseVersion, "Unexpected char in version range: " & s[i]) inc(i) proc `$`*(verRange: PVersionRange): String = case verRange.kind of verLater: result = "> " of verEarlier: result = "< " of verEqLater: result = ">= " of verEqEarlier: result = "<= " of verEq: result = "" of verIntersect: return $verRange.verILeft & " & " & $verRange.verIRight of verAny: return "any version" result.add(string(verRange.ver)) proc newVRAny*(): PVersionRange = new(result) result.kind = verAny proc newVREarlier*(ver: String): PVersionRange = new(result) result.kind = verEarlier result.ver = newVersion(ver) proc newVREq*(ver: string): PVersionRange = new(result) result.kind = verEq result.ver = newVersion(ver) proc findLatest*(verRange: PVersionRange, versions: TTable[TVersion, string]): tuple[ver: TVersion, tag: string] = result = (newVersion(""), "") for ver, tag in versions: if not withinRange(ver, verRange): continue if ver > result.ver: result = (ver, tag) when isMainModule: doAssert(newVersion("1.0") < newVersion("1.4")) doAssert(newVersion("1.0.1") > newVersion("1.0")) doAssert(newVersion("1.0.6") <= newVersion("1.0.6")) #doAssert(not withinRange(newVersion("0.1.0"), parseVersionRange("> 0.1"))) doAssert(not (newVersion("0.1.0") < newVersion("0.1"))) doAssert(not (newVersion("0.1.0") > newVersion("0.1"))) doAssert(newVersion("0.1.0") < newVersion("0.1.0.0.1")) doAssert(newVersion("0.1.0") <= newVersion("0.1")) var inter1 = parseVersionRange(">= 1.0 & <= 1.5") var inter2 = parseVersionRange("1.0") doAssert(inter2.kind == verEq) #echo(parseVersionRange(">= 0.8 0.9")) doAssert(not withinRange(newVersion("1.5.1"), inter1)) doAssert(withinRange(newVersion("1.0.2.3.4.5.6.7.8.9.10.11.12"), inter1)) doAssert(newVersion("1") == newVersion("1")) doAssert(newVersion("1.0.2.4.6.1.2.123") == newVersion("1.0.2.4.6.1.2.123")) doAssert(newVersion("1.0.2") != newVersion("1.0.2.4.6.1.2.123")) doAssert(not (newVersion("") < newVersion("0.0.0"))) doAssert(newVersion("") < newVersion("1.0.0")) doAssert(newVersion("") < newVersion("0.1.0")) var versions = toTable[TVersion, string]({newVersion("0.1.1"): "v0.1.1", newVersion("0.2.3"): "v0.2.3", newVersion("0.5"): "v0.5"}) doAssert findLatest(parseVersionRange(">= 0.1 & <= 0.4"), versions) == (newVersion("0.2.3"), "v0.2.3") doAssert newVersion("0.1-rc1") < newVersion("0.2") doAssert newVersion("0.1-rc1") < newVersion("0.1") echo("Everything works!")