feat: builtins.compareVersions
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
import { force } from "../thunk";
|
||||
import { CatchableError } from "../types";
|
||||
import type { NixBool, NixStrictValue, NixValue } from "../types";
|
||||
import { forceList, forceAttrs, forceFunction } from "../type-assert";
|
||||
import { forceList, forceAttrs, forceFunction, forceString } from "../type-assert";
|
||||
import * as context from "./context";
|
||||
|
||||
export const addErrorContext =
|
||||
@@ -48,10 +48,103 @@ export const addDrvOutputDependencies = context.addDrvOutputDependencies;
|
||||
|
||||
export const compareVersions =
|
||||
(s1: NixValue) =>
|
||||
(s2: NixValue): never => {
|
||||
throw new Error("Not implemented: compareVersions");
|
||||
(s2: NixValue): NixValue => {
|
||||
const str1 = forceString(s1);
|
||||
const str2 = forceString(s2);
|
||||
|
||||
let i1 = 0;
|
||||
let i2 = 0;
|
||||
|
||||
while (i1 < str1.length || i2 < str2.length) {
|
||||
const c1 = nextComponent(str1, i1);
|
||||
const c2 = nextComponent(str2, i2);
|
||||
|
||||
i1 = c1.nextIndex;
|
||||
i2 = c2.nextIndex;
|
||||
|
||||
if (componentsLT(c1.component, c2.component)) {
|
||||
return -1n;
|
||||
} else if (componentsLT(c2.component, c1.component)) {
|
||||
return 1n;
|
||||
}
|
||||
}
|
||||
|
||||
return 0n;
|
||||
};
|
||||
|
||||
interface ComponentResult {
|
||||
component: string;
|
||||
nextIndex: number;
|
||||
}
|
||||
|
||||
function nextComponent(s: string, startIdx: number): ComponentResult {
|
||||
let p = startIdx;
|
||||
|
||||
// Skip any dots and dashes (component separators)
|
||||
while (p < s.length && (s[p] === "." || s[p] === "-")) {
|
||||
p++;
|
||||
}
|
||||
|
||||
if (p >= s.length) {
|
||||
return { component: "", nextIndex: p };
|
||||
}
|
||||
|
||||
const start = p;
|
||||
|
||||
// If the first character is a digit, consume the longest sequence of digits
|
||||
if (s[p] >= "0" && s[p] <= "9") {
|
||||
while (p < s.length && s[p] >= "0" && s[p] <= "9") {
|
||||
p++;
|
||||
}
|
||||
} else {
|
||||
// Otherwise, consume the longest sequence of non-digit, non-separator characters
|
||||
while (
|
||||
p < s.length &&
|
||||
!(s[p] >= "0" && s[p] <= "9") &&
|
||||
s[p] !== "." &&
|
||||
s[p] !== "-"
|
||||
) {
|
||||
p++;
|
||||
}
|
||||
}
|
||||
|
||||
return { component: s.substring(start, p), nextIndex: p };
|
||||
}
|
||||
|
||||
function componentsLT(c1: string, c2: string): boolean {
|
||||
const n1 = c1.match(/^[0-9]+$/) ? BigInt(c1) : null;
|
||||
const n2 = c2.match(/^[0-9]+$/) ? BigInt(c2) : null;
|
||||
|
||||
// Both are numbers: compare numerically
|
||||
if (n1 !== null && n2 !== null) {
|
||||
return n1 < n2;
|
||||
}
|
||||
|
||||
// Empty string < number
|
||||
if (c1 === "" && n2 !== null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Special case: "pre" comes before everything except another "pre"
|
||||
if (c1 === "pre" && c2 !== "pre") {
|
||||
return true;
|
||||
}
|
||||
if (c2 === "pre") {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Assume that `2.3a' < `2.3.1'
|
||||
if (n2 !== null) {
|
||||
return true;
|
||||
}
|
||||
if (n1 !== null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Both are strings: compare lexicographically
|
||||
return c1 < c2;
|
||||
}
|
||||
|
||||
export const dirOf = (s: NixValue): never => {
|
||||
throw new Error("Not implemented: dirOf");
|
||||
};
|
||||
|
||||
@@ -147,3 +147,71 @@ fn builtins_concat_lists() {
|
||||
]))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builtins_compare_versions_basic() {
|
||||
assert_eq!(eval("builtins.compareVersions \"1.0\" \"2.3\""), Value::Int(-1));
|
||||
assert_eq!(eval("builtins.compareVersions \"2.1\" \"2.3\""), Value::Int(-1));
|
||||
assert_eq!(eval("builtins.compareVersions \"2.3\" \"2.3\""), Value::Int(0));
|
||||
assert_eq!(eval("builtins.compareVersions \"2.5\" \"2.3\""), Value::Int(1));
|
||||
assert_eq!(eval("builtins.compareVersions \"3.1\" \"2.3\""), Value::Int(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builtins_compare_versions_components() {
|
||||
assert_eq!(eval("builtins.compareVersions \"2.3.1\" \"2.3\""), Value::Int(1));
|
||||
assert_eq!(eval("builtins.compareVersions \"2.3\" \"2.3.1\""), Value::Int(-1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builtins_compare_versions_numeric_vs_alpha() {
|
||||
// Numeric component comes before alpha component
|
||||
assert_eq!(eval("builtins.compareVersions \"2.3.1\" \"2.3a\""), Value::Int(1));
|
||||
assert_eq!(eval("builtins.compareVersions \"2.3a\" \"2.3.1\""), Value::Int(-1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builtins_compare_versions_pre() {
|
||||
// "pre" is special: comes before everything except another "pre"
|
||||
assert_eq!(eval("builtins.compareVersions \"2.3pre1\" \"2.3\""), Value::Int(-1));
|
||||
assert_eq!(eval("builtins.compareVersions \"2.3pre3\" \"2.3pre12\""), Value::Int(-1));
|
||||
assert_eq!(eval("builtins.compareVersions \"2.3pre1\" \"2.3c\""), Value::Int(-1));
|
||||
assert_eq!(eval("builtins.compareVersions \"2.3pre1\" \"2.3q\""), Value::Int(-1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builtins_compare_versions_alpha() {
|
||||
// Alphabetic comparison
|
||||
assert_eq!(eval("builtins.compareVersions \"2.3a\" \"2.3c\""), Value::Int(-1));
|
||||
assert_eq!(eval("builtins.compareVersions \"2.3c\" \"2.3a\""), Value::Int(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builtins_compare_versions_symmetry() {
|
||||
// Test symmetry: compareVersions(a, b) == -compareVersions(b, a)
|
||||
assert_eq!(
|
||||
eval("builtins.compareVersions \"1.0\" \"2.3\""),
|
||||
Value::Int(-1)
|
||||
);
|
||||
assert_eq!(
|
||||
eval("builtins.compareVersions \"2.3\" \"1.0\""),
|
||||
Value::Int(1)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builtins_compare_versions_complex() {
|
||||
// Complex version strings with multiple components
|
||||
assert_eq!(
|
||||
eval("builtins.compareVersions \"1.2.3.4\" \"1.2.3.5\""),
|
||||
Value::Int(-1)
|
||||
);
|
||||
assert_eq!(
|
||||
eval("builtins.compareVersions \"1.2.10\" \"1.2.9\""),
|
||||
Value::Int(1)
|
||||
);
|
||||
assert_eq!(
|
||||
eval("builtins.compareVersions \"1.2a3\" \"1.2a10\""),
|
||||
Value::Int(-1)
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user