Guide
From a string to a range
Start with one version and end with a range that picks the right ones. Every output below is what the package printed when this page was built.
Add the package
The module is moonbitstack/moonsver and it exports as @moonsver. Add it to your package's moon.pkg:
import {
"moonbitstack/moonsver",
}
With the MoonBit package manager, moon add moonbitstack/moonsver writes the same import. Nothing else is needed at runtime.
Parse and print
Version::parse reads a version and returns a value with the three numbers and the two identifier lists. Printing it gives the canonical form back.
let v = @moonsver.Version::parse("1.2.3-alpha.1+build.5")
println(v.to_string()) // 1.2.3-alpha.1+build.5
println(v.core_string()) // 1.2.3
println(v.is_prerelease()) // true
println(v.major()) // 1
1.2.3-alpha.1+build.5
1.2.3
true
1A leading zero is refused rather than repaired: 01.2.3 raises Refused. The same holds for a numeric prerelease identifier such as 1.2.3-01.
Order versions
Precedence follows §11: compare the numbers, then the prerelease identifiers. A version with a prerelease ranks below the same numbers without one.
main.mbtlet chain = ["1.0.0-alpha", "1.0.0-alpha.1", "1.0.0-alpha.beta",
"1.0.0-beta", "1.0.0-beta.2", "1.0.0-beta.11",
"1.0.0-rc.1", "1.0.0"]
let vs : Array[@moonsver.Version] = []
for s in chain { vs.push(@moonsver.Version::parse(s)) }
vs.sort()
let names : Array[String] = []
for v in vs { names.push(v.to_string()) }
println(names.join(" < "))
1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-alpha.beta < 1.0.0-beta < 1.0.0-beta.2 < 1.0.0-beta.11 < 1.0.0-rc.1 < 1.0.0For a single comparison, precedence_cmp returns -1, 0 or 1. Build metadata is ignored, exactly as the spec says.
let a = @moonsver.Version::parse("1.0.0-alpha")
let b = @moonsver.Version::parse("1.0.0")
println(a.precedence_cmp(b)) // -1
println(b.precedence_cmp(a)) // 1
println(a.precedence_cmp(a)) // 0
-1
1
0Equality is stricter than precedence. Two versions that differ only in build metadata are not equal, yet rank alike; compare breaks the tie on the metadata so sorting stays deterministic.
let p = @moonsver.Version::parse("1.0.0+a")
let q = @moonsver.Version::parse("1.0.0+b")
println(p == q) // false
println(p.precedence_cmp(q)) // 0
println(p.compare(q)) // -1
false
0
-1Constrain versions
A range is parsed into comparator sets, then asked whether a version fits. ^1.2.3 is stored as two comparators, not as the original text.
let r = @moonsver.Range::parse("^1.2.3")
println(r.to_string())
for s in ["1.2.2", "1.2.3", "1.9.0", "2.0.0", "2.0.0-rc.1"] {
println(s + " " + r.satisfies(@moonsver.Version::parse(s)).to_string())
}
>=1.2.3, <2.0.0
1.2.2 false
1.2.3 true
1.9.0 true
2.0.0 false
2.0.0-rc.1 falsesets() returns the disjuncts and each set's comparators, so you can walk the structure. max_satisfying, min_satisfying and satisfying answer over a list of versions; the playground on this site is built from the first of them.
Pick a grammar
Range::parse takes a Syntax. The same string can mean different things, because ranges are not in the spec and each ecosystem chose its own rules.
for name in ["npm", "cargo", "ruby", "strict"] {
let syn = match @moonsver.Syntax::from_name(name) {
Some(s) => s
None => @moonsver.Syntax::npm()
}
println(name + " " + @moonsver.Range::parse("1.2.3", syntax=syn).to_string())
}
npm =1.2.3
cargo >=1.2.3, <2.0.0
ruby =1.2.3
strict =1.2.3| Written | npm | Cargo | Ruby |
|---|---|---|---|
1.2.3 | =1.2.3 | >=1.2.3, <2.0.0 | =1.2.3 |
~1.2 | >=1.2.0, <1.3.0 | >=1.2.0, <1.3.0 | ~> 1.2 gives >=1.2.0, <2.0.0 |
1.2.* | >=1.2.0, <1.3.0 | >=1.2.0, <1.3.0 | not supported |
A spelling the grammar does not allow is refused, not guessed at. The strict grammar has no caret, so this call raises this grammar does not support '^':
let _ = @moonsver.Range::parse("^1.2.3 || ^2",
syntax=@moonsver.Syntax::strict())
Prereleases in ranges
By default a range only admits a prerelease through a comparator that shares its major.minor.patch and carries a prerelease itself. satisfies_with overrides that for one call.
let v = @moonsver.Version::parse("1.5.0-beta")
let r = @moonsver.Range::parse(">=1.0.0")
println(r.satisfies(v)) // false — same tuple
println(r.satisfies_with(v, @moonsver.Always)) // true
println(r.satisfies_with(v, @moonsver.Never)) // false
false
true
falseSameTuple is the npm and Cargo convention; Always compares a prerelease like any other version; Never keeps a range to stable releases only.
Bump a version
inc takes a Release. The corners are where most hand-written bumpers drift.
let cases : Array[(@moonsver.Version, @moonsver.Release)] = [
(@moonsver.Version::parse("1.0.0-pre"), @moonsver.Major),
(@moonsver.Version::parse("1.2.0-pre"), @moonsver.Major),
(@moonsver.Version::parse("1.2.3-pre"), @moonsver.Patch),
(@moonsver.Version::parse("1.2.3"), @moonsver.Prerelease),
(@moonsver.Version::parse("1.2.3-alpha.1.2"), @moonsver.Prerelease),
(@moonsver.Version::parse("1.2.3-alpha.beta"), @moonsver.Prerelease),
(@moonsver.Version::parse("1.2.3-rc.1+build"), @moonsver.Stable),
]
for pair in cases {
let (from, rel) = pair
println(from.to_string() + " " + rel.to_string() + " " + from.inc(rel).to_string())
}
1.0.0-pre major 1.0.0
1.2.0-pre major 2.0.0
1.2.3-pre patch 1.2.3
1.2.3 prerelease 1.2.4-0
1.2.3-alpha.1.2 prerelease 1.2.3-alpha.1.3
1.2.3-alpha.beta prerelease 1.2.3-alpha.beta.0
1.2.3-rc.1+build stable 1.2.3Read a refusal
When input does not parse, Refused says what was wrong and where. message() and at() read it without matching on the variant.
try {
let _ = @moonsver.Version::parse("01.2.3")
} catch {
e => {
println(e.message())
println(e.at())
}
}
major number has a leading zero: 01
0The variants are Empty, Unexpected, Truncated, Number, LeadingZero, EmptyIdentifier, Identifier, Unsupported and Range. Each carries the index of the first offending character.
Store it as JSON
Version and Range implement ToJson and FromJson, serialised as strings so nothing is lost.
match @json.to_json(@moonsver.Range::parse("^1.2.3")) {
String(s) => println(s)
_ => println("?")
}
>=1.2.3, <2.0.0This is the form to put in a config file or an API response; @json.from_json reads it back through the same parser.
From the command line
The bin package wraps the same functions for shell use.
$ moon run bin -- range "^0.2.3"
>=0.2.3, <0.3.0
$ moon run bin -- satisfies 1.9.0 "^1.2.3"
true
$ moon run bin -- sort 2.0.0 1.0.0-beta 1.0.0 0.9.9
0.9.9
1.0.0-beta
1.0.0
2.0.0
A command exits 0 when it ran, even if the answer is false or no version fits; read stdout for the answer. Bad arguments and parse failures exit non-zero with the detail on stderr.
For every type, method and value, see the API reference. To try inputs yourself, open the playground.