feat: builtins.compareVersions

This commit is contained in:
2026-01-16 10:08:29 +08:00
parent 4f8edab795
commit 5341ad6c27
2 changed files with 164 additions and 3 deletions

View File

@@ -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");
};

View File

@@ -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)
);
}