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/src/index.ts
CHANGED
|
@@ -216,6 +216,11 @@ import { opQuantities } from './ops/quantities-op.js'
|
|
|
216
216
|
import { opFormulaTable } from './ops/formula-table-op.js'
|
|
217
217
|
import { opFeasibility } from './ops/feasibility-op.js'
|
|
218
218
|
import { opDerivation, DERIVATION_SUBCOMMANDS } from './ops/derivation-op.js'
|
|
219
|
+
import { familiesOfUnits, makeConverter, traditionalNote, unitFromDescription } from './param-units.js'
|
|
220
|
+
import { convert as convertUnitRaw, convert, findUnit } from './units.js'
|
|
221
|
+
|
|
222
|
+
/** 单位换算适配器(供同体系折算用)。`convert` 的三层返回结构集中在这一处适配。 */
|
|
223
|
+
const formulaConverter = makeConverter(convertUnitRaw)
|
|
219
224
|
import { WORLD_KEYS } from './world-params.js'
|
|
220
225
|
import { opHelp, HELP_TOPICS } from './help.js'
|
|
221
226
|
import { annotateMagnitude, annotateMetricPrefix, checkAnchors, checkAngleUnit, checkLabelMagnitude, explainUnitResult, findBatchAnomalies } from './guards.js'
|
|
@@ -628,6 +633,29 @@ function translateError(raw: unknown, fallbackType = 'internal', source = ''): C
|
|
|
628
633
|
if (toError) return toError
|
|
629
634
|
|
|
630
635
|
if (/Undefined symbol/i.test(text)) {
|
|
636
|
+
/**
|
|
637
|
+
* ## `Math.xxx` 是最常见的一种 —— 单独给提示(1108 报告问题 5)
|
|
638
|
+
*
|
|
639
|
+
* ```
|
|
640
|
+
* Math.min(3,5) → 错误[unknown_symbol]: Undefined symbol Math
|
|
641
|
+
* min(3,5) → 3
|
|
642
|
+
* ```
|
|
643
|
+
*
|
|
644
|
+
* 报错只说「Math 未定义」,读者不知道要做的只是**去掉前缀**。
|
|
645
|
+
* 而 JS 习惯让人本能地写 `Math.min`/`Math.abs`——
|
|
646
|
+
* 所以这一条其实命中率很高,值得单独说一句。
|
|
647
|
+
*/
|
|
648
|
+
const mathMatch = text.match(/Undefined symbol\s+(Math)\b/i)
|
|
649
|
+
if (mathMatch) {
|
|
650
|
+
const fnMatch = /Math\.([A-Za-z_][A-Za-z0-9_]*)/.exec(source)
|
|
651
|
+
const fn = fnMatch ? fnMatch[1] : ''
|
|
652
|
+
return new CalcError(
|
|
653
|
+
'unknown_symbol',
|
|
654
|
+
'本工具不支持 Math 命名空间(' + (fn ? 'Math.' + fn : 'Math.xxx') + ')',
|
|
655
|
+
(fn ? '去掉 Math. 前缀写成 ' + fn + '(...)' : '去掉 Math. 前缀即可') +
|
|
656
|
+
';可用 operation="help", helpTopic="evaluate" 看支持的函数清单。',
|
|
657
|
+
)
|
|
658
|
+
}
|
|
631
659
|
return new CalcError('unknown_symbol', text, '先定义变量,或通过 variables 参数传入,例如 {"m": 500}。')
|
|
632
660
|
}
|
|
633
661
|
/**
|
|
@@ -1061,11 +1089,39 @@ function buildScope(engine: any, variables: unknown): Record<string, any> {
|
|
|
1061
1089
|
}
|
|
1062
1090
|
scope[key] = engine.bignumber(String(raw))
|
|
1063
1091
|
} else if (typeof raw === 'string') {
|
|
1064
|
-
|
|
1092
|
+
/**
|
|
1093
|
+
* ## 字符串值:先按数解析,再按「带单位的量」解析
|
|
1094
|
+
*
|
|
1095
|
+
* 实测(本轮):`variables: {V:'10 m/s', T:'1 s'}` 里的值**被整个丢弃**,
|
|
1096
|
+
* 于是 `V*T` 里的 V、T 都不存在,mathjs 把单字母当单位符号相乘,
|
|
1097
|
+
* 静默返回 `V T` —— 既不报错,也不是数。
|
|
1098
|
+
*
|
|
1099
|
+
* ```
|
|
1100
|
+
* variables:{V:'10 m/s', T:'1 s'}, expression:'V*T'
|
|
1101
|
+
* → "V T"(变量被跳过) ← 应该是 10 m
|
|
1102
|
+
* ```
|
|
1103
|
+
*
|
|
1104
|
+
* 为什么这很要紧:物理量本来就该带单位写(那是本项目一直在推的方向),
|
|
1105
|
+
* 而带单位的值传进 variables 却完全失效,等于逼人回到裸数字的老路。
|
|
1106
|
+
*
|
|
1107
|
+
* 回退顺序是刻意的:纯数字优先,因为 `engine.bignumber` 对 `1e100` 这类
|
|
1108
|
+
* 字符串要精确得多;只有它失败时才试单位,避免把纯数字误走单位路径。
|
|
1109
|
+
*/
|
|
1110
|
+
const text = raw.trim()
|
|
1065
1111
|
try {
|
|
1066
|
-
scope[key] = engine.bignumber(
|
|
1112
|
+
scope[key] = engine.bignumber(text)
|
|
1067
1113
|
} catch {
|
|
1068
|
-
|
|
1114
|
+
/** 回退:把整串当表达式求值(`10 m/s` 会得到 Unit 对象)。 */
|
|
1115
|
+
try {
|
|
1116
|
+
const unitValue = engine.evaluate(normalizeChineseUnits(normalizeSource(text)))
|
|
1117
|
+
if (unitValue && (unitValue.isUnit || unitValue.isBigNumber || typeof unitValue === 'number')) {
|
|
1118
|
+
scope[key] = unitValue
|
|
1119
|
+
} else {
|
|
1120
|
+
skippedVariableKeys.push({ name: key, reason: '无法解析为数字或带单位的量' })
|
|
1121
|
+
}
|
|
1122
|
+
} catch {
|
|
1123
|
+
skippedVariableKeys.push({ name: key, reason: '无法解析为数字或带单位的量' })
|
|
1124
|
+
}
|
|
1069
1125
|
}
|
|
1070
1126
|
} else if (typeof raw === 'boolean') {
|
|
1071
1127
|
scope[key] = raw
|
|
@@ -1157,7 +1213,8 @@ function opEvaluate(engine: any, expression: string, scope: Record<string, any>,
|
|
|
1157
1213
|
try {
|
|
1158
1214
|
raw = node.evaluate(scope)
|
|
1159
1215
|
} catch (error) {
|
|
1160
|
-
|
|
1216
|
+
/** 把 source 传进去 —— 有些报错提示要用原表达式(如识别 `Math.xxx` 的前缀)。 */
|
|
1217
|
+
throw translateError(error, 'internal', expression)
|
|
1161
1218
|
}
|
|
1162
1219
|
|
|
1163
1220
|
const value = unwrapResult(raw)
|
|
@@ -2498,6 +2555,127 @@ function findInCurrentWorkspace(query: string, limit: number) {
|
|
|
2498
2555
|
return scoreFormulas(currentFormulas(), query, limit)
|
|
2499
2556
|
}
|
|
2500
2557
|
|
|
2558
|
+
/**
|
|
2559
|
+
* 把一批 overrides 按 `unitFrom` → `unitTo` 换算。
|
|
2560
|
+
*
|
|
2561
|
+
* ## 只换算「数值型」参数
|
|
2562
|
+
*
|
|
2563
|
+
* · 用户已经是「数值 + 单位」字符串(`"324375 L"`)→ **原样放行**,
|
|
2564
|
+
* 那是他自己指定的单位,比统一的 unitFrom 更精确。
|
|
2565
|
+
* · 数组型参数(份额、概率)→ 放行,它们多半无量纲。
|
|
2566
|
+
* · 纯数字 → 换算。
|
|
2567
|
+
*
|
|
2568
|
+
* ## 目标单位怎么定
|
|
2569
|
+
*
|
|
2570
|
+
* 调用方给了 `unitTo` 就用它;没给就用**该参数自己声明的单位**
|
|
2571
|
+
* (`V → "空间体积 m³"` 里的 `m³`)。
|
|
2572
|
+
* 这样 `unitFrom:"L"` 一个参数就够了,不必再写 unitTo。
|
|
2573
|
+
*
|
|
2574
|
+
* ## 换算失败怎么办
|
|
2575
|
+
*
|
|
2576
|
+
* **不静默按原数字算** —— 那正是报告批评的那种错。
|
|
2577
|
+
* 直接抛错并说明是哪个参数、两边是什么量纲。
|
|
2578
|
+
*
|
|
2579
|
+
* @param overrides - 调用方给的原始覆盖值
|
|
2580
|
+
* @param formula - 目标公式(用来取每个参数的声明单位)
|
|
2581
|
+
* @param unitFrom - 用户声明的输入单位
|
|
2582
|
+
* @param unitTo - 目标单位(空串表示用参数自己声明的)
|
|
2583
|
+
* @returns 换算后的 overrides
|
|
2584
|
+
*/
|
|
2585
|
+
function convertOverrides(
|
|
2586
|
+
overrides: Record<string, number | string>,
|
|
2587
|
+
formula: FormulaDef,
|
|
2588
|
+
unitFrom: string,
|
|
2589
|
+
unitTo: string,
|
|
2590
|
+
): Record<string, number | string> {
|
|
2591
|
+
const output: Record<string, number | string> = {}
|
|
2592
|
+
for (const [name, raw] of Object.entries(overrides)) {
|
|
2593
|
+
/** 数组与「带单位的字符串」原样放行(见上面的注释)。 */
|
|
2594
|
+
if (Array.isArray(raw)) {
|
|
2595
|
+
output[name] = raw as unknown as string
|
|
2596
|
+
continue
|
|
2597
|
+
}
|
|
2598
|
+
if (typeof raw === 'string' && /[a-zA-Z\u4e00-\u9fff]/.test(raw.trim())) {
|
|
2599
|
+
output[name] = raw
|
|
2600
|
+
continue
|
|
2601
|
+
}
|
|
2602
|
+
const numeric = typeof raw === 'number' ? raw : Number(String(raw).trim())
|
|
2603
|
+
if (!Number.isFinite(numeric)) {
|
|
2604
|
+
output[name] = raw
|
|
2605
|
+
continue
|
|
2606
|
+
}
|
|
2607
|
+
const declared = formaliveUnitOf(formula, name)
|
|
2608
|
+
/**
|
|
2609
|
+
* ## ★ 无量纲参数一律不参与换算
|
|
2610
|
+
*
|
|
2611
|
+
* 这是本轮最隐蔽的一个 bug,实测复现如下:
|
|
2612
|
+
*
|
|
2613
|
+
* ```
|
|
2614
|
+
* {formula:"oxygen_depletion", overrides:{V:324375, n:20}, unitFrom:"L"}
|
|
2615
|
+
* → 11353.125 min ✓ 正确(V 换了、n 不动)
|
|
2616
|
+
*
|
|
2617
|
+
* {同上,但加 unitTo:"m³"}
|
|
2618
|
+
* → 11353125 min ✗ 差 1000 倍
|
|
2619
|
+
* ```
|
|
2620
|
+
*
|
|
2621
|
+
* 根因:`n` 是「人数」,参数描述里**没有单位**(`declared === ''`),
|
|
2622
|
+
* 但我写的兜底 `unitTo !== '' ? unitTo : declared` 在 declared 为空时
|
|
2623
|
+
* 让 unitTo 生效了 —— 于是 `n: 20` 被当成 `20 L` 换算成 `0.02 m³`。
|
|
2624
|
+
*
|
|
2625
|
+
* 结果虚高正是**人数被缩小了 1000 倍**,而输出看着完全正常。
|
|
2626
|
+
*
|
|
2627
|
+
* 判据:**只有声明了单位的参数才谈得上「从哪个单位换成哪个」**。
|
|
2628
|
+
* 无量纲的(人数、个数、份额、概率)跳过 —— 它们本来就没有单位可换。
|
|
2629
|
+
*/
|
|
2630
|
+
if (declared === '') {
|
|
2631
|
+
output[name] = numeric
|
|
2632
|
+
continue
|
|
2633
|
+
}
|
|
2634
|
+
/**
|
|
2635
|
+
* ## 上标写法要归一化
|
|
2636
|
+
*
|
|
2637
|
+
* 公式描述里写的是 `m³`(上标),而用户习惯写 `m^3`。
|
|
2638
|
+
* 单位表用的是 `m³`,所以 `unitTo:"m^3"` 会报「不认识单位」。
|
|
2639
|
+
* 在入口统一成上标形式,两种写法都能过。
|
|
2640
|
+
*/
|
|
2641
|
+
const target = unitTo !== '' ? normalizeUnitSpelling(unitTo) : declared
|
|
2642
|
+
if (target === '' || target === unitFrom) {
|
|
2643
|
+
output[name] = numeric
|
|
2644
|
+
continue
|
|
2645
|
+
}
|
|
2646
|
+
const result = convert(numeric, unitFrom, target)
|
|
2647
|
+
if (!result.ok) {
|
|
2648
|
+
throw new CalcError(
|
|
2649
|
+
'unit_mismatch',
|
|
2650
|
+
`参数 "${name}" 无法从 ${unitFrom} 换算到 ${target}:${result.error}`,
|
|
2651
|
+
(result.hint ?? '') + ' 不给 unitFrom 就按该参数声明的单位直接代入。',
|
|
2652
|
+
)
|
|
2653
|
+
}
|
|
2654
|
+
output[name] = result.conversion.result
|
|
2655
|
+
}
|
|
2656
|
+
return output
|
|
2657
|
+
}
|
|
2658
|
+
|
|
2659
|
+
|
|
2660
|
+
/**
|
|
2661
|
+
* 单位写法归一:`m^3` → `m³`、`m^2` → `m²`(与单位表一致)。
|
|
2662
|
+
*
|
|
2663
|
+
* 为什么需要:公式描述里是上标(`空间体积 m³`),而用户与 AI 常写 `m^3`。
|
|
2664
|
+
* `units.ts` 的 `findUnit` 只认上标形式,所以这里做一次映射 ——
|
|
2665
|
+
* 不做的话 `unitTo:"m^3"` 会被判成「不认识单位」,而那是**写法差异,不是能力缺失**。
|
|
2666
|
+
*/
|
|
2667
|
+
function normalizeUnitSpelling(name: string): string {
|
|
2668
|
+
return name
|
|
2669
|
+
.trim()
|
|
2670
|
+
.replace(/\^3$/, '³')
|
|
2671
|
+
.replace(/\^2$/, '²')
|
|
2672
|
+
}
|
|
2673
|
+
/** 取某参数在公式描述里声明的单位(`"空间体积 m³"` → `m³`)。 */
|
|
2674
|
+
function formaliveUnitOf(formula: FormulaDef, name: string): string {
|
|
2675
|
+
const spec = formula.params?.[name]
|
|
2676
|
+
if (!spec || typeof spec.desc !== 'string') return ''
|
|
2677
|
+
return unitFromDescription(spec.desc, (name) => { try { return findUnit(name) !== undefined } catch { return false } })
|
|
2678
|
+
}
|
|
2501
2679
|
function opFormula(args: MathArgs, engine: any, precision: number): Outcome {
|
|
2502
2680
|
const query = (args.formula ?? args.expression ?? '').trim()
|
|
2503
2681
|
if (query === '') {
|
|
@@ -2522,13 +2700,42 @@ function opFormula(args: MathArgs, engine: any, precision: number): Outcome {
|
|
|
2522
2700
|
)
|
|
2523
2701
|
}
|
|
2524
2702
|
|
|
2525
|
-
|
|
2703
|
+
/**
|
|
2704
|
+
* ## `unitFrom` / `unitTo`:**让它们真的参与计算**(1108 报告 P0-1)
|
|
2705
|
+
*
|
|
2706
|
+
* 报告实测:
|
|
2707
|
+
*
|
|
2708
|
+
* ```
|
|
2709
|
+
* {formula:"oxygen_depletion", overrides:{V:324375}, unitFrom:"L"}
|
|
2710
|
+
* → 11353125 min ← 与不传 unitFrom 完全相同
|
|
2711
|
+
* ```
|
|
2712
|
+
*
|
|
2713
|
+
* 根因:`opFormula` 里**根本没有读取这两个参数** —— 它们只是被 schema 声明了,
|
|
2714
|
+
* 于是「传了却不生效」这种最难发现的情况。
|
|
2715
|
+
*
|
|
2716
|
+
* 而公式参数的声明单位是 m³,用户按 L 给数字,结果就虚高 **1000 倍**,
|
|
2717
|
+
* 且数字看起来完全正常(11353125 分钟 ≈ 21 年,荒谬但不像错)。
|
|
2718
|
+
*
|
|
2719
|
+
* ## 语义定成「把所有数值参数按 unitFrom → unitTo 换算」
|
|
2720
|
+
*
|
|
2721
|
+
* 公式内部一律 SI,所以 `unitFrom` 是「我给的数是什么单位」,
|
|
2722
|
+
* `unitTo` 是「目标单位」(默认就用参数自己声明的那个)。
|
|
2723
|
+
* 逐个参数换算而不是只换一个,因为一次调用里参数通常同单位。
|
|
2724
|
+
*
|
|
2725
|
+
* 注意:这与「参数直接带单位」(`overrides:{V:"324375 L"}`)是两条等价路径,
|
|
2726
|
+
* 后者更精确(可逐参数给不同单位),前者更省事(一次给全)。两条都保留。
|
|
2727
|
+
*/
|
|
2728
|
+
const unitFrom = typeof args.unitFrom === 'string' ? args.unitFrom.trim() : ''
|
|
2729
|
+
const unitTo = typeof args.unitTo === 'string' ? args.unitTo.trim() : ''
|
|
2730
|
+
const rawOverrides = (args.overrides ?? {}) as Record<string, number | string>
|
|
2731
|
+
const overrides = unitFrom === '' ? rawOverrides : convertOverrides(rawOverrides, formula, unitFrom, unitTo)
|
|
2526
2732
|
|
|
2527
2733
|
let resolved: {
|
|
2528
2734
|
values: Record<string, unknown>
|
|
2529
2735
|
overridden: string[]
|
|
2530
2736
|
missing: string[]
|
|
2531
2737
|
unknownKeys: string[]
|
|
2738
|
+
converted: Record<string, { from: string; to: string; original: number | null }>
|
|
2532
2739
|
}
|
|
2533
2740
|
try {
|
|
2534
2741
|
resolved = resolveParams(formula, overrides)
|
|
@@ -2617,6 +2824,76 @@ function opFormula(args: MathArgs, engine: any, precision: number): Outcome {
|
|
|
2617
2824
|
* 紧跟在结果后面(而不是压到输出末尾),因为这类问题会直接改变结论,
|
|
2618
2825
|
* 读者必须第一眼就看到。
|
|
2619
2826
|
*/
|
|
2827
|
+
if (Object.keys(resolved.converted).length > 0) {
|
|
2828
|
+
const items = Object.entries(resolved.converted).map(function ([name, info]) {
|
|
2829
|
+
const original = info.original === null ? info.from : String(info.original) + ' ' + info.from
|
|
2830
|
+
return name + ': ' + original + ' → ' + info.to
|
|
2831
|
+
})
|
|
2832
|
+
parts.push('')
|
|
2833
|
+
parts.push('【单位换算】' + items.join(';') + '(换算到参数声明的单位后代入公式)')
|
|
2834
|
+
}
|
|
2835
|
+
/**
|
|
2836
|
+
* ## 同体系折算(用户 2026-11-07 要求)
|
|
2837
|
+
*
|
|
2838
|
+
* 「最好使用同一体系单位,把 60 km 折算成 里。」
|
|
2839
|
+
*
|
|
2840
|
+
* 输入用了里/时辰时,主结果仍是 SI(那是公式参数的口径,也是与其它计算对接的基础),
|
|
2841
|
+
* 但补一行折算到传统体系的说法 —— 作者写正文时直接能用,不必自己再换一遍。
|
|
2842
|
+
*
|
|
2843
|
+
* ```
|
|
2844
|
+
* 结果: 2.08333333 m/s
|
|
2845
|
+
* 【同体系折算】约 30 里/时辰(你的输入用了 里、时辰)
|
|
2846
|
+
* ```
|
|
2847
|
+
*/
|
|
2848
|
+
{
|
|
2849
|
+
/**
|
|
2850
|
+
* 输入里的单位来源有两处,**都要看**:
|
|
2851
|
+
* · `resolved.converted` —— 做过换算的(`60 里` → `30000 m`)
|
|
2852
|
+
* · 调用方原文 —— 单位恰好等于声明单位时不会产生换算记录
|
|
2853
|
+
* (如参数声明 `m/s`、调用方就写 `10 m/s`),但那时也不算「传统单位」
|
|
2854
|
+
*
|
|
2855
|
+
* 踩过:第一版只看 `converted`,于是 `60 里` 能折出里、
|
|
2856
|
+
* 而其它写法(如 `200 斤` 换算后)虽然 converted 里有、
|
|
2857
|
+
* 但**没走这个分支**的形态就漏了。现在两处合并取单位名。
|
|
2858
|
+
*/
|
|
2859
|
+
const seenUnits: string[] = []
|
|
2860
|
+
for (const info of Object.values(resolved.converted)) {
|
|
2861
|
+
if (!seenUnits.includes(info.from)) seenUnits.push(info.from)
|
|
2862
|
+
}
|
|
2863
|
+
/**
|
|
2864
|
+
* ★ 变量名必须与外层 `raw`(公式结果)区分开。
|
|
2865
|
+
*
|
|
2866
|
+
* 踩过:第一版写成 `for (const [name, raw] of ...)`,于是**遮蔽了外层的
|
|
2867
|
+
* `raw`(公式算出来的值)**,`formatValue(engine, raw, ...)` 拿到的是
|
|
2868
|
+
* 调用方输入的字符串 `"60 里"` 而不是 `"2.08333333 m/s"` ——
|
|
2869
|
+
* 折算函数收不到合法数值,静默返回 null。
|
|
2870
|
+
*
|
|
2871
|
+
* 这类 shadowing bug 编译不报错、测试不覆盖就很难发现。
|
|
2872
|
+
*/
|
|
2873
|
+
for (const [, rawInput] of Object.entries(overrides)) {
|
|
2874
|
+
if (typeof rawInput !== 'string') continue
|
|
2875
|
+
const piece = rawInput.trim().match(/^[+-]?[0-9][0-9.]*(?:e[+-]?[0-9]+)?\s*(.+)$/i)
|
|
2876
|
+
if (!piece) continue
|
|
2877
|
+
const unitName = piece[1].trim()
|
|
2878
|
+
if (unitName !== '' && !seenUnits.includes(unitName)) seenUnits.push(unitName)
|
|
2879
|
+
}
|
|
2880
|
+
const families = familiesOfUnits(seenUnits)
|
|
2881
|
+
if (families.length > 0) {
|
|
2882
|
+
/**
|
|
2883
|
+
* ★ 单位要**自己拼上**。
|
|
2884
|
+
*
|
|
2885
|
+
* `formatValue` 对 `s / t` 这种纯数值表达式只给 `2.08333333` ——
|
|
2886
|
+
* 单位 `m/s` 是在渲染那一行时用 `formula.unit` 拼上去的,不在值里。
|
|
2887
|
+
* 踩过:直接把 `formatValue` 的结果喂给折算函数,它拿不到单位、
|
|
2888
|
+
* 静默返回 null,于是折算行永远不出现。
|
|
2889
|
+
*/
|
|
2890
|
+
const siText = formula.unit
|
|
2891
|
+
? formatValue(engine, raw, precision, currentDisplayPrecision()) + ' ' + formula.unit
|
|
2892
|
+
: formatValue(engine, raw, precision, currentDisplayPrecision())
|
|
2893
|
+
const note = traditionalNote(siText, families, formulaConverter)
|
|
2894
|
+
if (note !== null) parts.push(note)
|
|
2895
|
+
}
|
|
2896
|
+
}
|
|
2620
2897
|
if (unknownOverrideKeys.length > 0) {
|
|
2621
2898
|
parts.push('')
|
|
2622
2899
|
parts.push(
|
|
@@ -2657,6 +2934,62 @@ function opFormula(args: MathArgs, engine: any, precision: number): Outcome {
|
|
|
2657
2934
|
}
|
|
2658
2935
|
}
|
|
2659
2936
|
|
|
2937
|
+
/**
|
|
2938
|
+
* ## `terse`:只回一行(1108 报告问题 9)
|
|
2939
|
+
*
|
|
2940
|
+
* 报告实测:`formula:"kleiber", terse:true` 返回的仍是完整结构化输出,
|
|
2941
|
+
* `terse` 对 formula 完全无效。
|
|
2942
|
+
*
|
|
2943
|
+
* ## 但风险提示不能省
|
|
2944
|
+
*
|
|
2945
|
+
* 与 evaluate 的 terse 同一条约定:**省掉的是标题与参数表,不是风险**。
|
|
2946
|
+
* 所以 terse 下仍保留三样:结果、覆盖了哪些参数(一句话)、
|
|
2947
|
+
* 以及「这个数的单位是什么」。
|
|
2948
|
+
*
|
|
2949
|
+
* 尤其经验公式的 **【适用域】绝不能砍** —— 那是「这个系数只在 1g 世界成立」
|
|
2950
|
+
* 的唯一提示,砍掉就等于让人在非地球世界里照抄。所以 terse 时会用半行压缩它。
|
|
2951
|
+
*/
|
|
2952
|
+
if (args.terse === true) {
|
|
2953
|
+
const answer = formatValue(engine, raw, precision, currentDisplayPrecision())
|
|
2954
|
+
const unitSuffix = formula.unit ? ' ' + formula.unit : ''
|
|
2955
|
+
const terseLines: string[] = [formula.id + ' = ' + answer + unitSuffix]
|
|
2956
|
+
/** 覆盖提示:一句话,不列明细。 */
|
|
2957
|
+
if (resolved.overridden.length > 0) {
|
|
2958
|
+
terseLines.push(' (覆盖: ' + resolved.overridden.join(',') + ')')
|
|
2959
|
+
}
|
|
2960
|
+
/** 单位换算与同体系折算:与完整版同一判据,只是不加标题行。 */
|
|
2961
|
+
if (Object.keys(resolved.converted).length > 0) {
|
|
2962
|
+
const items = Object.entries(resolved.converted).map(function ([name, info]) {
|
|
2963
|
+
const original = info.original === null ? info.from : String(info.original) + ' ' + info.from
|
|
2964
|
+
return name + ': ' + original + '→' + info.to
|
|
2965
|
+
})
|
|
2966
|
+
terseLines.push(' (换算: ' + items.join('; ') + ')')
|
|
2967
|
+
}
|
|
2968
|
+
/** 经验式适用域:压成一行,但绝不省略。 */
|
|
2969
|
+
const terseEmpirical = empiricalNoteOf(formula.id)
|
|
2970
|
+
if (terseEmpirical) {
|
|
2971
|
+
terseLines.push(' 【适用域:' + KIND_LABEL[terseEmpirical.kind] + '】' +
|
|
2972
|
+
(terseEmpirical.breaksWhen ? ' 何时失效: ' + terseEmpirical.breaksWhen : ''))
|
|
2973
|
+
}
|
|
2974
|
+
/** 未知参数名必须保留 —— 它会直接改变结论。 */
|
|
2975
|
+
if (unknownOverrideKeys.length > 0) {
|
|
2976
|
+
terseLines.push(' ⚠ 未生效的 key: ' + unknownOverrideKeys.join('、') +
|
|
2977
|
+
'(该公式参数是 ' + Object.keys(formula.params).join('、') + ')')
|
|
2978
|
+
}
|
|
2979
|
+
return {
|
|
2980
|
+
result: terseLines.join('\n'),
|
|
2981
|
+
value: {
|
|
2982
|
+
formula: formula.id,
|
|
2983
|
+
category: formula.cat,
|
|
2984
|
+
expression,
|
|
2985
|
+
params: toJsonValue(resolved.values),
|
|
2986
|
+
overridden: resolved.overridden,
|
|
2987
|
+
answer: toJsonValue(raw),
|
|
2988
|
+
unit: formula.unit ?? null,
|
|
2989
|
+
},
|
|
2990
|
+
}
|
|
2991
|
+
}
|
|
2992
|
+
|
|
2660
2993
|
return {
|
|
2661
2994
|
result: parts.join('\n'),
|
|
2662
2995
|
value: {
|
|
@@ -2995,6 +3328,26 @@ export function runMathTool(rawArguments: unknown): MathResult {
|
|
|
2995
3328
|
'(如 terrain 的字段要放在 terrain 对象里)。',
|
|
2996
3329
|
)
|
|
2997
3330
|
}
|
|
3331
|
+
/**
|
|
3332
|
+
* ## 记下「调用方是否显式调低了计算精度」(1108 报告问题 4)
|
|
3333
|
+
*
|
|
3334
|
+
* 报告实测:
|
|
3335
|
+
*
|
|
3336
|
+
* ```
|
|
3337
|
+
* precision=8: 10^20 + 1 → 100000000000000000000 ← 加 1 被吃掉
|
|
3338
|
+
* precision=16: (10^20+12345678901234567890)-(10^20) → 12345678901234600000
|
|
3339
|
+
* ```
|
|
3340
|
+
*
|
|
3341
|
+
* 文档已警告「不要为好看调低 precision」,但**没有任何反馈**——
|
|
3342
|
+
* 数字看着正常,只是末几位不是真的。
|
|
3343
|
+
*
|
|
3344
|
+
* 这里只记录「是否低于默认档」,具体要不要提示由结果判断:
|
|
3345
|
+
* 若结果的位数超过 precision,说明确实被截了,才提示。
|
|
3346
|
+
*/
|
|
3347
|
+
const requestedPrecision =
|
|
3348
|
+
typeof args.precision === 'number' && Number.isFinite(args.precision)
|
|
3349
|
+
? Math.round(args.precision)
|
|
3350
|
+
: undefined
|
|
2998
3351
|
precision = clampPrecision(args.precision)
|
|
2999
3352
|
/**
|
|
3000
3353
|
* 显示精度:本次显式给 > 工作区固化值 > 沿用计算精度。
|
|
@@ -3383,6 +3736,12 @@ export function runMathTool(rawArguments: unknown): MathResult {
|
|
|
3383
3736
|
const hintNote = maybeDisplayPrecisionHint(result.result)
|
|
3384
3737
|
if (hintNote) result.notice = hintNote
|
|
3385
3738
|
|
|
3739
|
+
/** 调低计算精度导致结果被截 —— 那种「看起来正常、末位是假的」情况。 */
|
|
3740
|
+
const truncNote = checkPrecisionTruncation(requestedPrecision, precision, result.result)
|
|
3741
|
+
if (truncNote) {
|
|
3742
|
+
result.notice = result.notice ? result.notice + '\n' + truncNote : truncNote
|
|
3743
|
+
}
|
|
3744
|
+
|
|
3386
3745
|
/**
|
|
3387
3746
|
* 三角函数按弧度算时补一句说明。
|
|
3388
3747
|
*
|
|
@@ -4148,6 +4507,57 @@ const PARAMETER_SPEC = {
|
|
|
4148
4507
|
*/
|
|
4149
4508
|
let ctxRef: any = null
|
|
4150
4509
|
|
|
4510
|
+
|
|
4511
|
+
/**
|
|
4512
|
+
* 调用方显式调低计算精度时,检查结果是否**因此被截**。
|
|
4513
|
+
*
|
|
4514
|
+
* ## 为什么要提示(1108 报告问题 4)
|
|
4515
|
+
*
|
|
4516
|
+
* ```
|
|
4517
|
+
* precision=8: 10^20 + 1 → 100000000000000000000 ← 加 1 被吃掉
|
|
4518
|
+
* precision=16: (10^20+大数)-(10^20) → 12345678901234600000 ← 末位失真
|
|
4519
|
+
* ```
|
|
4520
|
+
*
|
|
4521
|
+
* 文档已经写了「不要为好看调低 precision」,但**说了不等于看见了**:
|
|
4522
|
+
* 这两个结果看起来都是正常的大整数,没有任何迹象表明末几位是假的。
|
|
4523
|
+
*
|
|
4524
|
+
* ## 判据
|
|
4525
|
+
*
|
|
4526
|
+
* 只有两件事同时成立才提示:
|
|
4527
|
+
* 1. 调用方**显式**给了低于默认(64)的 precision;
|
|
4528
|
+
* 2. 结果的**整数位数超过** precision —— 那说明确实丢了低位。
|
|
4529
|
+
*
|
|
4530
|
+
* 第 2 条是关键:`precision:8` 算 `1+1` 不该被念;
|
|
4531
|
+
* 只有数字真的长到装不下时才提示。
|
|
4532
|
+
*
|
|
4533
|
+
* @param requested - 调用方显式给的 precision(未给则 undefined)
|
|
4534
|
+
* @param effective - 实际生效的 precision
|
|
4535
|
+
* @param text - 已格式化的结果文本
|
|
4536
|
+
* @returns 提示文本;不需要时返回空串
|
|
4537
|
+
*/
|
|
4538
|
+
function checkPrecisionTruncation(
|
|
4539
|
+
requested: number | undefined,
|
|
4540
|
+
effective: number,
|
|
4541
|
+
text: string,
|
|
4542
|
+
): string {
|
|
4543
|
+
/**
|
|
4544
|
+
* 判据:调用方显式给的精度**低于默认 64** 才算「调低了」,
|
|
4545
|
+
* 然后看结果是否真的装不下。
|
|
4546
|
+
*
|
|
4547
|
+
* 踩过:第一版写 `requested >= effective` 就跳过 —— 而两者本来相等
|
|
4548
|
+
* (effective 就是 clamp 过的 requested),于是这条永远不触发。
|
|
4549
|
+
*/
|
|
4550
|
+
if (requested === undefined || requested >= DEFAULT_PRECISION) return ''
|
|
4551
|
+
/** 只看纯数字结果:带单位的另有提示通道。 */
|
|
4552
|
+
const numeric = text.trim().match(/^-?([0-9]+)(?:\.([0-9]+))?$/)
|
|
4553
|
+
if (!numeric) return ''
|
|
4554
|
+
const integerDigits = numeric[1].replace(/^0+(?=.)/, '').length
|
|
4555
|
+
if (integerDigits <= effective) return ''
|
|
4556
|
+
return '本次结果有 ' + integerDigits + ' 位有效数字,而 precision=' + effective +
|
|
4557
|
+
' 只能精确表示 ' + effective + ' 位——末 ' + (integerDigits - effective) +
|
|
4558
|
+
' 位可能已失真。需要精确值时请用 precision:64(默认),' +
|
|
4559
|
+
'只想让输出短些请用 displayPrecision。'
|
|
4560
|
+
}
|
|
4151
4561
|
/**
|
|
4152
4562
|
* 数学计算工具(`math_execute`)。
|
|
4153
4563
|
*
|
package/src/ops/shared.ts
CHANGED
|
@@ -828,6 +828,28 @@ export function normalizeSource(input: string): string {
|
|
|
828
828
|
*/
|
|
829
829
|
.replace(/²/g, '^2')
|
|
830
830
|
.replace(/³/g, '^3')
|
|
831
|
+
/**
|
|
832
|
+
* ## `min` 撞函数名:单位位置的 min 换成 minute
|
|
833
|
+
*
|
|
834
|
+
* mathjs 里 `min` 是**函数**(求最小值),同时我们的单位表里 `min` 是**分钟**。
|
|
835
|
+
* 于是 `1 L/min` 会被解析成「L 除以 min 函数」,报一个看不懂的错:
|
|
836
|
+
*
|
|
837
|
+
* ```
|
|
838
|
+
* 1 L/min
|
|
839
|
+
* → internal: Unexpected type of argument in function divide
|
|
840
|
+
* (expected: number or Complex or ... actual: function)
|
|
841
|
+
* ```
|
|
842
|
+
*
|
|
843
|
+
* 这个错**完全指向不到修复方向** —— 用户只觉得「工具不支持复合单位」,
|
|
844
|
+
* 而真实原因只是一个名字冲突(1108 报告的问题 5 与问题 6 是同一个根)。
|
|
845
|
+
*
|
|
846
|
+
* 判据只认**单位位置**的 min,避免改坏真正的函数调用:
|
|
847
|
+
* · 紧跟 `/` 之后(`L/min`、`mol/min`)
|
|
848
|
+
* · 紧跟数字之后(`5 min`)
|
|
849
|
+
* · 紧跟 `to` 之后(`1 s to min`)
|
|
850
|
+
* 而 `min(3,5)` 这类调用(后面紧跟左括号)**不动**。
|
|
851
|
+
*/
|
|
852
|
+
.replace(/(^|[\/(\s])min(?=\s*(?:to\b|[)*\-+^/;]|$))/g, '$1minute')
|
|
831
853
|
}
|
|
832
854
|
|
|
833
855
|
/**
|