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 →