dsh-math-calculator 2.2.0 → 2.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +261 -0
- package/README.md +7 -7
- package/dsh.plugin.json +1 -1
- package/lib/format.js +52 -2
- package/lib/formula-library-bioecon.js +216 -0
- package/lib/formula-library-biol-general.js +175 -0
- package/lib/formula-library-engineering.js +384 -0
- package/lib/formula-library-macro-materials.js +452 -0
- package/lib/formulas.js +91 -6
- package/lib/help.js +552 -79
- package/lib/index.js +406 -5
- package/lib/ops/shared.js +23 -1
- package/lib/param-units.js +315 -0
- package/lib/types/formula-library-bioecon.d.ts +26 -0
- package/lib/types/formula-library-biol-general.d.ts +37 -0
- package/lib/types/formula-library-engineering.d.ts +34 -0
- package/lib/types/formula-library-macro-materials.d.ts +41 -0
- package/lib/types/formulas.d.ts +11 -1
- package/lib/types/help.d.ts +39 -0
- package/lib/types/param-units.d.ts +156 -0
- package/package.json +1 -1
- package/src/format.ts +52 -4
- package/src/formula-library-bioecon.ts +220 -0
- package/src/formula-library-biol-general.ts +179 -0
- package/src/formula-library-engineering.ts +399 -0
- package/src/formula-library-macro-materials.ts +476 -0
- package/src/formulas.ts +91 -8
- package/src/help.ts +593 -80
- package/src/index.ts +415 -5
- package/src/ops/shared.ts +22 -0
- package/src/param-units.ts +336 -0
- package/lib/__wtest.txt +0 -1
- package/lib/types/client.d.ts +0 -353
package/lib/index.js
CHANGED
|
@@ -46,6 +46,10 @@ import { opQuantities } from './ops/quantities-op.js';
|
|
|
46
46
|
import { opFormulaTable } from './ops/formula-table-op.js';
|
|
47
47
|
import { opFeasibility } from './ops/feasibility-op.js';
|
|
48
48
|
import { opDerivation } from './ops/derivation-op.js';
|
|
49
|
+
import { familiesOfUnits, makeConverter, traditionalNote, unitFromDescription } from './param-units.js';
|
|
50
|
+
import { convert as convertUnitRaw, convert, findUnit } from './units.js';
|
|
51
|
+
/** 单位换算适配器(供同体系折算用)。`convert` 的三层返回结构集中在这一处适配。 */
|
|
52
|
+
const formulaConverter = makeConverter(convertUnitRaw);
|
|
49
53
|
import { WORLD_KEYS } from './world-params.js';
|
|
50
54
|
import { opHelp } from './help.js';
|
|
51
55
|
import { annotateMagnitude, annotateMetricPrefix, checkAnchors, checkAngleUnit, checkLabelMagnitude, explainUnitResult, findBatchAnomalies } from './guards.js';
|
|
@@ -387,6 +391,25 @@ function translateError(raw, fallbackType = 'internal', source = '') {
|
|
|
387
391
|
if (toError)
|
|
388
392
|
return toError;
|
|
389
393
|
if (/Undefined symbol/i.test(text)) {
|
|
394
|
+
/**
|
|
395
|
+
* ## `Math.xxx` 是最常见的一种 —— 单独给提示(1108 报告问题 5)
|
|
396
|
+
*
|
|
397
|
+
* ```
|
|
398
|
+
* Math.min(3,5) → 错误[unknown_symbol]: Undefined symbol Math
|
|
399
|
+
* min(3,5) → 3
|
|
400
|
+
* ```
|
|
401
|
+
*
|
|
402
|
+
* 报错只说「Math 未定义」,读者不知道要做的只是**去掉前缀**。
|
|
403
|
+
* 而 JS 习惯让人本能地写 `Math.min`/`Math.abs`——
|
|
404
|
+
* 所以这一条其实命中率很高,值得单独说一句。
|
|
405
|
+
*/
|
|
406
|
+
const mathMatch = text.match(/Undefined symbol\s+(Math)\b/i);
|
|
407
|
+
if (mathMatch) {
|
|
408
|
+
const fnMatch = /Math\.([A-Za-z_][A-Za-z0-9_]*)/.exec(source);
|
|
409
|
+
const fn = fnMatch ? fnMatch[1] : '';
|
|
410
|
+
return new CalcError('unknown_symbol', '本工具不支持 Math 命名空间(' + (fn ? 'Math.' + fn : 'Math.xxx') + ')', (fn ? '去掉 Math. 前缀写成 ' + fn + '(...)' : '去掉 Math. 前缀即可') +
|
|
411
|
+
';可用 operation="help", helpTopic="evaluate" 看支持的函数清单。');
|
|
412
|
+
}
|
|
390
413
|
return new CalcError('unknown_symbol', text, '先定义变量,或通过 variables 参数传入,例如 {"m": 500}。');
|
|
391
414
|
}
|
|
392
415
|
/**
|
|
@@ -814,12 +837,42 @@ function buildScope(engine, variables) {
|
|
|
814
837
|
scope[key] = engine.bignumber(String(raw));
|
|
815
838
|
}
|
|
816
839
|
else if (typeof raw === 'string') {
|
|
817
|
-
|
|
840
|
+
/**
|
|
841
|
+
* ## 字符串值:先按数解析,再按「带单位的量」解析
|
|
842
|
+
*
|
|
843
|
+
* 实测(本轮):`variables: {V:'10 m/s', T:'1 s'}` 里的值**被整个丢弃**,
|
|
844
|
+
* 于是 `V*T` 里的 V、T 都不存在,mathjs 把单字母当单位符号相乘,
|
|
845
|
+
* 静默返回 `V T` —— 既不报错,也不是数。
|
|
846
|
+
*
|
|
847
|
+
* ```
|
|
848
|
+
* variables:{V:'10 m/s', T:'1 s'}, expression:'V*T'
|
|
849
|
+
* → "V T"(变量被跳过) ← 应该是 10 m
|
|
850
|
+
* ```
|
|
851
|
+
*
|
|
852
|
+
* 为什么这很要紧:物理量本来就该带单位写(那是本项目一直在推的方向),
|
|
853
|
+
* 而带单位的值传进 variables 却完全失效,等于逼人回到裸数字的老路。
|
|
854
|
+
*
|
|
855
|
+
* 回退顺序是刻意的:纯数字优先,因为 `engine.bignumber` 对 `1e100` 这类
|
|
856
|
+
* 字符串要精确得多;只有它失败时才试单位,避免把纯数字误走单位路径。
|
|
857
|
+
*/
|
|
858
|
+
const text = raw.trim();
|
|
818
859
|
try {
|
|
819
|
-
scope[key] = engine.bignumber(
|
|
860
|
+
scope[key] = engine.bignumber(text);
|
|
820
861
|
}
|
|
821
862
|
catch {
|
|
822
|
-
|
|
863
|
+
/** 回退:把整串当表达式求值(`10 m/s` 会得到 Unit 对象)。 */
|
|
864
|
+
try {
|
|
865
|
+
const unitValue = engine.evaluate(normalizeChineseUnits(normalizeSource(text)));
|
|
866
|
+
if (unitValue && (unitValue.isUnit || unitValue.isBigNumber || typeof unitValue === 'number')) {
|
|
867
|
+
scope[key] = unitValue;
|
|
868
|
+
}
|
|
869
|
+
else {
|
|
870
|
+
skippedVariableKeys.push({ name: key, reason: '无法解析为数字或带单位的量' });
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
catch {
|
|
874
|
+
skippedVariableKeys.push({ name: key, reason: '无法解析为数字或带单位的量' });
|
|
875
|
+
}
|
|
823
876
|
}
|
|
824
877
|
}
|
|
825
878
|
else if (typeof raw === 'boolean') {
|
|
@@ -911,7 +964,8 @@ function opEvaluate(engine, expression, scope, precision) {
|
|
|
911
964
|
raw = node.evaluate(scope);
|
|
912
965
|
}
|
|
913
966
|
catch (error) {
|
|
914
|
-
|
|
967
|
+
/** 把 source 传进去 —— 有些报错提示要用原表达式(如识别 `Math.xxx` 的前缀)。 */
|
|
968
|
+
throw translateError(error, 'internal', expression);
|
|
915
969
|
}
|
|
916
970
|
const value = unwrapResult(raw);
|
|
917
971
|
/**
|
|
@@ -2158,6 +2212,122 @@ function currentFormulas() {
|
|
|
2158
2212
|
function findInCurrentWorkspace(query, limit) {
|
|
2159
2213
|
return scoreFormulas(currentFormulas(), query, limit);
|
|
2160
2214
|
}
|
|
2215
|
+
/**
|
|
2216
|
+
* 把一批 overrides 按 `unitFrom` → `unitTo` 换算。
|
|
2217
|
+
*
|
|
2218
|
+
* ## 只换算「数值型」参数
|
|
2219
|
+
*
|
|
2220
|
+
* · 用户已经是「数值 + 单位」字符串(`"324375 L"`)→ **原样放行**,
|
|
2221
|
+
* 那是他自己指定的单位,比统一的 unitFrom 更精确。
|
|
2222
|
+
* · 数组型参数(份额、概率)→ 放行,它们多半无量纲。
|
|
2223
|
+
* · 纯数字 → 换算。
|
|
2224
|
+
*
|
|
2225
|
+
* ## 目标单位怎么定
|
|
2226
|
+
*
|
|
2227
|
+
* 调用方给了 `unitTo` 就用它;没给就用**该参数自己声明的单位**
|
|
2228
|
+
* (`V → "空间体积 m³"` 里的 `m³`)。
|
|
2229
|
+
* 这样 `unitFrom:"L"` 一个参数就够了,不必再写 unitTo。
|
|
2230
|
+
*
|
|
2231
|
+
* ## 换算失败怎么办
|
|
2232
|
+
*
|
|
2233
|
+
* **不静默按原数字算** —— 那正是报告批评的那种错。
|
|
2234
|
+
* 直接抛错并说明是哪个参数、两边是什么量纲。
|
|
2235
|
+
*
|
|
2236
|
+
* @param overrides - 调用方给的原始覆盖值
|
|
2237
|
+
* @param formula - 目标公式(用来取每个参数的声明单位)
|
|
2238
|
+
* @param unitFrom - 用户声明的输入单位
|
|
2239
|
+
* @param unitTo - 目标单位(空串表示用参数自己声明的)
|
|
2240
|
+
* @returns 换算后的 overrides
|
|
2241
|
+
*/
|
|
2242
|
+
function convertOverrides(overrides, formula, unitFrom, unitTo) {
|
|
2243
|
+
const output = {};
|
|
2244
|
+
for (const [name, raw] of Object.entries(overrides)) {
|
|
2245
|
+
/** 数组与「带单位的字符串」原样放行(见上面的注释)。 */
|
|
2246
|
+
if (Array.isArray(raw)) {
|
|
2247
|
+
output[name] = raw;
|
|
2248
|
+
continue;
|
|
2249
|
+
}
|
|
2250
|
+
if (typeof raw === 'string' && /[a-zA-Z\u4e00-\u9fff]/.test(raw.trim())) {
|
|
2251
|
+
output[name] = raw;
|
|
2252
|
+
continue;
|
|
2253
|
+
}
|
|
2254
|
+
const numeric = typeof raw === 'number' ? raw : Number(String(raw).trim());
|
|
2255
|
+
if (!Number.isFinite(numeric)) {
|
|
2256
|
+
output[name] = raw;
|
|
2257
|
+
continue;
|
|
2258
|
+
}
|
|
2259
|
+
const declared = formaliveUnitOf(formula, name);
|
|
2260
|
+
/**
|
|
2261
|
+
* ## ★ 无量纲参数一律不参与换算
|
|
2262
|
+
*
|
|
2263
|
+
* 这是本轮最隐蔽的一个 bug,实测复现如下:
|
|
2264
|
+
*
|
|
2265
|
+
* ```
|
|
2266
|
+
* {formula:"oxygen_depletion", overrides:{V:324375, n:20}, unitFrom:"L"}
|
|
2267
|
+
* → 11353.125 min ✓ 正确(V 换了、n 不动)
|
|
2268
|
+
*
|
|
2269
|
+
* {同上,但加 unitTo:"m³"}
|
|
2270
|
+
* → 11353125 min ✗ 差 1000 倍
|
|
2271
|
+
* ```
|
|
2272
|
+
*
|
|
2273
|
+
* 根因:`n` 是「人数」,参数描述里**没有单位**(`declared === ''`),
|
|
2274
|
+
* 但我写的兜底 `unitTo !== '' ? unitTo : declared` 在 declared 为空时
|
|
2275
|
+
* 让 unitTo 生效了 —— 于是 `n: 20` 被当成 `20 L` 换算成 `0.02 m³`。
|
|
2276
|
+
*
|
|
2277
|
+
* 结果虚高正是**人数被缩小了 1000 倍**,而输出看着完全正常。
|
|
2278
|
+
*
|
|
2279
|
+
* 判据:**只有声明了单位的参数才谈得上「从哪个单位换成哪个」**。
|
|
2280
|
+
* 无量纲的(人数、个数、份额、概率)跳过 —— 它们本来就没有单位可换。
|
|
2281
|
+
*/
|
|
2282
|
+
if (declared === '') {
|
|
2283
|
+
output[name] = numeric;
|
|
2284
|
+
continue;
|
|
2285
|
+
}
|
|
2286
|
+
/**
|
|
2287
|
+
* ## 上标写法要归一化
|
|
2288
|
+
*
|
|
2289
|
+
* 公式描述里写的是 `m³`(上标),而用户习惯写 `m^3`。
|
|
2290
|
+
* 单位表用的是 `m³`,所以 `unitTo:"m^3"` 会报「不认识单位」。
|
|
2291
|
+
* 在入口统一成上标形式,两种写法都能过。
|
|
2292
|
+
*/
|
|
2293
|
+
const target = unitTo !== '' ? normalizeUnitSpelling(unitTo) : declared;
|
|
2294
|
+
if (target === '' || target === unitFrom) {
|
|
2295
|
+
output[name] = numeric;
|
|
2296
|
+
continue;
|
|
2297
|
+
}
|
|
2298
|
+
const result = convert(numeric, unitFrom, target);
|
|
2299
|
+
if (!result.ok) {
|
|
2300
|
+
throw new CalcError('unit_mismatch', `参数 "${name}" 无法从 ${unitFrom} 换算到 ${target}:${result.error}`, (result.hint ?? '') + ' 不给 unitFrom 就按该参数声明的单位直接代入。');
|
|
2301
|
+
}
|
|
2302
|
+
output[name] = result.conversion.result;
|
|
2303
|
+
}
|
|
2304
|
+
return output;
|
|
2305
|
+
}
|
|
2306
|
+
/**
|
|
2307
|
+
* 单位写法归一:`m^3` → `m³`、`m^2` → `m²`(与单位表一致)。
|
|
2308
|
+
*
|
|
2309
|
+
* 为什么需要:公式描述里是上标(`空间体积 m³`),而用户与 AI 常写 `m^3`。
|
|
2310
|
+
* `units.ts` 的 `findUnit` 只认上标形式,所以这里做一次映射 ——
|
|
2311
|
+
* 不做的话 `unitTo:"m^3"` 会被判成「不认识单位」,而那是**写法差异,不是能力缺失**。
|
|
2312
|
+
*/
|
|
2313
|
+
function normalizeUnitSpelling(name) {
|
|
2314
|
+
return name
|
|
2315
|
+
.trim()
|
|
2316
|
+
.replace(/\^3$/, '³')
|
|
2317
|
+
.replace(/\^2$/, '²');
|
|
2318
|
+
}
|
|
2319
|
+
/** 取某参数在公式描述里声明的单位(`"空间体积 m³"` → `m³`)。 */
|
|
2320
|
+
function formaliveUnitOf(formula, name) {
|
|
2321
|
+
const spec = formula.params?.[name];
|
|
2322
|
+
if (!spec || typeof spec.desc !== 'string')
|
|
2323
|
+
return '';
|
|
2324
|
+
return unitFromDescription(spec.desc, (name) => { try {
|
|
2325
|
+
return findUnit(name) !== undefined;
|
|
2326
|
+
}
|
|
2327
|
+
catch {
|
|
2328
|
+
return false;
|
|
2329
|
+
} });
|
|
2330
|
+
}
|
|
2161
2331
|
function opFormula(args, engine, precision) {
|
|
2162
2332
|
const query = (args.formula ?? args.expression ?? '').trim();
|
|
2163
2333
|
if (query === '') {
|
|
@@ -2176,7 +2346,35 @@ function opFormula(args, engine, precision) {
|
|
|
2176
2346
|
if (!formula) {
|
|
2177
2347
|
throw new CalcError('unknown_formula', `没有匹配 "${query}" 的公式`, '用 operation="formulas" 按关键词检索,或直接 operation="evaluate" 写自己的表达式。');
|
|
2178
2348
|
}
|
|
2179
|
-
|
|
2349
|
+
/**
|
|
2350
|
+
* ## `unitFrom` / `unitTo`:**让它们真的参与计算**(1108 报告 P0-1)
|
|
2351
|
+
*
|
|
2352
|
+
* 报告实测:
|
|
2353
|
+
*
|
|
2354
|
+
* ```
|
|
2355
|
+
* {formula:"oxygen_depletion", overrides:{V:324375}, unitFrom:"L"}
|
|
2356
|
+
* → 11353125 min ← 与不传 unitFrom 完全相同
|
|
2357
|
+
* ```
|
|
2358
|
+
*
|
|
2359
|
+
* 根因:`opFormula` 里**根本没有读取这两个参数** —— 它们只是被 schema 声明了,
|
|
2360
|
+
* 于是「传了却不生效」这种最难发现的情况。
|
|
2361
|
+
*
|
|
2362
|
+
* 而公式参数的声明单位是 m³,用户按 L 给数字,结果就虚高 **1000 倍**,
|
|
2363
|
+
* 且数字看起来完全正常(11353125 分钟 ≈ 21 年,荒谬但不像错)。
|
|
2364
|
+
*
|
|
2365
|
+
* ## 语义定成「把所有数值参数按 unitFrom → unitTo 换算」
|
|
2366
|
+
*
|
|
2367
|
+
* 公式内部一律 SI,所以 `unitFrom` 是「我给的数是什么单位」,
|
|
2368
|
+
* `unitTo` 是「目标单位」(默认就用参数自己声明的那个)。
|
|
2369
|
+
* 逐个参数换算而不是只换一个,因为一次调用里参数通常同单位。
|
|
2370
|
+
*
|
|
2371
|
+
* 注意:这与「参数直接带单位」(`overrides:{V:"324375 L"}`)是两条等价路径,
|
|
2372
|
+
* 后者更精确(可逐参数给不同单位),前者更省事(一次给全)。两条都保留。
|
|
2373
|
+
*/
|
|
2374
|
+
const unitFrom = typeof args.unitFrom === 'string' ? args.unitFrom.trim() : '';
|
|
2375
|
+
const unitTo = typeof args.unitTo === 'string' ? args.unitTo.trim() : '';
|
|
2376
|
+
const rawOverrides = (args.overrides ?? {});
|
|
2377
|
+
const overrides = unitFrom === '' ? rawOverrides : convertOverrides(rawOverrides, formula, unitFrom, unitTo);
|
|
2180
2378
|
let resolved;
|
|
2181
2379
|
try {
|
|
2182
2380
|
resolved = resolveParams(formula, overrides);
|
|
@@ -2258,6 +2456,81 @@ function opFormula(args, engine, precision) {
|
|
|
2258
2456
|
* 紧跟在结果后面(而不是压到输出末尾),因为这类问题会直接改变结论,
|
|
2259
2457
|
* 读者必须第一眼就看到。
|
|
2260
2458
|
*/
|
|
2459
|
+
if (Object.keys(resolved.converted).length > 0) {
|
|
2460
|
+
const items = Object.entries(resolved.converted).map(function ([name, info]) {
|
|
2461
|
+
const original = info.original === null ? info.from : String(info.original) + ' ' + info.from;
|
|
2462
|
+
return name + ': ' + original + ' → ' + info.to;
|
|
2463
|
+
});
|
|
2464
|
+
parts.push('');
|
|
2465
|
+
parts.push('【单位换算】' + items.join(';') + '(换算到参数声明的单位后代入公式)');
|
|
2466
|
+
}
|
|
2467
|
+
/**
|
|
2468
|
+
* ## 同体系折算(用户 2026-11-07 要求)
|
|
2469
|
+
*
|
|
2470
|
+
* 「最好使用同一体系单位,把 60 km 折算成 里。」
|
|
2471
|
+
*
|
|
2472
|
+
* 输入用了里/时辰时,主结果仍是 SI(那是公式参数的口径,也是与其它计算对接的基础),
|
|
2473
|
+
* 但补一行折算到传统体系的说法 —— 作者写正文时直接能用,不必自己再换一遍。
|
|
2474
|
+
*
|
|
2475
|
+
* ```
|
|
2476
|
+
* 结果: 2.08333333 m/s
|
|
2477
|
+
* 【同体系折算】约 30 里/时辰(你的输入用了 里、时辰)
|
|
2478
|
+
* ```
|
|
2479
|
+
*/
|
|
2480
|
+
{
|
|
2481
|
+
/**
|
|
2482
|
+
* 输入里的单位来源有两处,**都要看**:
|
|
2483
|
+
* · `resolved.converted` —— 做过换算的(`60 里` → `30000 m`)
|
|
2484
|
+
* · 调用方原文 —— 单位恰好等于声明单位时不会产生换算记录
|
|
2485
|
+
* (如参数声明 `m/s`、调用方就写 `10 m/s`),但那时也不算「传统单位」
|
|
2486
|
+
*
|
|
2487
|
+
* 踩过:第一版只看 `converted`,于是 `60 里` 能折出里、
|
|
2488
|
+
* 而其它写法(如 `200 斤` 换算后)虽然 converted 里有、
|
|
2489
|
+
* 但**没走这个分支**的形态就漏了。现在两处合并取单位名。
|
|
2490
|
+
*/
|
|
2491
|
+
const seenUnits = [];
|
|
2492
|
+
for (const info of Object.values(resolved.converted)) {
|
|
2493
|
+
if (!seenUnits.includes(info.from))
|
|
2494
|
+
seenUnits.push(info.from);
|
|
2495
|
+
}
|
|
2496
|
+
/**
|
|
2497
|
+
* ★ 变量名必须与外层 `raw`(公式结果)区分开。
|
|
2498
|
+
*
|
|
2499
|
+
* 踩过:第一版写成 `for (const [name, raw] of ...)`,于是**遮蔽了外层的
|
|
2500
|
+
* `raw`(公式算出来的值)**,`formatValue(engine, raw, ...)` 拿到的是
|
|
2501
|
+
* 调用方输入的字符串 `"60 里"` 而不是 `"2.08333333 m/s"` ——
|
|
2502
|
+
* 折算函数收不到合法数值,静默返回 null。
|
|
2503
|
+
*
|
|
2504
|
+
* 这类 shadowing bug 编译不报错、测试不覆盖就很难发现。
|
|
2505
|
+
*/
|
|
2506
|
+
for (const [, rawInput] of Object.entries(overrides)) {
|
|
2507
|
+
if (typeof rawInput !== 'string')
|
|
2508
|
+
continue;
|
|
2509
|
+
const piece = rawInput.trim().match(/^[+-]?[0-9][0-9.]*(?:e[+-]?[0-9]+)?\s*(.+)$/i);
|
|
2510
|
+
if (!piece)
|
|
2511
|
+
continue;
|
|
2512
|
+
const unitName = piece[1].trim();
|
|
2513
|
+
if (unitName !== '' && !seenUnits.includes(unitName))
|
|
2514
|
+
seenUnits.push(unitName);
|
|
2515
|
+
}
|
|
2516
|
+
const families = familiesOfUnits(seenUnits);
|
|
2517
|
+
if (families.length > 0) {
|
|
2518
|
+
/**
|
|
2519
|
+
* ★ 单位要**自己拼上**。
|
|
2520
|
+
*
|
|
2521
|
+
* `formatValue` 对 `s / t` 这种纯数值表达式只给 `2.08333333` ——
|
|
2522
|
+
* 单位 `m/s` 是在渲染那一行时用 `formula.unit` 拼上去的,不在值里。
|
|
2523
|
+
* 踩过:直接把 `formatValue` 的结果喂给折算函数,它拿不到单位、
|
|
2524
|
+
* 静默返回 null,于是折算行永远不出现。
|
|
2525
|
+
*/
|
|
2526
|
+
const siText = formula.unit
|
|
2527
|
+
? formatValue(engine, raw, precision, currentDisplayPrecision()) + ' ' + formula.unit
|
|
2528
|
+
: formatValue(engine, raw, precision, currentDisplayPrecision());
|
|
2529
|
+
const note = traditionalNote(siText, families, formulaConverter);
|
|
2530
|
+
if (note !== null)
|
|
2531
|
+
parts.push(note);
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2261
2534
|
if (unknownOverrideKeys.length > 0) {
|
|
2262
2535
|
parts.push('');
|
|
2263
2536
|
parts.push('⚠ 以下 overrides 的 key 不在公式「' + formula.name + '」的参数里,**已被忽略**:' +
|
|
@@ -2300,6 +2573,61 @@ function opFormula(args, engine, precision) {
|
|
|
2300
2573
|
parts.push(' 需要这些量:' + empiricalNote.needsFrom.join('、'));
|
|
2301
2574
|
}
|
|
2302
2575
|
}
|
|
2576
|
+
/**
|
|
2577
|
+
* ## `terse`:只回一行(1108 报告问题 9)
|
|
2578
|
+
*
|
|
2579
|
+
* 报告实测:`formula:"kleiber", terse:true` 返回的仍是完整结构化输出,
|
|
2580
|
+
* `terse` 对 formula 完全无效。
|
|
2581
|
+
*
|
|
2582
|
+
* ## 但风险提示不能省
|
|
2583
|
+
*
|
|
2584
|
+
* 与 evaluate 的 terse 同一条约定:**省掉的是标题与参数表,不是风险**。
|
|
2585
|
+
* 所以 terse 下仍保留三样:结果、覆盖了哪些参数(一句话)、
|
|
2586
|
+
* 以及「这个数的单位是什么」。
|
|
2587
|
+
*
|
|
2588
|
+
* 尤其经验公式的 **【适用域】绝不能砍** —— 那是「这个系数只在 1g 世界成立」
|
|
2589
|
+
* 的唯一提示,砍掉就等于让人在非地球世界里照抄。所以 terse 时会用半行压缩它。
|
|
2590
|
+
*/
|
|
2591
|
+
if (args.terse === true) {
|
|
2592
|
+
const answer = formatValue(engine, raw, precision, currentDisplayPrecision());
|
|
2593
|
+
const unitSuffix = formula.unit ? ' ' + formula.unit : '';
|
|
2594
|
+
const terseLines = [formula.id + ' = ' + answer + unitSuffix];
|
|
2595
|
+
/** 覆盖提示:一句话,不列明细。 */
|
|
2596
|
+
if (resolved.overridden.length > 0) {
|
|
2597
|
+
terseLines.push(' (覆盖: ' + resolved.overridden.join(',') + ')');
|
|
2598
|
+
}
|
|
2599
|
+
/** 单位换算与同体系折算:与完整版同一判据,只是不加标题行。 */
|
|
2600
|
+
if (Object.keys(resolved.converted).length > 0) {
|
|
2601
|
+
const items = Object.entries(resolved.converted).map(function ([name, info]) {
|
|
2602
|
+
const original = info.original === null ? info.from : String(info.original) + ' ' + info.from;
|
|
2603
|
+
return name + ': ' + original + '→' + info.to;
|
|
2604
|
+
});
|
|
2605
|
+
terseLines.push(' (换算: ' + items.join('; ') + ')');
|
|
2606
|
+
}
|
|
2607
|
+
/** 经验式适用域:压成一行,但绝不省略。 */
|
|
2608
|
+
const terseEmpirical = empiricalNoteOf(formula.id);
|
|
2609
|
+
if (terseEmpirical) {
|
|
2610
|
+
terseLines.push(' 【适用域:' + KIND_LABEL[terseEmpirical.kind] + '】' +
|
|
2611
|
+
(terseEmpirical.breaksWhen ? ' 何时失效: ' + terseEmpirical.breaksWhen : ''));
|
|
2612
|
+
}
|
|
2613
|
+
/** 未知参数名必须保留 —— 它会直接改变结论。 */
|
|
2614
|
+
if (unknownOverrideKeys.length > 0) {
|
|
2615
|
+
terseLines.push(' ⚠ 未生效的 key: ' + unknownOverrideKeys.join('、') +
|
|
2616
|
+
'(该公式参数是 ' + Object.keys(formula.params).join('、') + ')');
|
|
2617
|
+
}
|
|
2618
|
+
return {
|
|
2619
|
+
result: terseLines.join('\n'),
|
|
2620
|
+
value: {
|
|
2621
|
+
formula: formula.id,
|
|
2622
|
+
category: formula.cat,
|
|
2623
|
+
expression,
|
|
2624
|
+
params: toJsonValue(resolved.values),
|
|
2625
|
+
overridden: resolved.overridden,
|
|
2626
|
+
answer: toJsonValue(raw),
|
|
2627
|
+
unit: formula.unit ?? null,
|
|
2628
|
+
},
|
|
2629
|
+
};
|
|
2630
|
+
}
|
|
2303
2631
|
return {
|
|
2304
2632
|
result: parts.join('\n'),
|
|
2305
2633
|
value: {
|
|
@@ -2628,6 +2956,25 @@ export function runMathTool(rawArguments) {
|
|
|
2628
2956
|
'常见原因:拼写笔误、大小写、把某个 operation 的子字段提到了顶层' +
|
|
2629
2957
|
'(如 terrain 的字段要放在 terrain 对象里)。');
|
|
2630
2958
|
}
|
|
2959
|
+
/**
|
|
2960
|
+
* ## 记下「调用方是否显式调低了计算精度」(1108 报告问题 4)
|
|
2961
|
+
*
|
|
2962
|
+
* 报告实测:
|
|
2963
|
+
*
|
|
2964
|
+
* ```
|
|
2965
|
+
* precision=8: 10^20 + 1 → 100000000000000000000 ← 加 1 被吃掉
|
|
2966
|
+
* precision=16: (10^20+12345678901234567890)-(10^20) → 12345678901234600000
|
|
2967
|
+
* ```
|
|
2968
|
+
*
|
|
2969
|
+
* 文档已警告「不要为好看调低 precision」,但**没有任何反馈**——
|
|
2970
|
+
* 数字看着正常,只是末几位不是真的。
|
|
2971
|
+
*
|
|
2972
|
+
* 这里只记录「是否低于默认档」,具体要不要提示由结果判断:
|
|
2973
|
+
* 若结果的位数超过 precision,说明确实被截了,才提示。
|
|
2974
|
+
*/
|
|
2975
|
+
const requestedPrecision = typeof args.precision === 'number' && Number.isFinite(args.precision)
|
|
2976
|
+
? Math.round(args.precision)
|
|
2977
|
+
: undefined;
|
|
2631
2978
|
precision = clampPrecision(args.precision);
|
|
2632
2979
|
/**
|
|
2633
2980
|
* 显示精度:本次显式给 > 工作区固化值 > 沿用计算精度。
|
|
@@ -3002,6 +3349,11 @@ export function runMathTool(rawArguments) {
|
|
|
3002
3349
|
const hintNote = maybeDisplayPrecisionHint(result.result);
|
|
3003
3350
|
if (hintNote)
|
|
3004
3351
|
result.notice = hintNote;
|
|
3352
|
+
/** 调低计算精度导致结果被截 —— 那种「看起来正常、末位是假的」情况。 */
|
|
3353
|
+
const truncNote = checkPrecisionTruncation(requestedPrecision, precision, result.result);
|
|
3354
|
+
if (truncNote) {
|
|
3355
|
+
result.notice = result.notice ? result.notice + '\n' + truncNote : truncNote;
|
|
3356
|
+
}
|
|
3005
3357
|
/**
|
|
3006
3358
|
* 三角函数按弧度算时补一句说明。
|
|
3007
3359
|
*
|
|
@@ -3699,6 +4051,55 @@ const PARAMETER_SPEC = {
|
|
|
3699
4051
|
* 但 defineTool 的定义在模块级、拿不到 apply 的局部变量。
|
|
3700
4052
|
*/
|
|
3701
4053
|
let ctxRef = null;
|
|
4054
|
+
/**
|
|
4055
|
+
* 调用方显式调低计算精度时,检查结果是否**因此被截**。
|
|
4056
|
+
*
|
|
4057
|
+
* ## 为什么要提示(1108 报告问题 4)
|
|
4058
|
+
*
|
|
4059
|
+
* ```
|
|
4060
|
+
* precision=8: 10^20 + 1 → 100000000000000000000 ← 加 1 被吃掉
|
|
4061
|
+
* precision=16: (10^20+大数)-(10^20) → 12345678901234600000 ← 末位失真
|
|
4062
|
+
* ```
|
|
4063
|
+
*
|
|
4064
|
+
* 文档已经写了「不要为好看调低 precision」,但**说了不等于看见了**:
|
|
4065
|
+
* 这两个结果看起来都是正常的大整数,没有任何迹象表明末几位是假的。
|
|
4066
|
+
*
|
|
4067
|
+
* ## 判据
|
|
4068
|
+
*
|
|
4069
|
+
* 只有两件事同时成立才提示:
|
|
4070
|
+
* 1. 调用方**显式**给了低于默认(64)的 precision;
|
|
4071
|
+
* 2. 结果的**整数位数超过** precision —— 那说明确实丢了低位。
|
|
4072
|
+
*
|
|
4073
|
+
* 第 2 条是关键:`precision:8` 算 `1+1` 不该被念;
|
|
4074
|
+
* 只有数字真的长到装不下时才提示。
|
|
4075
|
+
*
|
|
4076
|
+
* @param requested - 调用方显式给的 precision(未给则 undefined)
|
|
4077
|
+
* @param effective - 实际生效的 precision
|
|
4078
|
+
* @param text - 已格式化的结果文本
|
|
4079
|
+
* @returns 提示文本;不需要时返回空串
|
|
4080
|
+
*/
|
|
4081
|
+
function checkPrecisionTruncation(requested, effective, text) {
|
|
4082
|
+
/**
|
|
4083
|
+
* 判据:调用方显式给的精度**低于默认 64** 才算「调低了」,
|
|
4084
|
+
* 然后看结果是否真的装不下。
|
|
4085
|
+
*
|
|
4086
|
+
* 踩过:第一版写 `requested >= effective` 就跳过 —— 而两者本来相等
|
|
4087
|
+
* (effective 就是 clamp 过的 requested),于是这条永远不触发。
|
|
4088
|
+
*/
|
|
4089
|
+
if (requested === undefined || requested >= DEFAULT_PRECISION)
|
|
4090
|
+
return '';
|
|
4091
|
+
/** 只看纯数字结果:带单位的另有提示通道。 */
|
|
4092
|
+
const numeric = text.trim().match(/^-?([0-9]+)(?:\.([0-9]+))?$/);
|
|
4093
|
+
if (!numeric)
|
|
4094
|
+
return '';
|
|
4095
|
+
const integerDigits = numeric[1].replace(/^0+(?=.)/, '').length;
|
|
4096
|
+
if (integerDigits <= effective)
|
|
4097
|
+
return '';
|
|
4098
|
+
return '本次结果有 ' + integerDigits + ' 位有效数字,而 precision=' + effective +
|
|
4099
|
+
' 只能精确表示 ' + effective + ' 位——末 ' + (integerDigits - effective) +
|
|
4100
|
+
' 位可能已失真。需要精确值时请用 precision:64(默认),' +
|
|
4101
|
+
'只想让输出短些请用 displayPrecision。';
|
|
4102
|
+
}
|
|
3702
4103
|
/**
|
|
3703
4104
|
* 数学计算工具(`math_execute`)。
|
|
3704
4105
|
*
|
package/lib/ops/shared.js
CHANGED
|
@@ -412,7 +412,29 @@ export function normalizeSource(input) {
|
|
|
412
412
|
* 所以表达式里出现 ²/³ 时统一换成 `^2`/`^3`。
|
|
413
413
|
*/
|
|
414
414
|
.replace(/²/g, '^2')
|
|
415
|
-
.replace(/³/g, '^3')
|
|
415
|
+
.replace(/³/g, '^3')
|
|
416
|
+
/**
|
|
417
|
+
* ## `min` 撞函数名:单位位置的 min 换成 minute
|
|
418
|
+
*
|
|
419
|
+
* mathjs 里 `min` 是**函数**(求最小值),同时我们的单位表里 `min` 是**分钟**。
|
|
420
|
+
* 于是 `1 L/min` 会被解析成「L 除以 min 函数」,报一个看不懂的错:
|
|
421
|
+
*
|
|
422
|
+
* ```
|
|
423
|
+
* 1 L/min
|
|
424
|
+
* → internal: Unexpected type of argument in function divide
|
|
425
|
+
* (expected: number or Complex or ... actual: function)
|
|
426
|
+
* ```
|
|
427
|
+
*
|
|
428
|
+
* 这个错**完全指向不到修复方向** —— 用户只觉得「工具不支持复合单位」,
|
|
429
|
+
* 而真实原因只是一个名字冲突(1108 报告的问题 5 与问题 6 是同一个根)。
|
|
430
|
+
*
|
|
431
|
+
* 判据只认**单位位置**的 min,避免改坏真正的函数调用:
|
|
432
|
+
* · 紧跟 `/` 之后(`L/min`、`mol/min`)
|
|
433
|
+
* · 紧跟数字之后(`5 min`)
|
|
434
|
+
* · 紧跟 `to` 之后(`1 s to min`)
|
|
435
|
+
* 而 `min(3,5)` 这类调用(后面紧跟左括号)**不动**。
|
|
436
|
+
*/
|
|
437
|
+
.replace(/(^|[\/(\s])min(?=\s*(?:to\b|[)*\-+^/;]|$))/g, '$1minute');
|
|
416
438
|
}
|
|
417
439
|
/**
|
|
418
440
|
* 正则元字符转义。
|