jsoncomparisonjqsemantic-versioning

Semver comparison using JQ


I have a array that looks like this:

[
  {
    "id": 1,
    "version": "2.3.4"
  },
  {
    "id": 2,
    "version": "1.4.4"
  },
  {
    "id": 3,
    "version": "0.0.4"
  },
  {
    "id": 4,
    "version": "1.3.4"
  },
]

And I need to get all the objects where the version is "1.2.0". I am interested in a built in way using JQ but I cannot find anything related. Maybe it does not exist?

I know I could do some ugly regex hack here, but what would be the right way to solve this so I can easily swap my condition so if instead of 1.2.0 maybe in a short time in the future lets say I want the objects with version greater than 1.2.7 for instance?


Solution

  • You always have the option of parsing and implementing the comparisons.

    def _parse_semver($with_op):
        if type == "string" then
            capture(if $with_op then "(?<op>[~])?" else "" end
            + "(?<major>\\d+)\\.(?<minor>\\d+)(?:\\.(?<patch>\\d+))?"
            + "(?:-(?<prerelease>[A-Z0-9]+(?:\\.[A-Z0-9]+)*))?"
            + "(?:\\+(?<build>[A-Z0-9]+(?:\\.[A-Z0-9]+)*))?"; "i")
            | (.major, .minor, .patch) |= (tonumber? // 0)
        elif type == "object" and ([has(("major,minor,patch,prerelease,build"/",")[])]|all) then .
        else empty end;
    def parse_semver: _parse_semver(false);
    def cmp_semver($other): parse_semver as $a | ($other|_parse_semver(true)) as $b |
        def _cmp($other): if . == $other then 0 elif . > $other then 1 else -1 end;
        def _cmp_dotted($other):
            if . == null then 1
            elif $other == null then -1
            else
                reduce ([split("."), ($other|split("."))] | transpose[]) as [$a, $b] (0;
                    if . != 0 then .
                    elif $a == null then -1
                    elif $b == null then 1
                    else
                        ($a|test("^\\d+$")) as $anum | ($b|test("^\\d+$")) as $bnum |
                        if [$anum,$bnum] == [true,true] then $a | tonumber | _cmp($b | tonumber)
                        elif $anum then -1
                        elif $bnum then 1
                        else $a | _cmp($b) end
                    end
                )
            end;
        # slightly modified version of https://semver.org/#spec-item-11
        if $a.major != $b.major then
            if $a.major > $b.major then 1 else -1 end
        elif $a.minor != $b.minor then
            if $a.minor > $b.minor then 1 else -1 end
        elif $a.patch != $b.patch then
            if $a.patch > $b.patch then 1 else -1 end
        elif $b.op == "~" then
            0
        elif $a.prerelease != $b.prerelease then
            ($a.prerelease | _cmp_dotted($b.prerelease))
        elif $a.build != $b.build then
            ($a.build | _cmp_dotted($b.build))
        else
            0
        end;
    def cmp_semver($first; $second): $first | cmp_semver($second);
    

    Then utilize the new comparison functions:

    $ jq 'map(select(.version | cmp_semver("1.2.0") > 0))' input.json
    [                                                                                                                                     
      {                                                                                                                                   
        "id": 1,                                                                                                                          
        "version": "2.3.4"                                                                                                                
      },                                                                                                                                  
      {                                                                                                                                   
        "id": 2,                                                                                                                          
        "version": "1.4.4"                                                                                                                
      },                                                                                                                                  
      {                                                                                                                                   
        "id": 4,                                                                                                                          
        "version": "1.3.4"                                                                                                                
      }                                                                                                                                   
    ]
    
    $ jq -rn '("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-rc.1<1.0.0"/"<") as $input |
        range($input | length-1) |
        "cmp_semver(\($input[.]|tojson);\t\($input[.+1]|tojson))\t-> "
        + "\(cmp_semver($input[.]; $input[.+1]))"'
    cmp_semver("1.0.0-alpha";       "1.0.0-alpha.1")        -> -1                                                                         
    cmp_semver("1.0.0-alpha.1";     "1.0.0-alpha.beta")     -> -1                                                                         
    cmp_semver("1.0.0-alpha.beta";  "1.0.0-beta")   -> -1                                                                                 
    cmp_semver("1.0.0-beta";        "1.0.0-beta.2") -> -1                                                                                 
    cmp_semver("1.0.0-beta.2";      "1.0.0-beta.11")        -> -1                                                                         
    cmp_semver("1.0.0-beta.11";     "1.0.0-rc.1")   -> -1                                                                                 
    cmp_semver("1.0.0-rc.1";        "1.0.0")        -> -1                                                                                 
    

    At first I wasn't sure if it was possible to use arrays as the semver key, but it appears it is possible, but requires some additional data points to sort on.

    def semver_key: parse_semver | [
        .major, .minor, .patch,
        .prerelease==null, ((.prerelease//"")/"."|map(tonumber? //.)),
        .build==null, ((.build//"")/"."|map(tonumber? //.))
    ];
    

    This allows you to sort by the versions.

    $ jq -rn '
    ("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-rc.1<1.0.0"/"<") as $input |
    [$input, ($input | sort_by(semver_key))] | transpose[] | "\(.[0])\t\(.[1])"
    '
    1.0.0-alpha     1.0.0-alpha                                                                                                           
    1.0.0-alpha.1   1.0.0-alpha.1                                                                                                         
    1.0.0-alpha.beta        1.0.0-alpha.beta                                                                                              
    1.0.0-beta      1.0.0-beta                                                                                                            
    1.0.0-beta.2    1.0.0-beta.2                                                                                                          
    1.0.0-beta.11   1.0.0-beta.11                                                                                                         
    1.0.0-rc.1      1.0.0-rc.1                                                                                                            
    1.0.0   1.0.0                                                                                                                         
    

    Assuming this works, this could simplify the cmp_semver/1 function greatly.

    def cmp_semver2($other): semver_key as $a | ($other|semver_key) as $b |
        if $a == $b then 0
        elif $a > $b then 1
        else -1 end;