Semver Checker — Java 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 Java 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. Java (17+) port of src/lib/semver.ts — same logic as this dir's python.py; build metadata is parsed but ignored (it never affects precedence).
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/** A parsed version: major.minor.patch plus dot-separated prerelease identifiers. */
record SemVer(long major, long minor, long patch, List<String> pre) {
static final Pattern RX = Pattern.compile("^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)" +
"(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?" +
"(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$");
/** Parse strict "M.m.p[-pre][+build]"; a leading v/V is tolerated. Null on invalid. */
static SemVer parse(String input) {
String t = input.trim().replaceFirst("^[vV]", ""); // tolerate the common "v1.2.3" form
Matcher m = RX.matcher(t);
return m.matches() ? new SemVer(Long.parseLong(m.group(1)), Long.parseLong(m.group(2)),
Long.parseLong(m.group(3)),
m.group(4) == null ? List.of() : Arrays.asList(m.group(4).split("\\."))) : null;
}
}
/** Partial range version: -1 in a field = absent/wildcard (fields cascade down). */
record Triple(long major, long minor, long patch) {}
/** One primitive test: op in {">=", ">", "<", "<=", "="} against a full version. */
record Test(String op, Triple v) {}
public class SemVerKit {
static boolean digits(String s) { return !s.isEmpty() && s.chars().allMatch(c -> c >= '0' && c <= '9'); }
static int cmp(long a, long b) { return (a > b) - (a < b); }
static int identCmp(String x, String y) {
boolean xd = digits(x), yd = digits(y);
if (xd && yd) return cmp(Long.parseLong(x), Long.parseLong(y)); // numeric ids compare numerically
if (xd || yd) return xd ? -1 : 1; // numeric ids rank below alphanumeric
return x.compareTo(y);
}
/** Compare by precedence: -1 a<b, 0 equal, 1 a>b. Build metadata is ignored. */
public static int compare(SemVer a, SemVer b) {
int c = cmp(a.major(), b.major()); if (c != 0) return c;
if ((c = cmp(a.minor(), b.minor())) != 0) return c;
if ((c = cmp(a.patch(), b.patch())) != 0) return c;
if (a.pre().isEmpty() || b.pre().isEmpty()) return cmp(b.pre().size(), a.pre().size()); // release > prerelease
for (int i = 0; i < a.pre().size() && i < b.pre().size(); i++)
if ((c = identCmp(a.pre().get(i), b.pre().get(i))) != 0) return c;
return cmp(a.pre().size(), 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 Triple parsePartial(String s) {
s = s.replaceFirst("^[vV]", "");
if (s.isEmpty() || s.equals("*") || s.equalsIgnoreCase("x")) return new Triple(-1, -1, -1);
String[] f = s.split("\\.", -1);
if (f.length > 3) return null;
long[] v = {-1, -1, -1};
for (int i = 0; i < f.length; i++) {
if (f[i].equals("*") || f[i].equalsIgnoreCase("x")) continue; // wildcard: trailing fields drop
if (!digits(f[i])) return null;
v[i] = Long.parseLong(f[i]); }
return v[0] < 0 ? new Triple(-1, -1, -1) : v[1] < 0 ? new Triple(v[0], -1, -1) : new Triple(v[0], v[1], v[2]);
}
/** Desugar one comparator (op + partial) into primitive >=/< tests. */
static List<Test> desugar(String op, Triple t) {
long M = t.major(), m = t.minor() < 0 ? 0 : t.minor(), p = t.patch() < 0 ? 0 : t.patch();
List<Test> out = new ArrayList<>();
if (M < 0) return out; // wildcard matches everything
if (op.equals("^")) { // never break the left-most non-zero part
out.add(new Test(">=", new Triple(M, m, p)));
if (M > 0 || t.minor() < 0) out.add(new Test("<", new Triple(M + 1, 0, 0)));
else if (t.minor() > 0 || t.patch() < 0) out.add(new Test("<", new Triple(0, t.minor() + 1, 0)));
else out.add(new Test("<", new Triple(0, 0, t.patch() + 1))); }
else if (op.equals("~")) {
out.add(new Test(">=", new Triple(M, m, p)));
out.add(new Test("<", t.minor() < 0 ? new Triple(M + 1, 0, 0) : new Triple(M, t.minor() + 1, 0))); }
else if (op.equals("bare") || op.equals("=")) { // bare "1.2" acts as >=1.2.0 <1.3.0
if (t.minor() < 0) { out.add(new Test(">=", new Triple(M, 0, 0))); out.add(new Test("<", new Triple(M + 1, 0, 0))); }
else if (t.patch() < 0) { out.add(new Test(">=", new Triple(M, t.minor(), 0))); out.add(new Test("<", new Triple(M, t.minor() + 1, 0))); }
else out.add(new Test("=", t)); }
else if (op.equals(">=")) out.add(new Test(">=", new Triple(M, m, p)));
else if (op.equals(">")) {
if (t.minor() < 0) out.add(new Test(">=", new Triple(M + 1, 0, 0))); // >1 -> >=2.0.0
else if (t.patch() < 0) out.add(new Test(">=", new Triple(M, t.minor() + 1, 0)));
else out.add(new Test(">", t)); }
else if (op.equals("<=")) {
if (t.minor() < 0) out.add(new Test("<", new Triple(M + 1, 0, 0)));
else if (t.patch() < 0) out.add(new Test("<", new Triple(M, t.minor() + 1, 0)));
else out.add(new Test("<=", t)); }
else if (t.minor() < 0) out.add(new Test("<", new Triple(M, 0, 0))); // "<"
else if (t.patch() < 0) out.add(new Test("<", new Triple(M, t.minor(), 0)));
else out.add(new Test("<", t));
return out;
}
static boolean check(Test t, SemVer v) {
int c = compare(v, new SemVer(t.v().major(), t.v().minor(), t.v().patch(), List.of()));
return t.op().equals(">") ? c > 0 : t.op().equals("<") ? c < 0 : t.op().equals("<=") ? c <= 0 : t.op().equals("=") ? c == 0 : c >= 0;
}
/** Evaluate one AND-clause (already split from ||): hyphen ranges, ^ ~ comparators, partials. */
static boolean clauseMatches(SemVer v, String clause) {
String[] toks = clause.trim().isEmpty() ? new String[0] : clause.trim().split("\\s+");
List<Test> tests = new ArrayList<>();
if (toks.length == 3 && toks[1].equals("-")) { // hyphen range: >=lo <=hi, partials floor/ceiling
Triple lo = parsePartial(toks[0]), hi = parsePartial(toks[2]);
if (lo == null || hi == null) return false;
tests.addAll(desugar(">=", lo)); tests.addAll(desugar("<=", hi)); }
else for (String tok : toks) {
String op = "bare", r = tok;
if (r.startsWith("^") || r.startsWith("~")) { op = r.substring(0, 1); r = r.substring(1); }
else if (r.startsWith(">=") || r.startsWith("<=")) { op = r.substring(0, 2); r = r.substring(2); }
else if (r.startsWith(">") || r.startsWith("<") || r.startsWith("=")) { op = r.substring(0, 1); r = r.substring(1); }
Triple part = parsePartial(r);
if (part == null) return false; // invalid comparator -> unsatisfiable
tests.addAll(desugar(op, part)); }
return tests.stream().allMatch(t -> check(t, v)); // no tests ("*" clause) -> true
}
/** Does {@code version} satisfy the npm-style {@code range}? An invalid version yields false. */
public static boolean satisfies(String version, String range) {
SemVer v = SemVer.parse(version);
if (v == null) return false;
return Arrays.stream(range.split("\\|\\|")).anyMatch(cl -> clauseMatches(v, cl));
}
/** Bump by kind; the prerelease bump increments a trailing numeric id (or appends -0 / .1). Invalid input passes through. */
public static String bump(String input, String kind) {
SemVer s = SemVer.parse(input);
if (s == null) return input;
switch (kind) {
case "major": return (s.major() + 1) + ".0.0";
case "minor": return s.major() + "." + (s.minor() + 1) + ".0";
case "patch": return s.major() + "." + s.minor() + "." + (s.patch() + 1); }
if (s.pre().isEmpty()) return s.major() + "." + s.minor() + "." + (s.patch() + 1) + "-0";
String last = s.pre().get(s.pre().size() - 1);
String head = s.pre().size() > 1 ? String.join(".", s.pre().subList(0, s.pre().size() - 1)) + "." : "";
return s.major() + "." + s.minor() + "." + s.patch() + "-" + head
+ (digits(last) ? Long.toString(Long.parseLong(last) + 1) : last + ".1");
}
public static void main(String[] args) {
SemVer a = SemVer.parse("1.2.3-alpha.1+build.5");
System.out.printf(Locale.ROOT, "parse 1.2.3-alpha.1+build.5 -> %d.%d.%d-%s%n",
a.major(), a.minor(), a.patch(), String.join(".", a.pre()));
String[] 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 (int i = 0; i + 1 < chain.length; i++)
System.out.printf(Locale.ROOT, "compare %s %c %s%n", chain[i],
compare(SemVer.parse(chain[i]), SemVer.parse(chain[i + 1])) < 0 ? '<' : '!', chain[i + 1]);
String[][] vr = {{"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 (String[] r : vr) System.out.printf(Locale.ROOT, "satisfies %-7s %-26s -> %s%n", r[0], r[1], satisfies(r[0], r[1]));
System.out.println("bump 1.2.3 minor -> " + bump("1.2.3", "minor"));
System.out.println("bump 1.2.3-beta.2 pre -> " + bump("1.2.3-beta.2", "prerelease"));
}
}
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 →