Package

semver

Semantic Versioning 2.0.0: parse, compare, sort and bump versions, and match Cargo and npm version ranges

0.1.0 MIT

Install

sspur add semver@^0.1.0

or in sspur.toml:

semver = { version = "^0.1.0" }

README

semver

Semantic Versioning 2.0.0 for SSPUR: parse versions with pre-release and build metadata, compare them by SemVer precedence, sort, bump and diff them, and parse and match version ranges in both Cargo and npm (node-semver) syntax. Everything is pure: no effects, and every parse returns a Res with a typed reason instead of raising.

The tests include the version lists of semver.org and the range tables of node-semver and of Rust's semver crate, written out as literals.

Example

use semver.{parse, parse_npm, parse_cargo, format_version, format_range, max_satisfying, sort_versions, bump_pre}

fn show(r: Res[semver.Version, semver.VersionErr]) -> Str
= match r
  | ok(v) => format_version(v)
  | err(e) => semver.describe_error(e)

fn main() -> Unit ! log
= do
  vs = ["1.2.3", "1.10.0", "1.2.10-rc.1", "2.0.0", "1.9.0"].map(s => parse(s).or(semver.version(0, 0, 0)))
  log(sort_versions(vs).map(format_version).join(" < "))
  match parse_npm("^1.2 || >=3.0.0-rc.1")
  | ok(r) => log("npm: {format_range(r)}, best {max_satisfying(vs, r).map(format_version).or("none")}")
  | err(e) => log(e.reason)
  match parse_cargo(">=1.2.3, <1.10")
  | ok(r) => log("cargo: {format_range(r)}, best {max_satisfying(vs, r).map(format_version).or("none")}")
  | err(e) => log(e.reason)
  log(show(parse("1.2.3").map(v => bump_pre(v, "rc").or(v))))
  log(show(parse("1.02.3")))

Output (the same natively and with --interp):

1.2.3 < 1.2.10-rc.1 < 1.9.0 < 1.10.0 < 2.0.0
npm: >=1.2.0 <2.0.0-0 || >=3.0.0-rc.1, best 1.10.0
cargo: >=1.2.3 <1.10.0-0, best 1.9.0
1.2.4-rc.0
'02' has a leading zero

API

Types

  • Version: {major, minor, patch, prerelease: List[Ident], build: List[Str]}; the numbers are refined to >= 0.
  • Ident: a pre-release identifier, NumId{n} or StrId{s}.
  • VersionErr: why a version is invalid: EmptyVersion, WrongPartCount{found}, BadNumber{part}, LeadingZero{part}, NumberTooLarge{part}, EmptyIdentifier{section}, BadIdentifier{ident}.
  • Part: Major | Minor | Patch | Prerelease | Build, the result of diff.
  • Range, Comparator, Op: a range is a list of comparator sets (OR of ANDs); Op is OpEq | OpGt | OpGe | OpLt | OpLe.
  • RangeErr: {text, reason}, the comparator that failed and why.

Parsing and printing versions

  • version(major, minor, patch): a release version.
  • parse(s): strict SemVer 2.0.0; no spaces, no v, no leading zeros in numbers or numeric pre-release identifiers, numbers must fit an Int.
  • parse_loose(s): trims spaces and drops leading =, v and V, then parses strictly (" =v1.2.3 ").
  • coerce(s): the first version-like run of digits, as node-semver's coerce ("v1.2" is 1.2.0, "1.2.3.4" is 1.2.3, "x 7 y" is 7.0.0); none if there is none.
  • is_valid(s): whether parse succeeds.
  • format_version(v): the canonical text.
  • describe_error(e): a readable message for a VersionErr.

Comparing and sorting

  • compare(a, b): SemVer precedence (-1, 0, 1); build metadata is ignored.
  • compare_total(a, b): precedence, then build metadata, so it is consistent with ==. pub impl Ord for Version uses it, so <, <=, >, >= work on versions.
  • precedence_eq(a, b): same precedence.
  • sort_versions(vs), sort_versions_desc(vs): stable sorts in SemVer order. The builtin sort stays structural and does not follow SemVer, so use these.
  • highest(vs), lowest(vs): the extremes, none for an empty list.
  • diff(a, b): the highest component that differs.
  • is_prerelease(v), is_stable(v) (a release with major 1 or more), is_compatible(base, candidate) (Cargo's caret rule).

Changing versions

  • bump_major(v), bump_minor(v), bump_patch(v): as npm's inc: a pre-release of the target version becomes that release (2.0.0-rc.1 to 2.0.0).
  • bump_pre(v, id): the next pre-release (1.2.3 to 1.2.4-rc.0 to 1.2.4-rc.1; another id restarts at id.0; "" uses bare numbers).
  • bump_premajor(v, id), bump_preminor(v, id), bump_prepatch(v, id): the next version as its first pre-release.
  • release(v), strip_build(v), with_prerelease(v, text), with_build(v, text).

Ranges

  • parse_cargo(s): Cargo requirements: ^, ~, =, >, >=, <, <=, bare versions (caret), wildcards *, 1.x, 1.2.* (exact), comma-separated ANDs.
  • parse_npm(s): node-semver ranges: ||, hyphen ranges, X-ranges, ~ and ~>, ^ with the 0.x rules, v and = prefixes, space-separated ANDs, "" and * for anything.
  • parse_npm_with(s, include_prerelease): node's includePrerelease reading, where wildcard lower bounds become -0.
  • matches(r, v): with the pre-release rule both ecosystems use: a pre-release matches only a set with a comparator on the same major.minor.patch that has a pre-release.
  • matches_with(r, v, include_prerelease): the same, optionally without the pre-release rule.
  • filter_matching(vs, r), max_satisfying(vs, r), min_satisfying(vs, r).
  • min_version(r): the lowest version that can match, as node-semver's minVersion.
  • intersects(a, b): whether some version (pre-releases included) matches both; exact, since it knows nothing lies between 1.0.0 and 1.0.1-0.
  • any_range(), exact(v), caret(v), tilde(v), range_or(a, b), range_and(a, b): build ranges in code.
  • format_range(r): the canonical comparator form, >=1.2.0 <2.0.0-0 || >=3.0.0-rc.1.

Ranges are desugared to plain comparators the way node-semver does it: derived upper bounds are written <2.0.0-0, so no pre-release of the bound slips in, and Cargo requirements match what the Cargo semver crate matches on its test tables. Exact versions print with their = (=1.2.3), and sets join with || .

Out of scope

  • Loose parsing in node's sense (1.2.3beta, numbers with leading zeros): use coerce or parse_loose.
  • subset, simplifyRange, gtr/ltr/outside from node-semver, and Ruby's or Python's (PEP 440) version schemes.
  • With include_prerelease, a range parsed with parse_npm (not parse_npm_with) keeps its lower bounds as written, so 1.x does not take 1.0.0-rc.
  • The registry's own version type (in tools/registry/registry.ssp of sspur-packages) is private to that tool, has no pre-release identifiers and no ranges, so this package does not build on it.

Install

sspur add semver

MIT licensed.

API

Ident type

pub type Ident = NumId{n: Int} | StrId{s: Str} derive Eq, Show

One pre-release identifier: a number (compared numerically) or an alphanumeric word (compared in ASCII order). Numbers sort before words.

Version type

pub type Version = {major: Int where _ >= 0, minor: Int where _ >= 0, patch: Int where _ >= 0, prerelease: List[Ident], build: List[Str]} derive Eq, Show

A version major.minor.patch[-prerelease][+build]. Build metadata never affects precedence.

VersionErr type

pub type VersionErr = EmptyVersion | WrongPartCount{found: Int} | BadNumber{part: Str} | LeadingZero{part: Str} | NumberTooLarge{part: Str} | EmptyIdentifier{section: Str} | BadIdentifier{ident: Str} derive Eq, Show

Why a text is not a valid version. part is the offending piece of the input.

Part type

pub type Part = Major | Minor | Patch | Prerelease | Build derive Eq, Show

The highest component in which two versions differ.

Op type

pub type Op = OpEq | OpGt | OpGe | OpLt | OpLe derive Eq, Show

A comparison operator of a range comparator.

Comparator type

pub type Comparator = {op: Op, ver: Version} derive Eq, Show

One comparator, such as >=1.2.0.

Range type

pub type Range = {sets: List[List[Comparator]]} derive Eq, Show

A range: a version matches when it matches every comparator of at least one set. An empty set matches every version (pre-releases only with include_prerelease).

RangeErr type

pub type RangeErr = {text: Str, reason: Str} derive Eq, Show

Why a text is not a valid range: text is the offending comparator or the whole input.

version fn

fn version(major: Int where _ >= 0, minor: Int where _ >= 0, patch: Int where _ >= 0) -> Version

The release version major.minor.patch, with no pre-release or build.

parse fn

fn parse(s: Str) -> Res[Version, VersionErr]

Parses a version exactly as SemVer 2.0.0 writes it: three numbers without leading zeros, then optional - pre-release and + build identifiers. No surrounding spaces or v.

parse_loose fn

fn parse_loose(s: Str) -> Res[Version, VersionErr]

Parses like parse after trimming spaces and dropping any leading =, v or V characters (" =v1.2.3 " is 1.2.3). Everything after them must still be a strict version.

is_valid fn

fn is_valid(s: Str) -> Bool

True when s is a strict SemVer 2.0.0 version.

coerce fn

fn coerce(s: Str) -> Opt[Version]

The first version-like run of digits in s, as node-semver's coerce: up to three dot-separated numbers, missing ones are 0, and anything around them (a v, a fourth number, a pre-release) is ignored: "v1.2" is 1.2.0, "1.2.3.4" is 1.2.3, "x 7 y" is 7.0.0. A number too large for Int makes the result none.

describe_error fn

fn describe_error(e: VersionErr) -> Str

A readable message for a version error.

format_version fn

fn format_version(v: Version) -> Str

The canonical text of a version, 1.2.3-rc.1+build.5.

compare fn

fn compare(a: Version, b: Version) -> Int

SemVer precedence (spec item 11): -1, 0 or 1. Numbers compare numerically, a pre-release is lower than its release, identifiers compare left to right, and build metadata is ignored.

compare_total fn

fn compare_total(a: Version, b: Version) -> Int

A total order consistent with ==: precedence first, then build metadata (none first, then identifier by identifier, numeric ones numerically and before alphanumeric ones, as the Cargo semver crate orders them).

precedence_eq fn

fn precedence_eq(a: Version, b: Version) -> Bool

True when the two versions have the same precedence (they may differ in build metadata).

sort_versions fn

fn sort_versions(vs: List[Version]) -> List[Version]

The versions in ascending compare_total order (stable). The builtin sort is structural and does not follow SemVer order, so use this instead.

sort_versions_desc fn

fn sort_versions_desc(vs: List[Version]) -> List[Version]

The versions in descending compare_total order.

highest fn

fn highest(vs: List[Version]) -> Opt[Version]

The highest version by compare_total, none for an empty list.

lowest fn

fn lowest(vs: List[Version]) -> Opt[Version]

The lowest version by compare_total, none for an empty list.

diff fn

fn diff(a: Version, b: Version) -> Opt[Part]

The highest component in which a and b differ, none when they are equal.

is_prerelease fn

fn is_prerelease(v: Version) -> Bool

True when v has pre-release identifiers.

is_stable fn

fn is_stable(v: Version) -> Bool

True when v is a release with major version 1 or more: SemVer's stable public API.

is_compatible fn

fn is_compatible(base: Version, candidate: Version) -> Bool

True when candidate can replace base under Cargo's default (caret) rule: the same leftmost non-zero component and not lower; pre-releases only of the same major.minor.patch.

release fn

fn release(v: Version) -> Version

The release without pre-release or build identifiers.

strip_build fn

fn strip_build(v: Version) -> Version

v without build metadata.

with_prerelease fn

fn with_prerelease(v: Version, text: Str) -> Res[Version, VersionErr]

v with the pre-release identifiers parsed from text ("rc.1"); "" removes them.

with_build fn

fn with_build(v: Version, text: Str) -> Res[Version, VersionErr]

v with the build identifiers parsed from text ("sha.5114f85"); "" removes them.

bump_major fn

fn bump_major(v: Version) -> Version

The next major version, as npm's inc: 1.2.3 becomes 2.0.0, and a pre-release of a major version (2.0.0-rc.1) becomes that release. Build metadata is dropped.

bump_minor fn

fn bump_minor(v: Version) -> Version

The next minor version: 1.2.3 becomes 1.3.0, and 1.3.0-rc.1 becomes 1.3.0.

bump_patch fn

fn bump_patch(v: Version) -> Version

The next patch version: 1.2.3 becomes 1.2.4, and 1.2.4-rc.1 becomes 1.2.4.

bump_pre fn

fn bump_pre(v: Version, id: Str) -> Res[Version, VersionErr]

The next pre-release, as npm's inc("prerelease", id): a release gets the next patch and -id.0 (1.2.3 to 1.2.4-rc.0); a pre-release whose first identifier is id (or any when id is "") gets its last number incremented (1.2.4-rc.0 to 1.2.4-rc.1) or .0 appended; another identifier restarts at -id.0. id must be "" or an alphanumeric identifier that is not a number; otherwise the result is an error.

bump_premajor fn

fn bump_premajor(v: Version, id: Str) -> Res[Version, VersionErr]

The next major version as a first pre-release: 1.2.3 with "rc" becomes 2.0.0-rc.0.

bump_preminor fn

fn bump_preminor(v: Version, id: Str) -> Res[Version, VersionErr]

The next minor version as a first pre-release: 1.2.3 with "rc" becomes 1.3.0-rc.0.

bump_prepatch fn

fn bump_prepatch(v: Version, id: Str) -> Res[Version, VersionErr]

The next patch version as a first pre-release: 1.2.3 with "rc" becomes 1.2.4-rc.0.

any_range fn

fn any_range() -> Range

The range that matches every release (*).

exact fn

fn exact(v: Version) -> Range

The range =v: only versions with the same precedence as v.

caret fn

fn caret(v: Version) -> Range

The caret range ^v: from v up to the next change of its leftmost non-zero component (^1.2.3 is >=1.2.3 <2.0.0-0, ^0.2.3 is >=0.2.3 <0.3.0-0, ^0.0.3 is >=0.0.3 <0.0.4-0).

tilde fn

fn tilde(v: Version) -> Range

The tilde range ~v: from v up to the next minor version (~1.2.3 is >=1.2.3 <1.3.0-0).

range_or fn

fn range_or(a: Range, b: Range) -> Range

A version matches range_or(a, b) when it matches a or b.

range_and fn

fn range_and(a: Range, b: Range) -> Range

A version matches range_and(a, b) when it matches both a and b.

parse_cargo fn

fn parse_cargo(s: Str) -> Res[Range, RangeErr]

Parses a Cargo version requirement: comparators separated by commas, each ^, ~, =, >, >=, <, <= or none (a bare version is a caret requirement, a bare wildcard such as 1.2.* an exact one), with partial versions and *, x or X wildcards. Spaces around operators are allowed. Cargo has no ||, hyphen ranges or v prefixes, so those are errors, as is an empty text.

parse_npm fn

fn parse_npm(s: Str) -> Res[Range, RangeErr]

Parses an npm (node-semver) range: sets separated by ||, each a space-separated list of comparators (>=1.2.7 <1.3.0) or one hyphen range (1.2 - 2.3.4). Comparators take =, <, <=, >, >=, ~ (or ~>), ^ or no operator, partial versions, x, X and * wildcards, and a v or = before the version. A bare version is exact, a partial one an X-range (1.2 is >=1.2.0 <1.3.0-0), and an empty set means any version. Derived upper bounds are written <2.0.0-0, as node-semver does, so pre-releases of the bound never match.

parse_npm_with fn

fn parse_npm_with(s: Str, include_prerelease: Bool) -> Res[Range, RangeErr]

Like parse_npm with node-semver's includePrerelease reading: lower bounds that come from partial versions and wildcards are written with -0 (1.x is >=1.0.0-0 <2.0.0-0), so with matches_with(r, v, true) the pre-releases inside the range match too.

matches fn

fn matches(r: Range, v: Version) -> Bool

True when v matches r. A pre-release matches only a set with a comparator of the same major.minor.patch that has a pre-release itself (so >=1.2.3-beta takes 1.2.3-rc but not 1.2.4-rc), the rule both Cargo and npm follow.

matches_with fn

fn matches_with(r: Range, v: Version, include_prerelease: Bool) -> Bool

Like matches, but include_prerelease drops the pre-release rule, as node-semver's option of that name: a pre-release then matches whenever every comparator holds.

filter_matching fn

fn filter_matching(vs: List[Version], r: Range) -> List[Version]

The versions of vs that match r, in their order.

max_satisfying fn

fn max_satisfying(vs: List[Version], r: Range) -> Opt[Version]

The highest version of vs that matches r.

min_satisfying fn

fn min_satisfying(vs: List[Version], r: Range) -> Opt[Version]

The lowest version of vs that matches r.

intersects fn

fn intersects(a: Range, b: Range) -> Bool

True when some version, counting pre-releases (the include_prerelease reading), matches both a and b. Exact: it knows that nothing lies between 1.0.0 and 1.0.1-0.

min_version fn

fn min_version(r: Range) -> Opt[Version]

The lowest version that can match r under include_prerelease, as node-semver's minVersion: 0.0.0 or 0.0.0-0 when they match, else the lowest of the sets' tightest lower bounds (>1.2.3 gives 1.2.4, >1.2.3-rc gives 1.2.3-rc.0) when it matches; none when no such version matches.

format_range fn

fn format_range(r: Range) -> Str

The canonical text of a range, in node-semver's form: comparators joined by spaces, sets by || , an empty set as * (parse_npm("^1.2") gives >=1.2.0 <2.0.0-0).

Versions

VersionPublishedCommitSourceEffects
0.1.0 2026-10-1650e4772cb1snapshot