Skip to content

Semver Checker — C++ source

Parse, compare, and validate Semantic Versioning 2.0.0 strings. Check which of two versions is greater (with full prerelease precedence), test whether a version satisfies an npm-style range (^, ~, comparators, hyphen, ||), and bump major/minor/patch/prerelease. Runs 100% client-side.

This is the C++ implementation — the same logic the interactive tool runs, in a shareable, citable form.

// semver — Semantic Versioning 2.0.0: strict parse, precedence compare (incl. prerelease ordering), npm-style ranges (^ ~ comparators partials * hyphen AND ||) and bumping. C++ (20) port of src/lib/semver.ts — same logic as this dir's python.py; build metadata is parsed but ignored (it never affects precedence).
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <optional>
#include <string>
#include <vector>

struct Semver {
    long major{}, minor{}, patch{};
    std::vector<std::string> pre;   // dot-separated prerelease identifiers
};
struct Triple { long major{-1}, minor{-1}, patch{-1}; };  // -1 = absent/wildcard
struct Test { std::string op; Triple v; };

static bool digits_only(const std::string& s) { return !s.empty() && std::all_of(s.begin(), s.end(), [](unsigned char c) { return std::isdigit(c); }); }
static int cmp3(long a, long b) { return (a > b) - (a < b); }

// Parse strict "M.m.p[-pre][+build]"; a leading v/V is tolerated. nullopt on invalid.
static std::optional<Semver> parse_semver(const std::string& input) {
    std::string s = input;
    s.erase(0, s.find_first_not_of(" \t"));
    if (!s.empty() && (s[0] == 'v' || s[0] == 'V')) s.erase(0, 1);
    auto num = [](std::string& t) -> std::optional<long> {           // no leading zeros
        size_t i = 0; long n = 0;
        while (i < t.size() && std::isdigit((unsigned char)t[i])) n = n * 10 + (t[i++] - '0');
        if (i == 0 || (t[0] == '0' && i > 1)) return std::nullopt;
        t.erase(0, i); return n; };
    auto ident = [](const std::string& t) { int d = 1;
        for (char c : t) { if (!(std::isalnum((unsigned char)c) || c == '-')) return false; d = d && std::isdigit((unsigned char)c); }
        return !t.empty() && !(d && t.size() > 1 && t[0] == '0'); };
    Semver v;
    auto M = num(s); if (!M || s.empty() || s[0] != '.') return std::nullopt; v.major = *M; s.erase(0, 1);
    auto m = num(s); if (!m || s.empty() || s[0] != '.') return std::nullopt; v.minor = *m; s.erase(0, 1);
    auto p = num(s); if (!p) return std::nullopt; v.patch = *p;
    auto plus = s.find('+'); if (plus != std::string::npos) s.erase(plus);   // build never affects precedence
    if (!s.empty()) {
        if (s[0] != '-' || s.size() < 2) return std::nullopt;
        for (size_t start = 1, dot; start <= s.size() && (start == 1 || dot != std::string::npos);) {
            dot = s.find('.', start); std::string id = s.substr(start, dot == std::string::npos ? dot : dot - start);
            if (!ident(id)) return std::nullopt; v.pre.push_back(id);
            start = dot == std::string::npos ? s.size() + 1 : dot + 1; }
    }
    return v;
}

static int ident_cmp(const std::string& x, const std::string& y) {
    bool xd = digits_only(x), yd = digits_only(y);
    if (xd && yd) return cmp3(std::stol(x), std::stol(y));  // numeric ids compare numerically
    if (xd || yd) return xd ? -1 : 1;                       // numeric ids rank below alphanumeric
    return cmp3(x.compare(y), 0);
}

// Compare by precedence: -1 a<b, 0 equal, 1 a>b. Build metadata is ignored.
static int compare(const Semver& a, const Semver& b) {
    int c = cmp3(a.major, b.major); if (!c) c = cmp3(a.minor, b.minor); if (!c) c = cmp3(a.patch, b.patch);
    if (c) return c;
    if (a.pre.empty() || b.pre.empty()) return cmp3((long)b.pre.size(), (long)a.pre.size());  // release > prerelease
    for (size_t i = 0; i < a.pre.size() && i < b.pre.size(); i++) if ((c = ident_cmp(a.pre[i], b.pre[i]))) return c;
    return cmp3((long)a.pre.size(), (long)b.pre.size());   // equal shared ids: larger set wins
}

// Parse a range partial: "1", "1.2", "1.2.3", "1.x", "*" — wildcards cascade down.
static std::optional<Triple> parse_partial(std::string s) {
    if (!s.empty() && (s[0] == 'v' || s[0] == 'V')) s.erase(0, 1);
    if (s.empty() || s == "*" || s == "x" || s == "X") return Triple{};
    Triple t; std::vector<std::string> f; size_t pos = 0;
    while ((pos = s.find('.')) != std::string::npos) { f.push_back(s.substr(0, pos)); s.erase(0, pos + 1); }
    f.push_back(s);
    if (f.size() > 3) return std::nullopt;
    long v[3] = {-1, -1, -1};
    for (size_t i = 0; i < f.size(); i++) {
        if (f[i] == "*" || f[i] == "x" || f[i] == "X") continue;     // wildcard: rest cascades away
        if (!digits_only(f[i])) return std::nullopt;
        v[i] = std::stol(f[i]); }
    if (v[0] < 0) return Triple{};
    if (v[1] < 0) return Triple{v[0], -1, -1};
    return Triple{v[0], v[1], v[2]};
}

// Desugar one comparator (op + partial) into primitive >=/< tests.
static std::vector<Test> desugar(const std::string& op, const Triple& t) {
    long M = t.major, m = t.minor < 0 ? 0 : t.minor, p = t.patch < 0 ? 0 : t.patch;
    auto T = [&](long a, long b, long c) { return Triple{a, b, c}; };
    if (M < 0) return {};                                  // wildcard matches everything
    if (op == "^") {                                       // never break the left-most non-zero part
        if (M > 0 || t.minor < 0) return {{">=", T(M, m, p)}, {"<", T(M + 1, 0, 0)}};
        if (t.minor > 0 || t.patch < 0) return {{">=", T(M, m, p)}, {"<", T(0, t.minor + 1, 0)}};
        return {{">=", T(M, m, p)}, {"<", T(0, 0, t.patch + 1)}}; }
    if (op == "~") return {{">=", T(M, m, p)}, t.minor < 0 ? Test{"<", T(M + 1, 0, 0)} : Test{"<", T(M, t.minor + 1, 0)}};
    if (op == "bare" || op == "=") {                       // bare "1.2" acts as >=1.2.0 <1.3.0
        if (t.minor < 0) return {{">=", T(M, 0, 0)}, {"<", T(M + 1, 0, 0)}};
        if (t.patch < 0) return {{">=", T(M, t.minor, 0)}, {"<", T(M, t.minor + 1, 0)}};
        return {{"=", t}}; }
    if (op == ">=") return {{">=", T(M, m, p)}};
    if (op == ">") { if (t.minor < 0) return {{">=", T(M + 1, 0, 0)}}; if (t.patch < 0) return {{">=", T(M, t.minor + 1, 0)}}; return {{">", t}}; }
    if (op == "<=") { if (t.minor < 0) return {{"<", T(M + 1, 0, 0)}}; if (t.patch < 0) return {{"<", T(M, t.minor + 1, 0)}}; return {{"<=", t}}; }
    if (t.minor < 0) return {{"<", T(M, 0, 0)}};           // "<"
    if (t.patch < 0) return {{"<", T(M, t.minor, 0)}};
    return {{"<", t}};
}

static bool check(const Test& t, const Semver& v) {
    Semver w; w.major = t.v.major; w.minor = t.v.minor; w.patch = t.v.patch;
    int c = compare(v, w);
    return t.op == ">" ? c > 0 : t.op == "<" ? c < 0 : t.op == "<=" ? c <= 0 : t.op == "=" ? c == 0 : c >= 0;  // ">="
}

// Evaluate one AND-clause (already split from ||): hyphen ranges, ^ ~ comparators, partials.
static bool clause_matches(const Semver& v, std::string clause) {
    std::vector<Test> tests; std::vector<std::string> toks; size_t pos = 0;
    while ((pos = clause.find(' ')) != std::string::npos) { toks.push_back(clause.substr(0, pos)); clause.erase(0, pos + 1); }
    toks.push_back(clause);
    if (toks.size() == 3 && toks[1] == "-") {              // hyphen range: >=lo <=hi, partials floor/ceiling
        auto lo = parse_partial(toks[0]), hi = parse_partial(toks[2]);
        if (!lo || !hi) return false;
        auto a = desugar(">=", *lo), b = desugar("<=", *hi);
        tests.insert(tests.end(), a.begin(), a.end()); tests.insert(tests.end(), b.begin(), b.end());
    } else for (auto& tok : toks) {
        std::string op = "bare";
        if (tok[0] == '^' || tok[0] == '~') { op = tok.substr(0, 1); tok.erase(0, 1); }
        else if (tok.rfind(">=", 0) == 0 || tok.rfind("<=", 0) == 0) { op = tok.substr(0, 2); tok.erase(0, 2); }
        else if (tok[0] == '>' || tok[0] == '<' || tok[0] == '=') { op = tok.substr(0, 1); tok.erase(0, 1); }
        auto t = parse_partial(tok);
        if (!t) return false;
        auto d = desugar(op, *t); tests.insert(tests.end(), d.begin(), d.end()); }
    return std::all_of(tests.begin(), tests.end(), [&](const Test& t) { return check(t, v); });
}

// Does `version` satisfy the npm-style `range`? An invalid version yields false.
static bool satisfies(const std::string& version, const std::string& range) {
    auto v = parse_semver(version);
    if (!v) return false;
    size_t or_ = 0;
    while (or_ != std::string::npos) {                     // "||" alternatives
        size_t next = range.find("||", or_);
        if (clause_matches(*v, range.substr(or_, next == std::string::npos ? next : next - or_))) return true;
        or_ = next == std::string::npos ? next : next + 2; }
    return false;
}

// Bump by kind; the prerelease bump increments a trailing numeric id (or appends -0 / .1). Invalid input passes through.
static std::string bump(const std::string& in, const std::string& kind) {
    auto s = parse_semver(in);
    if (!s) return in;
    char buf[64];
    if (kind == "major") { std::snprintf(buf, sizeof buf, "%ld.0.0", s->major + 1); return buf; }
    if (kind == "minor") { std::snprintf(buf, sizeof buf, "%ld.%ld.0", s->major, s->minor + 1); return buf; }
    if (kind == "patch") { std::snprintf(buf, sizeof buf, "%ld.%ld.%ld", s->major, s->minor, s->patch + 1); return buf; }
    std::string out = std::to_string(s->major) + "." + std::to_string(s->minor) + "." + std::to_string(s->patch) + "-";
    if (s->pre.empty()) return out + "0";
    std::string last = s->pre.back();
    s->pre.pop_back();
    for (auto& id : s->pre) out += id + ".";
    return out + (digits_only(last) ? std::to_string(std::stol(last) + 1) : last + ".1");
}

int main() {
    auto a = parse_semver("1.2.3-alpha.1+build.5");
    std::printf("parse   1.2.3-alpha.1+build.5 -> %ld.%ld.%ld-%s.%s\n", a->major, a->minor, a->patch, a->pre[0].c_str(), a->pre[1].c_str());
    const char* chain[] = {"1.0.0-alpha", "1.0.0-alpha.1", "1.0.0-beta", "1.0.0-beta.11", "1.0.0-rc.1", "1.0.0"};
    for (size_t i = 0; i + 1 < sizeof chain / sizeof *chain; i++)
        std::printf("compare %s %c %s\n", chain[i], compare(*parse_semver(chain[i]), *parse_semver(chain[i + 1])) < 0 ? '<' : '!', chain[i + 1]);
    const char* vr[][2] = {{"1.2.3", "^1.2.0"}, {"1.9.0", "~1.2.0"}, {"0.2.5", "^0.2.3"}, {"0.3.0", "^0.2.3"}, {"3.1.0", ">=1.0.0 <2.0.0 || >=3.0.0"}, {"1.5.0", "1.2.3 - 1.9.0"}, {"1.2.3", "*"}};
    for (auto& r : vr) std::printf("satisfies %-7s %-26s -> %s\n", r[0], r[1], satisfies(r[0], r[1]) ? "true" : "false");
    std::printf("bump    1.2.3 minor         -> %s\n", bump("1.2.3", "minor").c_str());
    std::printf("bump    1.2.3-beta.2 pre    -> %s\n", bump("1.2.3-beta.2", "prerelease").c_str());
}

Also available in 13 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →