debsort/debsort.js

176 lines
5.6 KiB
JavaScript

// aiden@cmp.bz
function compareDebianVersions(a, b) {
if (a == b) {
return 0;
}
// Helper function to compare two numbers (like the `<=>` operator from other languages):
const compare = (a, b) => Number(a != b && (a < b ? -1 : 1));
// Helper function to make two arrays the same length by padding the right of the shorter array with empty strings:
const ensureSameLength = (arr1, arr2) => {
while (arr1.length < arr2.length) {
arr1.push("");
}
while (arr2.length < arr1.length) {
arr2.push("");
}
}
/*
Breaks a string into an object describing:
- epoch
- version
- revision
It's easier to work with.
*/
let parseString = str => {
// `epoch` and `revision` default to 0.
let epoch = 0;
if (str.includes(":")) {
epoch = str.substring(0, str.indexOf(":"));
epoch = parseInt(epoch);
}
let version = str.substr(str.indexOf(":") + 1);
let revision = "0";
let finalDash = version.lastIndexOf("-");
if (finalDash != -1) {
revision = version.substr(finalDash + 1);
version = version.substring(0, finalDash);
}
if (!version) {
throw new Error("`version` is not optional.");
}
return { epoch, version, revision };
}
a = parseString(a), b = parseString(b);
// Compare `epoch`s.
let epochResult = compare(a.epoch, b.epoch);
if (epochResult != 0) {
return epochResult;
}
// Function to break down a version string according to the spec (I think):
const splitVersion = str => {
if (!str) {
return [""];
}
// `split` is the array that this function will return.
// `splitIndex` is the current index in `split`.
let split = [];
let splitIndex = 0;
/*
`exps` is an array of two regular expressions.
The first regex matches the allowed non-digit
characters, and the second matches digits.
*/
// `expsIndex` is the current index in `exps`.
let exps = [/^[A-Z.\-+~]*$/i, /^[0-9]*$/];
let expsIndex = Number(exps[1].test(str[0]));
// Helper functions:
let pushCharacter = char => {
if (typeof split[splitIndex] != "string") split[splitIndex] = "";
split[splitIndex] += char;
}
for (let char of str) {
// Ensure that the character is valid.
if (!/^[A-Z.\-+~0-9]*$/i.test(char)) {
throw new Error("Bad character.");
}
// If the character doesn't match the selected regex...
if (!exps[expsIndex].test(char)) {
// Increase `splitIndex` to make a new string.
splitIndex++;
// Switch `expsIndex` (i.e. 1 becomes 0, 0 becomes 1).
// This basically means that we select the unselected regex.
expsIndex ^= 1;
}
pushCharacter(char);
}
// There may be empty slots in the array (`split`). The following code fills them with empty strings:
for (let idx = 0; idx < split.length; ++idx) {
if (typeof split[idx] == "undefined") {
split[idx] = "";
}
}
return split;
}
// Function to compare two strings according to the spec:
const strcmp = (str1, str2) => {
// According to the spec, we can't compare standard ASCII character codes.
// This function returns more useful "character codes" according to the spec:
const orderedCode = char => {
// '~' before `undefined` before /^[0-9]$/ before /^[A-Z]$/i before '+' before '-' before '.' (I think.)
let numbers = [];
for (let idx = 0; idx <= 9; ++idx) {
numbers.push(idx.toString());
}
let uppercaseAlphabet = [];
for (let idx = "A".charCodeAt(0); idx <= "Z".charCodeAt(0); ++idx) {
uppercaseAlphabet.push(String.fromCharCode(idx));
}
let lowercaseAlphabet = uppercaseAlphabet.map(e => e.toLowerCase());
let characterArray = ["~", undefined, ...numbers, ...uppercaseAlphabet, ...lowercaseAlphabet, "+", "-", "."];
return characterArray.indexOf(char);
}
for (let idx = 0; idx < Math.max(str1.length, str2.length); ++idx) {
// Compare the result of `orderedCode` for both characters.
let result = compare(orderedCode(str1[idx]), orderedCode(str2[idx]));
if (!result) {
continue;
}
return result;
}
return 0;
}
// Function to compare two parts:
const compareParts = (part1, part2) => {
// If both parts are numeric, compare as numbers.
if (/^[0-9]+$/.test(part1) && /^[0-9]+$/.test(part2)) {
part1 = parseInt(part1), part2 = parseInt(part2);
return compare(part1, part2);
}
// Otherwise, use `strcmp`.
return strcmp(part1, part2);
}
// `version` and `revision` are compared in the exact same way.
for (let prop of ["version", "revision"]) {
let array1 = splitVersion(a[prop]), array2 = splitVersion(b[prop]);
ensureSameLength(array1, array2);
for (let idx = 0; idx < array1.length; ++idx) {
let result = compareParts(array1[idx], array2[idx]);
if (!result) {
continue;
}
return result;
}
}
return 0;
}