Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: omichelsen/compare-versions
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v4.0.2
Choose a base ref
...
head repository: omichelsen/compare-versions
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v4.1.0
Choose a head ref
  • 2 commits
  • 10 files changed
  • 1 contributor

Commits on Nov 12, 2021

  1. feat: add satisfies() to test for range match

    Using ~ and ^ to verify if a version is in the range.
    omichelsen committed Nov 12, 2021
    Copy the full SHA
    aad9932 View commit details
  2. 4.1.0

    omichelsen committed Nov 12, 2021
    Copy the full SHA
    e185fea View commit details
Showing with 208 additions and 56 deletions.
  1. +3 −0 CHANGELOG.md
  2. +18 −8 README.md
  3. +1 −1 bower.json
  4. +22 −9 index.d.ts
  5. +76 −26 index.js
  6. +23 −6 index.mjs
  7. +2 −2 package-lock.json
  8. +1 −1 package.json
  9. +59 −0 test/satisfies.mjs
  10. +3 −3 test/sort.mjs
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Changelog

## [4.1.0](https://github.com/omichelsen/compare-versions/releases/tag/v4.1.0) - 2021-11-12
- Add `satisfies()` function to test for range match

## [4.0.2](https://github.com/omichelsen/compare-versions/releases/tag/v4.0.2) - 2021-11-12
- Fix wildcard comparisons.

26 changes: 18 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -49,14 +49,6 @@ const sorted = versions.sort(compareVersions);
'1.5.19'
]
*/
const sortDescending = versions.sort(compareVersions).reverse();
/*
[
'1.5.19'
'1.5.5',
'1.2.3',
]
*/
```

### "Human Readable" Compare
@@ -73,6 +65,24 @@ compare('10.1.1', '10.2.2', '<='); // true
compare('10.1.1', '10.2.2', '>='); // false
```

### Version ranges

The `satisfies` function accepts a range to compare, compatible with [npm package versioning](https://docs.npmjs.com/cli/v6/using-npm/semver):

```js
import { satisfies } from 'compare-versions';

satisfies('10.0.1', '~10.0.0'); // true
satisfies('10.1.0', '~10.0.0'); // false
satisfies('10.1.2', '^10.0.0'); // true
satisfies('11.0.0', '^10.0.0'); // false
satisfies('10.1.8', '>10.0.4'); // true
satisfies('10.0.1', '=10.0.1'); // true
satisfies('10.1.1', '<10.2.2'); // true
satisfies('10.1.1', '<=10.2.2'); // true
satisfies('10.1.1', '>=10.2.2'); // false
```

### Validate version numbers

Applies the same ruleset used comparing version numbers and returns a boolean:
2 changes: 1 addition & 1 deletion bower.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "compare-versions",
"version": "4.0.2",
"version": "4.1.0",
"description": "Compare semver version strings to find greater, equal or lesser.",
"main": "index.js",
"authors": ["Ole Bjørn Michelsen <ole@michelsen.dk>"],
31 changes: 22 additions & 9 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -17,7 +17,7 @@ declare const compareVersions: {

/**
* Compare [semver](https://semver.org/) version strings using the specified operator.
*
*
* @param firstVersion First version to compare
* @param secondVersion Second version to compare
* @param operator Allowed arithmetic operator to use
@@ -32,15 +32,15 @@ declare const compareVersions: {
* compareVersions.compare('10.1.1', '10.2.2', '>='); // return false
* ```
*/
compare(
firstVersion: string,
secondVersion: string,
compare(
firstVersion: string,
secondVersion: string,
operator: compareVersions.CompareOperator
): boolean;

/**
* Validate [semver](https://semver.org/) version strings.
*
*
* @param version Version number to validate
* @returns `true` if the version number is a valid semver version number, `false` otherwise.
*
@@ -51,9 +51,22 @@ declare const compareVersions: {
* compareVersions.validate('foo'); // return false
* ```
*/
validate(
version: string
): boolean;
validate(version: string): boolean;

/**
* Match [npm semver](https://docs.npmjs.com/cli/v6/using-npm/semver) version range.
*
* @param version Version number to match
* @param range Range pattern for version
* @returns `true` if the version number is within the range, `false` otherwise.
*
* @example
* ```
* satisfies('1.1.0', '^1.0.0'); // return true
* satisfies('1.1.0', '~1.0.0'); // return false
* ```
*/
satisfies(version: string, range: string): boolean;
};

export = compareVersions;
export = compareVersions;
102 changes: 76 additions & 26 deletions index.js
Original file line number Diff line number Diff line change
@@ -8,9 +8,9 @@
} else {
root.compareVersions = factory();
}
}(this, function () {

var semver = /^v?(?:\d+)(\.(?:[x*]|\d+)(\.(?:[x*]|\d+)(\.(?:[x*]|\d+))?(?:-[\da-z\-]+(?:\.[\da-z\-]+)*)?(?:\+[\da-z\-]+(?:\.[\da-z\-]+)*)?)?)?$/i;
})(this, function () {
var semver =
/^[v^~<>=]*?(\d+)(?:\.([x*]|\d+)(?:\.([x*]|\d+)(?:\.([x*]|\d+))?(?:-([\da-z\-]+(?:\.[\da-z\-]+)*))?(?:\+[\da-z\-]+(?:\.[\da-z\-]+)*)?)?)?$/i;

function indexOrEnd(str, q) {
return str.indexOf(q) === -1 ? str.length : str.indexOf(q);
@@ -25,20 +25,45 @@
}

function tryParse(v) {
return isNaN(Number(v)) ? v : Number(v);
var n = parseInt(v, 10);
return isNaN(n) ? v : n;
}

function validate(version) {
if (typeof version !== 'string') {
function validateAndParse(v) {
if (typeof v !== 'string') {
throw new TypeError('Invalid argument expected string');
}
if (!semver.test(version)) {
throw new Error('Invalid argument not valid semver (\''+version+'\' received)');
var match = v.match(semver);
if (!match) {
throw new Error(
"Invalid argument not valid semver ('" + v + "' received)"
);
}
match.shift();
return match;
}

function forceType(a, b) {
return typeof a !== typeof b ? [String(a), String(b)] : [a, b];
}

function compareStrings(a, b) {
var [ap, bp] = forceType(tryParse(a), tryParse(b));
if (ap > bp) return 1;
if (ap < bp) return -1;
return 0;
}

function compareSegments(a, b) {
for (var i = 0; i < Math.max(a.length, b.length); i++) {
var r = compareStrings(a[i] || 0, b[i] || 0);
if (r !== 0) return r;
}
return 0;
}

function compareVersions(v1, v2) {
[v1, v2].forEach(validate);
[v1, v2].forEach(validateAndParse);

var s1 = split(v1);
var s2 = split(v2);
@@ -59,8 +84,16 @@
var p2 = sp2.split('.').map(tryParse);

for (i = 0; i < Math.max(p1.length, p2.length); i++) {
if (p1[i] === undefined || typeof p2[i] === 'string' && typeof p1[i] === 'number') return -1;
if (p2[i] === undefined || typeof p1[i] === 'string' && typeof p2[i] === 'number') return 1;
if (
p1[i] === undefined ||
(typeof p2[i] === 'string' && typeof p1[i] === 'number')
)
return -1;
if (
p2[i] === undefined ||
(typeof p1[i] === 'string' && typeof p2[i] === 'number')
)
return 1;

if (p1[i] > p2[i]) return 1;
if (p2[i] > p1[i]) return -1;
@@ -70,36 +103,34 @@
}

return 0;
};
}

var allowedOperators = [
'>',
'>=',
'=',
'<',
'<='
];
var allowedOperators = ['>', '>=', '=', '<', '<='];

var operatorResMap = {
'>': [1],
'>=': [0, 1],
'=': [0],
'<=': [-1, 0],
'<': [-1]
'<': [-1],
};

function validateOperator(op) {
if (typeof op !== 'string') {
throw new TypeError('Invalid operator type, expected string but got ' + typeof op);
throw new TypeError(
'Invalid operator type, expected string but got ' + typeof op
);
}
if (allowedOperators.indexOf(op) === -1) {
throw new TypeError('Invalid operator, expected one of ' + allowedOperators.join('|'));
throw new TypeError(
'Invalid operator, expected one of ' + allowedOperators.join('|')
);
}
}

compareVersions.validate = function(version) {
compareVersions.validate = function (version) {
return typeof version === 'string' && semver.test(version);
}
};

compareVersions.compare = function (v1, v2, operator) {
// Validate operator
@@ -109,7 +140,26 @@
// a simple map can be used to replace switch
var res = compareVersions(v1, v2);
return operatorResMap[operator].indexOf(res) > -1;
}
};

compareVersions.satisfies = function (v, r) {
// if no range operator then "="
var match = r.match(/^([<>=~^]+)/);
var op = match ? match[1] : '=';

// if gt/lt/eq then operator compare
if (op !== '^' && op !== '~') return compareVersions.compare(v, r, op);

// else range of either "~" or "^" is assumed
var [v1, v2, v3] = validateAndParse(v);
var [m1, m2, m3] = validateAndParse(r);
if (compareStrings(v1, m1) !== 0) return false;
if (op === '^') {
return compareSegments([v2, v3], [m2, m3]) >= 0;
}
if (compareStrings(v2, m2) !== 0) return false;
return compareStrings(v3, m3) >= 0;
};

return compareVersions;
}));
});
29 changes: 23 additions & 6 deletions index.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export default function compareVersions(v1, v2) {
// validate input and split into segments
const n1 = validateAndParseVersion(v1);
const n2 = validateAndParseVersion(v2);
const n1 = validateAndParse(v1);
const n2 = validateAndParse(v2);

// pop off the patch
const p1 = n1.pop();
@@ -21,8 +21,7 @@ export default function compareVersions(v1, v2) {
return 0;
}

export const validate = (version) =>
typeof version === 'string' && semver.test(version);
export const validate = (v) => typeof v === 'string' && semver.test(v);

export const compare = (v1, v2, operator) => {
// validate input operator
@@ -35,10 +34,28 @@ export const compare = (v1, v2, operator) => {
return operatorResMap[operator].includes(res);
};

export const satisfies = (v, r) => {
// if no range operator then "="
const op = r.match(/^([<>=~^]+)/)?.[1] || '=';

// if gt/lt/eq then operator compare
if (op !== '^' && op !== '~') return compare(v, r, op);

// else range of either "~" or "^" is assumed
const [v1, v2, v3] = validateAndParse(v);
const [m1, m2, m3] = validateAndParse(r);
if (compareStrings(v1, m1) !== 0) return false;
if (op === '^') {
return compareSegments([v2, v3], [m2, m3]) >= 0;
}
if (compareStrings(v2, m2) !== 0) return false;
return compareStrings(v3, m3) >= 0;
};

const semver =
/^v?(\d+)(?:\.([x*]|\d+)(?:\.([x*]|\d+)(?:\.([x*]|\d+))?(?:-([\da-z\-]+(?:\.[\da-z\-]+)*))?(?:\+[\da-z\-]+(?:\.[\da-z\-]+)*)?)?)?$/i;
/^[v^~<>=]*?(\d+)(?:\.([x*]|\d+)(?:\.([x*]|\d+)(?:\.([x*]|\d+))?(?:-([\da-z\-]+(?:\.[\da-z\-]+)*))?(?:\+[\da-z\-]+(?:\.[\da-z\-]+)*)?)?)?$/i;

const validateAndParseVersion = (v) => {
const validateAndParse = (v) => {
if (typeof v !== 'string') {
throw new TypeError('Invalid argument expected string');
}
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "compare-versions",
"version": "4.0.2",
"version": "4.1.0",
"description": "Compare semver version strings to find greater, equal or lesser.",
"repository": {
"type": "git",
Loading