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:

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.

main.mbt
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
output
1.2.3-alpha.1+build.5
1.2.3
true
1

A 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.mbt
let 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(" < "))
output
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

For 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
output
-1
1
0

Equality 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
output
false
0
-1

Constrain 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.

main.mbt
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())
}
output
>=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 false

sets() 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.

main.mbt
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())
}
output
npm =1.2.3
cargo >=1.2.3, <2.0.0
ruby =1.2.3
strict =1.2.3
WrittennpmCargoRuby
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.0not 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.

main.mbt
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
output
false
true
false

SameTuple 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.

main.mbt
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())
}
output
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.3

Read a refusal

When input does not parse, Refused says what was wrong and where. message() and at() read it without matching on the variant.

main.mbt
try {
  let _ = @moonsver.Version::parse("01.2.3")
} catch {
  e => {
    println(e.message())
    println(e.at())
  }
}
output
major number has a leading zero: 01
0

The 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.

main.mbt
match @json.to_json(@moonsver.Range::parse("^1.2.3")) {
  String(s) => println(s)
  _ => println("?")
}
output
>=1.2.3, <2.0.0

This 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.

shell
$ 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.