postcss-calc 11.2.0 → 11.2.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/package.json +5 -5
- package/src/lib/analyze.js +87 -43
- package/src/lib/functions.js +40 -10
- package/src/lib/node.js +1 -1
- package/src/lib/parser.js +4 -14
- package/src/lib/serialize.js +143 -99
- package/types/lib/analyze.d.ts +27 -0
- package/types/lib/functions.d.ts +19 -1
- package/src/lib/calculation-type.js +0 -25
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "postcss-calc",
|
|
3
|
-
"version": "11.2.
|
|
3
|
+
"version": "11.2.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "PostCSS plugin to reduce calc()",
|
|
6
6
|
"keywords": [
|
|
@@ -42,16 +42,16 @@
|
|
|
42
42
|
}
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
|
-
"@csstools/css-calc": "^3.
|
|
46
|
-
"@types/node": "^26.
|
|
47
|
-
"fast-check": "^4.10.
|
|
45
|
+
"@csstools/css-calc": "^3.4.0",
|
|
46
|
+
"@types/node": "^26.6.1",
|
|
47
|
+
"fast-check": "^4.10.1",
|
|
48
48
|
"oxfmt": "^0.68.0",
|
|
49
49
|
"oxlint": "^1.83.0",
|
|
50
50
|
"postcss": "^8.5.28",
|
|
51
51
|
"typescript": "~7.0.2"
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
|
-
"@csstools/css-tokenizer": "^4.0.
|
|
54
|
+
"@csstools/css-tokenizer": "^4.0.1"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
57
57
|
"postcss": "^8.5.28"
|
package/src/lib/analyze.js
CHANGED
|
@@ -1,16 +1,32 @@
|
|
|
1
1
|
import { baseOf } from './convertUnits.js';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
addTypes,
|
|
4
|
+
failureType,
|
|
5
|
+
isFailure,
|
|
6
|
+
isPercentage,
|
|
7
|
+
mathFunctions,
|
|
8
|
+
numberType,
|
|
9
|
+
percentageType,
|
|
10
|
+
unknownType,
|
|
11
|
+
} from './functions.js';
|
|
3
12
|
import { assertDepth } from './limits.js';
|
|
4
13
|
|
|
5
14
|
/** @typedef {import('./node.js').Node} Node */
|
|
6
15
|
/** @typedef {import('./functions.js').CalculationType} CalculationType */
|
|
16
|
+
/** @typedef {Extract<CalculationType, {kind: 'dimension'}>} DimensionType */
|
|
7
17
|
|
|
8
18
|
/** @typedef {'number' | 'unknown' | {dimension: string | null}} AnalysisType */
|
|
9
19
|
/** @typedef {{type: AnalysisType, valid: boolean, unresolved: boolean}} Analysis */
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
20
|
+
/**
|
|
21
|
+
* @typedef {Object} ProductFactors
|
|
22
|
+
* @property {boolean} valid
|
|
23
|
+
* @property {boolean} structurallyValid
|
|
24
|
+
* @property {boolean} hasUnresolved
|
|
25
|
+
* @property {DimensionType | null} numerator
|
|
26
|
+
* @property {DimensionType | null} denominator
|
|
27
|
+
* @property {boolean} hasOpaqueNumerator
|
|
28
|
+
* @property {boolean} hasOpaqueDenominator
|
|
29
|
+
*/
|
|
14
30
|
|
|
15
31
|
/**
|
|
16
32
|
* Analyze the original complete tree and return its root summary. Analysis
|
|
@@ -34,10 +50,11 @@ function analyzeType(node, depth = 0) {
|
|
|
34
50
|
case 'Num':
|
|
35
51
|
return resolved(numberType);
|
|
36
52
|
case 'Dim':
|
|
37
|
-
// Percentages are contextual
|
|
38
|
-
//
|
|
53
|
+
// Percentages are contextual; their percent-ness is tracked so a
|
|
54
|
+
// `% / %` product cancels to a number. Unknown units are opaque, while
|
|
55
|
+
// known families can still reject px + seconds.
|
|
39
56
|
return node.unit === '%'
|
|
40
|
-
? finish(
|
|
57
|
+
? finish(percentageType, true, true)
|
|
41
58
|
: resolved({ kind: 'dimension', base: baseOf(node.unit) });
|
|
42
59
|
case 'Ident':
|
|
43
60
|
return markUnresolved(unknownType);
|
|
@@ -56,6 +73,7 @@ function analyzeType(node, depth = 0) {
|
|
|
56
73
|
function analyzeSum(node, depth) {
|
|
57
74
|
let type = null;
|
|
58
75
|
let hasUnknown = false;
|
|
76
|
+
let hasPercentage = false;
|
|
59
77
|
let valid = true;
|
|
60
78
|
let hasUnresolved = false;
|
|
61
79
|
for (const term of node.terms) {
|
|
@@ -65,7 +83,11 @@ function analyzeSum(node, depth) {
|
|
|
65
83
|
if (isFailure(child.type)) {
|
|
66
84
|
type = failureType;
|
|
67
85
|
} else if (child.type.kind === 'unknown') {
|
|
68
|
-
|
|
86
|
+
// A pure percentage sum stays percentage-typed so a surrounding
|
|
87
|
+
// product can cancel `% / %`; any other opaque term must widen the
|
|
88
|
+
// sum back to unknown.
|
|
89
|
+
if (isPercentage(child.type)) hasPercentage = true;
|
|
90
|
+
else hasUnknown = true;
|
|
69
91
|
} else if (type === null) {
|
|
70
92
|
type = child.type;
|
|
71
93
|
} else if (!isFailure(type)) {
|
|
@@ -75,11 +97,10 @@ function analyzeSum(node, depth) {
|
|
|
75
97
|
if (type !== null && isFailure(type)) {
|
|
76
98
|
return finish(failureType, false, hasUnresolved);
|
|
77
99
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
);
|
|
100
|
+
let fallback = numberType;
|
|
101
|
+
if (hasUnknown) fallback = unknownType;
|
|
102
|
+
else if (hasPercentage) fallback = percentageType;
|
|
103
|
+
return finish(type ?? fallback, valid, hasUnresolved);
|
|
83
104
|
}
|
|
84
105
|
|
|
85
106
|
/** @param {Extract<Node, {type: 'Product'}>} node @param {number} depth @return {{type: CalculationType, valid: boolean, unresolved: boolean}} */
|
|
@@ -88,8 +109,13 @@ function analyzeProduct(node, depth) {
|
|
|
88
109
|
let denominator = null;
|
|
89
110
|
let valid = true;
|
|
90
111
|
let structurallyValid = true;
|
|
91
|
-
|
|
92
|
-
|
|
112
|
+
// Opaque factors (unknowns and pure percentages) are counted per side so
|
|
113
|
+
// the pass below can decide whether any unknown remains after cancelling
|
|
114
|
+
// `% / %` pairs.
|
|
115
|
+
let opaqueNumerator = 0;
|
|
116
|
+
let opaqueDenominator = 0;
|
|
117
|
+
let percentageNumerator = 0;
|
|
118
|
+
let percentageDenominator = 0;
|
|
93
119
|
let hasUnresolved = false;
|
|
94
120
|
for (const factor of node.factors) {
|
|
95
121
|
const child = analyzeType(factor.node, depth + 1);
|
|
@@ -99,8 +125,13 @@ function analyzeProduct(node, depth) {
|
|
|
99
125
|
continue;
|
|
100
126
|
}
|
|
101
127
|
if (child.type.kind === 'unknown') {
|
|
102
|
-
if (factor.exponent === 1)
|
|
103
|
-
|
|
128
|
+
if (factor.exponent === 1) {
|
|
129
|
+
opaqueNumerator++;
|
|
130
|
+
if (isPercentage(child.type)) percentageNumerator++;
|
|
131
|
+
} else {
|
|
132
|
+
opaqueDenominator++;
|
|
133
|
+
if (isPercentage(child.type)) percentageDenominator++;
|
|
134
|
+
}
|
|
104
135
|
continue;
|
|
105
136
|
}
|
|
106
137
|
if (child.type.kind !== 'dimension') continue;
|
|
@@ -112,10 +143,36 @@ function analyzeProduct(node, depth) {
|
|
|
112
143
|
else denominator = child.type;
|
|
113
144
|
}
|
|
114
145
|
}
|
|
146
|
+
// A percentage divided by a percentage is always a plain number: both
|
|
147
|
+
// operands resolve in the same context, so their contextual type cancels.
|
|
148
|
+
// Consume one such pair before judging the remaining unknowns so a
|
|
149
|
+
// surrounding sum does not mistake `% / %` for a length-compatible term.
|
|
150
|
+
const cancelled = Math.min(percentageNumerator, percentageDenominator);
|
|
151
|
+
const hasOpaqueNumerator = opaqueNumerator - cancelled > 0;
|
|
152
|
+
const hasOpaqueDenominator = opaqueDenominator - cancelled > 0;
|
|
153
|
+
return finishProduct({
|
|
154
|
+
valid,
|
|
155
|
+
structurallyValid,
|
|
156
|
+
hasUnresolved,
|
|
157
|
+
numerator,
|
|
158
|
+
denominator,
|
|
159
|
+
hasOpaqueNumerator,
|
|
160
|
+
hasOpaqueDenominator,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Classify a product once its factors are analyzed. Opaque factors can supply
|
|
166
|
+
* missing type information, but they cannot make an already-invalid
|
|
167
|
+
* combination of known dimensions valid.
|
|
168
|
+
* @param {ProductFactors} factors
|
|
169
|
+
* @return {{type: CalculationType, valid: boolean, unresolved: boolean}}
|
|
170
|
+
*/
|
|
171
|
+
function finishProduct(factors) {
|
|
172
|
+
const { valid, structurallyValid, hasUnresolved } = factors;
|
|
115
173
|
if (!valid) return finish(failureType, false, hasUnresolved);
|
|
116
|
-
// Opaque factors can supply missing type information, but they cannot make
|
|
117
|
-
// an already-invalid combination of known dimensions valid.
|
|
118
174
|
if (!structurallyValid) return finish(failureType, false, hasUnresolved);
|
|
175
|
+
const { numerator, denominator } = factors;
|
|
119
176
|
if (
|
|
120
177
|
numerator !== null &&
|
|
121
178
|
denominator !== null &&
|
|
@@ -127,18 +184,13 @@ function analyzeProduct(node, depth) {
|
|
|
127
184
|
// only leave that dimension in place (when it resolves to a number) or make
|
|
128
185
|
// the product invalid. It can never make the product a bare number. Keep
|
|
129
186
|
// that known constraint so a surrounding sum can reject `1px * 1% + 1`.
|
|
130
|
-
const constrained = constrainedNumerator(
|
|
131
|
-
hasUnknownNumerator,
|
|
132
|
-
hasUnknownDenominator,
|
|
133
|
-
numerator,
|
|
134
|
-
denominator
|
|
135
|
-
);
|
|
187
|
+
const constrained = constrainedNumerator(factors);
|
|
136
188
|
if (constrained !== null) {
|
|
137
189
|
return finish(constrained, true, hasUnresolved);
|
|
138
190
|
}
|
|
139
191
|
// Other opaque factors may supply type information that changes how the
|
|
140
192
|
// known dimensions combine once the known factors are structurally valid.
|
|
141
|
-
if (
|
|
193
|
+
if (factors.hasOpaqueNumerator || factors.hasOpaqueDenominator) {
|
|
142
194
|
return finish(unknownType, true, hasUnresolved);
|
|
143
195
|
}
|
|
144
196
|
if (numerator !== null && denominator !== null) {
|
|
@@ -153,23 +205,15 @@ function analyzeProduct(node, depth) {
|
|
|
153
205
|
}
|
|
154
206
|
|
|
155
207
|
/**
|
|
156
|
-
* @param {
|
|
157
|
-
* @
|
|
158
|
-
* @param {CalculationType | null} numerator
|
|
159
|
-
* @param {CalculationType | null} denominator
|
|
160
|
-
* @return {CalculationType | null}
|
|
208
|
+
* @param {ProductFactors} factors
|
|
209
|
+
* @return {DimensionType | null}
|
|
161
210
|
*/
|
|
162
|
-
function constrainedNumerator(
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
return hasUnknownNumerator &&
|
|
169
|
-
!hasUnknownDenominator &&
|
|
170
|
-
numerator !== null &&
|
|
171
|
-
denominator === null
|
|
172
|
-
? numerator
|
|
211
|
+
function constrainedNumerator(factors) {
|
|
212
|
+
return factors.hasOpaqueNumerator &&
|
|
213
|
+
!factors.hasOpaqueDenominator &&
|
|
214
|
+
factors.numerator !== null &&
|
|
215
|
+
factors.denominator === null
|
|
216
|
+
? factors.numerator
|
|
173
217
|
: null;
|
|
174
218
|
}
|
|
175
219
|
|
package/src/lib/functions.js
CHANGED
|
@@ -14,13 +14,23 @@ import { simplifyLog } from './simplify/log.js';
|
|
|
14
14
|
import { simplifyHypot } from './simplify/hypot.js';
|
|
15
15
|
|
|
16
16
|
/** @typedef {import('./node.js').Node} Node */
|
|
17
|
-
/**
|
|
17
|
+
/**
|
|
18
|
+
* `percent` marks a value that resolves in the same percentage context as its
|
|
19
|
+
* peers (a pure percentage). It is only produced at leaves, by abs(), and by
|
|
20
|
+
* homogeneous sums and calls — never by a product, where an unpaired
|
|
21
|
+
* percentage could no longer cancel against anything.
|
|
22
|
+
* @typedef {{kind: 'number'} | {kind: 'dimension', base: string | null} | {kind: 'unknown', percent?: true} | {kind: 'failure'}} CalculationType
|
|
23
|
+
*/
|
|
18
24
|
/** @typedef {(name: string, args: Node[]) => Node} MathSimplifier */
|
|
19
25
|
/** @typedef {(args: CalculationType[], nodes: Node[]) => CalculationType} TypeAnalyzer */
|
|
20
26
|
/** @typedef {{analyze: TypeAnalyzer, simplify?: MathSimplifier, isKeyword?: (node: Node, index: number) => boolean, calculation?: boolean}} MathFunction */
|
|
21
27
|
|
|
22
28
|
/** @type {CalculationType} */ const numberType = { kind: 'number' };
|
|
23
29
|
/** @type {CalculationType} */ const unknownType = { kind: 'unknown' };
|
|
30
|
+
/** @type {CalculationType} */ const percentageType = {
|
|
31
|
+
kind: 'unknown',
|
|
32
|
+
percent: true,
|
|
33
|
+
};
|
|
24
34
|
/** @type {CalculationType} */ const failureType = { kind: 'failure' };
|
|
25
35
|
|
|
26
36
|
/** @param {CalculationType} type @return {boolean} */
|
|
@@ -28,10 +38,23 @@ function isFailure(type) {
|
|
|
28
38
|
return type.kind === 'failure';
|
|
29
39
|
}
|
|
30
40
|
|
|
41
|
+
/** @param {CalculationType} type @return {boolean} */
|
|
42
|
+
function isPercentage(type) {
|
|
43
|
+
return type.kind === 'unknown' && type.percent === true;
|
|
44
|
+
}
|
|
45
|
+
|
|
31
46
|
/** @param {CalculationType} a @param {CalculationType} b @return {CalculationType} */
|
|
32
47
|
function addTypes(a, b) {
|
|
33
48
|
if (isFailure(a) || isFailure(b)) return failureType;
|
|
34
|
-
if (a.kind === 'unknown' || b.kind === 'unknown')
|
|
49
|
+
if (a.kind === 'unknown' || b.kind === 'unknown') {
|
|
50
|
+
// A pure percentage keeps its contextual type so a surrounding product
|
|
51
|
+
// can cancel `% / %`; mixing it with any other opaque operand loses the
|
|
52
|
+
// guarantee that it resolves in the same context as its peers. A known
|
|
53
|
+
// number + percentage sum is likewise kept unknown and valid: the spec
|
|
54
|
+
// resolves the percentage against its surrounding context, which this
|
|
55
|
+
// coarse type model does not track.
|
|
56
|
+
return isPercentage(a) && isPercentage(b) ? percentageType : unknownType;
|
|
57
|
+
}
|
|
35
58
|
if (a.kind === 'number' && b.kind === 'number') return numberType;
|
|
36
59
|
if (a.kind === 'dimension' && b.kind === 'dimension') {
|
|
37
60
|
if (a.base === null || b.base === null) return unknownType;
|
|
@@ -129,9 +152,11 @@ function analyzeRound(args, nodes) {
|
|
|
129
152
|
/** @param {CalculationType[]} args @return {CalculationType} */
|
|
130
153
|
function analyzeAtan2(args) {
|
|
131
154
|
const type = matchingArguments(args, 2, 2);
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
155
|
+
if (isFailure(type)) return type;
|
|
156
|
+
// The type table gives atan2() «["angle" → 1]»; an unresolved result is
|
|
157
|
+
// plain unknown, never the percentage of its arguments.
|
|
158
|
+
if (type.kind === 'unknown') return unknownType;
|
|
159
|
+
return { kind: 'dimension', base: 'angle' };
|
|
135
160
|
}
|
|
136
161
|
|
|
137
162
|
/** @param {CalculationType[]} args @return {CalculationType} */
|
|
@@ -354,11 +379,16 @@ function hasPotentialMathFunction(value) {
|
|
|
354
379
|
|
|
355
380
|
export {
|
|
356
381
|
addTypes,
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
QUICK_MATH_TEST,
|
|
360
|
-
isFailure,
|
|
382
|
+
failureType,
|
|
383
|
+
hasPotentialMathFunction,
|
|
361
384
|
isCalculationFunction,
|
|
385
|
+
isFailure,
|
|
386
|
+
isPercentage,
|
|
362
387
|
isSupportedMathFunction,
|
|
363
|
-
|
|
388
|
+
lookupMathFunction,
|
|
389
|
+
mathFunctions,
|
|
390
|
+
numberType,
|
|
391
|
+
percentageType,
|
|
392
|
+
QUICK_MATH_TEST,
|
|
393
|
+
unknownType,
|
|
364
394
|
};
|
package/src/lib/node.js
CHANGED
|
@@ -215,7 +215,7 @@ function negate(node) {
|
|
|
215
215
|
}
|
|
216
216
|
if (node.type === 'Sum') {
|
|
217
217
|
// A grouped sum may contain opaque terms whose meaning depends on the
|
|
218
|
-
// surrounding context. Keep the group intact so `-(a + b)` cannot turn
|
|
218
|
+
// surrounding context. Keep the group intact so `-1 * (a + b)` cannot turn
|
|
219
219
|
// into `-a - b` while it is still unresolved.
|
|
220
220
|
if (node.grouped) {
|
|
221
221
|
return mkSum([{ sign: -1, node }]);
|
package/src/lib/parser.js
CHANGED
|
@@ -1,16 +1,7 @@
|
|
|
1
1
|
// Pratt parser over native @csstools/css-tokenizer tokens.
|
|
2
2
|
import { TokenType as CssType } from '@csstools/css-tokenizer';
|
|
3
3
|
import { baseOf } from './convertUnits.js';
|
|
4
|
-
import {
|
|
5
|
-
call,
|
|
6
|
-
dim,
|
|
7
|
-
ident,
|
|
8
|
-
mkProduct,
|
|
9
|
-
mkSum,
|
|
10
|
-
negate,
|
|
11
|
-
num,
|
|
12
|
-
opaqueCall,
|
|
13
|
-
} from './node.js';
|
|
4
|
+
import { call, dim, ident, mkProduct, mkSum, num, opaqueCall } from './node.js';
|
|
14
5
|
import { isCalculationFunction, isSupportedMathFunction } from './functions.js';
|
|
15
6
|
import { assertDepth } from './limits.js';
|
|
16
7
|
import { CSS_NUMBER_PREFIX } from './regex.js';
|
|
@@ -293,11 +284,10 @@ function parsePrefix(input, cursor, token, depth) {
|
|
|
293
284
|
? { ...expression, grouped: true }
|
|
294
285
|
: expression;
|
|
295
286
|
}
|
|
296
|
-
case '-':
|
|
297
|
-
return negate(parseExpr(input, cursor, 7, depth + 1));
|
|
298
|
-
case '+':
|
|
299
|
-
return parseExpr(input, cursor, 7, depth + 1);
|
|
300
287
|
}
|
|
288
|
+
// No unary `+`/`-` production exists in the <calc-value> grammar; a
|
|
289
|
+
// sign is only valid inside a number/dimension token or as a binary
|
|
290
|
+
// operator. `-(...)` therefore fails to parse and is preserved.
|
|
301
291
|
}
|
|
302
292
|
throw new Error(`Unexpected token "${token.raw}" at position ${token.pos}`);
|
|
303
293
|
}
|
package/src/lib/serialize.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { serializeComponents } from './opaque.js';
|
|
6
6
|
import { checkCalculationDepth } from './limits.js';
|
|
7
7
|
import { isCalculationFunction } from './functions.js';
|
|
8
|
+
import { num } from './node.js';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* @typedef {import('./node.js').Node} Node
|
|
@@ -23,20 +24,73 @@ import { isCalculationFunction } from './functions.js';
|
|
|
23
24
|
const SUM_PRECEDENCE = 1;
|
|
24
25
|
const PRODUCT_PRECEDENCE = 2;
|
|
25
26
|
const ATOMIC_PRECEDENCE = 3;
|
|
26
|
-
//
|
|
27
|
-
// for deciding whether
|
|
27
|
+
// Negation (-1 * ...) binds more tightly than a sum but has the same atomic boundary
|
|
28
|
+
// for deciding whether the operand needs parentheses.
|
|
28
29
|
const UNARY_PRECEDENCE = ATOMIC_PRECEDENCE;
|
|
29
30
|
const NOISE_FLOOR = 1e-12;
|
|
30
31
|
|
|
31
32
|
/**
|
|
32
|
-
*
|
|
33
|
+
* Divide a decimal digit string by 10^k, rounding half away from zero, and
|
|
34
|
+
* return the resulting integer digit string. `digits` has no leading zeros.
|
|
35
|
+
* @param {string} digits
|
|
36
|
+
* @param {number} k
|
|
37
|
+
* @return {string}
|
|
38
|
+
*/
|
|
39
|
+
function divideByPowerOfTen(digits, k) {
|
|
40
|
+
// 0x30/0x35/0x39 are the char codes of '0'/'5'/'9'.
|
|
41
|
+
if (digits.length <= k) {
|
|
42
|
+
return digits.length === k && digits.charCodeAt(0) >= 0x35 ? '1' : '0';
|
|
43
|
+
}
|
|
44
|
+
const cut = digits.length - k;
|
|
45
|
+
if (digits.charCodeAt(cut) < 0x35) return digits.slice(0, cut);
|
|
46
|
+
// Round up and propagate the carry through trailing nines.
|
|
47
|
+
let index = cut - 1;
|
|
48
|
+
while (index >= 0 && digits.charCodeAt(index) === 0x39) index--;
|
|
49
|
+
if (index < 0) return `1${'0'.repeat(cut)}`;
|
|
50
|
+
return `${digits.slice(0, index)}${String.fromCharCode(
|
|
51
|
+
digits.charCodeAt(index) + 1
|
|
52
|
+
)}${'0'.repeat(cut - index - 1)}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Round the shortest decimal representation of a non-negative double to `p`
|
|
57
|
+
* fractional digits, half away from zero.
|
|
33
58
|
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
59
|
+
* `Number(text + 'e' + p)` reads the exact intended decimal (so `1.005` at
|
|
60
|
+
* precision 2 becomes `1.01`), but it is only exact while the shifted value
|
|
61
|
+
* fits in `Number.MAX_SAFE_INTEGER`; beyond that the intermediate double
|
|
62
|
+
* rounds and can move the rounding boundary (e.g. `312834450754803.44` at
|
|
63
|
+
* precision 1 or 6 drifted to `312834450754803.5`). Round the decimal digits
|
|
64
|
+
* directly instead.
|
|
39
65
|
*
|
|
66
|
+
* @param {number} abs
|
|
67
|
+
* @param {number} p
|
|
68
|
+
* @return {number}
|
|
69
|
+
*/
|
|
70
|
+
function roundDecimal(abs, p) {
|
|
71
|
+
const text = String(abs);
|
|
72
|
+
const eIdx = text.indexOf('e');
|
|
73
|
+
const mantissa = eIdx === -1 ? text : text.slice(0, eIdx);
|
|
74
|
+
let exponent = eIdx === -1 ? 0 : Number(text.slice(eIdx + 1));
|
|
75
|
+
const dot = mantissa.indexOf('.');
|
|
76
|
+
let digits = mantissa;
|
|
77
|
+
if (dot !== -1) {
|
|
78
|
+
digits = mantissa.slice(0, dot) + mantissa.slice(dot + 1);
|
|
79
|
+
exponent -= mantissa.length - dot - 1;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// value = digits * 10^exponent, so the shortest decimal has -exponent
|
|
83
|
+
// fractional digits when it is smaller than 1.
|
|
84
|
+
if (exponent >= -p) return abs;
|
|
85
|
+
|
|
86
|
+
let start = 0;
|
|
87
|
+
while (start < digits.length - 1 && digits.charCodeAt(start) === 0x30)
|
|
88
|
+
start++;
|
|
89
|
+
const rounded = divideByPowerOfTen(digits.slice(start), -(exponent + p));
|
|
90
|
+
return Number(`${rounded}e-${p}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
40
94
|
* @param {number} v
|
|
41
95
|
* @param {number | false} prec
|
|
42
96
|
* @return {number}
|
|
@@ -53,36 +107,9 @@ function round(v, prec) {
|
|
|
53
107
|
// or exponent overflows into Infinity/NaN (e.g. exponent + prec > 308).
|
|
54
108
|
const p = Math.min(100, Math.max(0, Math.trunc(prec)));
|
|
55
109
|
const sign = v < 0 ? -1 : 1;
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
// Fast path: rounding to integer with "round half away from zero".
|
|
60
|
-
rounded = sign * Math.round(abs);
|
|
61
|
-
} else {
|
|
62
|
-
// Avoid .split('e') allocations: for numbers between 1e-6 and MAX_SAFE_INTEGER,
|
|
63
|
-
// String(abs) never contains exponential notation ('e').
|
|
64
|
-
const absStr = String(abs);
|
|
65
|
-
const eIdx = absStr.indexOf('e');
|
|
66
|
-
let shifted;
|
|
67
|
-
if (eIdx === -1) {
|
|
68
|
-
shifted = Math.round(Number(absStr + 'e' + p));
|
|
69
|
-
} else {
|
|
70
|
-
const mantissa = absStr.slice(0, eIdx);
|
|
71
|
-
const exponent = Number(absStr.slice(eIdx + 1));
|
|
72
|
-
shifted = Math.round(Number(mantissa + 'e' + (exponent + p)));
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// shifted is an integer. It only contains exponential notation ('e') if >= 1e21.
|
|
76
|
-
if (shifted >= 1e21) {
|
|
77
|
-
const shiftedStr = String(shifted);
|
|
78
|
-
const seIdx = shiftedStr.indexOf('e');
|
|
79
|
-
const sMantissa = shiftedStr.slice(0, seIdx);
|
|
80
|
-
const sExponent = Number(shiftedStr.slice(seIdx + 1));
|
|
81
|
-
rounded = sign * Number(sMantissa + 'e' + (sExponent - p));
|
|
82
|
-
} else {
|
|
83
|
-
rounded = sign * Number(shifted + 'e-' + p);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
110
|
+
// Fast path: rounding to integer with "round half away from zero".
|
|
111
|
+
const rounded =
|
|
112
|
+
p === 0 ? sign * Math.round(abs) : sign * roundDecimal(abs, p);
|
|
86
113
|
|
|
87
114
|
// Preserve non-zero values smaller than precision (e.g. 1/1000000) from collapsing
|
|
88
115
|
// to zero, while still snapping true floating-point dust (< 1e-12) to zero.
|
|
@@ -163,20 +190,21 @@ function emitFiniteScalar(node, session, value) {
|
|
|
163
190
|
*/
|
|
164
191
|
function emitScalar(node, session, value) {
|
|
165
192
|
const buffer = session.buffer;
|
|
166
|
-
|
|
167
|
-
|
|
193
|
+
const effective = value ?? node.value;
|
|
194
|
+
if (Object.is(effective, -0)) emitSignedZero(buffer, node);
|
|
195
|
+
else if (isDegenerate(effective)) {
|
|
168
196
|
if (node.type === 'Dim') {
|
|
169
197
|
buffer.push(
|
|
170
198
|
'calc(',
|
|
171
|
-
degenerateKeyword(
|
|
199
|
+
degenerateKeyword(effective),
|
|
172
200
|
' * 1',
|
|
173
201
|
node.rawUnit ?? node.unit,
|
|
174
202
|
')'
|
|
175
203
|
);
|
|
176
204
|
} else {
|
|
177
|
-
buffer.push(degenerateKeyword(
|
|
205
|
+
buffer.push(degenerateKeyword(effective));
|
|
178
206
|
}
|
|
179
|
-
} else emitFiniteScalar(node, session,
|
|
207
|
+
} else emitFiniteScalar(node, session, effective);
|
|
180
208
|
}
|
|
181
209
|
|
|
182
210
|
/**
|
|
@@ -224,15 +252,13 @@ function needsParentheses(node, parentPrecedence, groupedRequired) {
|
|
|
224
252
|
* @param {ReturnType<typeof makeContext>} session
|
|
225
253
|
* @param {number} [parentPrecedence]
|
|
226
254
|
* @param {boolean} [groupedRequired]
|
|
227
|
-
* @param {number} [scalarValueOverride]
|
|
228
255
|
* @return {void}
|
|
229
256
|
*/
|
|
230
257
|
function emitNode(
|
|
231
258
|
node,
|
|
232
259
|
session,
|
|
233
260
|
parentPrecedence = 0,
|
|
234
|
-
groupedRequired = false
|
|
235
|
-
scalarValueOverride
|
|
261
|
+
groupedRequired = false
|
|
236
262
|
) {
|
|
237
263
|
const parenthesized = needsParentheses(
|
|
238
264
|
node,
|
|
@@ -240,22 +266,21 @@ function emitNode(
|
|
|
240
266
|
groupedRequired
|
|
241
267
|
);
|
|
242
268
|
if (parenthesized) session.buffer.push('(');
|
|
243
|
-
emitNodeBody(node, session
|
|
269
|
+
emitNodeBody(node, session);
|
|
244
270
|
if (parenthesized) session.buffer.push(')');
|
|
245
271
|
}
|
|
246
272
|
|
|
247
273
|
/**
|
|
248
274
|
* @param {Node} node
|
|
249
275
|
* @param {ReturnType<typeof makeContext>} session
|
|
250
|
-
* @param {number} [scalarValueOverride]
|
|
251
276
|
* @return {void}
|
|
252
277
|
*/
|
|
253
|
-
function emitNodeBody(node, session
|
|
278
|
+
function emitNodeBody(node, session) {
|
|
254
279
|
const buffer = session.buffer;
|
|
255
280
|
switch (node.type) {
|
|
256
281
|
case 'Num':
|
|
257
282
|
case 'Dim':
|
|
258
|
-
emitScalar(node, session
|
|
283
|
+
emitScalar(node, session);
|
|
259
284
|
return;
|
|
260
285
|
case 'Ident':
|
|
261
286
|
buffer.push(node.rawName ?? node.name);
|
|
@@ -306,18 +331,31 @@ function emitOpaqueCall(node, session, callNameOverride) {
|
|
|
306
331
|
buffer.push(')');
|
|
307
332
|
}
|
|
308
333
|
|
|
334
|
+
/**
|
|
335
|
+
* Whether a scalar node is strictly negative after precision rounding
|
|
336
|
+
* (excluding signed zero and sub-precision values that round to zero).
|
|
337
|
+
* @param {Node} node
|
|
338
|
+
* @param {number | false} precision
|
|
339
|
+
* @return {node is import('./node.js').Num | import('./node.js').Dim}
|
|
340
|
+
*/
|
|
341
|
+
function isEffectivelyNegative(node, precision) {
|
|
342
|
+
return (
|
|
343
|
+
isScalar(node) &&
|
|
344
|
+
!Object.is(node.value, -0) &&
|
|
345
|
+
Number.isFinite(node.value) &&
|
|
346
|
+
round(node.value, precision) < 0
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
|
|
309
350
|
/**
|
|
310
351
|
* @param {import('./node.js').SumTerm} term
|
|
311
352
|
* @param {1 | -1} multiplier
|
|
353
|
+
* @param {number | false} precision
|
|
312
354
|
* @return {1 | -1}
|
|
313
|
-
|
|
314
|
-
function termSign(term, multiplier) {
|
|
355
|
+
*/
|
|
356
|
+
function termSign(term, multiplier, precision) {
|
|
315
357
|
let sign = /** @type {1 | -1} */ (term.sign * multiplier);
|
|
316
|
-
if (
|
|
317
|
-
isScalar(term.node) &&
|
|
318
|
-
Number.isFinite(term.node.value) &&
|
|
319
|
-
term.node.value < 0
|
|
320
|
-
) {
|
|
358
|
+
if (isEffectivelyNegative(term.node, precision)) {
|
|
321
359
|
sign = /** @type {1 | -1} */ (-sign);
|
|
322
360
|
}
|
|
323
361
|
return sign;
|
|
@@ -327,14 +365,13 @@ function termSign(term, multiplier) {
|
|
|
327
365
|
* @param {import('./node.js').SumTerm} term
|
|
328
366
|
* @param {ReturnType<typeof makeContext>} session
|
|
329
367
|
* @param {1 | -1} sign
|
|
330
|
-
* @param {number | undefined} scalarValueOverride
|
|
331
368
|
* @return {void}
|
|
332
369
|
*/
|
|
333
|
-
function emitSumTerm(term, session, sign
|
|
370
|
+
function emitSumTerm(term, session, sign) {
|
|
334
371
|
if (sign === 1) {
|
|
335
|
-
emitNode(term.node, session, SUM_PRECEDENCE, true
|
|
372
|
+
emitNode(term.node, session, SUM_PRECEDENCE, true);
|
|
336
373
|
} else {
|
|
337
|
-
emitLeadingNeg(term.node, session
|
|
374
|
+
emitLeadingNeg(term.node, session);
|
|
338
375
|
}
|
|
339
376
|
}
|
|
340
377
|
|
|
@@ -349,25 +386,38 @@ function emitSumTerms(terms, session, multiplier = 1) {
|
|
|
349
386
|
for (let i = 0; i < terms.length; i++) {
|
|
350
387
|
const term = terms[i];
|
|
351
388
|
const termNode = term.node;
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
389
|
+
if (isScalar(termNode)) {
|
|
390
|
+
const effectiveVal = term.sign * multiplier * termNode.value;
|
|
391
|
+
if (Object.is(effectiveVal, -0)) {
|
|
392
|
+
if (i > 0) buffer.push(' + ');
|
|
393
|
+
emitSignedZero(buffer, termNode);
|
|
394
|
+
} else if (isDegenerate(effectiveVal)) {
|
|
395
|
+
const sign = /** @type {1 | -1} */ (term.sign * multiplier);
|
|
396
|
+
if (i === 0) {
|
|
397
|
+
if (sign === -1) buffer.push('-');
|
|
398
|
+
emitScalar(termNode, session);
|
|
399
|
+
} else {
|
|
400
|
+
buffer.push(sign === 1 ? ' + ' : ' - ');
|
|
401
|
+
emitScalar(termNode, session);
|
|
402
|
+
}
|
|
362
403
|
} else {
|
|
363
|
-
|
|
404
|
+
const rounded = round(effectiveVal, session.precision);
|
|
405
|
+
if (rounded < 0) {
|
|
406
|
+
if (i === 0) buffer.push('-');
|
|
407
|
+
else buffer.push(' - ');
|
|
408
|
+
emitRoundedScalar(termNode, buffer, -rounded);
|
|
409
|
+
} else {
|
|
410
|
+
if (i > 0) buffer.push(' + ');
|
|
411
|
+
emitRoundedScalar(termNode, buffer, rounded);
|
|
412
|
+
}
|
|
364
413
|
}
|
|
365
414
|
continue;
|
|
366
415
|
}
|
|
367
|
-
|
|
368
|
-
if (
|
|
369
|
-
|
|
416
|
+
const sign = /** @type {1 | -1} */ (term.sign * multiplier);
|
|
417
|
+
if (i === 0) {
|
|
418
|
+
emitSumTerm(term, session, sign);
|
|
370
419
|
} else {
|
|
420
|
+
buffer.push(sign === 1 ? ' + ' : ' - ');
|
|
371
421
|
emitNode(termNode, session, SUM_PRECEDENCE, true);
|
|
372
422
|
}
|
|
373
423
|
}
|
|
@@ -381,30 +431,24 @@ function emitSum(sum, session) {
|
|
|
381
431
|
/**
|
|
382
432
|
* @param {Node} node
|
|
383
433
|
* @param {ReturnType<typeof makeContext>} session
|
|
384
|
-
* @param {number} [scalarValueOverride]
|
|
385
434
|
* @return {void}
|
|
386
435
|
*/
|
|
387
|
-
function emitLeadingNeg(node, session
|
|
388
|
-
if (
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
436
|
+
function emitLeadingNeg(node, session) {
|
|
437
|
+
if (node.type === 'Product') {
|
|
438
|
+
if (
|
|
439
|
+
node.factors.length > 0 &&
|
|
440
|
+
node.factors[0].exponent === 1 &&
|
|
441
|
+
node.factors[0].node.type === 'Num'
|
|
442
|
+
) {
|
|
443
|
+
const head = node.factors[0].node;
|
|
444
|
+
emitProductFactors(node.factors, session, 1, -head.value, head);
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
emitProductFactors(node.factors, session, 0, -1, num(-1));
|
|
398
448
|
return;
|
|
399
449
|
}
|
|
400
|
-
session.buffer.push('-');
|
|
401
|
-
emitNode(
|
|
402
|
-
node,
|
|
403
|
-
session,
|
|
404
|
-
UNARY_PRECEDENCE,
|
|
405
|
-
false,
|
|
406
|
-
isScalar(node) ? scalarValueOverride : undefined
|
|
407
|
-
);
|
|
450
|
+
session.buffer.push('-1 * ');
|
|
451
|
+
emitNode(node, session, UNARY_PRECEDENCE, false);
|
|
408
452
|
}
|
|
409
453
|
|
|
410
454
|
/**
|
|
@@ -459,9 +503,9 @@ function emitRootExpr(node, session) {
|
|
|
459
503
|
node.type === 'Sum' &&
|
|
460
504
|
node.grouped &&
|
|
461
505
|
node.terms.length > 1 &&
|
|
462
|
-
termSign(node.terms[0], 1) === -1
|
|
506
|
+
termSign(node.terms[0], 1, session.precision) === -1
|
|
463
507
|
) {
|
|
464
|
-
session.buffer.push('-(');
|
|
508
|
+
session.buffer.push('-1 * (');
|
|
465
509
|
emitSumTerms(node.terms, session, -1);
|
|
466
510
|
session.buffer.push(')');
|
|
467
511
|
return;
|
|
@@ -500,9 +544,9 @@ function emitMathResult(node, session, wrapper) {
|
|
|
500
544
|
node.type === 'Sum' &&
|
|
501
545
|
node.grouped &&
|
|
502
546
|
node.terms.length > 1 &&
|
|
503
|
-
termSign(node.terms[0], 1) === -1
|
|
547
|
+
termSign(node.terms[0], 1, session.precision) === -1
|
|
504
548
|
) {
|
|
505
|
-
session.buffer.push(wrapper, '(-(');
|
|
549
|
+
session.buffer.push(wrapper, '(-1 * (');
|
|
506
550
|
emitSumTerms(node.terms, session, -1);
|
|
507
551
|
session.buffer.push('))');
|
|
508
552
|
return;
|
package/types/lib/analyze.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
export type Node = import('./node.js').Node;
|
|
2
2
|
export type CalculationType = import('./functions.js').CalculationType;
|
|
3
|
+
export type DimensionType = Extract<CalculationType, {
|
|
4
|
+
kind: 'dimension';
|
|
5
|
+
}>;
|
|
3
6
|
export type AnalysisType = 'number' | 'unknown' | {
|
|
4
7
|
dimension: string | null;
|
|
5
8
|
};
|
|
@@ -8,6 +11,30 @@ export type Analysis = {
|
|
|
8
11
|
valid: boolean;
|
|
9
12
|
unresolved: boolean;
|
|
10
13
|
};
|
|
14
|
+
export type ProductFactors = {
|
|
15
|
+
valid: boolean;
|
|
16
|
+
structurallyValid: boolean;
|
|
17
|
+
hasUnresolved: boolean;
|
|
18
|
+
numerator: DimensionType | null;
|
|
19
|
+
denominator: DimensionType | null;
|
|
20
|
+
hasOpaqueNumerator: boolean;
|
|
21
|
+
hasOpaqueDenominator: boolean;
|
|
22
|
+
};
|
|
23
|
+
/** @typedef {import('./node.js').Node} Node */
|
|
24
|
+
/** @typedef {import('./functions.js').CalculationType} CalculationType */
|
|
25
|
+
/** @typedef {Extract<CalculationType, {kind: 'dimension'}>} DimensionType */
|
|
26
|
+
/** @typedef {'number' | 'unknown' | {dimension: string | null}} AnalysisType */
|
|
27
|
+
/** @typedef {{type: AnalysisType, valid: boolean, unresolved: boolean}} Analysis */
|
|
28
|
+
/**
|
|
29
|
+
* @typedef {Object} ProductFactors
|
|
30
|
+
* @property {boolean} valid
|
|
31
|
+
* @property {boolean} structurallyValid
|
|
32
|
+
* @property {boolean} hasUnresolved
|
|
33
|
+
* @property {DimensionType | null} numerator
|
|
34
|
+
* @property {DimensionType | null} denominator
|
|
35
|
+
* @property {boolean} hasOpaqueNumerator
|
|
36
|
+
* @property {boolean} hasOpaqueDenominator
|
|
37
|
+
*/
|
|
11
38
|
/**
|
|
12
39
|
* Analyze the original complete tree and return its root summary. Analysis
|
|
13
40
|
* validates and classifies the tree; it is not a rewrite plan.
|
package/types/lib/functions.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export type CalculationType = {
|
|
|
6
6
|
base: string | null;
|
|
7
7
|
} | {
|
|
8
8
|
kind: 'unknown';
|
|
9
|
+
percent?: true;
|
|
9
10
|
} | {
|
|
10
11
|
kind: 'failure';
|
|
11
12
|
};
|
|
@@ -17,8 +18,25 @@ export type MathFunction = {
|
|
|
17
18
|
isKeyword?: (node: Node, index: number) => boolean;
|
|
18
19
|
calculation?: boolean;
|
|
19
20
|
};
|
|
21
|
+
/** @typedef {import('./node.js').Node} Node */
|
|
22
|
+
/**
|
|
23
|
+
* `percent` marks a value that resolves in the same percentage context as its
|
|
24
|
+
* peers (a pure percentage). It is only produced at leaves, by abs(), and by
|
|
25
|
+
* homogeneous sums and calls — never by a product, where an unpaired
|
|
26
|
+
* percentage could no longer cancel against anything.
|
|
27
|
+
* @typedef {{kind: 'number'} | {kind: 'dimension', base: string | null} | {kind: 'unknown', percent?: true} | {kind: 'failure'}} CalculationType
|
|
28
|
+
*/
|
|
29
|
+
/** @typedef {(name: string, args: Node[]) => Node} MathSimplifier */
|
|
30
|
+
/** @typedef {(args: CalculationType[], nodes: Node[]) => CalculationType} TypeAnalyzer */
|
|
31
|
+
/** @typedef {{analyze: TypeAnalyzer, simplify?: MathSimplifier, isKeyword?: (node: Node, index: number) => boolean, calculation?: boolean}} MathFunction */
|
|
32
|
+
/** @type {CalculationType} */ declare const numberType: CalculationType;
|
|
33
|
+
/** @type {CalculationType} */ declare const unknownType: CalculationType;
|
|
34
|
+
/** @type {CalculationType} */ declare const percentageType: CalculationType;
|
|
35
|
+
/** @type {CalculationType} */ declare const failureType: CalculationType;
|
|
20
36
|
/** @param {CalculationType} type @return {boolean} */
|
|
21
37
|
declare function isFailure(type: CalculationType): boolean;
|
|
38
|
+
/** @param {CalculationType} type @return {boolean} */
|
|
39
|
+
declare function isPercentage(type: CalculationType): boolean;
|
|
22
40
|
/** @param {CalculationType} a @param {CalculationType} b @return {CalculationType} */
|
|
23
41
|
declare function addTypes(a: CalculationType, b: CalculationType): CalculationType;
|
|
24
42
|
declare const mathFunctions: Map<string, MathFunction>;
|
|
@@ -37,4 +55,4 @@ declare function isCalculationFunction(name: string): boolean;
|
|
|
37
55
|
declare function isSupportedMathFunction(name: string): boolean;
|
|
38
56
|
/** @param {string} value @return {boolean} */
|
|
39
57
|
declare function hasPotentialMathFunction(value: string): boolean;
|
|
40
|
-
export { addTypes,
|
|
58
|
+
export { addTypes, failureType, hasPotentialMathFunction, isCalculationFunction, isFailure, isPercentage, isSupportedMathFunction, lookupMathFunction, mathFunctions, numberType, percentageType, QUICK_MATH_TEST, unknownType, };
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
// Compatibility facade for the former calculation-type module. New code uses
|
|
2
|
-
// analyze() for the complete result and limits.js for depth policy.
|
|
3
|
-
|
|
4
|
-
import { analyze } from './analyze.js';
|
|
5
|
-
import { MAX_CALCULATION_DEPTH, checkCalculationDepth } from './limits.js';
|
|
6
|
-
|
|
7
|
-
/** @typedef {import('./node.js').Node} Node */
|
|
8
|
-
|
|
9
|
-
/** @typedef {{kind: 'number'} | {kind: 'dimension', base: string | null} | {kind: 'unknown'} | {kind: 'failure'}} CalculationType */
|
|
10
|
-
|
|
11
|
-
/** @param {Node} node @return {CalculationType} */
|
|
12
|
-
function checkCalculationType(node) {
|
|
13
|
-
const result = analyze(node);
|
|
14
|
-
if (!result.valid) return { kind: 'failure' };
|
|
15
|
-
if (result.type === 'number') return { kind: 'number' };
|
|
16
|
-
if (result.type === 'unknown') return { kind: 'unknown' };
|
|
17
|
-
return { kind: 'dimension', base: result.type.dimension };
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export {
|
|
21
|
-
MAX_CALCULATION_DEPTH,
|
|
22
|
-
checkCalculationDepth,
|
|
23
|
-
checkCalculationType,
|
|
24
|
-
analyze,
|
|
25
|
-
};
|