commit cb97053ba71bf1e361e69ad317f403984ecdc597 Author: aiden Date: Thu May 25 03:54:33 2023 +0100 i wrote this shit years ago diff --git a/debsort.js b/debsort.js new file mode 100644 index 0000000..51d8be4 --- /dev/null +++ b/debsort.js @@ -0,0 +1,175 @@ +// 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; +}