-
-
Notifications
You must be signed in to change notification settings - Fork 5.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore: use winget-specific version compare algorithm
- Loading branch information
Showing
3 changed files
with
174 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,144 @@ | ||
/** | ||
* Comparing versions with winget's version comparator. | ||
* | ||
* See https://github.com/microsoft/winget-cli/blob/ae566c7bf21cfcc75be7ec30e4036a30eede8396/src/AppInstallerSharedLib/Versions.cpp for original implementation. | ||
* | ||
* @module | ||
*/ | ||
|
||
/** | ||
* Compares two strings representing version numbers lexicographically and returns an integer value. | ||
* | ||
* @param {string} v1 - The first version to compare | ||
* @param {string} v2 - The second version to compare | ||
* @returns {number} -1 if v1 is smaller than v2, 1 if v1 is larger than v2, 0 if v1 and v2 are equal | ||
* @example | ||
* compareVersion('1.2.3', '1.2.4') // returns -1 because numeric part of first version is smaller than the numeric part of second version. | ||
*/ | ||
function compareVersion(v1, v2) { | ||
// https://github.com/microsoft/winget-cli/blob/ae566c7bf21cfcc75be7ec30e4036a30eede8396/src/AppInstallerSharedLib/Versions.cpp#L109-L173 | ||
const v1Trimmed = trimPrefix(v1) | ||
const v2Trimmed = trimPrefix(v2) | ||
|
||
const parts1 = v1Trimmed.split('.') | ||
const parts2 = v2Trimmed.split('.') | ||
|
||
for (let i = 0; i < Math.min(parts1.length, parts2.length); i++) { | ||
const part1 = parts1[i] | ||
const part2 = parts2[i] | ||
|
||
const compare = compareVersionPart(part1, part2) | ||
if (compare !== 0) { | ||
return compare | ||
} | ||
} | ||
|
||
if (parts1.length === parts2.length) { | ||
return 0 | ||
} | ||
|
||
// ignore .0s at the end | ||
if (parts1.length > parts2.length) { | ||
for (let i = parts2.length; i < parts1.length; i++) { | ||
if (parts1[i].trim() !== '0') { | ||
return 1 | ||
} | ||
} | ||
} else if (parts1.length < parts2.length) { | ||
for (let i = parts1.length; i < parts2.length; i++) { | ||
if (parts2[i].trim() !== '0') { | ||
return -1 | ||
} | ||
} | ||
} | ||
|
||
return 0 | ||
} | ||
|
||
/** | ||
* Removes all leading non-digit characters from a version number string | ||
* if there is a digit before the split character, or no split characters exist. | ||
* | ||
* @param {string} version The version number string to trim | ||
* @returns {string} The version number string with all leading non-digit characters removed | ||
*/ | ||
function trimPrefix(version) { | ||
// https://github.com/microsoft/winget-cli/blob/ae566c7bf21cfcc75be7ec30e4036a30eede8396/src/AppInstallerSharedLib/Versions.cpp#L66 | ||
// If there is a digit before the split character, or no split characters exist, trim off all leading non-digit characters | ||
|
||
const digitPos = version.match(/(\d.*)/) | ||
const splitPos = version.match(/\./) | ||
if (digitPos && (splitPos == null || digitPos.index < splitPos.index)) { | ||
// there is digit before the split character so strip off all leading non-digit characters | ||
return version.slice(digitPos.index) | ||
} | ||
return version | ||
} | ||
|
||
/** | ||
* Compares two strings representing version number parts lexicographically and returns an integer value. | ||
* | ||
* @param {string} part1 - The first version part to compare | ||
* @param {string} part2 - The second version part to compare | ||
* @returns {number} -1 if part1 is smaller than part2, 1 if part1 is larger than part2, 0 if part1 and part2 are equal | ||
* @example | ||
* compareVersionPart('3', '4') // returns -1 because numeric part of first part is smaller than the numeric part of second part. | ||
*/ | ||
function compareVersionPart(part1, part2) { | ||
// https://github.com/microsoft/winget-cli/blob/ae566c7bf21cfcc75be7ec30e4036a30eede8396/src/AppInstallerSharedLib/Versions.cpp#L324-L352 | ||
const [, numericString1, other1] = part1.trim().match(/^(\d*)(.*)$/) | ||
const [, numericString2, other2] = part2.trim().match(/^(\d*)(.*)$/) | ||
const numeric1 = parseInt(numericString1 || '0', 10) | ||
const numeric2 = parseInt(numericString2 || '0', 10) | ||
|
||
if (numeric1 < numeric2) { | ||
return -1 | ||
} else if (numeric1 > numeric2) { | ||
return 1 | ||
} | ||
// numeric1 === numeric2 | ||
|
||
const otherFolded1 = (other1 ?? '').toLowerCase() | ||
const otherFolded2 = (other2 ?? '').toLowerCase() | ||
|
||
if (otherFolded1.length !== 0 && otherFolded2.length === 0) { | ||
return -1 | ||
} else if (otherFolded1.length === 0 && otherFolded2.length !== 0) { | ||
return 1 | ||
} | ||
|
||
if (otherFolded1 < otherFolded2) { | ||
return -1 | ||
} else if (otherFolded1 > otherFolded2) { | ||
return 1 | ||
} | ||
|
||
return 0 | ||
} | ||
|
||
/** | ||
* Finds the largest version number lexicographically from an array of strings representing version numbers and returns it as a string. | ||
* | ||
* @param {string[]} versions - The array of version numbers to compare | ||
* @returns {string|undefined} The largest version number as a string, or undefined if the array is empty | ||
* @example | ||
* latest(['1.2.3', '1.2.4', '1.3', '2.0']) // returns '2.0' because it is the largest version number and pre-release versions are excluded. | ||
* latest(['1.2.3', '1.2.4', '1.3', '2.0']) // returns '2.0' because pre-release versions are included but none of them are present in the array. | ||
* latest(['1.2.3', '1.2.4', '1.3-alpha', '2.0-beta']) // returns '1.2.4' because pre-release versions are excluded and it is the largest version number among the remaining ones. | ||
*/ | ||
function latest(versions) { | ||
const len = versions.length | ||
if (len === 0) { | ||
return | ||
} | ||
|
||
let version = versions[0] | ||
for (let i = 1; i < len; i++) { | ||
if (compareVersion(version, versions[i]) < 0) { | ||
version = versions[i] | ||
} | ||
} | ||
return version | ||
} | ||
|
||
export { latest, compareVersion } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import { test, given } from 'sazerac' | ||
import { compareVersion } from './version.js' | ||
|
||
describe('Winget Version helpers', function () { | ||
test(compareVersion, () => { | ||
given('1', '2').expect(-1) | ||
given('1.0.0', '2.0.0').expect(-1) | ||
given('0.0.1', '0.0.2').expect(-1) | ||
given('0.0.1-alpha', '0.0.2-alpha').expect(-1) | ||
given('0.0.1-beta', '0.0.2-alpha').expect(-1) | ||
given('0.0.1-beta', '0.0.2-alpha').expect(-1) | ||
given('13.9.8', '14.1').expect(-1) | ||
|
||
given('1.0', '1.0.0').expect(0) | ||
|
||
// Ensure whitespace doesn't affect equality | ||
given('1.0', '1.0 ').expect(0) | ||
given('1.0', '1. 0').expect(0) | ||
|
||
// Ensure versions with preambles are sorted correctly | ||
given('1.0', 'Version 1.0').expect(0) | ||
given('foo1', 'bar1').expect(0) | ||
given('v0.0.1', '0.0.2').expect(-1) | ||
given('v0.0.1', 'v0.0.2').expect(-1) | ||
given('1.a2', '1.b1').expect(-1) | ||
given('alpha', 'beta').expect(-1) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters