Skip to content

Latest commit

 

History

History
256 lines (215 loc) · 9.22 KB

semver.org

File metadata and controls

256 lines (215 loc) · 9.22 KB

Semantic version comparison functions for Elvish

Comparison of semantic version numbers, as described in the Semantic Versioning specification.

This file is written in literate programming style, to make it easy to explain. See $name.elv for the generated file.

Table of Contents

Usage

Install the elvish-modules package using epm:

use epm
epm:install github.com/zzamboni/elvish-modules

In your rc.elv, load this module:

use github.com/zzamboni/elvish-modules/semver

The semver:cmp function receives two version numbers and returns -1, 0 or 1 depending on whether the first version number is older (“less”), the same or newer (“more”) than the second. It uses the rules as described in the Semantic Versioning specification.

vers = [
  1.0.1 1.0.0 2.0.0 2.1.0 2.1.1 1.0.0-alpha
  1.0.0-alpha.beta 1.0.0-alpha.1 1.0.0-beta
  1.0.0-beta.2 1.0.0-beta.11 1.0.0-rc.1 1.0.0
]
range (- (count $vers) 1) | each [i]{
  v1 v2 = $vers[$i (+ $i 1)]
  echo semver:cmp $v1 $v2
  semver:cmp $v1 $v2
}
semver:cmp 1.0.1 1.0.0
▶ -1
semver:cmp 1.0.0 2.0.0
▶ 1
semver:cmp 2.0.0 2.1.0
▶ 1
semver:cmp 2.1.0 2.1.1
▶ 1
semver:cmp 2.1.1 1.0.0-alpha
▶ -1
semver:cmp 1.0.0-alpha 1.0.0-alpha.beta
▶ 1
semver:cmp 1.0.0-alpha.beta 1.0.0-alpha.1
▶ -1
semver:cmp 1.0.0-alpha.1 1.0.0-beta
▶ 1
semver:cmp 1.0.0-beta 1.0.0-beta.2
▶ 1
semver:cmp 1.0.0-beta.2 1.0.0-beta.11
▶ -1
semver:cmp 1.0.0-beta.11 1.0.0-rc.1
▶ 1
semver:cmp 1.0.0-rc.1 1.0.0
▶ 1

The semver:eq, semver:not-eq, semver:<, semver:<=, semver:> and semver:>= functions behave just like their numeric or string versions, but with version numbers. They all use semver:cmp to do the comparison.

semver:<      1.0.0 2.0.0 2.1.0
semver:<      1.0.0-alpha 1.0.0 2.1.0
semver:<=     1.0.0 1.0.0 2.1.0
semver:>      1.0.0 1.0.0-rc1 0.9.0
semver:>=     1.0.0-rc1 1.0.0-rc1 0.9.0
semver:not-eq 1.0.0 1.0.1 2.0.0
▶ $true
▶ $true
▶ $true
▶ $true
▶ $true
▶ $true

Implementation

We start by including some necessary libraries.

use re
use str
use builtin
use ./util

Support functions

The -signed-compare function compares two values using a function which takes two values and returns -1, 0 or -1 to represent the order of the two values.

fn -signed-compare {|ltfn v1 v2|
  util:cond [
    { $ltfn $v1 $v2 }  1
    { $ltfn $v2 $v1 } -1
    :else              0
  ]
}

The -part-compare function receives two parsed values (as returned by semver:parse and returns their order according to the first component that differs (0 is both are equal).

fn -part-compare {|v1 v2|
  each {|k|
    var comp = (-signed-compare $'<~' $v1[$k] $v2[$k])
    if (!= $comp 0) {
      put $comp
      return
    }
  } [major minor patch]
  put 0
}

Parsing and validating version numbers

We use the regular expression provided in the SemVer specification to determine if a string is a valid version number. We have a “non-strict” variation which allows the string to start with a v or a V.

var semver-regex = '^(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)(?:-(?P<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$'
var semver-regex-nonstrict = '^[vV]?(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)(?:-(?P<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$'

In one concession to common usage, the &allow-v option (which can be set as default by assigning semver:allow-v-default = $true) allows the string to start with a v or a V.

var allow-v-default = $false

The get-regex function returns the regex to use based on the &allow-v option and the $allow-v-default variable.

fn get-regex {|&allow-v=$nil|
  set allow-v = (if (not-eq $allow-v $nil) { put $allow-v } else { put $allow-v-default })
  if $allow-v {
    put $semver-regex-nonstrict
  } else {
    put $semver-regex
  }
}

The semver:validate function checks whether the string is a valid semantic version number. If it’s invalid, an exception is thrown.

fn validate {|string &allow-v=$nil|
  if (not (re:match (get-regex &allow-v=$allow-v) $string)) {
    fail "Invalid SemVer string: "$string
  }
}

The semver:parse function returns a map containing the corresponding elements if the string is valid, or $nil otherwise. If the PRERELEASE or BUILDMETADATA parts are not present, those fields are set to $nil.

fn parse {|string &allow-v=$nil|
  if (validate $string &allow-v=$allow-v) {
    var parts = (re:find (get-regex &allow-v=$allow-v) $string)[groups]
    put [
      &major=  $parts[1][text]
      &minor=  $parts[2][text]
      &patch=  $parts[3][text]
      &prerel= (if (!=s $parts[4][text] '') { put $parts[4][text] } else { put $nil })
      &build=  (if (!=s $parts[5][text] '') { put $parts[5][text] } else { put $nil })
    ]
  } else {
    put $nil
  }
}

Main comparison function

The semver:cmp function receives two version numbers in SemVer format and returns their order as -1, 0 or 1. The algorithm as per the spec is as follows:

  • If the MAJOR.MINOR.PATCH parts of the two version numbers differ, return their order
  • Otherwise:
    • If one of them has a PRERELEASE part but the other not, the one without the label is higher.
    • If both have a PRERELEASE part, return the order of the labels.
  • The BUILDMETADATA part is ignored in any case.
fn cmp {|v1 v2 &allow-v=$nil|
  validate $v1 &allow-v=$allow-v
  validate $v2 &allow-v=$allow-v
  var p1 = (parse $v1 &allow-v=$allow-v)
  var p2 = (parse $v2 &allow-v=$allow-v)
  var comp = (-part-compare $p1 $p2)
  if (!= $comp 0) {
    # If there is a difference in the MAJOR.MINOR.PATCH part, that's the result
    put $comp
  } else {
    # Otherwise, check the prerelease strings
    var prerel1 prerel2 = $p1[prerel] $p2[prerel]
    if (and $prerel1 $prerel2) {
      # If both prerel strings are present, compare them
      -signed-compare $'<s~' $prerel1 $prerel2
    } else {
      # Otherwise, the one without a string is "more than" the other
      -signed-compare {|v1 v2| and $v1 (not $v2) } $prerel1 $prerel2
    }
  }
}

Comparing lists of version numbers

The -seq-compare function receives a list of version numbers, an operator and an expected value. All neighboring pairs in the list are compared using semver:cmp, and the result is compared against the expected using the operator. The function returns $true if the list is empty, or if all the pairs satisfy the condition. This allows us to implement all the list-comparison functions below just by modifying the operator and the expected value.

fn -seq-compare {|op expected @vers &allow-v=$nil|
  var res = $true
  var last = $false
  each {|v|
    if $last {
      set res = (and $res ($op (cmp $last $v &allow-v=$allow-v) $expected))
    }
    set last = $v
  } $vers
  put $res
}

All of the user-facing functions are implemented by passing the corresponding functions and values to -seq-compare.

fn '<'    {|@vers &allow-v=$nil| -seq-compare $builtin:eq~      1 $@vers &allow-v=$allow-v }
fn '>'    {|@vers &allow-v=$nil| -seq-compare $builtin:eq~     -1 $@vers &allow-v=$allow-v }
fn eq     {|@vers &allow-v=$nil| -seq-compare $builtin:eq~      0 $@vers &allow-v=$allow-v }
fn not-eq {|@vers &allow-v=$nil| -seq-compare $builtin:not-eq~  0 $@vers &allow-v=$allow-v }
fn '<='   {|@vers &allow-v=$nil| -seq-compare $builtin:not-eq~ -1 $@vers &allow-v=$allow-v }
fn '>='   {|@vers &allow-v=$nil| -seq-compare $builtin:not-eq~  1 $@vers &allow-v=$allow-v }