Skip to content

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 →