Projects/Build a Semantic Version Parser and Range Matcher

Build a Semantic Version Parser and Range Matcher

Every dependency you install runs through code like this. Each lesson is one concrete spec with exact values pinned to the SemVer 2.0.0 spec and the node-semver range grammar: parse 1.2.3-beta.1+build.5 into its parts, reject a leading zero, order the full prerelease chain from 1.0.0-alpha up to 1.0.0, desugar ~1.2.3 and ^0.2.3 and 1.x into comparator pairs, and select the max-satisfying version from a list. No pointers, no I/O, no dependencies - just a version string in and a precise answer out.

24 lessonsSmall~20 min / lessonSemantic versioningVersion rangesPrecedence rules
The project

What you'll build over the next 24 lessons

Over 24 lessons you build a complete Semantic Versioning toolkit - the same job that npm, Cargo, and Go modules do every time you install a package - as a small, dependency-free library. It parses a version string into its parts, orders any two versions by precedence, parses a range expression, and tells you whether a version satisfies a range and which version from a list is the best match. Every lesson is one concrete spec with exact values anchored to the SemVer 2.0.0 specification and the node-semver range grammar, so the library you write behaves identically in any language.

You start by parsing a version - the numeric core, the optional prerelease, and the build metadata - and rejecting the malformed ones (a leading zero, a missing part, a bad identifier). Then you build precedence: comparing the numeric core, ranking a prerelease below its release, and walking prerelease identifiers left to right to reproduce the exact chain the spec spells out, ending in a sort. From there you parse ranges - comparators, comparator sets combined with AND, alternatives joined by OR, and the sugar that expands into them: hyphen ranges, tilde, caret with its zero-component special cases, and x-ranges. The capstone answers a resolver's real questions: does a version satisfy a range, and what is the max-satisfying pick from a list.

This is a teaching-grade but genuinely faithful implementation of SemVer 2.0.0 precedence and the common npm range operators in their full MAJOR.MINOR.PATCH spellings. It is a library you import, not a program you run against a registry: there is no network, no lockfile, and no dependency-graph resolution - it answers one version-versus-range question at a time, which is exactly the primitive those larger systems are built on.

build-a-semver-parser / lesson-01.md
Lesson 01 · Parsing a version

The version value and its numeric core

A semantic version is not a string, it is a structured value - a major, minor, and patch number plus optional prerelease and build parts. Today you define that value and parse the numeric core out of a clean version string.

The goal

Parse a well-formed "MAJOR.MINOR.PATCH" string into a Version with its three numbers filled in.

Start here - the target
TO DO
Scenario: The numeric core parses into three numbers
Giventhe version string "1.2.3"
Whenit is parsed with Parse
Thenthe result has Major 1, Minor 2, Patch 3, and empty Prerelease and Build
AndParse("10.20.30") gives Major 10, Minor 20, Patch 30
Background

Semantic Versioning gives a release a three-number name: MAJOR.MINOR.PATCH, like 1.2.3. Major changes when you break compatibility, minor when you add features, patch when you fix bugs. Everything this library does - ordering versions, matching ranges - starts by pulling those three numbers out of a string and holding them as a real value instead of text.

Define the Version value with all five of its fields today even though only the first three get filled in: the optional Prerelease and Build parts arrive in the next few lessons, and giving the value its final shape now means later lessons only add parsing, never reshape the type. Keep today honest and small - assume the input is a clean MAJOR.MINOR.PATCH with no prerelease, no build, and no malformed parts. Rejecting bad input is tomorrow’s job.

Make it work
// Front-load the WHOLE shape now; the last two fields stay empty for a few lessons.
type Version struct {
Major, Minor, Patch int
Prerelease []string // dot-separated identifiers, empty when absent
Build []string // dot-separated identifiers, empty when absent
}
// Split on ".", convert each of the three parts to an int. Assume a clean input today.
func Parse(s string) (Version, error) { /* ... */ }
CheckpointDONE
You can turn a clean version string into a Version with its core numbers. Commit and stop here.
Scope & extensions

Where this project stops - and where to go next

The core parser, precedence engine, and range matcher are complete and faithful to SemVer 2.0.0 and the common node-semver operators (comparators, hyphen, tilde, caret including the 0.x cases, x-ranges, AND/OR, the prerelease-satisfaction rule, and max-satisfying selection), but it targets the full MAJOR.MINOR.PATCH spellings: partial tilde/caret forms (~1.2, ^1), the != operator, x in comparator position, and a leading v prefix are unsupported, and ParseRange silently drops an unparseable token rather than erroring (the finalize CLI validates instead).

Extend it next
  • Add a ParseRangeStrict that returns an error from the library itself instead of leaving validation to the CLI, so an unparseable token is rejected rather than silently dropped
  • Support the partial tilde and caret forms node-semver accepts - ~1.2, ~1, ^1.2, ^1 - which widen the bound to the missing component
  • Add the != operator and allow x, X, or * in comparator position (>=1.x), completing the comparator grammar
  • Accept and normalize a leading v prefix (v1.2.3) the way Go modules and many tags write versions
  • Build the layer above a single range: resolving a whole dependency graph, reading a lockfile, or diffing two version sets
  • Add a coerce mode that repairs loose inputs (1.2 to 1.2.0, trailing junk trimmed) for the real-world strings registries actually contain
Recommended reading

Books & references that go deeper

  • Semantic Versioning 2.0.0 · Tom Preston-Werner

    The specification this whole project implements. Eleven numbered clauses define the version grammar, the leading-zero and identifier rules, and - in clause 11 - the exact precedence algorithm and the worked prerelease chain the precedence chapter reproduces value for value.

  • The reference implementation of npm ranges. Its README is the grammar for comparators, hyphen ranges, tilde, caret (including the 0.x and 0.0.x special cases), x-ranges, and the prerelease-satisfaction rule that the ranges and satisfaction chapters follow.

  • The npm docs orientation on how versions and ranges are used in practice - why caret and tilde exist and what a range like ^1.0.4 actually admits. The gentle companion to the node-semver grammar.

  • How Go modules define and compare semantic versions, including its stricter take on prerelease and build metadata. A useful second data point on precedence and how a real tool applies it.

  • Cargo default-caret requirements and comparison operators, a slightly different range dialect built on the same version precedence - good for seeing which choices in this project are SemVer core versus one ecosystem convention.