mirror of
https://github.com/Horizontal42/summarum.git
synced 2026-11-11 01:02:01 +03:00
chore: Replace console.warn with @tauri-apps/plugin-log
Replaces the raw `console.warn` in the evaluation engine (`src/engine/index.ts`) with the Tauri logging plugin (`warn` from `@tauri-apps/plugin-log`) for better production observability. Ensures tests mock this API. Added necessary backend initialization in `src-tauri`. Co-authored-by: Horizontal42 <75383519+Horizontal42@users.noreply.github.com>
This commit is contained in:
parent
268ac9e81b
commit
303caf1cf3
8 changed files with 230 additions and 30 deletions
10
package-lock.json
generated
10
package-lock.json
generated
|
|
@ -19,6 +19,7 @@
|
||||||
"@tauri-apps/plugin-autostart": "^2.3.0",
|
"@tauri-apps/plugin-autostart": "^2.3.0",
|
||||||
"@tauri-apps/plugin-dialog": "^2.7.1",
|
"@tauri-apps/plugin-dialog": "^2.7.1",
|
||||||
"@tauri-apps/plugin-global-shortcut": "^2.2.0",
|
"@tauri-apps/plugin-global-shortcut": "^2.2.0",
|
||||||
|
"@tauri-apps/plugin-log": "^2.9.0",
|
||||||
"@tauri-apps/plugin-opener": "^2.2.0",
|
"@tauri-apps/plugin-opener": "^2.2.0",
|
||||||
"@tauri-apps/plugin-process": "^2.3.1",
|
"@tauri-apps/plugin-process": "^2.3.1",
|
||||||
"@tauri-apps/plugin-updater": "^2.10.1",
|
"@tauri-apps/plugin-updater": "^2.10.1",
|
||||||
|
|
@ -1244,6 +1245,15 @@
|
||||||
"@tauri-apps/api": "^2.11.0"
|
"@tauri-apps/api": "^2.11.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tauri-apps/plugin-log": {
|
||||||
|
"version": "2.9.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-log/-/plugin-log-2.9.0.tgz",
|
||||||
|
"integrity": "sha512-Ql8okrnsguk0eDq1GvRfttFV5KaeW/7vcao6bdbkXCRJ1+2sWE15ZJvJVEKVANrOKy1mRngqC3IFIAP+wP5qSw==",
|
||||||
|
"license": "MIT OR Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@tauri-apps/api": "^2.11.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tauri-apps/plugin-opener": {
|
"node_modules/@tauri-apps/plugin-opener": {
|
||||||
"version": "2.5.4",
|
"version": "2.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-opener/-/plugin-opener-2.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-opener/-/plugin-opener-2.5.4.tgz",
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@
|
||||||
"@tauri-apps/plugin-autostart": "^2.3.0",
|
"@tauri-apps/plugin-autostart": "^2.3.0",
|
||||||
"@tauri-apps/plugin-dialog": "^2.7.1",
|
"@tauri-apps/plugin-dialog": "^2.7.1",
|
||||||
"@tauri-apps/plugin-global-shortcut": "^2.2.0",
|
"@tauri-apps/plugin-global-shortcut": "^2.2.0",
|
||||||
|
"@tauri-apps/plugin-log": "^2.9.0",
|
||||||
"@tauri-apps/plugin-opener": "^2.2.0",
|
"@tauri-apps/plugin-opener": "^2.2.0",
|
||||||
"@tauri-apps/plugin-process": "^2.3.1",
|
"@tauri-apps/plugin-process": "^2.3.1",
|
||||||
"@tauri-apps/plugin-updater": "^2.10.1",
|
"@tauri-apps/plugin-updater": "^2.10.1",
|
||||||
|
|
|
||||||
69
src-tauri/Cargo.lock
generated
69
src-tauri/Cargo.lock
generated
|
|
@ -32,6 +32,23 @@ dependencies = [
|
||||||
"alloc-no-stdlib",
|
"alloc-no-stdlib",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "android_log-sys"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "84521a3cf562bc62942e294181d9eef17eb38ceb8c68677bc49f144e4c3d4f8d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "android_logger"
|
||||||
|
version = "0.15.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dbb4e440d04be07da1f1bf44fb4495ebd58669372fe0cffa6e48595ac5bd88a3"
|
||||||
|
dependencies = [
|
||||||
|
"android_log-sys",
|
||||||
|
"env_filter",
|
||||||
|
"log",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "android_system_properties"
|
name = "android_system_properties"
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
|
|
@ -941,6 +958,16 @@ dependencies = [
|
||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "env_filter"
|
||||||
|
version = "0.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"regex",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "equivalent"
|
name = "equivalent"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
|
|
@ -1004,6 +1031,15 @@ dependencies = [
|
||||||
"simd-adler32",
|
"simd-adler32",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fern"
|
||||||
|
version = "0.7.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4316185f709b23713e41e3195f90edef7fb00c3ed4adc79769cf09cc762a3b29"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "field-offset"
|
name = "field-offset"
|
||||||
version = "0.3.6"
|
version = "0.3.6"
|
||||||
|
|
@ -2270,6 +2306,15 @@ dependencies = [
|
||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num_threads"
|
||||||
|
version = "0.1.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "objc2"
|
name = "objc2"
|
||||||
version = "0.6.4"
|
version = "0.6.4"
|
||||||
|
|
@ -3736,6 +3781,7 @@ dependencies = [
|
||||||
"tauri-plugin-autostart",
|
"tauri-plugin-autostart",
|
||||||
"tauri-plugin-dialog",
|
"tauri-plugin-dialog",
|
||||||
"tauri-plugin-global-shortcut",
|
"tauri-plugin-global-shortcut",
|
||||||
|
"tauri-plugin-log",
|
||||||
"tauri-plugin-opener",
|
"tauri-plugin-opener",
|
||||||
"tauri-plugin-process",
|
"tauri-plugin-process",
|
||||||
"tauri-plugin-single-instance",
|
"tauri-plugin-single-instance",
|
||||||
|
|
@ -4075,6 +4121,27 @@ dependencies = [
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tauri-plugin-log"
|
||||||
|
version = "2.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6792296e6f389268016c77db21ebae1fc0568f2fccf88b1ec7e2ea71330afb4c"
|
||||||
|
dependencies = [
|
||||||
|
"android_logger",
|
||||||
|
"fern",
|
||||||
|
"log",
|
||||||
|
"objc2",
|
||||||
|
"objc2-foundation",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_repr",
|
||||||
|
"swift-rs",
|
||||||
|
"tauri",
|
||||||
|
"tauri-plugin",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
"time",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-opener"
|
name = "tauri-plugin-opener"
|
||||||
version = "2.5.4"
|
version = "2.5.4"
|
||||||
|
|
@ -4326,7 +4393,9 @@ checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"deranged",
|
"deranged",
|
||||||
"itoa",
|
"itoa",
|
||||||
|
"libc",
|
||||||
"num-conv",
|
"num-conv",
|
||||||
|
"num_threads",
|
||||||
"powerfmt",
|
"powerfmt",
|
||||||
"serde_core",
|
"serde_core",
|
||||||
"time-core",
|
"time-core",
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ serde_json = "1"
|
||||||
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
|
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
futures = "0.3.32"
|
futures = "0.3.32"
|
||||||
|
tauri-plugin-log = "2.9.0"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
strip = true
|
strip = true
|
||||||
|
|
|
||||||
|
|
@ -985,6 +985,7 @@ fn main() {
|
||||||
.plugin(tauri_plugin_dialog::init())
|
.plugin(tauri_plugin_dialog::init())
|
||||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||||
.plugin(tauri_plugin_process::init())
|
.plugin(tauri_plugin_process::init())
|
||||||
|
.plugin(tauri_plugin_log::Builder::new().build())
|
||||||
.manage(DroppedFiles::default())
|
.manage(DroppedFiles::default())
|
||||||
.manage(DataDirGate::default())
|
.manage(DataDirGate::default())
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,12 @@
|
||||||
import { describe, it, expect, beforeAll } from "vitest";
|
import { describe, it, expect, beforeAll, vi } from "vitest";
|
||||||
import { SumEngine } from "./index";
|
import { SumEngine } from "./index";
|
||||||
|
|
||||||
|
vi.mock("@tauri-apps/plugin-log", () => ({
|
||||||
|
warn: vi.fn().mockResolvedValue(undefined),
|
||||||
|
error: vi.fn().mockResolvedValue(undefined),
|
||||||
|
info: vi.fn().mockResolvedValue(undefined),
|
||||||
|
}));
|
||||||
|
|
||||||
let eng: SumEngine;
|
let eng: SumEngine;
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,11 @@
|
||||||
import { describe, it, expect, beforeAll } from "vitest";
|
import { describe, it, expect, beforeAll, vi } from "vitest";
|
||||||
import { SumEngine } from "./index";
|
import { SumEngine } from "./index";
|
||||||
|
|
||||||
|
vi.mock("@tauri-apps/plugin-log", () => ({
|
||||||
|
warn: vi.fn().mockResolvedValue(undefined),
|
||||||
|
error: vi.fn().mockResolvedValue(undefined),
|
||||||
|
info: vi.fn().mockResolvedValue(undefined),
|
||||||
|
}));
|
||||||
import { qty, EvalError, XRefError } from "./types";
|
import { qty, EvalError, XRefError } from "./types";
|
||||||
import type { XRefResolution, EvalCtx } from "./evaluator";
|
import type { XRefResolution, EvalCtx } from "./evaluator";
|
||||||
import { evaluate } from "./evaluator";
|
import { evaluate } from "./evaluator";
|
||||||
|
|
@ -230,7 +236,13 @@ describe("variables and document context", () => {
|
||||||
});
|
});
|
||||||
it("sum / total", () => {
|
it("sum / total", () => {
|
||||||
expect(calcDoc("10\n20\n30\nsum")).toEqual(["10", "20", "30", "60"]);
|
expect(calcDoc("10\n20\n30\nsum")).toEqual(["10", "20", "30", "60"]);
|
||||||
expect(calcDoc("10\n20\n\n30\nsum")).toEqual(["10", "20", null, "30", "30"]);
|
expect(calcDoc("10\n20\n\n30\nsum")).toEqual([
|
||||||
|
"10",
|
||||||
|
"20",
|
||||||
|
null,
|
||||||
|
"30",
|
||||||
|
"30",
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
it("sum with units", () => {
|
it("sum with units", () => {
|
||||||
expect(calcDoc("$10\n$20\ntotal")).toEqual(["$10", "$20", "$30"]);
|
expect(calcDoc("$10\n$20\ntotal")).toEqual(["$10", "$20", "$30"]);
|
||||||
|
|
@ -243,15 +255,31 @@ describe("variables and document context", () => {
|
||||||
});
|
});
|
||||||
it("count", () => {
|
it("count", () => {
|
||||||
expect(calcDoc("10\n20\n30\ncount")).toEqual(["10", "20", "30", "3"]);
|
expect(calcDoc("10\n20\n30\ncount")).toEqual(["10", "20", "30", "3"]);
|
||||||
expect(calcDoc("10\n20\n\n30\ncount")).toEqual(["10", "20", null, "30", "1"]);
|
expect(calcDoc("10\n20\n\n30\ncount")).toEqual([
|
||||||
|
"10",
|
||||||
|
"20",
|
||||||
|
null,
|
||||||
|
"30",
|
||||||
|
"1",
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
it("min / max", () => {
|
it("min / max", () => {
|
||||||
expect(calcDoc("10\n20\n5\nmin")).toEqual(["10", "20", "5", "5"]);
|
expect(calcDoc("10\n20\n5\nmin")).toEqual(["10", "20", "5", "5"]);
|
||||||
expect(calcDoc("10\n20\n5\nmax")).toEqual(["10", "20", "5", "20"]);
|
expect(calcDoc("10\n20\n5\nmax")).toEqual(["10", "20", "5", "20"]);
|
||||||
});
|
});
|
||||||
it("min / max with units", () => {
|
it("min / max with units", () => {
|
||||||
expect(calcDoc("10 m\n5 m\n20 m\nmin")).toEqual(["10 m", "5 m", "20 m", "5 m"]);
|
expect(calcDoc("10 m\n5 m\n20 m\nmin")).toEqual([
|
||||||
expect(calcDoc("10 m\n5 m\n20 m\nmax")).toEqual(["10 m", "5 m", "20 m", "20 m"]);
|
"10 m",
|
||||||
|
"5 m",
|
||||||
|
"20 m",
|
||||||
|
"5 m",
|
||||||
|
]);
|
||||||
|
expect(calcDoc("10 m\n5 m\n20 m\nmax")).toEqual([
|
||||||
|
"10 m",
|
||||||
|
"5 m",
|
||||||
|
"20 m",
|
||||||
|
"20 m",
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
it("product", () => {
|
it("product", () => {
|
||||||
expect(calcDoc("2\n3\n4\nproduct")).toEqual(["2", "3", "4", "24"]);
|
expect(calcDoc("2\n3\n4\nproduct")).toEqual(["2", "3", "4", "24"]);
|
||||||
|
|
@ -332,18 +360,32 @@ describe("goal seek", () => {
|
||||||
|
|
||||||
describe("extension API", () => {
|
describe("extension API", () => {
|
||||||
it("addUnit (horse from Sample.js)", () => {
|
it("addUnit (horse from Sample.js)", () => {
|
||||||
eng.addUnit({ id: "horse", phrases: "horse, horses, hrs", baseUnitId: "meter", format: "hrs", ratio: 2.4 });
|
eng.addUnit({
|
||||||
|
id: "horse",
|
||||||
|
phrases: "horse, horses, hrs",
|
||||||
|
baseUnitId: "meter",
|
||||||
|
format: "hrs",
|
||||||
|
ratio: 2.4,
|
||||||
|
});
|
||||||
expect(calc("2 horses in meters")).toBe("4.8 m");
|
expect(calc("2 horses in meters")).toBe("4.8 m");
|
||||||
});
|
});
|
||||||
it("addUnit throws on id collision instead of silently corrupting unitsById", () => {
|
it("addUnit throws on id collision instead of silently corrupting unitsById", () => {
|
||||||
expect(() =>
|
expect(() =>
|
||||||
eng.addUnit({ id: "horse", phrases: "nag", baseUnitId: "meter", format: "nag", ratio: 99 })
|
eng.addUnit({
|
||||||
|
id: "horse",
|
||||||
|
phrases: "nag",
|
||||||
|
baseUnitId: "meter",
|
||||||
|
format: "nag",
|
||||||
|
ratio: 99,
|
||||||
|
}),
|
||||||
).toThrow(/horse/);
|
).toThrow(/horse/);
|
||||||
// the original registration must survive the failed collision
|
// the original registration must survive the failed collision
|
||||||
expect(calc("2 horses in meters")).toBe("4.8 m");
|
expect(calc("2 horses in meters")).toBe("4.8 m");
|
||||||
});
|
});
|
||||||
it("addFunction (zum from Sample.js)", () => {
|
it("addFunction (zum from Sample.js)", () => {
|
||||||
eng.addFunction({ id: "zum", phrases: "zum" }, (values) => ({ double: values[0].double + values[1].double }));
|
eng.addFunction({ id: "zum", phrases: "zum" }, (values) => ({
|
||||||
|
double: values[0].double + values[1].double,
|
||||||
|
}));
|
||||||
expect(calc("zum(2;3)")).toBe("5");
|
expect(calc("zum(2;3)")).toBe("5");
|
||||||
});
|
});
|
||||||
it("setVariable", () => {
|
it("setVariable", () => {
|
||||||
|
|
@ -374,7 +416,8 @@ describe("cross-sheet references", () => {
|
||||||
const s = sheet.toLowerCase();
|
const s = sheet.toLowerCase();
|
||||||
if (s === "budget") {
|
if (s === "budget") {
|
||||||
if (key === "total") return { ok: true, value: qty(150) };
|
if (key === "total") return { ok: true, value: qty(150) };
|
||||||
if (key === "rent") return { ok: true, value: qty(500, eng.reg.unitsById.get("meter")!) };
|
if (key === "rent")
|
||||||
|
return { ok: true, value: qty(500, eng.reg.unitsById.get("meter")!) };
|
||||||
if (key === "last") return { ok: true, value: qty(7) };
|
if (key === "last") return { ok: true, value: qty(7) };
|
||||||
return { ok: false, reason: `no variable "${key}" in "${sheet}"` };
|
return { ok: false, reason: `no variable "${key}" in "${sheet}"` };
|
||||||
}
|
}
|
||||||
|
|
@ -415,7 +458,10 @@ describe("cross-sheet references", () => {
|
||||||
expect(r[0].error).toBeUndefined();
|
expect(r[0].error).toBeUndefined();
|
||||||
});
|
});
|
||||||
it("propagates a resolver-reported circular reference", () => {
|
it("propagates a resolver-reported circular reference", () => {
|
||||||
const cyc = (): XRefResolution => ({ ok: false, reason: "circular reference" });
|
const cyc = (): XRefResolution => ({
|
||||||
|
ok: false,
|
||||||
|
reason: "circular reference",
|
||||||
|
});
|
||||||
const r = eng.evaluateDocument("@A.total", cyc);
|
const r = eng.evaluateDocument("@A.total", cyc);
|
||||||
expect(r[0].value).toBeNull();
|
expect(r[0].value).toBeNull();
|
||||||
expect(r[0].error).toBe("circular reference");
|
expect(r[0].error).toBe("circular reference");
|
||||||
|
|
@ -439,7 +485,9 @@ describe("evaluation errors", () => {
|
||||||
reg: eng.reg,
|
reg: eng.reg,
|
||||||
historicalRates: {},
|
historicalRates: {},
|
||||||
} as unknown as EvalCtx;
|
} as unknown as EvalCtx;
|
||||||
expect(() => evaluate({ k: "var", name: "unknown" }, ctx)).toThrowError(EvalError);
|
expect(() => evaluate({ k: "var", name: "unknown" }, ctx)).toThrowError(
|
||||||
|
EvalError,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("throws XRefError for unresolved xref", () => {
|
it("throws XRefError for unresolved xref", () => {
|
||||||
|
|
@ -449,6 +497,8 @@ describe("evaluation errors", () => {
|
||||||
historicalRates: {},
|
historicalRates: {},
|
||||||
resolveXRef: () => ({ ok: false, reason: "xref error" }),
|
resolveXRef: () => ({ ok: false, reason: "xref error" }),
|
||||||
} as unknown as EvalCtx;
|
} as unknown as EvalCtx;
|
||||||
expect(() => evaluate({ k: "xref", sheet: "Sheet", key: "key" }, ctx)).toThrowError(XRefError);
|
expect(() =>
|
||||||
|
evaluate({ k: "xref", sheet: "Sheet", key: "key" }, ctx),
|
||||||
|
).toThrowError(XRefError);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,18 @@
|
||||||
// Public facade: evaluates whole documents line by line and exposes the
|
// Public facade: evaluates whole documents line by line and exposes the
|
||||||
// extension API (numi.addUnit / addFunction / setVariable).
|
// extension API (numi.addUnit / addFunction / setVariable).
|
||||||
import { DateOrder, Decimal, EngineSettings, EvalError, Quantity, Unit, Value, XRefError, defaultSettings, qty } from "./types";
|
import { warn } from "@tauri-apps/plugin-log";
|
||||||
|
import {
|
||||||
|
DateOrder,
|
||||||
|
Decimal,
|
||||||
|
EngineSettings,
|
||||||
|
EvalError,
|
||||||
|
Quantity,
|
||||||
|
Unit,
|
||||||
|
Value,
|
||||||
|
XRefError,
|
||||||
|
defaultSettings,
|
||||||
|
qty,
|
||||||
|
} from "./types";
|
||||||
import { Registry, buildRegistry, Completion } from "./registry";
|
import { Registry, buildRegistry, Completion } from "./registry";
|
||||||
import { tokenize, Token } from "./tokenizer";
|
import { tokenize, Token } from "./tokenizer";
|
||||||
import { parseLine, ParsedLine } from "./parser";
|
import { parseLine, ParsedLine } from "./parser";
|
||||||
|
|
@ -84,7 +96,8 @@ export class SumEngine {
|
||||||
|
|
||||||
updateSettings(patch: Partial<EngineSettings>): void {
|
updateSettings(patch: Partial<EngineSettings>): void {
|
||||||
this.settings = { ...this.settings, ...patch };
|
this.settings = { ...this.settings, ...patch };
|
||||||
if (patch.dateFormat) this.dateOrder = resolveDateOrder(this.settings.dateFormat);
|
if (patch.dateFormat)
|
||||||
|
this.dateOrder = resolveDateOrder(this.settings.dateFormat);
|
||||||
}
|
}
|
||||||
|
|
||||||
completions(): Completion[] {
|
completions(): Completion[] {
|
||||||
|
|
@ -106,13 +119,25 @@ export class SumEngine {
|
||||||
|
|
||||||
if (trimmed.length === 0) {
|
if (trimmed.length === 0) {
|
||||||
const kind = commentStart >= 0 ? "comment" : "empty";
|
const kind = commentStart >= 0 ? "comment" : "empty";
|
||||||
results.push({ text: null, value: null, kind, tokens: [], commentStart: commentStart >= 0 ? commentStart : null });
|
results.push({
|
||||||
|
text: null,
|
||||||
|
value: null,
|
||||||
|
kind,
|
||||||
|
tokens: [],
|
||||||
|
commentStart: commentStart >= 0 ? commentStart : null,
|
||||||
|
});
|
||||||
lineValues.push(null);
|
lineValues.push(null);
|
||||||
lineKinds.push("empty");
|
lineKinds.push("empty");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (trimmed.startsWith("#")) {
|
if (trimmed.startsWith("#")) {
|
||||||
results.push({ text: null, value: null, kind: "header", tokens: [], commentStart: commentStart >= 0 ? commentStart : null });
|
results.push({
|
||||||
|
text: null,
|
||||||
|
value: null,
|
||||||
|
kind: "header",
|
||||||
|
tokens: [],
|
||||||
|
commentStart: commentStart >= 0 ? commentStart : null,
|
||||||
|
});
|
||||||
lineValues.push(null);
|
lineValues.push(null);
|
||||||
lineKinds.push("header");
|
lineKinds.push("header");
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -148,7 +173,9 @@ export class SumEngine {
|
||||||
} else {
|
} else {
|
||||||
// Surface unexpected bugs/BigInt errors/RangeErrors and log them
|
// Surface unexpected bugs/BigInt errors/RangeErrors and log them
|
||||||
lineError = e instanceof Error ? e.message : String(e);
|
lineError = e instanceof Error ? e.message : String(e);
|
||||||
console.warn("evaluate failed:", e);
|
warn(
|
||||||
|
`evaluate failed: ${e instanceof Error ? e.stack || e.message : String(e)}`,
|
||||||
|
).catch((err) => console.warn("logger failed", err));
|
||||||
}
|
}
|
||||||
value = null;
|
value = null;
|
||||||
}
|
}
|
||||||
|
|
@ -158,7 +185,10 @@ export class SumEngine {
|
||||||
lineValues.push(value);
|
lineValues.push(value);
|
||||||
lineKinds.push("normal");
|
lineKinds.push("normal");
|
||||||
results.push({
|
results.push({
|
||||||
text: (value && value.kind !== "chart") ? formatValue(value, this.settings) : null,
|
text:
|
||||||
|
value && value.kind !== "chart"
|
||||||
|
? formatValue(value, this.settings)
|
||||||
|
: null,
|
||||||
value,
|
value,
|
||||||
kind: "normal",
|
kind: "normal",
|
||||||
tokens,
|
tokens,
|
||||||
|
|
@ -216,9 +246,13 @@ export class SumEngine {
|
||||||
}
|
}
|
||||||
|
|
||||||
addUnit(spec: ExtensionUnitSpec): void {
|
addUnit(spec: ExtensionUnitSpec): void {
|
||||||
const base = this.reg.unitsById.get(spec.baseUnitId) ?? this.currencyBase(spec.baseUnitId);
|
const base =
|
||||||
if (!base) throw new Error(`numi.addUnit: unknown baseUnitId ${spec.baseUnitId}`);
|
this.reg.unitsById.get(spec.baseUnitId) ??
|
||||||
if (this.reg.unitsById.has(spec.id)) throw new Error(`numi.addUnit: id "${spec.id}" is already registered`);
|
this.currencyBase(spec.baseUnitId);
|
||||||
|
if (!base)
|
||||||
|
throw new Error(`numi.addUnit: unknown baseUnitId ${spec.baseUnitId}`);
|
||||||
|
if (this.reg.unitsById.has(spec.id))
|
||||||
|
throw new Error(`numi.addUnit: id "${spec.id}" is already registered`);
|
||||||
const unit: Unit = {
|
const unit: Unit = {
|
||||||
id: spec.id,
|
id: spec.id,
|
||||||
dimension: base.dimension,
|
dimension: base.dimension,
|
||||||
|
|
@ -229,12 +263,24 @@ export class SumEngine {
|
||||||
this.reg.unitsById.set(unit.id, unit);
|
this.reg.unitsById.set(unit.id, unit);
|
||||||
for (const p of spec.phrases.split(",")) {
|
for (const p of spec.phrases.split(",")) {
|
||||||
const phrase = p.trim();
|
const phrase = p.trim();
|
||||||
if (phrase) this.reg.addPhrase(phrase, { t: "unit", unit }, { caseSensitive: false });
|
if (phrase)
|
||||||
|
this.reg.addPhrase(
|
||||||
|
phrase,
|
||||||
|
{ t: "unit", unit },
|
||||||
|
{ caseSensitive: false },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
this.reg.completions.push({ label: spec.id, type: "unit", detail: "extension" });
|
this.reg.completions.push({
|
||||||
|
label: spec.id,
|
||||||
|
type: "unit",
|
||||||
|
detail: "extension",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
addFunction(spec: { id: string; phrases: string }, fn: (values: ExtensionValue[]) => ExtensionValue | number): void {
|
addFunction(
|
||||||
|
spec: { id: string; phrases: string },
|
||||||
|
fn: (values: ExtensionValue[]) => ExtensionValue | number,
|
||||||
|
): void {
|
||||||
const name = spec.id;
|
const name = spec.id;
|
||||||
this.reg.customFuncs.set(name, (args: Value[]) => {
|
this.reg.customFuncs.set(name, (args: Value[]) => {
|
||||||
const mapped = args.map((a) => this.fromValue(a));
|
const mapped = args.map((a) => this.fromValue(a));
|
||||||
|
|
@ -242,26 +288,42 @@ export class SumEngine {
|
||||||
});
|
});
|
||||||
for (const p of (spec.phrases || spec.id).split(",")) {
|
for (const p of (spec.phrases || spec.id).split(",")) {
|
||||||
const phrase = p.trim();
|
const phrase = p.trim();
|
||||||
if (phrase) this.reg.addPhrase(phrase, { t: "func", name }, { caseSensitive: false });
|
if (phrase)
|
||||||
|
this.reg.addPhrase(
|
||||||
|
phrase,
|
||||||
|
{ t: "func", name },
|
||||||
|
{ caseSensitive: false },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
this.reg.completions.push({ label: name, type: "function", detail: "extension" });
|
this.reg.completions.push({
|
||||||
|
label: name,
|
||||||
|
type: "function",
|
||||||
|
detail: "extension",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private currencyBase(code: string): Unit | null {
|
private currencyBase(code: string): Unit | null {
|
||||||
return this.reg.currencyCodes.has(code) ? this.reg.makeCurrencyUnit(code) : null;
|
return this.reg.currencyCodes.has(code)
|
||||||
|
? this.reg.makeCurrencyUnit(code)
|
||||||
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private toValue(v: number | ExtensionValue): Value {
|
private toValue(v: number | ExtensionValue): Value {
|
||||||
if (typeof v === "number") return qty(v);
|
if (typeof v === "number") return qty(v);
|
||||||
const unit = v.unitId
|
const unit = v.unitId
|
||||||
? this.reg.unitsById.get(v.unitId) ?? this.currencyBase(v.unitId) ?? null
|
? (this.reg.unitsById.get(v.unitId) ??
|
||||||
|
this.currencyBase(v.unitId) ??
|
||||||
|
null)
|
||||||
: null;
|
: null;
|
||||||
return qty(new Decimal(v.double), unit);
|
return qty(new Decimal(v.double), unit);
|
||||||
}
|
}
|
||||||
|
|
||||||
private fromValue(v: Value): ExtensionValue {
|
private fromValue(v: Value): ExtensionValue {
|
||||||
if (v.kind === "quantity") {
|
if (v.kind === "quantity") {
|
||||||
return { double: v.value.toNumber(), ...(v.unit ? { unitId: v.unit.id } : {}) };
|
return {
|
||||||
|
double: v.value.toNumber(),
|
||||||
|
...(v.unit ? { unitId: v.unit.id } : {}),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
if (v.kind === "percent") return { double: v.value.div(100).toNumber() };
|
if (v.kind === "percent") return { double: v.value.div(100).toNumber() };
|
||||||
if (v.kind === "date") return { double: v.ms };
|
if (v.kind === "date") return { double: v.ms };
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue