* Parse a DSL group, which may contain parenthesized sub-expressions. * Handles `(m*s^2)` by stripping outer parens and recursing. * * A single pass finds both the first top-level `/` and all top-level `*` * split points. `/` binds more loosely than `*`, so if a slash is found * the string is
(s: string)
| 532 | * Flatten a compound unit expression into a map from unit symbol to its |
| 533 | * summed exponent. |
| 534 | * |
| 535 | * - `string` → add `exp` to the symbol's running total. |
| 536 | * - `["Multiply", ...]` → recurse each child with the same exponent. |
| 537 | * - `["Divide", a, b]` → recurse `a` with `+exp`, `b` with `−exp`. |
| 538 | * - `["Power", base, n]` (n a number) → recurse `base` with `exp·n`. |
| 539 | * |
| 540 | * Returns `null` on any other shape (the caller then leaves the unit |
| 541 | * untouched). Because same-symbol exponents are summed, `in¹·in⁻¹` cancels |
| 542 | * exactly, with no floating-point scale involved. |
| 543 | */ |
| 544 | export function flattenUnitFactors( |
| 545 | expr: UnitExpression |
| 546 | ): Map<string, number> | null { |
| 547 | const factors = new Map<string, number>(); |
| 548 | |
| 549 | function walk(e: UnitExpression, exp: number): boolean { |
| 550 | if (typeof e === 'string') { |
| 551 | factors.set(e, (factors.get(e) ?? 0) + exp); |
| 552 | return true; |
| 553 | } |
| 554 | if (!Array.isArray(e) || e.length < 2) return false; |
| 555 | const op = e[0]; |
| 556 | if (op === 'Multiply') { |
| 557 | for (let i = 1; i < e.length; i++) if (!walk(e[i], exp)) return false; |
| 558 | return true; |
| 559 | } |
| 560 | if (op === 'Divide') { |
| 561 | if (e.length !== 3) return false; |
| 562 | return walk(e[1], exp) && walk(e[2], -exp); |
| 563 | } |
| 564 | if (op === 'Power') { |
| 565 | if (e.length !== 3) return false; |
| 566 | const n = e[2]; |
| 567 | if (typeof n !== 'number') return false; |
| 568 | return walk(e[1], exp * n); |
| 569 | } |
| 570 | return false; |
| 571 | } |
| 572 | |
| 573 | if (!walk(expr, 1)) return null; |
| 574 | return factors; |
| 575 | } |
| 576 | |
| 577 | /** |
| 578 | * Cancel repeated unit factors that share a physical dimension. |
| 579 | * |
| 580 | * Zero-exponent entries are dropped. The remaining symbols are grouped by |
| 581 | * their dimension vector. For each group with MIXED-SIGN exponents (genuine |
| 582 | * cancellation potential, e.g. `m` and `in` with `+1`/`−1`), the member with |
| 583 | * the largest scale is chosen as the representative (same convention as |
| 584 | * `quantityAdd`); every other member `s` with exponent `e` folds into the |
| 585 | * representative (`magnitudeScale *= (scale_s / scale_rep) ** e`, and `e` is |
| 586 | * added to the representative's exponent). The representative itself is |
| 587 | * dropped if its summed exponent reaches 0. Groups whose exponents are all |
| 588 | * the same sign are left untouched (so `in·ft` area survives as written). |
| 589 | * |
| 590 | * Offsets (degC/degF) are intentionally ignored, consistent with |
| 591 | * `getExpressionScale`/`convertCompoundUnit` in compound contexts. |
no test coverage detected