@jarenjs/calc 0.34.0
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/README.md +60 -0
- package/dist/types/ast.d.ts +65 -0
- package/dist/types/compile.d.ts +52 -0
- package/dist/types/component/index.d.ts +259 -0
- package/dist/types/component/rates/binance.d.ts +34 -0
- package/dist/types/component/rates/coingecko.d.ts +47 -0
- package/dist/types/component/rates/index.d.ts +84 -0
- package/dist/types/component/rules.d.ts +180 -0
- package/dist/types/component/schema.d.ts +38 -0
- package/dist/types/env.d.ts +24 -0
- package/dist/types/errors.d.ts +18 -0
- package/dist/types/index.d.ts +42 -0
- package/dist/types/modes/converter.d.ts +46 -0
- package/dist/types/modes/financial.d.ts +59 -0
- package/dist/types/modes/index.d.ts +38 -0
- package/dist/types/modes/programmer.d.ts +50 -0
- package/dist/types/modes/scientific.d.ts +28 -0
- package/dist/types/modes/standard.d.ts +32 -0
- package/dist/types/parser/index.d.ts +32 -0
- package/dist/types/plot/plot2d.d.ts +70 -0
- package/dist/types/plot/plot3d.d.ts +68 -0
- package/dist/types/render/error.d.ts +19 -0
- package/dist/types/theme.d.ts +35 -0
- package/dist/types/to-expr.d.ts +13 -0
- package/dist/types/utils.d.ts +9 -0
- package/docs/CALC-FORMAT.md +79 -0
- package/package.json +71 -0
- package/schemas/financial-inputs.schema.json +15 -0
- package/schemas/jaren-calc-ast.schema.json +91 -0
- package/schemas/jaren-calc-state.schema.json +52 -0
- package/src/ast.js +96 -0
- package/src/compile.js +119 -0
- package/src/component/index.js +352 -0
- package/src/component/rates/binance.js +44 -0
- package/src/component/rates/coingecko.js +63 -0
- package/src/component/rates/index.js +116 -0
- package/src/component/rules.js +118 -0
- package/src/component/schema.js +24 -0
- package/src/env.js +163 -0
- package/src/errors.js +23 -0
- package/src/index.js +85 -0
- package/src/modes/converter.js +73 -0
- package/src/modes/financial.js +89 -0
- package/src/modes/index.js +24 -0
- package/src/modes/programmer.js +69 -0
- package/src/modes/scientific.js +31 -0
- package/src/modes/standard.js +39 -0
- package/src/parser/index.js +220 -0
- package/src/plot/plot2d.js +221 -0
- package/src/plot/plot3d.js +177 -0
- package/src/render/error.js +25 -0
- package/src/theme.js +76 -0
- package/src/to-expr.js +84 -0
- package/src/utils.js +11 -0
- package/styles/calc.css +128 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file The `calculator` JSLT view. Rules render the
|
|
4
|
+
* viewModel document produced by `contributeCalcViewModel`. Convention
|
|
5
|
+
* (mirrors the website views): `match` patterns are absolute from the
|
|
6
|
+
* document root; `$apply` paths and body `$` are relative to the matched
|
|
7
|
+
* node; list children wrap `[{ $apply: '…[*]' }]`; a ready-made vnode
|
|
8
|
+
* (the plot SVG, `$.plot.svg`) splices in verbatim as a query-string
|
|
9
|
+
* child.
|
|
10
|
+
*
|
|
11
|
+
* Mounted by the host with `{ $apply: ['$.ui.calculator', 'calculator'] }`.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export const CALCULATOR_RULES = [
|
|
15
|
+
{
|
|
16
|
+
match: '$.ui.calculator', mode: 'calculator',
|
|
17
|
+
body: ['div', { class: 'calc' },
|
|
18
|
+
['div', { class: 'calc-modes' }, [{ $apply: '$.modes[*]' }]],
|
|
19
|
+
|
|
20
|
+
['div', { class: 'calc-display' },
|
|
21
|
+
['div', { class: 'calc-entry' }, '$.display.entry'],
|
|
22
|
+
['div', { class: 'calc-result' }, '$.display.result'],
|
|
23
|
+
{ $if: ['$.display.error', ['div', { class: 'calc-err' }, '$.display.error']] },
|
|
24
|
+
],
|
|
25
|
+
|
|
26
|
+
// programmer four-base view + word size toggles
|
|
27
|
+
{ $if: ['$.isProgrammer', ['div', { class: 'calc-bases' },
|
|
28
|
+
['div', { class: 'calc-baserow' }, ['span', { class: 'calc-baselabel' }, 'HEX'], ['span', { class: 'calc-baseval' }, '$.bases.views.hex']],
|
|
29
|
+
['div', { class: 'calc-baserow' }, ['span', { class: 'calc-baselabel' }, 'DEC'], ['span', { class: 'calc-baseval' }, '$.bases.views.dec']],
|
|
30
|
+
['div', { class: 'calc-baserow' }, ['span', { class: 'calc-baselabel' }, 'OCT'], ['span', { class: 'calc-baseval' }, '$.bases.views.oct']],
|
|
31
|
+
['div', { class: 'calc-baserow' }, ['span', { class: 'calc-baselabel' }, 'BIN'], ['span', { class: 'calc-baseval' }, '$.bases.views.bin']],
|
|
32
|
+
['div', { class: 'calc-wordopts' }, [{ $apply: '$.bases.wordOptions[*]' }]],
|
|
33
|
+
]] },
|
|
34
|
+
|
|
35
|
+
// scientific angle mode toggle
|
|
36
|
+
{ $if: ['$.isScientific', ['div', { class: 'calc-angle' }, [{ $apply: '$.angle.options[*]' }]]] },
|
|
37
|
+
|
|
38
|
+
// keypad (standard / scientific / programmer)
|
|
39
|
+
{ $if: ['$.keypad', ['div', { class: 'calc-keypad' }, [{ $apply: '$.keypad[*]' }]]] },
|
|
40
|
+
|
|
41
|
+
// memory row (present for calculator-style modes)
|
|
42
|
+
{ $if: ['$.keypad', ['div', { class: 'calc-mem' },
|
|
43
|
+
['button', { type: 'button', class: 'calc-key calc-key-mem', on: { click: { action: 'calc/mem-add' } } }, 'M+'],
|
|
44
|
+
['button', { type: 'button', class: 'calc-key calc-key-mem', on: { click: { action: 'calc/mem-clear' } } }, 'MC'],
|
|
45
|
+
['span', { class: 'calc-memval' }, 'M = ', '$.memory'],
|
|
46
|
+
]] },
|
|
47
|
+
|
|
48
|
+
// financial panel — the form comes from @jarenjs/forms
|
|
49
|
+
{ $if: ['$.financial', ['div', { class: 'calc-financial' },
|
|
50
|
+
['div', { class: 'calc-fin-form' }, { $apply: '$.financial.form' }],
|
|
51
|
+
['div', { class: 'calc-fin-result' }, ['span', {}, 'Result ('], ['span', {}, '$.financial.solveFor'], ['span', {}, '): '], ['strong', {}, '$.financial.result']],
|
|
52
|
+
]] },
|
|
53
|
+
|
|
54
|
+
// converter panel — every conversion via @jarenjs/core/convert
|
|
55
|
+
{ $if: ['$.converter', ['div', { class: 'calc-converter' },
|
|
56
|
+
['label', { class: 'calc-conv-dimlabel' }, 'Dimension ',
|
|
57
|
+
['select', { class: 'calc-dim', on: { change: { action: 'calc/conv-dim' } } }, [{ $apply: '$.converter.dimensions[*]' }]],
|
|
58
|
+
],
|
|
59
|
+
['div', { class: 'calc-conv-row' },
|
|
60
|
+
['input', { type: 'text', class: 'calc-conv-input', value: '$.converter.value', on: { input: { action: 'calc/conv-value' } } }],
|
|
61
|
+
['select', { class: 'calc-conv-from', on: { change: { action: 'calc/conv-from' } } }, [{ $apply: '$.converter.unitsFrom[*]' }]],
|
|
62
|
+
['button', { type: 'button', class: 'calc-swap', on: { click: { action: 'calc/conv-swap' } } }, '⇄'],
|
|
63
|
+
['select', { class: 'calc-conv-to', on: { change: { action: 'calc/conv-to' } } }, [{ $apply: '$.converter.unitsTo[*]' }]],
|
|
64
|
+
],
|
|
65
|
+
['div', { class: 'calc-conv-result' }, ['strong', {}, '$.converter.result']],
|
|
66
|
+
{ $if: ['$.converter.isCurrency', ['div', { class: 'calc-rates' },
|
|
67
|
+
['span', {}, 'rates: '], ['span', {}, '$.converter.rates.status'],
|
|
68
|
+
{ $if: ['$.converter.rates.stale', ['span', { class: 'calc-rates-stale' }, ' · static fallback']] },
|
|
69
|
+
['button', { type: 'button', class: 'calc-rates-refresh', on: { click: { action: 'calc/rates-refresh' } } }, '↻'],
|
|
70
|
+
]] },
|
|
71
|
+
]] },
|
|
72
|
+
|
|
73
|
+
// plot panel (standard / scientific)
|
|
74
|
+
{ $if: ['$.plot', ['div', { class: 'calc-plot-panel' },
|
|
75
|
+
['div', { class: 'calc-plot-kinds' }, [{ $apply: '$.plot.kinds[*]' }]],
|
|
76
|
+
['input', { type: 'text', class: 'calc-plot-expr', value: '$.plot.expr', on: { input: { action: 'calc/plot-expr' } } }],
|
|
77
|
+
['div', { class: 'calc-plot-svg' }, '$.plot.svg'],
|
|
78
|
+
]] },
|
|
79
|
+
|
|
80
|
+
// history tape
|
|
81
|
+
{ $if: ['$.tape', ['div', { class: 'calc-tape' }, [{ $apply: '$.tape[*]' }]]] },
|
|
82
|
+
],
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
{ match: '$.ui.calculator.modes[*]', mode: 'calculator',
|
|
86
|
+
body: ['button', { type: 'button', class: { $if: ['$.active', 'calc-mode calc-mode-active', 'calc-mode'] }, on: '$.on' }, '$.label'] },
|
|
87
|
+
|
|
88
|
+
{ match: '$.ui.calculator.keypad[*]', mode: 'calculator',
|
|
89
|
+
body: ['div', { class: 'calc-row' }, [{ $apply: '$.keys[*]' }]] },
|
|
90
|
+
|
|
91
|
+
{ match: '$.ui.calculator.keypad[*].keys[*]', mode: 'calculator',
|
|
92
|
+
body: ['button', { type: 'button', class: '$.cls', on: '$.on' }, '$.label'] },
|
|
93
|
+
|
|
94
|
+
{ match: '$.ui.calculator.angle.options[*]', mode: 'calculator',
|
|
95
|
+
body: ['button', { type: 'button', class: { $if: ['$.active', 'calc-angle-btn active', 'calc-angle-btn'] }, on: '$.on' }, '$.label'] },
|
|
96
|
+
|
|
97
|
+
{ match: '$.ui.calculator.bases.wordOptions[*]', mode: 'calculator',
|
|
98
|
+
body: ['button', { type: 'button', class: { $if: ['$.active', 'calc-word active', 'calc-word'] }, on: '$.on' }, '$.label'] },
|
|
99
|
+
|
|
100
|
+
{ match: '$.ui.calculator.plot.kinds[*]', mode: 'calculator',
|
|
101
|
+
body: ['button', { type: 'button', class: { $if: ['$.active', 'calc-plotkind active', 'calc-plotkind'] }, on: '$.on' }, '$.label'] },
|
|
102
|
+
|
|
103
|
+
{ match: '$.ui.calculator.converter.dimensions[*]', mode: 'calculator',
|
|
104
|
+
body: ['option', { value: '$.id', selected: '$.selected' }, '$.label'] },
|
|
105
|
+
|
|
106
|
+
{ match: '$.ui.calculator.converter.unitsFrom[*]', mode: 'calculator',
|
|
107
|
+
body: ['option', { value: '$.id', selected: '$.selected' }, '$.symbol'] },
|
|
108
|
+
|
|
109
|
+
{ match: '$.ui.calculator.converter.unitsTo[*]', mode: 'calculator',
|
|
110
|
+
body: ['option', { value: '$.id', selected: '$.selected' }, '$.symbol'] },
|
|
111
|
+
|
|
112
|
+
{ match: '$.ui.calculator.tape[*]', mode: 'calculator',
|
|
113
|
+
body: ['div', { class: 'calc-tape-row' },
|
|
114
|
+
['span', { class: 'calc-tape-expr' }, '$.expr'],
|
|
115
|
+
['span', { class: 'calc-tape-eq' }, ' = '],
|
|
116
|
+
['span', { class: 'calc-tape-res' }, '$.result'],
|
|
117
|
+
] },
|
|
118
|
+
];
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file The financial-inputs JSON Schema (draft-neutral). Rendered by
|
|
4
|
+
* `@jarenjs/forms` (`buildFormModel` → `buildFormViewModel`) in the
|
|
5
|
+
* viewModel, and available as `schemas/financial-inputs.schema.json` for
|
|
6
|
+
* `validateState`. The calculator holds no formulas — only these inputs.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const FINANCIAL_SCHEMA = {
|
|
10
|
+
type: 'object',
|
|
11
|
+
title: 'Time Value of Money',
|
|
12
|
+
properties: {
|
|
13
|
+
nper: { type: 'number', title: 'N — number of periods', minimum: 0 },
|
|
14
|
+
rate: { type: 'number', title: 'I/Y — interest % per period' },
|
|
15
|
+
pv: { type: 'number', title: 'PV — present value' },
|
|
16
|
+
pmt: { type: 'number', title: 'PMT — payment' },
|
|
17
|
+
fv: { type: 'number', title: 'FV — future value' },
|
|
18
|
+
solveFor: {
|
|
19
|
+
title: 'Solve for',
|
|
20
|
+
enum: ['pmt', 'pv', 'fv', 'nper', 'rate'],
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
required: ['solveFor'],
|
|
24
|
+
};
|
package/src/env.js
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file Evaluation environments — the operator/function binding sets the
|
|
4
|
+
* compiler resolves against. The AST is neutral; an
|
|
5
|
+
* environment gives it meaning. `defaultEnv` covers standard/scientific
|
|
6
|
+
* (float, `^` = power, angle-aware trig); `programmerEnv` overrides the
|
|
7
|
+
* bitwise operators with `@jarenjs/core/math/word.js` word math and adds
|
|
8
|
+
* the programmer functions. Every binding closes over `scope` so angle
|
|
9
|
+
* mode and word size are read at evaluation time.
|
|
10
|
+
*
|
|
11
|
+
* Binding shapes (all monomorphic): `func(args[], scope)`,
|
|
12
|
+
* `binop(a, b, scope)`, `unop(a, scope)`, `postop(a, scope)`.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
Float64,
|
|
17
|
+
mathf64_sin, mathf64_cos, mathf64_tan, mathf64_asin, mathf64_acos, mathf64_atan,
|
|
18
|
+
mathf64_sinh, mathf64_cosh, mathf64_tanh, mathf64_atan2,
|
|
19
|
+
mathf64_log, mathf64_log2, mathf64_log10, mathf64_exp, mathf64_expm1,
|
|
20
|
+
mathf64_sqrt, mathf64_cbrt, mathf64_abs, mathf64_floor, mathf64_ceil,
|
|
21
|
+
mathf64_round, mathf64_min, mathf64_max, mathf64_pow,
|
|
22
|
+
toWord, wAnd, wOr, wXor, wNot, wShl, wShr, wRol, wRor, wMod,
|
|
23
|
+
} from '@jarenjs/core/math';
|
|
24
|
+
import { CONSTANTS } from './ast.js';
|
|
25
|
+
|
|
26
|
+
/** Convert a user angle to radians per the scope's angle mode. */
|
|
27
|
+
function toRad(x, scope) {
|
|
28
|
+
const m = scope && scope.angleMode;
|
|
29
|
+
if (m === 'deg') return (x * Math.PI) / 180;
|
|
30
|
+
if (m === 'grad') return (x * Math.PI) / 200;
|
|
31
|
+
return x;
|
|
32
|
+
}
|
|
33
|
+
/** Convert a radian result back to the scope's angle mode. */
|
|
34
|
+
function fromRad(x, scope) {
|
|
35
|
+
const m = scope && scope.angleMode;
|
|
36
|
+
if (m === 'deg') return (x * 180) / Math.PI;
|
|
37
|
+
if (m === 'grad') return (x * 200) / Math.PI;
|
|
38
|
+
return x;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Coerce to a BigInt word using the scope's word size/sign. */
|
|
42
|
+
function big(v, scope) {
|
|
43
|
+
return toWord(BigInt(Math.trunc(+v)), scope?.wordBits ?? 32, scope?.signed ?? false);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** @type {Record<string, (args: number[], scope: any) => number>} */
|
|
47
|
+
const FLOAT_FUNCS = {
|
|
48
|
+
sin: (a, s) => mathf64_sin(toRad(a[0], s)),
|
|
49
|
+
cos: (a, s) => mathf64_cos(toRad(a[0], s)),
|
|
50
|
+
tan: (a, s) => mathf64_tan(toRad(a[0], s)),
|
|
51
|
+
asin: (a, s) => fromRad(mathf64_asin(a[0]), s),
|
|
52
|
+
acos: (a, s) => fromRad(mathf64_acos(a[0]), s),
|
|
53
|
+
atan: (a, s) => fromRad(mathf64_atan(a[0]), s),
|
|
54
|
+
atan2: (a, s) => fromRad(mathf64_atan2(a[0], a[1]), s),
|
|
55
|
+
sinh: (a) => mathf64_sinh(a[0]),
|
|
56
|
+
cosh: (a) => mathf64_cosh(a[0]),
|
|
57
|
+
tanh: (a) => mathf64_tanh(a[0]),
|
|
58
|
+
ln: (a) => mathf64_log(a[0]),
|
|
59
|
+
log: (a) => (a.length > 1 ? Float64.logBase(a[0], a[1]) : mathf64_log10(a[0])),
|
|
60
|
+
log2: (a) => mathf64_log2(a[0]),
|
|
61
|
+
log10: (a) => mathf64_log10(a[0]),
|
|
62
|
+
exp: (a) => mathf64_exp(a[0]),
|
|
63
|
+
expm1: (a) => mathf64_expm1(a[0]),
|
|
64
|
+
sqrt: (a) => mathf64_sqrt(a[0]),
|
|
65
|
+
cbrt: (a) => mathf64_cbrt(a[0]),
|
|
66
|
+
root: (a) => Float64.nthroot(a[0], a[1]),
|
|
67
|
+
abs: (a) => mathf64_abs(a[0]),
|
|
68
|
+
sign: (a) => Float64.sign(a[0]),
|
|
69
|
+
floor: (a) => mathf64_floor(a[0]),
|
|
70
|
+
ceil: (a) => mathf64_ceil(a[0]),
|
|
71
|
+
round: (a) => (a.length > 1 ? Float64.roundTo(a[0], a[1]) : mathf64_round(a[0])),
|
|
72
|
+
trunc: (a) => Math.trunc(a[0]),
|
|
73
|
+
min: (a) => mathf64_min(...a),
|
|
74
|
+
max: (a) => mathf64_max(...a),
|
|
75
|
+
hypot: (a) => Float64.hypot(...a),
|
|
76
|
+
fact: (a) => Float64.factorial(a[0]),
|
|
77
|
+
gamma: (a) => Float64.gamma(a[0]),
|
|
78
|
+
pow: (a) => mathf64_pow(a[0], a[1]),
|
|
79
|
+
mod: (a) => a[0] % a[1],
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/** @type {Record<string, (a: number, b: number, scope: any) => number>} */
|
|
83
|
+
const FLOAT_BINOPS = {
|
|
84
|
+
'+': (a, b) => a + b,
|
|
85
|
+
'-': (a, b) => a - b,
|
|
86
|
+
'*': (a, b) => a * b,
|
|
87
|
+
'/': (a, b) => a / b,
|
|
88
|
+
'^': (a, b) => mathf64_pow(a, b),
|
|
89
|
+
'&': (a, b) => (a | 0) & (b | 0),
|
|
90
|
+
'|': (a, b) => (a | 0) | (b | 0),
|
|
91
|
+
'<<': (a, b) => (a | 0) << (b | 0),
|
|
92
|
+
'>>': (a, b) => (a | 0) >> (b | 0),
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/** @type {Record<string, (a: number, scope: any) => number>} */
|
|
96
|
+
const FLOAT_UNOPS = {
|
|
97
|
+
'-': (a) => -a,
|
|
98
|
+
'+': (a) => a,
|
|
99
|
+
'~': (a) => ~(a | 0),
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/** @type {Record<string, (a: number, scope: any) => number>} */
|
|
103
|
+
const FLOAT_POSTOPS = {
|
|
104
|
+
'!': (a) => Float64.factorial(a),
|
|
105
|
+
'%': (a) => a / 100,
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The default (standard/scientific) environment.
|
|
110
|
+
* @returns {any}
|
|
111
|
+
*/
|
|
112
|
+
export function defaultEnv() {
|
|
113
|
+
return {
|
|
114
|
+
constants: { ...CONSTANTS },
|
|
115
|
+
funcs: FLOAT_FUNCS,
|
|
116
|
+
binops: FLOAT_BINOPS,
|
|
117
|
+
unops: FLOAT_UNOPS,
|
|
118
|
+
postops: FLOAT_POSTOPS,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Word-aware programmer functions (return `Number`; see the >2^53 caveat in docs). */
|
|
123
|
+
const WORD_FUNCS = {
|
|
124
|
+
...FLOAT_FUNCS,
|
|
125
|
+
and: (a, s) => Number(wAnd(big(a[0], s), big(a[1], s), s?.wordBits ?? 32, s?.signed ?? false)),
|
|
126
|
+
or: (a, s) => Number(wOr(big(a[0], s), big(a[1], s), s?.wordBits ?? 32, s?.signed ?? false)),
|
|
127
|
+
xor: (a, s) => Number(wXor(big(a[0], s), big(a[1], s), s?.wordBits ?? 32, s?.signed ?? false)),
|
|
128
|
+
not: (a, s) => Number(wNot(big(a[0], s), s?.wordBits ?? 32, s?.signed ?? false)),
|
|
129
|
+
shl: (a, s) => Number(wShl(big(a[0], s), BigInt(Math.trunc(a[1])), s?.wordBits ?? 32, s?.signed ?? false)),
|
|
130
|
+
shr: (a, s) => Number(wShr(big(a[0], s), BigInt(Math.trunc(a[1])), s?.wordBits ?? 32, s?.signed ?? false)),
|
|
131
|
+
rol: (a, s) => Number(wRol(big(a[0], s), BigInt(Math.trunc(a[1])), s?.wordBits ?? 32, s?.signed ?? false)),
|
|
132
|
+
ror: (a, s) => Number(wRor(big(a[0], s), BigInt(Math.trunc(a[1])), s?.wordBits ?? 32, s?.signed ?? false)),
|
|
133
|
+
mod: (a, s) => Number(wMod(big(a[0], s), big(a[1], s), s?.wordBits ?? 32, s?.signed ?? false)),
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const WORD_BINOPS = {
|
|
137
|
+
...FLOAT_BINOPS,
|
|
138
|
+
'&': (a, b, s) => Number(wAnd(big(a, s), big(b, s), s?.wordBits ?? 32, s?.signed ?? false)),
|
|
139
|
+
'|': (a, b, s) => Number(wOr(big(a, s), big(b, s), s?.wordBits ?? 32, s?.signed ?? false)),
|
|
140
|
+
'<<': (a, b, s) => Number(wShl(big(a, s), BigInt(Math.trunc(b)), s?.wordBits ?? 32, s?.signed ?? false)),
|
|
141
|
+
'>>': (a, b, s) => Number(wShr(big(a, s), BigInt(Math.trunc(b)), s?.wordBits ?? 32, s?.signed ?? false)),
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const WORD_UNOPS = {
|
|
145
|
+
...FLOAT_UNOPS,
|
|
146
|
+
'~': (a, s) => Number(wNot(big(a, s), s?.wordBits ?? 32, s?.signed ?? false)),
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* The programmer environment: word-masked bitwise operators plus the
|
|
151
|
+
* `and/or/xor/not/shl/shr/rol/ror/mod` functions, read against the
|
|
152
|
+
* scope's `wordBits`/`signed`.
|
|
153
|
+
* @returns {any}
|
|
154
|
+
*/
|
|
155
|
+
export function programmerEnv() {
|
|
156
|
+
return {
|
|
157
|
+
constants: { ...CONSTANTS },
|
|
158
|
+
funcs: WORD_FUNCS,
|
|
159
|
+
binops: WORD_BINOPS,
|
|
160
|
+
unops: WORD_UNOPS,
|
|
161
|
+
postops: FLOAT_POSTOPS,
|
|
162
|
+
};
|
|
163
|
+
}
|
package/src/errors.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file The engine's parse error, carrying a 1-based `line`/`column`
|
|
4
|
+
* (the `fail(message, position)` idiom from `packages/json/src/path.js`,
|
|
5
|
+
* adapted to line/column like the mermaid parser). The evaluate/render
|
|
6
|
+
* path catches it and never rethrows into the app loop.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export class CalcParseError extends Error {
|
|
10
|
+
/**
|
|
11
|
+
* @param {string} message
|
|
12
|
+
* @param {number} [line] 1-based line
|
|
13
|
+
* @param {number} [column] 1-based column
|
|
14
|
+
* @param {number} [position] 0-based char offset
|
|
15
|
+
*/
|
|
16
|
+
constructor(message, line = 1, column = 1, position = 0) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = 'CalcParseError';
|
|
19
|
+
this.line = line;
|
|
20
|
+
this.column = column;
|
|
21
|
+
this.position = position;
|
|
22
|
+
}
|
|
23
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file `@jarenjs/calc` — the ENGINE (part one of the two-layer
|
|
4
|
+
* package). Pure functions over data — expression text ⇄ AST ⇄
|
|
5
|
+
* value, and AST/scene → pure-vnode SVG — that know only `@jarenjs/core`
|
|
6
|
+
* and `@jarenjs/view`. It imports nothing from the component,
|
|
7
|
+
* `@jarenjs/app`, `@jarenjs/forms` or the DOM. The boundary is one-way
|
|
8
|
+
* (the component imports the engine, never the reverse).
|
|
9
|
+
*
|
|
10
|
+
* The signature duality mirrors the rest of the suite:
|
|
11
|
+
* `parseExpression` ⇄ `toExpression` is a round-trip fixed point.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { renderToString } from '@jarenjs/view';
|
|
15
|
+
import { evaluate } from './compile.js';
|
|
16
|
+
import { plot2d } from './plot/plot2d.js';
|
|
17
|
+
import { plot3d } from './plot/plot3d.js';
|
|
18
|
+
import { errorToVnode } from './render/error.js';
|
|
19
|
+
|
|
20
|
+
export { parseExpression } from './parser/index.js';
|
|
21
|
+
export { toExpression } from './to-expr.js';
|
|
22
|
+
export { compileExpr, evaluate } from './compile.js';
|
|
23
|
+
export { defaultEnv, programmerEnv } from './env.js';
|
|
24
|
+
export { CalcParseError } from './errors.js';
|
|
25
|
+
export {
|
|
26
|
+
num, constant, variable, unary, postfix, binary, call,
|
|
27
|
+
CONSTANTS, isConstant, astEqual, CALC_AST_VERSION,
|
|
28
|
+
} from './ast.js';
|
|
29
|
+
export { plot2d, buildScene2d, scene2dToVnode } from './plot/plot2d.js';
|
|
30
|
+
export { plot3d, buildScene3d, scene3dToVnode } from './plot/plot3d.js';
|
|
31
|
+
export { errorToVnode } from './render/error.js';
|
|
32
|
+
export { createTheme, THEMES, HOST_VARS } from './theme.js';
|
|
33
|
+
export { hashContent } from './utils.js';
|
|
34
|
+
export {
|
|
35
|
+
MODES, MODE_BY_ID,
|
|
36
|
+
standardMode, scientificMode, programmerMode, financialMode, converterMode,
|
|
37
|
+
wordViews, solveTvm, buildAmortization, npvOf, irrOf,
|
|
38
|
+
convertValue, unitOptions, converterDimensions, CURRENCY,
|
|
39
|
+
} from './modes/index.js';
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Render an expression to a plot vnode, choosing 2D or 3D by whether the
|
|
43
|
+
* expression's free variables include a second axis. Error-safe: a parse
|
|
44
|
+
* failure yields the error vnode, never a throw.
|
|
45
|
+
*
|
|
46
|
+
* @param {string} source
|
|
47
|
+
* @param {{ kind?: '2d'|'3d', [k: string]: any }} [options]
|
|
48
|
+
* @returns {any} an SVG vnode
|
|
49
|
+
*/
|
|
50
|
+
export function calcToVnode(source, options = {}) {
|
|
51
|
+
const ev = evaluate(source, {}, { env: options.env });
|
|
52
|
+
if (!ev.ok && ev.error && ev.error.line !== undefined) {
|
|
53
|
+
// a genuine parse error (not just an unbound variable like x/y)
|
|
54
|
+
if (/unexpected|expected|invalid|empty/.test(ev.error.message)) {
|
|
55
|
+
return errorToVnode(ev.error, options);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const kind = options.kind ?? (usesVar(source, 'y') ? '3d' : '2d');
|
|
59
|
+
try {
|
|
60
|
+
return kind === '3d' ? plot3d(source, options) : plot2d(source, options);
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
return errorToVnode({ message: String(/** @type {any} */ (err)?.message ?? err) }, options);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Cheap check for whether a source references a bare identifier `name`. */
|
|
68
|
+
function usesVar(source, name) {
|
|
69
|
+
try {
|
|
70
|
+
const re = new RegExp(`(^|[^A-Za-z0-9_])${name}([^A-Za-z0-9_(]|$)`);
|
|
71
|
+
return re.test(source);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Render any calc SVG vnode to a standalone SVG string (SSR / headless).
|
|
80
|
+
* @param {any} vnode
|
|
81
|
+
* @returns {string}
|
|
82
|
+
*/
|
|
83
|
+
export function toSvgString(vnode) {
|
|
84
|
+
return renderToString(vnode);
|
|
85
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file The converter mode kernel. Contains
|
|
4
|
+
* **no factors** — static dimensions call `@jarenjs/core/convert`'s
|
|
5
|
+
* `convert(...)`, and the **currency** dimension calls the pure
|
|
6
|
+
* `convertCurrency(value, from, to, rateTable)` with the live table from
|
|
7
|
+
* `$.calc.rates`. The component's rates layer is the only place that
|
|
8
|
+
* fetches a rate; this module never does.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
convert, unitsOf, dimensions, convertCurrency, currenciesOf,
|
|
13
|
+
} from '@jarenjs/core/convert';
|
|
14
|
+
import { formatNumber } from '@jarenjs/core/math';
|
|
15
|
+
|
|
16
|
+
/** Currency is a fifth, rate-table-backed dimension beside the static ones. */
|
|
17
|
+
export const CURRENCY = 'currency';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* All selectable dimensions (static core dimensions + currency).
|
|
21
|
+
* @returns {string[]}
|
|
22
|
+
*/
|
|
23
|
+
export function converterDimensions() {
|
|
24
|
+
return [...dimensions(), CURRENCY];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The unit options for a dimension. Static dimensions come from core;
|
|
29
|
+
* the currency dimension enumerates the codes present in `rates`.
|
|
30
|
+
* @param {string} dimension
|
|
31
|
+
* @param {any} [rates] the rate table (currency only)
|
|
32
|
+
* @returns {Array<{ id: string, symbol: string }>}
|
|
33
|
+
*/
|
|
34
|
+
export function unitOptions(dimension, rates) {
|
|
35
|
+
if (dimension === CURRENCY) {
|
|
36
|
+
return currenciesOf(rates ?? {}).map((code) => ({ id: code, symbol: code }));
|
|
37
|
+
}
|
|
38
|
+
return unitsOf(dimension).map((u) => ({ id: u.id, symbol: u.symbol }));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Convert a value in a dimension. Delegates entirely to core: static
|
|
43
|
+
* dimensions to `convert`, currency to the pure `convertCurrency`.
|
|
44
|
+
* Returns `NaN` (never throws) so the display path stays total.
|
|
45
|
+
*
|
|
46
|
+
* @param {string} dimension @param {number} value @param {string} from @param {string} to
|
|
47
|
+
* @param {any} [rates] the rate table (currency only)
|
|
48
|
+
* @returns {number}
|
|
49
|
+
*/
|
|
50
|
+
export function convertValue(dimension, value, from, to, rates) {
|
|
51
|
+
try {
|
|
52
|
+
if (dimension === CURRENCY) return convertCurrency(value, from, to, rates ?? {});
|
|
53
|
+
return convert(value, from, to);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return NaN;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Format a converter result. @param {number} value */
|
|
61
|
+
export function format(value) {
|
|
62
|
+
if (!Number.isFinite(value)) return '—';
|
|
63
|
+
return formatNumber(value, { notation: 'auto', precision: 8, group: true });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const converterMode = {
|
|
67
|
+
id: 'converter',
|
|
68
|
+
label: 'Converter',
|
|
69
|
+
dimensions: converterDimensions,
|
|
70
|
+
unitOptions,
|
|
71
|
+
convertValue,
|
|
72
|
+
format,
|
|
73
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file The financial mode kernel. Contains **no
|
|
4
|
+
* formulas** — every number comes from `@jarenjs/core/finance`. This is
|
|
5
|
+
* pure orchestration: the solve-for-unknown dispatcher (given any four of
|
|
6
|
+
* {N, I/Y, PV, PMT, FV}, pick the core function for the fifth) plus thin
|
|
7
|
+
* wrappers that turn a cash-flow / amortization request into core calls.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
pmt, pv, fv, nper, rate,
|
|
12
|
+
npv, irr, amortizationSchedule,
|
|
13
|
+
} from '@jarenjs/core/finance';
|
|
14
|
+
import { formatNumber } from '@jarenjs/core/math';
|
|
15
|
+
|
|
16
|
+
/** The five TVM variables the panel exposes. */
|
|
17
|
+
export const TVM_FIELDS = ['nper', 'rate', 'pv', 'pmt', 'fv'];
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Solve for the one unknown TVM variable. `rate` is a **percent** per
|
|
21
|
+
* period on the way in and out (e.g. 6 ⇒ 6%); the other four are plain
|
|
22
|
+
* numbers under the standard sign convention.
|
|
23
|
+
*
|
|
24
|
+
* @param {{ nper?: number, rate?: number, pv?: number, pmt?: number, fv?: number, type?: number, solveFor: string }} inputs
|
|
25
|
+
* @returns {number}
|
|
26
|
+
*/
|
|
27
|
+
export function solveTvm(inputs) {
|
|
28
|
+
const N = +inputs.nper;
|
|
29
|
+
const i = +inputs.rate / 100;
|
|
30
|
+
const PV = +inputs.pv;
|
|
31
|
+
const PMT = +inputs.pmt;
|
|
32
|
+
const FV = +inputs.fv;
|
|
33
|
+
const type = inputs.type ?? 0;
|
|
34
|
+
switch (inputs.solveFor) {
|
|
35
|
+
case 'pmt': return pmt(i, N, PV, FV, type);
|
|
36
|
+
case 'pv': return pv(i, N, PMT, FV, type);
|
|
37
|
+
case 'fv': return fv(i, N, PMT, PV, type);
|
|
38
|
+
case 'nper': return nper(i, PMT, PV, FV, type);
|
|
39
|
+
case 'rate': return rate(N, PMT, PV, FV, type) * 100;
|
|
40
|
+
default: return NaN;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Amortization series (delegates to core), ready to become a table/plot.
|
|
46
|
+
* @param {{ principal: number, rate: number, nper: number, type?: number }} inputs
|
|
47
|
+
* @returns {import('@jarenjs/core/finance/amortization').AmortRow[]}
|
|
48
|
+
*/
|
|
49
|
+
export function buildAmortization(inputs) {
|
|
50
|
+
return amortizationSchedule(+inputs.principal, +inputs.rate / 100, +inputs.nper, { type: inputs.type ?? 0 });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* NPV of a discount rate (percent) and a comma/space-separated cash-flow
|
|
55
|
+
* string or array.
|
|
56
|
+
* @param {number} ratePct @param {number[]|string} cashflows
|
|
57
|
+
* @returns {number}
|
|
58
|
+
*/
|
|
59
|
+
export function npvOf(ratePct, cashflows) {
|
|
60
|
+
return npv(ratePct / 100, parseFlows(cashflows));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* IRR (percent) of a cash-flow series.
|
|
65
|
+
* @param {number[]|string} cashflows
|
|
66
|
+
* @returns {number}
|
|
67
|
+
*/
|
|
68
|
+
export function irrOf(cashflows) {
|
|
69
|
+
return irr(parseFlows(cashflows)) * 100;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** @param {number[]|string} flows */
|
|
73
|
+
function parseFlows(flows) {
|
|
74
|
+
if (Array.isArray(flows)) return flows.map(Number);
|
|
75
|
+
return String(flows).split(/[\s,]+/).filter((s) => s !== '').map(Number);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Format a money-ish result. @param {number} value */
|
|
79
|
+
export function format(value) {
|
|
80
|
+
return formatNumber(value, { notation: 'fixed', precision: 2, group: true });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export const financialMode = {
|
|
84
|
+
id: 'financial',
|
|
85
|
+
label: 'Financial',
|
|
86
|
+
tvmFields: TVM_FIELDS,
|
|
87
|
+
solveTvm,
|
|
88
|
+
format,
|
|
89
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file Mode-kernel barrel. Each mode is a data-driven descriptor
|
|
4
|
+
* (keypad/panel + function-binding env + formatter); switching mode is a
|
|
5
|
+
* patch of `$.calc.mode`.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export { standardMode } from './standard.js';
|
|
9
|
+
export { scientificMode } from './scientific.js';
|
|
10
|
+
export { programmerMode, wordViews, BASES, WORD_SIZES } from './programmer.js';
|
|
11
|
+
export { financialMode, solveTvm, buildAmortization, npvOf, irrOf } from './financial.js';
|
|
12
|
+
export { converterMode, convertValue, unitOptions, converterDimensions, CURRENCY } from './converter.js';
|
|
13
|
+
|
|
14
|
+
import { standardMode } from './standard.js';
|
|
15
|
+
import { scientificMode } from './scientific.js';
|
|
16
|
+
import { programmerMode } from './programmer.js';
|
|
17
|
+
import { financialMode } from './financial.js';
|
|
18
|
+
import { converterMode } from './converter.js';
|
|
19
|
+
|
|
20
|
+
/** All modes, in selector order. */
|
|
21
|
+
export const MODES = [standardMode, scientificMode, programmerMode, financialMode, converterMode];
|
|
22
|
+
|
|
23
|
+
/** Mode descriptor by id. @type {Record<string, any>} */
|
|
24
|
+
export const MODE_BY_ID = Object.fromEntries(MODES.map((m) => [m.id, m]));
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file The programmer mode kernel. Binds the
|
|
4
|
+
* word-math environment (`@jarenjs/core/math/word.js`) so `& | << >> ~`
|
|
5
|
+
* and the `and/or/xor/not/shl/shr/rol/ror/mod` functions operate at the
|
|
6
|
+
* chosen word size (8/16/32/64) and signedness. Literals are written with
|
|
7
|
+
* `0x`/`0o`/`0b` prefixes; the display shows all four bases live.
|
|
8
|
+
*
|
|
9
|
+
* No formulas live here — the reusable word math is in core; this module
|
|
10
|
+
* is the keypad + the four-base view derivation.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { toWord, toBase } from '@jarenjs/core/math';
|
|
14
|
+
import { programmerEnv } from '../env.js';
|
|
15
|
+
|
|
16
|
+
export const BASES = ['HEX', 'DEC', 'OCT', 'BIN'];
|
|
17
|
+
export const WORD_SIZES = [8, 16, 32, 64];
|
|
18
|
+
|
|
19
|
+
/** @type {Array<Array<{label:string,k:string,tone?:string,span?:number}>>} */
|
|
20
|
+
export const KEYPAD = [
|
|
21
|
+
[{ label: 'C', k: 'clear', tone: 'clear' }, { label: '⌫', k: 'back', tone: 'clear' }, { label: '(', k: '(' }, { label: ')', k: ')' }, { label: '~', k: '~', tone: 'op' }],
|
|
22
|
+
[{ label: 'A', k: 'A' }, { label: 'B', k: 'B' }, { label: '&', k: '&', tone: 'op' }, { label: '|', k: '|', tone: 'op' }, { label: 'xor', k: 'xor(', tone: 'fn' }],
|
|
23
|
+
[{ label: 'C', k: 'C' }, { label: 'D', k: 'D' }, { label: '«', k: '<<', tone: 'op' }, { label: '»', k: '>>', tone: 'op' }, { label: 'mod', k: 'mod(', tone: 'fn' }],
|
|
24
|
+
[{ label: 'E', k: 'E' }, { label: 'F', k: 'F' }, { label: '0x', k: '0x' }, { label: '0b', k: '0b' }, { label: '0o', k: '0o' }],
|
|
25
|
+
[{ label: '7', k: '7' }, { label: '8', k: '8' }, { label: '9', k: '9' }, { label: '×', k: '*', tone: 'op' }, { label: '÷', k: '/', tone: 'op' }],
|
|
26
|
+
[{ label: '4', k: '4' }, { label: '5', k: '5' }, { label: '6', k: '6' }, { label: '+', k: '+', tone: 'op' }, { label: '−', k: '-', tone: 'op' }],
|
|
27
|
+
[{ label: '1', k: '1' }, { label: '2', k: '2' }, { label: '3', k: '3' }, { label: '0', k: '0' }, { label: '=', k: 'equals', tone: 'equals' }],
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The live four-base view of an integer value at the given word size.
|
|
32
|
+
* @param {number} value @param {number} [wordBits] @param {boolean} [signed]
|
|
33
|
+
* @returns {{ hex: string, dec: string, oct: string, bin: string }}
|
|
34
|
+
*/
|
|
35
|
+
export function wordViews(value, wordBits = 32, signed = false) {
|
|
36
|
+
if (!Number.isFinite(value)) return { hex: '—', dec: '—', oct: '—', bin: '—' };
|
|
37
|
+
const w = toWord(BigInt(Math.trunc(value)), wordBits, signed);
|
|
38
|
+
// unsigned view for hex/oct/bin so the two's-complement bit pattern shows
|
|
39
|
+
const u = toWord(w, wordBits, false);
|
|
40
|
+
return {
|
|
41
|
+
hex: toBase(u, 16, { upper: true, group: 4 }),
|
|
42
|
+
dec: toBase(w, 10),
|
|
43
|
+
oct: toBase(u, 8, { group: 3 }),
|
|
44
|
+
bin: toBase(u, 2, { group: 4, pad: wordBits }),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Format the primary display in the currently-selected base.
|
|
50
|
+
* @param {number} value @param {any} [state]
|
|
51
|
+
* @returns {string}
|
|
52
|
+
*/
|
|
53
|
+
export function format(value, state) {
|
|
54
|
+
const bits = state?.wordBits ?? 32;
|
|
55
|
+
const signed = state?.signed ?? false;
|
|
56
|
+
const base = state?.base ?? 'DEC';
|
|
57
|
+
const v = wordViews(value, bits, signed);
|
|
58
|
+
return base === 'HEX' ? '0x' + v.hex : base === 'OCT' ? '0o' + v.oct : base === 'BIN' ? '0b' + v.bin : v.dec;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const programmerMode = {
|
|
62
|
+
id: 'programmer',
|
|
63
|
+
label: 'Programmer',
|
|
64
|
+
env: programmerEnv(),
|
|
65
|
+
keypad: KEYPAD,
|
|
66
|
+
format,
|
|
67
|
+
bases: BASES,
|
|
68
|
+
wordSizes: WORD_SIZES,
|
|
69
|
+
};
|