{
  "id": "P103",
  "slug": "version-sorter",
  "key": "P103-version-sorter",
  "title": "Version sorter",
  "summary": "Semantic Versioning: one list sorted as text, by a natural sort and by the SemVer rule, with the pairs they disagree on; two versions compared with the rule that decides; and the newest version matching a caret, tilde or comparator range.",
  "entry": "main.eml",
  "ui": "terminal",
  "readme": "# P103 - Version sorter\n\nVersion numbers by Semantic Versioning 2.0.0: MAJOR.MINOR.PATCH, then an\noptional pre-release after - and build metadata after +. One list is\nsorted three ways, two versions are compared with the rule that decides,\nand the newest version in a range is picked from the list.\n\n- `main.eml` - the menu, the three sorts and their disagreements,\n  comparing two, ranges, and a new list\n- `versions.eml` - reading a version, the SemVer rule, a natural sort,\n  sorting, and range bounds\n\nHow each part works:\n\n- Reading: three whole numbers without leading zeros. A pre-release is\n  dot-separated identifiers of letters, digits and hyphens, and a number\n  among them has no leading zero. Build metadata is the same without that\n  rule.\n- The SemVer rule:\n  - MAJOR, MINOR, then PATCH, as numbers;\n  - a pre-release comes before its release;\n  - pre-release identifiers one by one: numbers as numbers, words in\n    ASCII order, a number before a word;\n  - with every earlier identifier equal, more identifiers come later;\n  - build metadata is ignored.\n- Three sorts, as in the corpus case `version-string-ordering`:\n  - as text, which puts 1.10.0 before 1.9.1;\n  - a natural sort that reads runs of digits as numbers, but puts 1.0.0\n    before 1.0.0-alpha, the shorter text first;\n  - SemVer.\n\n  The program counts the pairs where text and the natural sort disagree\n  with SemVer.\n- Ranges:\n  - ^ keeps the first non-zero part: ^1.2.3 is >=1.2.3 <2.0.0, ^0.2.3 is\n    >=0.2.3 <0.3.0, and ^0.0.3 is >=0.0.3 <0.0.4;\n  - ~ keeps the minor: ~1.2.3 is >=1.2.3 <1.3.0;\n  - comparators >=, >, <=, < and = can be combined with spaces.\n\n  Pre-releases never match, and a range takes releases only.\n\nWhat is checked: menu choices 1 to 5; every version in the form above;\nranges of releases only; a list of 2 to 20 versions. An empty answer\ncancels.\n\nSessions: `sessions/basic.in` gives:\n- the starting list of 13 sorted three ways: 17 and 14 of the 78 pairs\n  disagree with SemVer;\n- six comparisons, each decided by a different rule;\n- the ranges ^1.0.0, ~1.9.0, >=1.2.0 <1.10.0 and ^2.0.0;\n- a list of 0.x versions, with ^0.2.3 and ^0.0.3, and its three sorts.\n\n`sessions/bad-input.in` gives:\n- menu choices 0 and x;\n- versions with two parts, a leading zero in the core and in a\n  pre-release, an empty pre-release and build, a stray #, and a v in front;\n  two cancels;\n- a cancel, a range with two parts, a range with a pre-release, and a\n  range nothing matches;\n- a cancel, a list of one, a list with a word in it; then the sorts again.\n\nBuilt on the verified corpus case `version-string-ordering` (a version\nis a tuple of numbers; as text it goes wrong at the tenth release).\n",
  "modules": [
    {
      "name": "main.eml",
      "eml": "# P103 version sorter: one list of versions sorted three ways, two versions\n# compared with the rule that decides, and the newest version in a range.\nimport versions\n\ndef plain(s):\n    0 => i\n    len(s) => j\n    while i < j and s[i] == \" \":\n        i + 1 => i\n    while j > i and s[j - 1] == \" \":\n        j - 1 => j\n    return s[i:j]\n\ndef words(s):\n    [] => out\n    \"\" => w\n    for c in s + \" \":\n        if c == \" \" or c == \",\":\n            if w != \"\":\n                out + [w] => out\n            \"\" => w\n        else:\n            w + c => w\n    return out\n\ndef joined(xs):\n    \"\" => out\n    for x in xs:\n        if out != \"\":\n            out + \"  \" => out\n        out + x => out\n    return out\n\ndef show_sorts(texts):\n    [\"text\", \"natural\", \"semver\"] => rules\n    [\"as text      \", \"natural sort \", \"SemVer       \"] => labels\n    for k in [0:2]:\n        (\"  \" + labels[k] + joined(versions.order(texts, rules[k]))) ^0\n    0 => pairs\n    0 => text_off\n    0 => natural_off\n    \"\" => example_text\n    \"\" => example_natural\n    for i in [0:len(texts) - 1]:\n        for j in [i + 1:len(texts) - 1]:\n            pairs + 1 => pairs\n            versions.compare(texts[i], texts[j], \"semver\") => s\n            if versions.compare(texts[i], texts[j], \"text\") != s:\n                text_off + 1 => text_off\n                if example_text == \"\":\n                    texts[i] + \" and \" + texts[j] => example_text\n            if versions.compare(texts[i], texts[j], \"natural\") != s:\n                natural_off + 1 => natural_off\n                if example_natural == \"\":\n                    texts[i] + \" and \" + texts[j] => example_natural\n    (\"Of the \" + str(pairs) + \" pairs, text order disagrees with SemVer on \" + str(text_off) + \" and the natural sort on \" + str(natural_off) + \".\") ^0\n    if example_text != \"\":\n        (\"  First text disagreement: \" + example_text + \".\") ^0\n    if example_natural != \"\":\n        (\"  First natural-sort disagreement: \" + example_natural + \".\") ^0\n    \"Text compares character by character, so 1.10.0 comes before 1.9.1. A natural sort reads numbers as numbers, but puts 1.0.0 before 1.0.0-alpha, the shorter text first. SemVer puts every pre-release before its release.\" ^0\n\ndef ask_version(prompt):\n    plain(input(prompt + \"> \")) => t\n    if t == \"\":\n        return [\"\", []]\n    versions.parse(t) => p\n    if p[1] != \"\":\n        (\"Not a version: \" + p[1] + \".\") ^0\n        return [\"!\", []]\n    return [t, p[0]]\n\ndef compare_two():\n    ask_version(\"first version\") => a\n    if a[0] == \"\":\n        \"Cancelled.\" ^0\n        return\n    if a[0] == \"!\":\n        return\n    ask_version(\"second version\") => b\n    if b[0] == \"\":\n        \"Cancelled.\" ^0\n        return\n    if b[0] == \"!\":\n        return\n    versions.semver(a[1], b[1]) => r\n    if r[0] < 0:\n        (a[0] + \" comes before \" + b[0] + \": \" + r[1] + \".\") ^0\n    elif r[0] > 0:\n        (a[0] + \" comes after \" + b[0] + \": \" + r[1] + \".\") ^0\n    else:\n        (a[0] + \" and \" + b[0] + \" rank the same: \" + r[1] + \".\") ^0\n    versions.sign(a[0], b[0]) => t\n    if r[0] != 0 and t == 0 - r[0]:\n        \"  As text the order is the other way round.\" ^0\n    elif r[0] == 0 and t != 0:\n        \"  As text they differ, so a text sort still puts one first.\" ^0\n\ndef bound_text(b):\n    \"\" => out\n    if len(b[0]) > 0:\n        \">=\" + str(b[0][0]) + \".\" + str(b[0][1]) + \".\" + str(b[0][2]) => out\n    if len(b[1]) > 0:\n        \"<\" => op\n        if b[2]:\n            \"<=\" => op\n        if out != \"\":\n            out + \" \" => out\n        out + op + str(b[1][0]) + \".\" + str(b[1][1]) + \".\" + str(b[1][2]) => out\n    return out\n\ndef pick(texts):\n    \"A range: ^1.2.3 (same major), ~1.2.3 (same minor), or comparators such as >=1.2.0 <1.5.0. Pre-releases never match.\" ^0\n    plain(input(\"range> \")) => r\n    if r == \"\":\n        \"Cancelled.\" ^0\n        return\n    [] => all_bounds\n    \"\" => means\n    for w in words(r):\n        versions.range_bounds(w) => b\n        if b[3] != \"\":\n            (\"Not a range: \" + b[3] + \".\") ^0\n            return\n        all_bounds + [b] => all_bounds\n        if means != \"\":\n            means + \" \" => means\n        means + bound_text(b) => means\n    (r + \" means \" + means + \".\") ^0\n    [] => hits\n    for t in texts:\n        versions.parse(t)[0] => v\n        True => ok\n        for b in all_bounds:\n            if not versions.in_range(v, b):\n                False => ok\n        if ok:\n            hits + [t] => hits\n    if len(hits) == 0:\n        \"No version in the list matches.\" ^0\n        return\n    versions.order(hits, \"semver\") => hits\n    (\"Matching: \" + joined(hits) + \". Newest: \" + hits[len(hits) - 1] + \".\") ^0\n\ndef new_list():\n    \"Versions separated by spaces, 2 to 20 of them.\" ^0\n    plain(input(\"versions> \")) => t\n    if t == \"\":\n        \"Cancelled.\" ^0\n        return []\n    words(t) => out\n    if len(out) < 2 or len(out) > 20:\n        \"Type 2 to 20 versions.\" ^0\n        return []\n    for w in out:\n        versions.parse(w) => p\n        if p[1] != \"\":\n            (\"Not a version: \" + w + \" - \" + p[1] + \".\") ^0\n            return []\n    (str(len(out)) + \" versions.\") ^0\n    return out\n\n[\"1.10.0\", \"1.2.0\", \"1.9.1\", \"1.0.0-alpha\", \"1.0.0\", \"1.0.0-beta.11\", \"1.0.0-beta.2\", \"1.0.0-alpha.1\", \"2.0.0-rc.1\", \"1.0.0-beta\", \"1.0.0+build.5\", \"1.0.0-alpha.beta\", \"2.0.0\"] => texts\n\n\"== Version sorter ==\" ^0\n\"Semantic Versioning: MAJOR.MINOR.PATCH, then a pre-release after - and build metadata after +.\" ^0\nTrue => running\nwhile running:\n    \"\" ^0\n    \"1) sort the list  2) compare two  3) newest in a range  4) new list  5) quit\" ^0\n    plain(input(\"choice> \")) => choice\n    if choice == \"1\":\n        show_sorts(texts)\n    elif choice == \"2\":\n        compare_two()\n    elif choice == \"3\":\n        pick(texts)\n    elif choice == \"4\":\n        new_list() => got\n        if len(got) > 0:\n            got => texts\n    elif choice == \"5\":\n        False => running\n    else:\n        \"Pick a number from 1 to 5.\" ^0\n\"Bye.\" ^0\n",
      "python": "import versions\n\ndef plain(s):\n    i = 0\n    j = len(s)\n    while i < j and s[i] == \" \":\n        i = i + 1\n    while j > i and s[j - 1] == \" \":\n        j = j - 1\n    return s[i:j]\n\ndef words(s):\n    out = []\n    w = \"\"\n    for c in s + \" \":\n        if c == \" \" or c == \",\":\n            if w != \"\":\n                out = out + [w]\n            w = \"\"\n        else:\n            w = w + c\n    return out\n\ndef joined(xs):\n    out = \"\"\n    for x in xs:\n        if out != \"\":\n            out = out + \"  \"\n        out = out + x\n    return out\n\ndef show_sorts(texts):\n    rules = [\"text\", \"natural\", \"semver\"]\n    labels = [\"as text      \", \"natural sort \", \"SemVer       \"]\n    for k in range(0, 3):\n        print(\"  \" + labels[k] + joined(versions.order(texts, rules[k])))\n    pairs = 0\n    text_off = 0\n    natural_off = 0\n    example_text = \"\"\n    example_natural = \"\"\n    for i in range(0, len(texts)):\n        for j in range(i + 1, len(texts)):\n            pairs = pairs + 1\n            s = versions.compare(texts[i], texts[j], \"semver\")\n            if versions.compare(texts[i], texts[j], \"text\") != s:\n                text_off = text_off + 1\n                if example_text == \"\":\n                    example_text = texts[i] + \" and \" + texts[j]\n            if versions.compare(texts[i], texts[j], \"natural\") != s:\n                natural_off = natural_off + 1\n                if example_natural == \"\":\n                    example_natural = texts[i] + \" and \" + texts[j]\n    print(\"Of the \" + str(pairs) + \" pairs, text order disagrees with SemVer on \" + str(text_off) + \" and the natural sort on \" + str(natural_off) + \".\")\n    if example_text != \"\":\n        print(\"  First text disagreement: \" + example_text + \".\")\n    if example_natural != \"\":\n        print(\"  First natural-sort disagreement: \" + example_natural + \".\")\n    print(\"Text compares character by character, so 1.10.0 comes before 1.9.1. A natural sort reads numbers as numbers, but puts 1.0.0 before 1.0.0-alpha, the shorter text first. SemVer puts every pre-release before its release.\")\n\ndef ask_version(prompt):\n    t = plain(input(prompt + \"> \"))\n    if t == \"\":\n        return [\"\", []]\n    p = versions.parse(t)\n    if p[1] != \"\":\n        print(\"Not a version: \" + p[1] + \".\")\n        return [\"!\", []]\n    return [t, p[0]]\n\ndef compare_two():\n    a = ask_version(\"first version\")\n    if a[0] == \"\":\n        print(\"Cancelled.\")\n        return\n    if a[0] == \"!\":\n        return\n    b = ask_version(\"second version\")\n    if b[0] == \"\":\n        print(\"Cancelled.\")\n        return\n    if b[0] == \"!\":\n        return\n    r = versions.semver(a[1], b[1])\n    if r[0] < 0:\n        print(a[0] + \" comes before \" + b[0] + \": \" + r[1] + \".\")\n    elif r[0] > 0:\n        print(a[0] + \" comes after \" + b[0] + \": \" + r[1] + \".\")\n    else:\n        print(a[0] + \" and \" + b[0] + \" rank the same: \" + r[1] + \".\")\n    t = versions.sign(a[0], b[0])\n    if r[0] != 0 and t == 0 - r[0]:\n        print(\"  As text the order is the other way round.\")\n    elif r[0] == 0 and t != 0:\n        print(\"  As text they differ, so a text sort still puts one first.\")\n\ndef bound_text(b):\n    out = \"\"\n    if len(b[0]) > 0:\n        out = \">=\" + str(b[0][0]) + \".\" + str(b[0][1]) + \".\" + str(b[0][2])\n    if len(b[1]) > 0:\n        op = \"<\"\n        if b[2]:\n            op = \"<=\"\n        if out != \"\":\n            out = out + \" \"\n        out = out + op + str(b[1][0]) + \".\" + str(b[1][1]) + \".\" + str(b[1][2])\n    return out\n\ndef pick(texts):\n    print(\"A range: ^1.2.3 (same major), ~1.2.3 (same minor), or comparators such as >=1.2.0 <1.5.0. Pre-releases never match.\")\n    r = plain(input(\"range> \"))\n    if r == \"\":\n        print(\"Cancelled.\")\n        return\n    all_bounds = []\n    means = \"\"\n    for w in words(r):\n        b = versions.range_bounds(w)\n        if b[3] != \"\":\n            print(\"Not a range: \" + b[3] + \".\")\n            return\n        all_bounds = all_bounds + [b]\n        if means != \"\":\n            means = means + \" \"\n        means = means + bound_text(b)\n    print(r + \" means \" + means + \".\")\n    hits = []\n    for t in texts:\n        v = versions.parse(t)[0]\n        ok = True\n        for b in all_bounds:\n            if not versions.in_range(v, b):\n                ok = False\n        if ok:\n            hits = hits + [t]\n    if len(hits) == 0:\n        print(\"No version in the list matches.\")\n        return\n    hits = versions.order(hits, \"semver\")\n    print(\"Matching: \" + joined(hits) + \". Newest: \" + hits[len(hits) - 1] + \".\")\n\ndef new_list():\n    print(\"Versions separated by spaces, 2 to 20 of them.\")\n    t = plain(input(\"versions> \"))\n    if t == \"\":\n        print(\"Cancelled.\")\n        return []\n    out = words(t)\n    if len(out) < 2 or len(out) > 20:\n        print(\"Type 2 to 20 versions.\")\n        return []\n    for w in out:\n        p = versions.parse(w)\n        if p[1] != \"\":\n            print(\"Not a version: \" + w + \" - \" + p[1] + \".\")\n            return []\n    print(str(len(out)) + \" versions.\")\n    return out\n\ntexts = [\"1.10.0\", \"1.2.0\", \"1.9.1\", \"1.0.0-alpha\", \"1.0.0\", \"1.0.0-beta.11\", \"1.0.0-beta.2\", \"1.0.0-alpha.1\", \"2.0.0-rc.1\", \"1.0.0-beta\", \"1.0.0+build.5\", \"1.0.0-alpha.beta\", \"2.0.0\"]\nprint(\"== Version sorter ==\")\nprint(\"Semantic Versioning: MAJOR.MINOR.PATCH, then a pre-release after - and build metadata after +.\")\nrunning = True\nwhile running:\n    print(\"\")\n    print(\"1) sort the list  2) compare two  3) newest in a range  4) new list  5) quit\")\n    choice = plain(input(\"choice> \"))\n    if choice == \"1\":\n        show_sorts(texts)\n    elif choice == \"2\":\n        compare_two()\n    elif choice == \"3\":\n        pick(texts)\n    elif choice == \"4\":\n        got = new_list()\n        if len(got) > 0:\n            texts = got\n    elif choice == \"5\":\n        running = False\n    else:\n        print(\"Pick a number from 1 to 5.\")\nprint(\"Bye.\")\n"
    },
    {
      "name": "versions.eml",
      "eml": "# P103 version sorter - Semantic Versioning 2.0.0: MAJOR.MINOR.PATCH, an\n# optional pre-release after \"-\" and build metadata after \"+\". As in the\n# corpus case version-string-ordering, a version is a tuple of numbers\n# written with dots, and comparing it as text goes wrong at the tenth\n# release; here it is set against a natural sort and the SemVer rule.\n#\n# A parsed version is [major, minor, patch, pre, build]: pre is a list of\n# identifiers (strings), build the text after \"+\" or \"\".\n\ndef is_digits(s):\n    if len(s) == 0:\n        return False\n    for c in s:\n        if not (c >= \"0\" and c <= \"9\"):\n            return False\n    return True\n\ndef ident_ok(s):\n    if len(s) == 0:\n        return False\n    for c in s:\n        if not ((c >= \"0\" and c <= \"9\") or (c >= \"a\" and c <= \"z\") or (c >= \"A\" and c <= \"Z\") or c == \"-\"):\n            return False\n    return True\n\ndef split(s, sep):\n    [] => out\n    \"\" => w\n    for c in s:\n        if c == sep:\n            out + [w] => out\n            \"\" => w\n        else:\n            w + c => w\n    out + [w] => out\n    return out\n\ndef parse(s):\n    # [version, \"\"] or [[], why]\n    \"\" => build\n    -1 => plus\n    for i in [0:len(s) - 1]:\n        if plus == -1 and s[i] == \"+\":\n            i => plus\n    if plus != -1:\n        s[plus + 1:len(s)] => build\n        s[0:plus] => s\n        for b in split(build, \".\"):\n            if not ident_ok(b):\n                return [[], \"build metadata is dot-separated letters, digits and hyphens\"]\n    [] => pre\n    -1 => dash\n    for i in [0:len(s) - 1]:\n        if dash == -1 and s[i] == \"-\":\n            i => dash\n    if dash != -1:\n        split(s[dash + 1:len(s)], \".\") => pre\n        s[0:dash] => s\n        for p in pre:\n            if not ident_ok(p):\n                return [[], \"a pre-release is dot-separated letters, digits and hyphens, none empty\"]\n            if is_digits(p) and len(p) > 1 and p[0] == \"0\":\n                return [[], \"a number in a pre-release has no leading zero (\" + p + \")\"]\n    split(s, \".\") => core\n    if len(core) != 3:\n        return [[], \"a version has three numbers, MAJOR.MINOR.PATCH\"]\n    for c in core:\n        if not is_digits(c):\n            return [[], \"MAJOR, MINOR and PATCH are whole numbers\"]\n        if len(c) > 1 and c[0] == \"0\":\n            return [[], \"MAJOR, MINOR and PATCH have no leading zeros (\" + c + \")\"]\n    return [[int(core[0]), int(core[1]), int(core[2]), pre, build], \"\"]\n\ndef text(v):\n    str(v[0]) + \".\" + str(v[1]) + \".\" + str(v[2]) => s\n    if len(v[3]) > 0:\n        \"\" => p\n        for x in v[3]:\n            if p != \"\":\n                p + \".\" => p\n            p + x => p\n        s + \"-\" + p => s\n    if v[4] != \"\":\n        s + \"+\" + v[4] => s\n    return s\n\ndef sign(a, b):\n    if a < b:\n        return -1\n    if a > b:\n        return 1\n    return 0\n\ndef semver(a, b):\n    # [-1, 0 or 1, the rule that decided]\n    [\"major\", \"minor\", \"patch\"] => names\n    for i in [0:2]:\n        if a[i] != b[i]:\n            return [sign(a[i], b[i]), names[i] + \" \" + str(a[i]) + \" vs \" + str(b[i])]\n    if len(a[3]) == 0 and len(b[3]) == 0:\n        if a[4] != b[4]:\n            return [0, \"build metadata is ignored\"]\n        return [0, \"the same version\"]\n    if len(a[3]) == 0:\n        return [1, \"a pre-release comes before its release\"]\n    if len(b[3]) == 0:\n        return [-1, \"a pre-release comes before its release\"]\n    0 => i\n    while i < len(a[3]) and i < len(b[3]):\n        a[3][i] => x\n        b[3][i] => y\n        if x != y:\n            if is_digits(x) and is_digits(y):\n                return [sign(int(x), int(y)), \"pre-release part \" + str(i + 1) + \": \" + x + \" vs \" + y + \" as numbers\"]\n            if is_digits(x):\n                return [-1, \"pre-release part \" + str(i + 1) + \": a number comes before a word\"]\n            if is_digits(y):\n                return [1, \"pre-release part \" + str(i + 1) + \": a number comes before a word\"]\n            return [sign(x, y), \"pre-release part \" + str(i + 1) + \": \" + x + \" vs \" + y + \" in ASCII order\"]\n        i + 1 => i\n    if len(a[3]) != len(b[3]):\n        return [sign(len(a[3]), len(b[3])), \"every earlier pre-release part is equal, and more parts come later\"]\n    if a[4] != b[4]:\n        return [0, \"build metadata is ignored\"]\n    return [0, \"the same version\"]\n\ndef chunks(s):\n    # a natural sort's view: runs of digits as numbers, everything else as text\n    [] => out\n    \"\" => w\n    False => digit\n    for c in s:\n        (c >= \"0\" and c <= \"9\") => d\n        if w != \"\" and d != digit:\n            out + [w] => out\n            \"\" => w\n        w + c => w\n        d => digit\n    if w != \"\":\n        out + [w] => out\n    return out\n\ndef natural(a, b):\n    chunks(a) => x\n    chunks(b) => y\n    0 => i\n    while i < len(x) and i < len(y):\n        if x[i] != y[i]:\n            if is_digits(x[i]) and is_digits(y[i]):\n                if int(x[i]) != int(y[i]):\n                    return sign(int(x[i]), int(y[i]))\n            return sign(x[i], y[i])\n        i + 1 => i\n    return sign(len(x), len(y))\n\ndef order(texts, rule):\n    # the texts sorted by rule (\"text\", \"natural\" or \"semver\"); stable\n    [] => out\n    for t in texts:\n        out + [t] => out\n    for i in [1:len(out) - 1]:\n        i => j\n        while j > 0 and compare(out[j - 1], out[j], rule) > 0:\n            out[j] => t\n            out[j - 1] => out[j]\n            t => out[j - 1]\n            j - 1 => j\n    return out\n\ndef compare(a, b, rule):\n    if rule == \"text\":\n        return sign(a, b)\n    if rule == \"natural\":\n        return natural(a, b)\n    return semver(parse(a)[0], parse(b)[0])[0]\n\ndef range_bounds(r):\n    # [low, high, inclusive high, why]: ^V and ~V, or one comparator\n    # >=V, >V, <=V, <V, =V or V; versions are [major, minor, patch];\n    # high [] for none, low [] for none\n    \"\" => op\n    for o in [\">=\", \"<=\", \"^\", \"~\", \">\", \"<\", \"=\"]:\n        if op == \"\" and r[0:len(o)] == o:\n            o => op\n    r[len(op):len(r)] => rest\n    parse(rest) => p\n    if p[1] != \"\":\n        return [[], [], False, rest + \": \" + p[1]]\n    p[0] => v\n    if len(v[3]) > 0:\n        return [[], [], False, \"a range here takes releases only, without a pre-release\"]\n    [v[0], v[1], v[2]] => base\n    if op == \"^\":\n        if v[0] > 0:\n            return [base, [v[0] + 1, 0, 0], False, \"\"]\n        if v[1] > 0:\n            return [base, [0, v[1] + 1, 0], False, \"\"]\n        return [base, [0, 0, v[2] + 1], False, \"\"]\n    if op == \"~\":\n        return [base, [v[0], v[1] + 1, 0], False, \"\"]\n    if op == \">=\":\n        return [base, [], False, \"\"]\n    if op == \">\":\n        return [[v[0], v[1], v[2] + 1], [], False, \"\"]\n    if op == \"<=\":\n        return [[], base, True, \"\"]\n    if op == \"<\":\n        return [[], base, False, \"\"]\n    return [base, base, True, \"\"]\n\ndef triple_cmp(a, b):\n    for i in [0:2]:\n        if a[i] != b[i]:\n            return sign(a[i], b[i])\n    return 0\n\ndef in_range(v, bounds):\n    # releases only: a pre-release never matches\n    if len(v[3]) > 0:\n        return False\n    [v[0], v[1], v[2]] => t\n    if len(bounds[0]) > 0 and triple_cmp(t, bounds[0]) < 0:\n        return False\n    if len(bounds[1]) > 0:\n        triple_cmp(t, bounds[1]) => c\n        if c > 0 or (c == 0 and not bounds[2]):\n            return False\n    return True\n",
      "python": "def is_digits(s):\n    if len(s) == 0:\n        return False\n    for c in s:\n        if not (c >= \"0\" and c <= \"9\"):\n            return False\n    return True\n\ndef ident_ok(s):\n    if len(s) == 0:\n        return False\n    for c in s:\n        if not (c >= \"0\" and c <= \"9\" or c >= \"a\" and c <= \"z\" or c >= \"A\" and c <= \"Z\" or c == \"-\"):\n            return False\n    return True\n\ndef split(s, sep):\n    out = []\n    w = \"\"\n    for c in s:\n        if c == sep:\n            out = out + [w]\n            w = \"\"\n        else:\n            w = w + c\n    out = out + [w]\n    return out\n\ndef parse(s):\n    build = \"\"\n    plus = -1\n    for i in range(0, len(s)):\n        if plus == -1 and s[i] == \"+\":\n            plus = i\n    if plus != -1:\n        build = s[plus + 1:len(s)]\n        s = s[0:plus]\n        for b in split(build, \".\"):\n            if not ident_ok(b):\n                return [[], \"build metadata is dot-separated letters, digits and hyphens\"]\n    pre = []\n    dash = -1\n    for i in range(0, len(s)):\n        if dash == -1 and s[i] == \"-\":\n            dash = i\n    if dash != -1:\n        pre = split(s[dash + 1:len(s)], \".\")\n        s = s[0:dash]\n        for p in pre:\n            if not ident_ok(p):\n                return [[], \"a pre-release is dot-separated letters, digits and hyphens, none empty\"]\n            if is_digits(p) and len(p) > 1 and p[0] == \"0\":\n                return [[], \"a number in a pre-release has no leading zero (\" + p + \")\"]\n    core = split(s, \".\")\n    if len(core) != 3:\n        return [[], \"a version has three numbers, MAJOR.MINOR.PATCH\"]\n    for c in core:\n        if not is_digits(c):\n            return [[], \"MAJOR, MINOR and PATCH are whole numbers\"]\n        if len(c) > 1 and c[0] == \"0\":\n            return [[], \"MAJOR, MINOR and PATCH have no leading zeros (\" + c + \")\"]\n    return [[int(core[0]), int(core[1]), int(core[2]), pre, build], \"\"]\n\ndef text(v):\n    s = str(v[0]) + \".\" + str(v[1]) + \".\" + str(v[2])\n    if len(v[3]) > 0:\n        p = \"\"\n        for x in v[3]:\n            if p != \"\":\n                p = p + \".\"\n            p = p + x\n        s = s + \"-\" + p\n    if v[4] != \"\":\n        s = s + \"+\" + v[4]\n    return s\n\ndef sign(a, b):\n    if a < b:\n        return -1\n    if a > b:\n        return 1\n    return 0\n\ndef semver(a, b):\n    names = [\"major\", \"minor\", \"patch\"]\n    for i in range(0, 3):\n        if a[i] != b[i]:\n            return [sign(a[i], b[i]), names[i] + \" \" + str(a[i]) + \" vs \" + str(b[i])]\n    if len(a[3]) == 0 and len(b[3]) == 0:\n        if a[4] != b[4]:\n            return [0, \"build metadata is ignored\"]\n        return [0, \"the same version\"]\n    if len(a[3]) == 0:\n        return [1, \"a pre-release comes before its release\"]\n    if len(b[3]) == 0:\n        return [-1, \"a pre-release comes before its release\"]\n    i = 0\n    while i < len(a[3]) and i < len(b[3]):\n        x = a[3][i]\n        y = b[3][i]\n        if x != y:\n            if is_digits(x) and is_digits(y):\n                return [sign(int(x), int(y)), \"pre-release part \" + str(i + 1) + \": \" + x + \" vs \" + y + \" as numbers\"]\n            if is_digits(x):\n                return [-1, \"pre-release part \" + str(i + 1) + \": a number comes before a word\"]\n            if is_digits(y):\n                return [1, \"pre-release part \" + str(i + 1) + \": a number comes before a word\"]\n            return [sign(x, y), \"pre-release part \" + str(i + 1) + \": \" + x + \" vs \" + y + \" in ASCII order\"]\n        i = i + 1\n    if len(a[3]) != len(b[3]):\n        return [sign(len(a[3]), len(b[3])), \"every earlier pre-release part is equal, and more parts come later\"]\n    if a[4] != b[4]:\n        return [0, \"build metadata is ignored\"]\n    return [0, \"the same version\"]\n\ndef chunks(s):\n    out = []\n    w = \"\"\n    digit = False\n    for c in s:\n        d = c >= \"0\" and c <= \"9\"\n        if w != \"\" and d != digit:\n            out = out + [w]\n            w = \"\"\n        w = w + c\n        digit = d\n    if w != \"\":\n        out = out + [w]\n    return out\n\ndef natural(a, b):\n    x = chunks(a)\n    y = chunks(b)\n    i = 0\n    while i < len(x) and i < len(y):\n        if x[i] != y[i]:\n            if is_digits(x[i]) and is_digits(y[i]):\n                if int(x[i]) != int(y[i]):\n                    return sign(int(x[i]), int(y[i]))\n            return sign(x[i], y[i])\n        i = i + 1\n    return sign(len(x), len(y))\n\ndef order(texts, rule):\n    out = []\n    for t in texts:\n        out = out + [t]\n    for i in range(1, len(out)):\n        j = i\n        while j > 0 and compare(out[j - 1], out[j], rule) > 0:\n            t = out[j]\n            out[j] = out[j - 1]\n            out[j - 1] = t\n            j = j - 1\n    return out\n\ndef compare(a, b, rule):\n    if rule == \"text\":\n        return sign(a, b)\n    if rule == \"natural\":\n        return natural(a, b)\n    return semver(parse(a)[0], parse(b)[0])[0]\n\ndef range_bounds(r):\n    op = \"\"\n    for o in [\">=\", \"<=\", \"^\", \"~\", \">\", \"<\", \"=\"]:\n        if op == \"\" and r[0:len(o)] == o:\n            op = o\n    rest = r[len(op):len(r)]\n    p = parse(rest)\n    if p[1] != \"\":\n        return [[], [], False, rest + \": \" + p[1]]\n    v = p[0]\n    if len(v[3]) > 0:\n        return [[], [], False, \"a range here takes releases only, without a pre-release\"]\n    base = [v[0], v[1], v[2]]\n    if op == \"^\":\n        if v[0] > 0:\n            return [base, [v[0] + 1, 0, 0], False, \"\"]\n        if v[1] > 0:\n            return [base, [0, v[1] + 1, 0], False, \"\"]\n        return [base, [0, 0, v[2] + 1], False, \"\"]\n    if op == \"~\":\n        return [base, [v[0], v[1] + 1, 0], False, \"\"]\n    if op == \">=\":\n        return [base, [], False, \"\"]\n    if op == \">\":\n        return [[v[0], v[1], v[2] + 1], [], False, \"\"]\n    if op == \"<=\":\n        return [[], base, True, \"\"]\n    if op == \"<\":\n        return [[], base, False, \"\"]\n    return [base, base, True, \"\"]\n\ndef triple_cmp(a, b):\n    for i in range(0, 3):\n        if a[i] != b[i]:\n            return sign(a[i], b[i])\n    return 0\n\ndef in_range(v, bounds):\n    if len(v[3]) > 0:\n        return False\n    t = [v[0], v[1], v[2]]\n    if len(bounds[0]) > 0 and triple_cmp(t, bounds[0]) < 0:\n        return False\n    if len(bounds[1]) > 0:\n        c = triple_cmp(t, bounds[1])\n        if c > 0 or c == 0 and not bounds[2]:\n            return False\n    return True\n"
    }
  ],
  "sessions": [
    {
      "name": "bad-input",
      "input": "0\nx\n2\n1.2\n2\n01.2.3\n2\n1.2.3-01\n2\n1.2.3-\n2\n1.2.3+\n2\n1.2.3-al#pha\n2\nv1.2.3\n2\n\n2\n1.0.0\n\n3\n\n3\n^1.2\n3\n^1.2.3-beta\n3\n>=3.0.0\n4\n\n4\n1.0.0\n4\n1.0.0 banana\n1\n5\n",
      "screen": "== Version sorter ==\nSemantic Versioning: MAJOR.MINOR.PATCH, then a pre-release after - and build metadata after +.\n\n1) sort the list  2) compare two  3) newest in a range  4) new list  5) quit\nchoice> 0\nPick a number from 1 to 5.\n\n1) sort the list  2) compare two  3) newest in a range  4) new list  5) quit\nchoice> x\nPick a number from 1 to 5.\n\n1) sort the list  2) compare two  3) newest in a range  4) new list  5) quit\nchoice> 2\nfirst version> 1.2\nNot a version: a version has three numbers, MAJOR.MINOR.PATCH.\n\n1) sort the list  2) compare two  3) newest in a range  4) new list  5) quit\nchoice> 2\nfirst version> 01.2.3\nNot a version: MAJOR, MINOR and PATCH have no leading zeros (01).\n\n1) sort the list  2) compare two  3) newest in a range  4) new list  5) quit\nchoice> 2\nfirst version> 1.2.3-01\nNot a version: a number in a pre-release has no leading zero (01).\n\n1) sort the list  2) compare two  3) newest in a range  4) new list  5) quit\nchoice> 2\nfirst version> 1.2.3-\nNot a version: a pre-release is dot-separated letters, digits and hyphens, none empty.\n\n1) sort the list  2) compare two  3) newest in a range  4) new list  5) quit\nchoice> 2\nfirst version> 1.2.3+\nNot a version: build metadata is dot-separated letters, digits and hyphens.\n\n1) sort the list  2) compare two  3) newest in a range  4) new list  5) quit\nchoice> 2\nfirst version> 1.2.3-al#pha\nNot a version: a pre-release is dot-separated letters, digits and hyphens, none empty.\n\n1) sort the list  2) compare two  3) newest in a range  4) new list  5) quit\nchoice> 2\nfirst version> v1.2.3\nNot a version: MAJOR, MINOR and PATCH are whole numbers.\n\n1) sort the list  2) compare two  3) newest in a range  4) new list  5) quit\nchoice> 2\nfirst version> \nCancelled.\n\n1) sort the list  2) compare two  3) newest in a range  4) new list  5) quit\nchoice> 2\nfirst version> 1.0.0\nsecond version> \nCancelled.\n\n1) sort the list  2) compare two  3) newest in a range  4) new list  5) quit\nchoice> 3\nA range: ^1.2.3 (same major), ~1.2.3 (same minor), or comparators such as >=1.2.0 <1.5.0. Pre-releases never match.\nrange> \nCancelled.\n\n1) sort the list  2) compare two  3) newest in a range  4) new list  5) quit\nchoice> 3\nA range: ^1.2.3 (same major), ~1.2.3 (same minor), or comparators such as >=1.2.0 <1.5.0. Pre-releases never match.\nrange> ^1.2\nNot a range: 1.2: a version has three numbers, MAJOR.MINOR.PATCH.\n\n1) sort the list  2) compare two  3) newest in a range  4) new list  5) quit\nchoice> 3\nA range: ^1.2.3 (same major), ~1.2.3 (same minor), or comparators such as >=1.2.0 <1.5.0. Pre-releases never match.\nrange> ^1.2.3-beta\nNot a range: a range here takes releases only, without a pre-release.\n\n1) sort the list  2) compare two  3) newest in a range  4) new list  5) quit\nchoice> 3\nA range: ^1.2.3 (same major), ~1.2.3 (same minor), or comparators such as >=1.2.0 <1.5.0. Pre-releases never match.\nrange> >=3.0.0\n>=3.0.0 means >=3.0.0.\nNo version in the list matches.\n\n1) sort the list  2) compare two  3) newest in a range  4) new list  5) quit\nchoice> 4\nVersions separated by spaces, 2 to 20 of them.\nversions> \nCancelled.\n\n1) sort the list  2) compare two  3) newest in a range  4) new list  5) quit\nchoice> 4\nVersions separated by spaces, 2 to 20 of them.\nversions> 1.0.0\nType 2 to 20 versions.\n\n1) sort the list  2) compare two  3) newest in a range  4) new list  5) quit\nchoice> 4\nVersions separated by spaces, 2 to 20 of them.\nversions> 1.0.0 banana\nNot a version: banana - a version has three numbers, MAJOR.MINOR.PATCH.\n\n1) sort the list  2) compare two  3) newest in a range  4) new list  5) quit\nchoice> 1\n  as text      1.0.0  1.0.0+build.5  1.0.0-alpha  1.0.0-alpha.1  1.0.0-alpha.beta  1.0.0-beta  1.0.0-beta.11  1.0.0-beta.2  1.10.0  1.2.0  1.9.1  2.0.0  2.0.0-rc.1\n  natural sort 1.0.0  1.0.0+build.5  1.0.0-alpha  1.0.0-alpha.1  1.0.0-alpha.beta  1.0.0-beta  1.0.0-beta.2  1.0.0-beta.11  1.2.0  1.9.1  1.10.0  2.0.0  2.0.0-rc.1\n  SemVer       1.0.0-alpha  1.0.0-alpha.1  1.0.0-alpha.beta  1.0.0-beta  1.0.0-beta.2  1.0.0-beta.11  1.0.0  1.0.0+build.5  1.2.0  1.9.1  1.10.0  2.0.0-rc.1  2.0.0\nOf the 78 pairs, text order disagrees with SemVer on 17 and the natural sort on 14.\n  First text disagreement: 1.10.0 and 1.2.0.\n  First natural-sort disagreement: 1.0.0-alpha and 1.0.0.\nText compares character by character, so 1.10.0 comes before 1.9.1. A natural sort reads numbers as numbers, but puts 1.0.0 before 1.0.0-alpha, the shorter text first. SemVer puts every pre-release before its release.\n\n1) sort the list  2) compare two  3) newest in a range  4) new list  5) quit\nchoice> 5\nBye.\n",
      "interpreter": "equal"
    },
    {
      "name": "basic",
      "input": "1\n2\n1.9.1\n1.10.0\n2\n1.0.0-beta.2\n1.0.0-beta.11\n2\n1.0.0-alpha\n1.0.0\n2\n1.0.0-alpha\n1.0.0-alpha.1\n2\n1.0.0-alpha.1\n1.0.0-alpha.beta\n2\n1.0.0\n1.0.0+build.5\n3\n^1.0.0\n3\n~1.9.0\n3\n>=1.2.0 <1.10.0\n3\n^2.0.0\n4\n0.2.3 0.2.10 0.3.0 0.0.3 0.0.4 0.10.0\n3\n^0.2.3\n3\n^0.0.3\n1\n5\n",
      "screen": "== Version sorter ==\nSemantic Versioning: MAJOR.MINOR.PATCH, then a pre-release after - and build metadata after +.\n\n1) sort the list  2) compare two  3) newest in a range  4) new list  5) quit\nchoice> 1\n  as text      1.0.0  1.0.0+build.5  1.0.0-alpha  1.0.0-alpha.1  1.0.0-alpha.beta  1.0.0-beta  1.0.0-beta.11  1.0.0-beta.2  1.10.0  1.2.0  1.9.1  2.0.0  2.0.0-rc.1\n  natural sort 1.0.0  1.0.0+build.5  1.0.0-alpha  1.0.0-alpha.1  1.0.0-alpha.beta  1.0.0-beta  1.0.0-beta.2  1.0.0-beta.11  1.2.0  1.9.1  1.10.0  2.0.0  2.0.0-rc.1\n  SemVer       1.0.0-alpha  1.0.0-alpha.1  1.0.0-alpha.beta  1.0.0-beta  1.0.0-beta.2  1.0.0-beta.11  1.0.0  1.0.0+build.5  1.2.0  1.9.1  1.10.0  2.0.0-rc.1  2.0.0\nOf the 78 pairs, text order disagrees with SemVer on 17 and the natural sort on 14.\n  First text disagreement: 1.10.0 and 1.2.0.\n  First natural-sort disagreement: 1.0.0-alpha and 1.0.0.\nText compares character by character, so 1.10.0 comes before 1.9.1. A natural sort reads numbers as numbers, but puts 1.0.0 before 1.0.0-alpha, the shorter text first. SemVer puts every pre-release before its release.\n\n1) sort the list  2) compare two  3) newest in a range  4) new list  5) quit\nchoice> 2\nfirst version> 1.9.1\nsecond version> 1.10.0\n1.9.1 comes before 1.10.0: minor 9 vs 10.\n  As text the order is the other way round.\n\n1) sort the list  2) compare two  3) newest in a range  4) new list  5) quit\nchoice> 2\nfirst version> 1.0.0-beta.2\nsecond version> 1.0.0-beta.11\n1.0.0-beta.2 comes before 1.0.0-beta.11: pre-release part 2: 2 vs 11 as numbers.\n  As text the order is the other way round.\n\n1) sort the list  2) compare two  3) newest in a range  4) new list  5) quit\nchoice> 2\nfirst version> 1.0.0-alpha\nsecond version> 1.0.0\n1.0.0-alpha comes before 1.0.0: a pre-release comes before its release.\n  As text the order is the other way round.\n\n1) sort the list  2) compare two  3) newest in a range  4) new list  5) quit\nchoice> 2\nfirst version> 1.0.0-alpha\nsecond version> 1.0.0-alpha.1\n1.0.0-alpha comes before 1.0.0-alpha.1: every earlier pre-release part is equal, and more parts come later.\n\n1) sort the list  2) compare two  3) newest in a range  4) new list  5) quit\nchoice> 2\nfirst version> 1.0.0-alpha.1\nsecond version> 1.0.0-alpha.beta\n1.0.0-alpha.1 comes before 1.0.0-alpha.beta: pre-release part 2: a number comes before a word.\n\n1) sort the list  2) compare two  3) newest in a range  4) new list  5) quit\nchoice> 2\nfirst version> 1.0.0\nsecond version> 1.0.0+build.5\n1.0.0 and 1.0.0+build.5 rank the same: build metadata is ignored.\n  As text they differ, so a text sort still puts one first.\n\n1) sort the list  2) compare two  3) newest in a range  4) new list  5) quit\nchoice> 3\nA range: ^1.2.3 (same major), ~1.2.3 (same minor), or comparators such as >=1.2.0 <1.5.0. Pre-releases never match.\nrange> ^1.0.0\n^1.0.0 means >=1.0.0 <2.0.0.\nMatching: 1.0.0  1.0.0+build.5  1.2.0  1.9.1  1.10.0. Newest: 1.10.0.\n\n1) sort the list  2) compare two  3) newest in a range  4) new list  5) quit\nchoice> 3\nA range: ^1.2.3 (same major), ~1.2.3 (same minor), or comparators such as >=1.2.0 <1.5.0. Pre-releases never match.\nrange> ~1.9.0\n~1.9.0 means >=1.9.0 <1.10.0.\nMatching: 1.9.1. Newest: 1.9.1.\n\n1) sort the list  2) compare two  3) newest in a range  4) new list  5) quit\nchoice> 3\nA range: ^1.2.3 (same major), ~1.2.3 (same minor), or comparators such as >=1.2.0 <1.5.0. Pre-releases never match.\nrange> >=1.2.0 <1.10.0\n>=1.2.0 <1.10.0 means >=1.2.0 <1.10.0.\nMatching: 1.2.0  1.9.1. Newest: 1.9.1.\n\n1) sort the list  2) compare two  3) newest in a range  4) new list  5) quit\nchoice> 3\nA range: ^1.2.3 (same major), ~1.2.3 (same minor), or comparators such as >=1.2.0 <1.5.0. Pre-releases never match.\nrange> ^2.0.0\n^2.0.0 means >=2.0.0 <3.0.0.\nMatching: 2.0.0. Newest: 2.0.0.\n\n1) sort the list  2) compare two  3) newest in a range  4) new list  5) quit\nchoice> 4\nVersions separated by spaces, 2 to 20 of them.\nversions> 0.2.3 0.2.10 0.3.0 0.0.3 0.0.4 0.10.0\n6 versions.\n\n1) sort the list  2) compare two  3) newest in a range  4) new list  5) quit\nchoice> 3\nA range: ^1.2.3 (same major), ~1.2.3 (same minor), or comparators such as >=1.2.0 <1.5.0. Pre-releases never match.\nrange> ^0.2.3\n^0.2.3 means >=0.2.3 <0.3.0.\nMatching: 0.2.3  0.2.10. Newest: 0.2.10.\n\n1) sort the list  2) compare two  3) newest in a range  4) new list  5) quit\nchoice> 3\nA range: ^1.2.3 (same major), ~1.2.3 (same minor), or comparators such as >=1.2.0 <1.5.0. Pre-releases never match.\nrange> ^0.0.3\n^0.0.3 means >=0.0.3 <0.0.4.\nMatching: 0.0.3. Newest: 0.0.3.\n\n1) sort the list  2) compare two  3) newest in a range  4) new list  5) quit\nchoice> 1\n  as text      0.0.3  0.0.4  0.10.0  0.2.10  0.2.3  0.3.0\n  natural sort 0.0.3  0.0.4  0.2.3  0.2.10  0.3.0  0.10.0\n  SemVer       0.0.3  0.0.4  0.2.3  0.2.10  0.3.0  0.10.0\nOf the 15 pairs, text order disagrees with SemVer on 4 and the natural sort on 0.\n  First text disagreement: 0.2.3 and 0.2.10.\nText compares character by character, so 1.10.0 comes before 1.9.1. A natural sort reads numbers as numbers, but puts 1.0.0 before 1.0.0-alpha, the shorter text first. SemVer puts every pre-release before its release.\n\n1) sort the list  2) compare two  3) newest in a range  4) new list  5) quit\nchoice> 5\nBye.\n",
      "interpreter": "equal"
    }
  ],
  "builtOn": [
    {
      "slug": "version-string-ordering",
      "caseId": "275-version-string-ordering",
      "title": "Version string ordering — correct until the tenth release"
    }
  ],
  "updated": "2026-10-16"
}
