Skip to content

Instantly share code, notes, and snippets.

@cxmeel
Last active May 23, 2023 11:25
Show Gist options
  • Save cxmeel/e6eea231c838cb7c9edbe55306f5a72a to your computer and use it in GitHub Desktop.
Save cxmeel/e6eea231c838cb7c9edbe55306f5a72a to your computer and use it in GitHub Desktop.
Basic semver string library implemented in Luau.
--!strict
--[[
Provides a set of functions for comparing semver strings.
See https://semver.org/ for more information.
Types:
type SemverRecord = {
major: number,
minor: number,
patch: number,
prerelease: string?,
build: string?,
}
Functions:
In all comparison functions, build strings are ignored, as per the
semver spec.
- semver.is(input: any): boolean
Checks if the given input is a valid semver string or record.
- semver.parse(version: any): SemverRecord?
Parses the given string into a SemverRecord. Returns nil if the
input is not a string. Will throw an error if the input is an
invalid semver string.
- semver.gte(first: (string | SemverRecord)?, second: (string | SemverRecord)?): boolean
Returns true if the first version is greater than or equal to the
second version.
- semver.lte(first: (string | SemverRecord)?, second: (string | SemverRecord)?): boolean
Returns true if the first version is less than or equal to the
second version.
- semver.gt(first: (string | SemverRecord)?, second: (string | SemverRecord)?): boolean
Returns true if the first version is greater than the second
version.
- semver.lt(first: (string | SemverRecord)?, second: (string | SemverRecord)?): boolean
Returns true if the first version is less than the second version.
- semver.eq(first: (string | SemverRecord)?, second: (string | SemverRecord)?): boolean
Returns true if the first version is exactly equal to the second version, including prerelease.
]]
local semver = {}
type SemverRecord = {
major: number,
minor: number,
patch: number,
prerelease: string?,
build: string?,
}
local function equal(first: any, second: any): boolean
if first == second then
return true
end
if typeof(first) ~= "table" or typeof(second) ~= "table" then
return false
end
for key, value in first do
if second[key] ~= value then
return false
end
end
for key, value in second do
if first[key] ~= value then
return false
end
end
return true
end
local function isRecord(input: any): boolean
return typeof(input) == "table"
and typeof(input.major) == "number"
and typeof(input.minor) == "number"
and typeof(input.patch) == "number"
end
function semver.is(input: any): boolean
local success, result = select(1, pcall(semver.parse, input))
return success and result ~= nil
end
function semver.parse(version: any): SemverRecord?
if isRecord(version) then
return version :: SemverRecord
end
if typeof(version) ~= "string" then
return
end
local major = tonumber(version:match("^v?(%d+)"))
if major == nil then
error(`Invalid semver string: {version}`)
end
local minor = tonumber(version:match("^v?%d+%.(%d+)")) or 0
local patch = tonumber(version:match("^v?%d+%.%d+%.(%d+)")) or 0
local prerelease = version:match("^v?%d+%.%d+%.%d+-([%w%-%.]+)")
local build = version:match("^v?%d+%.%d+%.%d+%-[%w%-%.]+%+(.+)")
return {
major = major,
minor = minor,
patch = patch,
prerelease = prerelease,
build = build,
}
end
local function prerelease_gte(first: string?, second: string?): boolean
if not second then
return true
end
if not first then
return false
end
local firstParts = first:split(".")
local secondParts = second:split(".")
for index, firstPart in firstParts do
local secondPart = secondParts[index]
if not secondPart then
return true
end
if firstPart ~= secondPart then
return firstPart > secondPart
end
end
return true
end
function semver.gte(first: (string | SemverRecord)?, second: (string | SemverRecord)?): boolean
if not second or equal(first, second) then
return true
end
local firstRecord = semver.parse(first)
local secondRecord = semver.parse(second)
if not firstRecord or not secondRecord then
return false
end
local fa, fb, fc, fd = firstRecord.major, firstRecord.minor, firstRecord.patch, firstRecord.prerelease
local sa, sb, sc, sd = secondRecord.major, secondRecord.minor, secondRecord.patch, secondRecord.prerelease
return (fa > sa)
or (
fa == sa
and (
fb > sb
or (fb == sb and (fc > sc or (fc == sc and (fd and not sd or (fd == sd or prerelease_gte(fd, sd))))))
)
)
end
function semver.lte(first: (string | SemverRecord)?, second: (string | SemverRecord)?): boolean
return semver.gte(second, first)
end
function semver.gt(first: (string | SemverRecord)?, second: (string | SemverRecord)?): boolean
return not semver.lte(first, second)
end
function semver.lt(first: (string | SemverRecord)?, second: (string | SemverRecord)?): boolean
return not semver.gte(first, second)
end
function semver.eq(first: (string | SemverRecord)?, second: (string | SemverRecord)?): boolean
return semver.gte(first, second) and semver.lte(first, second)
end
return semver
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment