Files

92 lines
4.3 KiB
TypeScript

import { DatabaseSync } from "node:sqlite";
// extract.ts、items.ts 共用:只读打开对照运行的账本,以及照 Python 的写法把值变成文字。
// 对照材料(/home/iomgaa/inforsi-eval/2026-10-03-route-control/)是 Python 脚本导出的,
// 移植要逐字节一致,所以这里复现 Python 的 json.dumps(默认分隔符 ", " 和 ": ")和 repr(单引号字符串、浮点写法)。
// 账本内容是 JSON 解析出来的任意值;照 Python 的动态写法用 any,便于和 extract.py / items.py 逐行对照。
export type J = any;
/** 运行目录名 → 账本全部记录(种类、内容),按序号。只读打开,不经 Ledger.open,不在对照目录里写任何东西。 */
export function readRun(run: string): Array<[string, J]> {
const db = new DatabaseSync(`/home/iomgaa/inforsi-eval/${run}/study/ledger.sqlite`, { readOnly: true });
try {
const rows = db.prepare("SELECT kind, content FROM records ORDER BY seq").all() as Array<{ kind: string; content: string }>;
return rows.map((r) => [r.kind, JSON.parse(r.content)]);
} finally {
db.close();
}
}
/**
* Python 读 JSON 后的数:没有小数点和指数的是 int,照写;其余是 float,用 repr 的写法:
* 最短的能还原的数字,指数在 [-4, 16) 内写成定点,否则写成 1e-05 这种(指数至少两位)。
* 账本由 JSON.stringify 写成,整数(小于 1e21)都写成不带小数点的样子,所以 Number.isInteger 就是 int。
*/
function pyNumber(x: number): string {
if (Number.isInteger(x) && Math.abs(x) < 1e21) return String(x);
const [mantissa, e] = x.toExponential().split("e") as [string, string];
const exp = Number(e);
const negative = mantissa.startsWith("-");
const digits = mantissa.replace("-", "").replace(".", "");
let body: string;
if (exp >= -4 && exp < 16) {
if (exp < 0) body = "0." + "0".repeat(-exp - 1) + digits;
else if (digits.length > exp + 1) body = digits.slice(0, exp + 1) + "." + digits.slice(exp + 1);
else body = digits + "0".repeat(exp + 1 - digits.length) + ".0";
} else {
const m = digits.length > 1 ? digits[0] + "." + digits.slice(1) : digits;
body = m + "e" + (exp < 0 ? "-" : "+") + String(Math.abs(exp)).padStart(2, "0");
}
return (negative ? "-" : "") + body;
}
const hex4 = (c: number) => "\\u" + c.toString(16).padStart(4, "0");
const JSON_ESC: Record<string, string> = { '"': '\\"', "\\": "\\\\", "\n": "\\n", "\r": "\\r", "\t": "\\t", "\b": "\\b", "\f": "\\f" };
/** Python json.dumps(o) / json.dumps(o, ensure_ascii=False)。 */
export function pyDumps(o: J, ensureAscii: boolean): string {
if (o === null) return "null";
if (o === true) return "true";
if (o === false) return "false";
if (typeof o === "number") return pyNumber(o);
if (typeof o === "string") {
let s = '"';
for (const ch of o) {
const c = ch.codePointAt(0)!;
if (JSON_ESC[ch] !== undefined) s += JSON_ESC[ch];
else if (c < 0x20 || (ensureAscii && c > 0x7e)) {
if (c > 0xffff) s += hex4(0xd800 + ((c - 0x10000) >> 10)) + hex4(0xdc00 + ((c - 0x10000) & 0x3ff));
else s += hex4(c);
} else s += ch;
}
return s + '"';
}
if (Array.isArray(o)) return "[" + o.map((v) => pyDumps(v, ensureAscii)).join(", ") + "]";
return "{" + Object.entries(o).map(([k, v]) => pyDumps(k, ensureAscii) + ": " + pyDumps(v, ensureAscii)).join(", ") + "}";
}
/** Python 的 str(o):列表、字典、字符串按 repr。字符串只处理本数据里会出现的转义(引号、反斜杠、控制字符)。 */
export function pyRepr(o: J): string {
if (o === null) return "None";
if (o === true) return "True";
if (o === false) return "False";
if (typeof o === "number") return pyNumber(o);
if (typeof o === "string") {
const q = o.includes("'") && !o.includes('"') ? '"' : "'";
let s = q;
for (const ch of o) {
const c = ch.codePointAt(0)!;
if (ch === q || ch === "\\") s += "\\" + ch;
else if (ch === "\n") s += "\\n";
else if (ch === "\r") s += "\\r";
else if (ch === "\t") s += "\\t";
else if (c < 0x20 || c === 0x7f) s += "\\x" + c.toString(16).padStart(2, "0");
else s += ch;
}
return s + q;
}
if (Array.isArray(o)) return "[" + o.map(pyRepr).join(", ") + "]";
return "{" + Object.entries(o).map(([k, v]) => pyRepr(k) + ": " + pyRepr(v)).join(", ") + "}";
}