calculation-breakdown 0.0.0-stage → 0.1.2
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-ARCHIVE.md +3 -0
- package/CHANGELOG.md +19 -0
- package/LICENSE +21 -0
- package/README.md +179 -2
- package/dist/calculation-breakdown-builder.d.ts +56 -0
- package/dist/calculation-breakdown-builder.d.ts.map +1 -0
- package/dist/calculation-breakdown-builder.js +242 -0
- package/dist/calculation-breakdown-builder.js.map +1 -0
- package/dist/calculation-breakdown-human-string.d.ts +24 -0
- package/dist/calculation-breakdown-human-string.d.ts.map +1 -0
- package/dist/calculation-breakdown-human-string.js +86 -0
- package/dist/calculation-breakdown-human-string.js.map +1 -0
- package/dist/calculation-breakdown.error.d.ts +14 -0
- package/dist/calculation-breakdown.error.d.ts.map +1 -0
- package/dist/calculation-breakdown.error.js +16 -0
- package/dist/calculation-breakdown.error.js.map +1 -0
- package/dist/calculation-breakdown.interface.d.ts +134 -0
- package/dist/calculation-breakdown.interface.d.ts.map +1 -0
- package/dist/calculation-breakdown.interface.js +2 -0
- package/dist/calculation-breakdown.interface.js.map +1 -0
- package/dist/calculation-breakdown.type.d.ts +20 -0
- package/dist/calculation-breakdown.type.d.ts.map +1 -0
- package/dist/calculation-breakdown.type.js +2 -0
- package/dist/calculation-breakdown.type.js.map +1 -0
- package/dist/cjs/calculation-breakdown-builder.d.ts +55 -0
- package/dist/cjs/calculation-breakdown-builder.js +245 -0
- package/dist/cjs/calculation-breakdown-human-string.d.ts +23 -0
- package/dist/cjs/calculation-breakdown-human-string.js +88 -0
- package/dist/cjs/calculation-breakdown.error.d.ts +13 -0
- package/dist/cjs/calculation-breakdown.error.js +19 -0
- package/dist/cjs/calculation-breakdown.interface.d.ts +133 -0
- package/dist/cjs/calculation-breakdown.interface.js +2 -0
- package/dist/cjs/calculation-breakdown.type.d.ts +19 -0
- package/dist/cjs/calculation-breakdown.type.js +2 -0
- package/dist/cjs/index.d.ts +5 -0
- package/dist/cjs/index.js +9 -0
- package/dist/cjs/package.json +3 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/docs/_config.yml +19 -0
- package/docs/assets/calculation-breakdown/calculation-breakdown-builder.js +242 -0
- package/docs/assets/calculation-breakdown/calculation-breakdown-human-string.js +86 -0
- package/docs/assets/calculation-breakdown/calculation-breakdown.error.js +16 -0
- package/docs/assets/calculation-breakdown/calculation-breakdown.interface.js +2 -0
- package/docs/assets/calculation-breakdown/calculation-breakdown.type.js +2 -0
- package/docs/assets/calculation-breakdown/index.js +4 -0
- package/docs/changelog-archive.md +9 -0
- package/docs/changelog.md +25 -0
- package/docs/index.md +13 -0
- package/docs/playground/index.html +110 -0
- package/docs/playground/playground.css +407 -0
- package/docs/playground/playground.js +1303 -0
- package/package.json +66 -4
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Notable changes, starting with 0.1.0. The latest five releases are detailed here; older releases are archived.
|
|
4
|
+
|
|
5
|
+
## 0.1.2 — 2026-10-26
|
|
6
|
+
|
|
7
|
+
- Added an opt-in locale mode to the playground that preserves calculation edits, keeps the default example limited to the calculation, and falls back to semantic keys without generated dictionary placeholders.
|
|
8
|
+
- Made the playground vertically scrollable and its three panels usable on mobile screens.
|
|
9
|
+
- Fixed the deployed playground by including every browser ESM module, and kept release artifact inspection JSON-safe by suppressing duplicate package lifecycle scripts.
|
|
10
|
+
|
|
11
|
+
## 0.1.1 — 2026-10-26
|
|
12
|
+
|
|
13
|
+
- Added the guarded npm, Git tag, release-PR, and GitHub Pages deployment workflow used by `payload-vars`.
|
|
14
|
+
|
|
15
|
+
## 0.1.0 — 2026-10-26
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- Added the typed calculation breakdown builder, localized human-readable formatter, and interactive invoice playground.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ThisMeansMore
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,180 @@
|
|
|
1
|
-
#
|
|
1
|
+
# calculation-breakdown
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Typed, serializable breakdowns of calculations performed elsewhere.
|
|
4
|
+
|
|
5
|
+
Use `calculation-breakdown` to record how an authoritative result was produced without coupling that record to a particular UI, locale, or business domain. A breakdown can describe invoice totals, taxes, discounts, fees, credits, exchange-rate conversions, usage charges, allocations, prorations, and other financial calculations.
|
|
6
|
+
|
|
7
|
+
The package has no runtime dependencies and supports browsers and Node.js 18+. **It does not perform arithmetic or calculate results.** Your application supplies every value and the authoritative result. The package only creates a structured record that other systems can render, localize, store, or audit.
|
|
8
|
+
|
|
9
|
+
## Why use it?
|
|
10
|
+
|
|
11
|
+
- Keep calculation traces typed alongside financial domain code.
|
|
12
|
+
- Serialize breakdowns as plain JSON for APIs, ledgers, invoices, logs, and audit records.
|
|
13
|
+
- Render the same calculation differently for customers, finance teams, and internal tools.
|
|
14
|
+
- Preserve decimal inputs exactly by representing monetary and numeric values as strings.
|
|
15
|
+
- Attach domain-specific semantic labels without embedding presentation text.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
npm install calculation-breakdown
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Playground
|
|
24
|
+
|
|
25
|
+
The deployed playground is available at
|
|
26
|
+
[thismeansmore.github.io/calculation-breakdown/playground/](https://thismeansmore.github.io/calculation-breakdown/playground/).
|
|
27
|
+
|
|
28
|
+
Explore the builder, customize the formula rendering with CSS, and see the result in
|
|
29
|
+
a live preview:
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
npm run playground
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Then open `http://127.0.0.1:4173/playground/`. The playground runs entirely in
|
|
36
|
+
the browser against the package's built ESM output. Builder and CSS changes are
|
|
37
|
+
previewed automatically, both editors provide syntax coloring, and the locale
|
|
38
|
+
switch demonstrates English and Czech
|
|
39
|
+
human-readable output. The sample UI color-codes values and explains their
|
|
40
|
+
semantics with accessible hover and keyboard-focus tooltips, demonstrating how a product
|
|
41
|
+
can make a calculation trace approachable without changing its serialized data. Its
|
|
42
|
+
money renderer keeps ISO currency codes in metadata while using locale-aware currency
|
|
43
|
+
symbols and placement in the preview. The invoice scenario combines quantity-based
|
|
44
|
+
and duration-based lines, percentage
|
|
45
|
+
discounts, dynamically prorated annual insurance through December 31, subtotal,
|
|
46
|
+
tax, and the final amount in one calculation tree.
|
|
47
|
+
The in-card Table / Formula / Formula with names switch demonstrates an annotated
|
|
48
|
+
expression with optional inline semantic names and an automatically inferred invoice
|
|
49
|
+
view. In the named formula mode, tooltips contain only the longer descriptions. The
|
|
50
|
+
table formatter discovers semantic groups and results directly from the calculation,
|
|
51
|
+
uses dictionary names for column labels, and groups compatible shapes together. It
|
|
52
|
+
contains no invoice-specific content rules.
|
|
53
|
+
Mark selected groups with
|
|
54
|
+
`group(expression, { wrap: true, semantic: 'invoiceLine' })` to give visual
|
|
55
|
+
renderers an explicit line-wrapping hint and a localized group heading. The
|
|
56
|
+
playground honors this metadata.
|
|
57
|
+
|
|
58
|
+
GitHub Pages deploys the committed `docs/` directory from `main`. `npm run build`
|
|
59
|
+
regenerates it from the playground source and the local ESM build, so the deployed
|
|
60
|
+
demo always exercises the package code from the same commit.
|
|
61
|
+
|
|
62
|
+
## Releasing
|
|
63
|
+
|
|
64
|
+
Releases follow the same guarded release-branch workflow as `payload-vars`.
|
|
65
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for the exact prepare, review, merge,
|
|
66
|
+
publish, npm-integrity, and GitHub Pages verification steps.
|
|
67
|
+
|
|
68
|
+
## Usage
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
import {
|
|
72
|
+
CalculationBreakdownBuilder,
|
|
73
|
+
toHumanString,
|
|
74
|
+
} from 'calculation-breakdown';
|
|
75
|
+
|
|
76
|
+
type TSemantic =
|
|
77
|
+
| 'invoiceSubtotal'
|
|
78
|
+
| 'taxRate'
|
|
79
|
+
| 'invoiceTotal';
|
|
80
|
+
|
|
81
|
+
const builder = new CalculationBreakdownBuilder<TSemantic>();
|
|
82
|
+
|
|
83
|
+
const breakdown = builder
|
|
84
|
+
.breakdown(({ money, percent, add, multiply }) =>
|
|
85
|
+
add(
|
|
86
|
+
money('invoiceSubtotal', '125.00', 'USD'),
|
|
87
|
+
multiply(
|
|
88
|
+
money('invoiceSubtotal', '125.00', 'USD'),
|
|
89
|
+
percent('taxRate', '0.20'),
|
|
90
|
+
),
|
|
91
|
+
),
|
|
92
|
+
)
|
|
93
|
+
.result(({ money }) => money('invoiceTotal', '150.00', 'USD'))
|
|
94
|
+
.build();
|
|
95
|
+
|
|
96
|
+
const english = toHumanString(breakdown, {
|
|
97
|
+
locale: 'en',
|
|
98
|
+
includeNames: true,
|
|
99
|
+
dictionary: {
|
|
100
|
+
en: {
|
|
101
|
+
invoiceSubtotal: 'subtotal',
|
|
102
|
+
taxRate: {
|
|
103
|
+
name: 'tax rate',
|
|
104
|
+
description: 'The percentage applied to the subtotal.',
|
|
105
|
+
},
|
|
106
|
+
invoiceTotal: 'invoice total',
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
The resulting JSON describes:
|
|
113
|
+
|
|
114
|
+
```text
|
|
115
|
+
125.00 USD subtotal + (125.00 USD subtotal × 20% tax rate) = 150.00 USD invoice total
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Values use decimal strings so their exact representation survives JSON serialization. The generic semantic union prevents unknown domain keys during type checking. Consumers can map each value's `type` to formatting and its `semantic` to localized labels, ledger concepts, or reporting categories.
|
|
119
|
+
|
|
120
|
+
Set `includeNames: false` to render only formatted values. It defaults to `true`.
|
|
121
|
+
|
|
122
|
+
## Builders
|
|
123
|
+
|
|
124
|
+
- `money(semantic, amount, currency)`
|
|
125
|
+
- `duration(semantic, amount, unit, options?)`
|
|
126
|
+
- `number(semantic, value)`
|
|
127
|
+
- `percent(semantic, value)` for fractional values (`'0.20'` renders as `20%`)
|
|
128
|
+
- `constant(value)` for semantic-free numeric constants
|
|
129
|
+
- `group(expression, options?)` for explicit brackets and optional `wrap`,
|
|
130
|
+
`semantic`, authoritative grouped `result` metadata (for example, a line
|
|
131
|
+
subtotal), a semantic `role` (`detail` by default or `summary`), and an
|
|
132
|
+
`orientation` hint (`horizontal` by default or `vertical`)
|
|
133
|
+
- `operator(left, operator, right)`
|
|
134
|
+
- Helpers: `add`, `subtract`, `multiply`, `divide`, `minimum`, `maximum`
|
|
135
|
+
- Decimal.js-style aliases: `sub`, `mul`, `div`, `min`, `max`
|
|
136
|
+
|
|
137
|
+
For inferred tables, detail groups expose their semantic values as columns. A
|
|
138
|
+
`role: 'summary'` group renders as `group name | group result`. Formula renderers
|
|
139
|
+
still retain the complete expression; the role describes meaning rather than a
|
|
140
|
+
universal visual layout.
|
|
141
|
+
|
|
142
|
+
Detail groups default to `orientation: 'horizontal'`, allowing compatible groups
|
|
143
|
+
to share inferred columns. Use `orientation: 'vertical'` to retain every detail as
|
|
144
|
+
stacked `semantic name | value` rows. Orientation does not remove formula details.
|
|
145
|
+
|
|
146
|
+
Decimal.js-style names are also accepted by `operator`. They normalize to the
|
|
147
|
+
long-form names in the serialized output, so both naming styles produce the
|
|
148
|
+
same stable schema.
|
|
149
|
+
|
|
150
|
+
## Human-readable output
|
|
151
|
+
|
|
152
|
+
Use the exported `toHumanString` helper to render any plain breakdown, including
|
|
153
|
+
one restored from JSON. The builder exposes the same formatter as a convenience:
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
const czech = builder.toHumanString({
|
|
157
|
+
locale: 'cs',
|
|
158
|
+
dictionary: {
|
|
159
|
+
cs: {
|
|
160
|
+
invoiceSubtotal: 'mezisoučet',
|
|
161
|
+
taxRate: {
|
|
162
|
+
name: 'sazba daně',
|
|
163
|
+
description: 'Procentní sazba použitá pro výpočet daně.',
|
|
164
|
+
},
|
|
165
|
+
invoiceTotal: 'celkem',
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
English (`en`) and Czech (`cs`) are supported. The locale controls decimal
|
|
172
|
+
separators and duration-unit names; the dictionary groups domain-specific labels
|
|
173
|
+
by locale. Each semantic accepts a short string or `{ name, description }` for
|
|
174
|
+
richer UIs. Missing dictionary entries fall back to their semantic keys. Rendering
|
|
175
|
+
does not modify the calculation, and `build()` continues to return detached,
|
|
176
|
+
deeply frozen plain JSON data.
|
|
177
|
+
|
|
178
|
+
Duration rounding metadata accepts `rounding: 'up' | 'down' | 'nearest'` and a positive numeric `step`. For example, steps `0.1`, `1`, and `10` mean tenths, whole units, and multiples of ten.
|
|
179
|
+
|
|
180
|
+
The built output is detached and deeply frozen. It contains no localized text, HTML, CSS, colors, or executable calculation logic, making it suitable for transport and storage across financial applications.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { type ICalculationBreakdownHumanStringOptions } from './calculation-breakdown-human-string.js';
|
|
2
|
+
import type { ICalculationBreakdown, ICalculationBreakdownNodeBuilder } from './calculation-breakdown.interface.js';
|
|
3
|
+
import type { TCalculationBreakdownNode, TCalculationBreakdownValue } from './calculation-breakdown.type.js';
|
|
4
|
+
/**
|
|
5
|
+
* Records a typed, serializable breakdown of a calculation performed elsewhere.
|
|
6
|
+
*
|
|
7
|
+
* This builder does not evaluate operations or calculate the result. The calling
|
|
8
|
+
* application supplies every value, including the authoritative result.
|
|
9
|
+
*
|
|
10
|
+
* @typeParam Semantic - String union used to label the domain meaning of values.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* const breakdown = new CalculationBreakdownBuilder<'price' | 'total'>()
|
|
15
|
+
* .breakdown(({ money, constant, multiply }) =>
|
|
16
|
+
* multiply(money('price', '100.00', 'USD'), constant('0.9')),
|
|
17
|
+
* )
|
|
18
|
+
* .result(({ money }) => money('total', '90.00', 'USD'))
|
|
19
|
+
* .build();
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export declare class CalculationBreakdownBuilder<Semantic extends string> {
|
|
23
|
+
private breakdownValue?;
|
|
24
|
+
private resultValue?;
|
|
25
|
+
private readonly nodeBuilder;
|
|
26
|
+
/**
|
|
27
|
+
* Sets the root node that describes the calculation.
|
|
28
|
+
*
|
|
29
|
+
* @param create - Callback that describes the calculation using typed factories.
|
|
30
|
+
* @returns This builder for fluent chaining.
|
|
31
|
+
*/
|
|
32
|
+
breakdown(create: (builder: ICalculationBreakdownNodeBuilder<Semantic>) => TCalculationBreakdownNode<Semantic>): this;
|
|
33
|
+
/**
|
|
34
|
+
* Sets the authoritative result calculated by the calling application.
|
|
35
|
+
*
|
|
36
|
+
* @param create - Callback that constructs a money, duration, or number value.
|
|
37
|
+
* @returns This builder for fluent chaining.
|
|
38
|
+
*/
|
|
39
|
+
result(create: (builder: ICalculationBreakdownNodeBuilder<Semantic>) => TCalculationBreakdownValue<Semantic>): this;
|
|
40
|
+
/**
|
|
41
|
+
* Produces a detached, deeply frozen calculation breakdown.
|
|
42
|
+
*
|
|
43
|
+
* @throws {CalculationBreakdownError} If either the breakdown or result is missing.
|
|
44
|
+
*/
|
|
45
|
+
build(): ICalculationBreakdown<Semantic>;
|
|
46
|
+
/** Builds and renders the calculation using localized semantic labels. */
|
|
47
|
+
toHumanString(options: ICalculationBreakdownHumanStringOptions<Semantic>): string;
|
|
48
|
+
private createMoney;
|
|
49
|
+
private createDuration;
|
|
50
|
+
private createNumber;
|
|
51
|
+
private createPercent;
|
|
52
|
+
private createConstant;
|
|
53
|
+
private createGroup;
|
|
54
|
+
private createOperation;
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=calculation-breakdown-builder.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"calculation-breakdown-builder.d.ts","sourceRoot":"","sources":["../src/calculation-breakdown-builder.ts"],"names":[],"mappings":"AACA,OAAO,EAEH,KAAK,uCAAuC,EAC/C,MAAM,yCAAyC,CAAC;AACjD,OAAO,KAAK,EACR,qBAAqB,EAIrB,gCAAgC,EAOnC,MAAM,sCAAsC,CAAC;AAC9C,OAAO,KAAK,EAIR,yBAAyB,EAIzB,0BAA0B,EAC7B,MAAM,iCAAiC,CAAC;AA6GzC;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,2BAA2B,CAAC,QAAQ,SAAS,MAAM;IAC5D,OAAO,CAAC,cAAc,CAAC,CAAsC;IAC7D,OAAO,CAAC,WAAW,CAAC,CAAuC;IAE3D,OAAO,CAAC,QAAQ,CAAC,WAAW,CA4BtB;IAEN;;;;;OAKG;IACH,SAAS,CACL,MAAM,EAAE,CACJ,OAAO,EAAE,gCAAgC,CAAC,QAAQ,CAAC,KAClD,yBAAyB,CAAC,QAAQ,CAAC,GACzC,IAAI;IAKP;;;;;OAKG;IACH,MAAM,CACF,MAAM,EAAE,CACJ,OAAO,EAAE,gCAAgC,CAAC,QAAQ,CAAC,KAClD,0BAA0B,CAAC,QAAQ,CAAC,GAC1C,IAAI;IAKP;;;;OAIG;IACH,KAAK,IAAI,qBAAqB,CAAC,QAAQ,CAAC;IAsBxC,0EAA0E;IAC1E,aAAa,CACT,OAAO,EAAE,uCAAuC,CAAC,QAAQ,CAAC,GAC3D,MAAM;IAIT,OAAO,CAAC,WAAW;IAgBnB,OAAO,CAAC,cAAc;IA0BtB,OAAO,CAAC,YAAY;IASpB,OAAO,CAAC,aAAa;IASrB,OAAO,CAAC,cAAc;IAKtB,OAAO,CAAC,WAAW;IA8CnB,OAAO,CAAC,eAAe;CAqB1B"}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { CalculationBreakdownError } from './calculation-breakdown.error.js';
|
|
2
|
+
import { toHumanString, } from './calculation-breakdown-human-string.js';
|
|
3
|
+
const DECIMAL_PATTERN = /^-?(?:0|[1-9]\d*)(?:\.\d+)?$/;
|
|
4
|
+
const CURRENCY_PATTERN = /^[A-Z]{3}$/;
|
|
5
|
+
const DURATION_UNITS = new Set([
|
|
6
|
+
'milliseconds',
|
|
7
|
+
'seconds',
|
|
8
|
+
'minutes',
|
|
9
|
+
'hours',
|
|
10
|
+
'days',
|
|
11
|
+
]);
|
|
12
|
+
const ROUNDING_MODES = new Set([
|
|
13
|
+
'up',
|
|
14
|
+
'down',
|
|
15
|
+
'nearest',
|
|
16
|
+
]);
|
|
17
|
+
const GROUP_ROLES = new Set([
|
|
18
|
+
'detail',
|
|
19
|
+
'summary',
|
|
20
|
+
]);
|
|
21
|
+
const GROUP_ORIENTATIONS = new Set([
|
|
22
|
+
'horizontal',
|
|
23
|
+
'vertical',
|
|
24
|
+
]);
|
|
25
|
+
const OPERATORS = new Set([
|
|
26
|
+
'add',
|
|
27
|
+
'subtract',
|
|
28
|
+
'multiply',
|
|
29
|
+
'divide',
|
|
30
|
+
'minimum',
|
|
31
|
+
'maximum',
|
|
32
|
+
]);
|
|
33
|
+
const OPERATOR_ALIASES = {
|
|
34
|
+
sub: 'subtract',
|
|
35
|
+
mul: 'multiply',
|
|
36
|
+
div: 'divide',
|
|
37
|
+
min: 'minimum',
|
|
38
|
+
max: 'maximum',
|
|
39
|
+
};
|
|
40
|
+
function assertSemantic(semantic) {
|
|
41
|
+
if (semantic.trim().length === 0) {
|
|
42
|
+
throw new CalculationBreakdownError('INVALID_SEMANTIC', 'Semantic must be a non-empty string.');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function assertDecimal(value) {
|
|
46
|
+
if (!DECIMAL_PATTERN.test(value)) {
|
|
47
|
+
throw new CalculationBreakdownError('INVALID_DECIMAL', `Invalid decimal string: ${value}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function assertDurationOptions(options) {
|
|
51
|
+
if (!options)
|
|
52
|
+
return;
|
|
53
|
+
const hasRounding = options.rounding !== undefined;
|
|
54
|
+
const hasStep = options.step !== undefined;
|
|
55
|
+
const step = options.step;
|
|
56
|
+
if (hasRounding &&
|
|
57
|
+
!ROUNDING_MODES.has(options.rounding)) {
|
|
58
|
+
throw new CalculationBreakdownError('INVALID_ROUNDING', `Invalid rounding mode: ${String(options.rounding)}`);
|
|
59
|
+
}
|
|
60
|
+
if (hasRounding !== hasStep) {
|
|
61
|
+
throw new CalculationBreakdownError('INVALID_ROUNDING_STEP', 'Rounding and step must be provided together.');
|
|
62
|
+
}
|
|
63
|
+
if (hasStep &&
|
|
64
|
+
(step === undefined || !Number.isFinite(step) || step <= 0)) {
|
|
65
|
+
throw new CalculationBreakdownError('INVALID_ROUNDING_STEP', 'Rounding step must be a positive finite number.');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function deepFreeze(value) {
|
|
69
|
+
if (typeof value !== 'object' || value === null || Object.isFrozen(value)) {
|
|
70
|
+
return value;
|
|
71
|
+
}
|
|
72
|
+
Object.freeze(value);
|
|
73
|
+
for (const child of Object.values(value))
|
|
74
|
+
deepFreeze(child);
|
|
75
|
+
return value;
|
|
76
|
+
}
|
|
77
|
+
function cloneJson(value) {
|
|
78
|
+
return JSON.parse(JSON.stringify(value));
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Records a typed, serializable breakdown of a calculation performed elsewhere.
|
|
82
|
+
*
|
|
83
|
+
* This builder does not evaluate operations or calculate the result. The calling
|
|
84
|
+
* application supplies every value, including the authoritative result.
|
|
85
|
+
*
|
|
86
|
+
* @typeParam Semantic - String union used to label the domain meaning of values.
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* ```ts
|
|
90
|
+
* const breakdown = new CalculationBreakdownBuilder<'price' | 'total'>()
|
|
91
|
+
* .breakdown(({ money, constant, multiply }) =>
|
|
92
|
+
* multiply(money('price', '100.00', 'USD'), constant('0.9')),
|
|
93
|
+
* )
|
|
94
|
+
* .result(({ money }) => money('total', '90.00', 'USD'))
|
|
95
|
+
* .build();
|
|
96
|
+
* ```
|
|
97
|
+
*/
|
|
98
|
+
export class CalculationBreakdownBuilder {
|
|
99
|
+
breakdownValue;
|
|
100
|
+
resultValue;
|
|
101
|
+
nodeBuilder = {
|
|
102
|
+
money: (semantic, amount, currency) => this.createMoney(semantic, amount, currency),
|
|
103
|
+
duration: (semantic, amount, unit, options) => this.createDuration(semantic, amount, unit, options),
|
|
104
|
+
number: (semantic, value) => this.createNumber(semantic, value),
|
|
105
|
+
percent: (semantic, value) => this.createPercent(semantic, value),
|
|
106
|
+
constant: (value) => this.createConstant(value),
|
|
107
|
+
group: (expression, options) => this.createGroup(expression, options),
|
|
108
|
+
operator: (left, operator, right) => this.createOperation(left, operator, right),
|
|
109
|
+
add: (left, right) => this.createOperation(left, 'add', right),
|
|
110
|
+
subtract: (left, right) => this.createOperation(left, 'subtract', right),
|
|
111
|
+
sub: (left, right) => this.createOperation(left, 'sub', right),
|
|
112
|
+
multiply: (left, right) => this.createOperation(left, 'multiply', right),
|
|
113
|
+
mul: (left, right) => this.createOperation(left, 'mul', right),
|
|
114
|
+
divide: (left, right) => this.createOperation(left, 'divide', right),
|
|
115
|
+
div: (left, right) => this.createOperation(left, 'div', right),
|
|
116
|
+
minimum: (left, right) => this.createOperation(left, 'minimum', right),
|
|
117
|
+
min: (left, right) => this.createOperation(left, 'min', right),
|
|
118
|
+
maximum: (left, right) => this.createOperation(left, 'maximum', right),
|
|
119
|
+
max: (left, right) => this.createOperation(left, 'max', right),
|
|
120
|
+
};
|
|
121
|
+
/**
|
|
122
|
+
* Sets the root node that describes the calculation.
|
|
123
|
+
*
|
|
124
|
+
* @param create - Callback that describes the calculation using typed factories.
|
|
125
|
+
* @returns This builder for fluent chaining.
|
|
126
|
+
*/
|
|
127
|
+
breakdown(create) {
|
|
128
|
+
this.breakdownValue = create(this.nodeBuilder);
|
|
129
|
+
return this;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Sets the authoritative result calculated by the calling application.
|
|
133
|
+
*
|
|
134
|
+
* @param create - Callback that constructs a money, duration, or number value.
|
|
135
|
+
* @returns This builder for fluent chaining.
|
|
136
|
+
*/
|
|
137
|
+
result(create) {
|
|
138
|
+
this.resultValue = create(this.nodeBuilder);
|
|
139
|
+
return this;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Produces a detached, deeply frozen calculation breakdown.
|
|
143
|
+
*
|
|
144
|
+
* @throws {CalculationBreakdownError} If either the breakdown or result is missing.
|
|
145
|
+
*/
|
|
146
|
+
build() {
|
|
147
|
+
if (!this.breakdownValue) {
|
|
148
|
+
throw new CalculationBreakdownError('MISSING_BREAKDOWN', 'A breakdown is required before building.');
|
|
149
|
+
}
|
|
150
|
+
if (!this.resultValue) {
|
|
151
|
+
throw new CalculationBreakdownError('MISSING_RESULT', 'A result is required before building.');
|
|
152
|
+
}
|
|
153
|
+
return deepFreeze(cloneJson({
|
|
154
|
+
breakdown: this.breakdownValue,
|
|
155
|
+
result: this.resultValue,
|
|
156
|
+
}));
|
|
157
|
+
}
|
|
158
|
+
/** Builds and renders the calculation using localized semantic labels. */
|
|
159
|
+
toHumanString(options) {
|
|
160
|
+
return toHumanString(this.build(), options);
|
|
161
|
+
}
|
|
162
|
+
createMoney(semantic, amount, currency) {
|
|
163
|
+
assertSemantic(semantic);
|
|
164
|
+
assertDecimal(amount);
|
|
165
|
+
if (!CURRENCY_PATTERN.test(currency)) {
|
|
166
|
+
throw new CalculationBreakdownError('INVALID_CURRENCY', `Currency must be a three-letter uppercase code: ${currency}`);
|
|
167
|
+
}
|
|
168
|
+
return deepFreeze({ type: 'money', semantic, amount, currency });
|
|
169
|
+
}
|
|
170
|
+
createDuration(semantic, amount, unit, options) {
|
|
171
|
+
assertSemantic(semantic);
|
|
172
|
+
assertDecimal(amount);
|
|
173
|
+
if (!DURATION_UNITS.has(unit)) {
|
|
174
|
+
throw new CalculationBreakdownError('INVALID_DURATION_UNIT', `Unsupported duration unit: ${unit}`);
|
|
175
|
+
}
|
|
176
|
+
assertDurationOptions(options);
|
|
177
|
+
return deepFreeze({
|
|
178
|
+
type: 'duration',
|
|
179
|
+
semantic,
|
|
180
|
+
amount,
|
|
181
|
+
unit,
|
|
182
|
+
...(options?.rounding ? { rounding: options.rounding } : {}),
|
|
183
|
+
...(options?.step !== undefined ? { step: options.step } : {}),
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
createNumber(semantic, value) {
|
|
187
|
+
assertSemantic(semantic);
|
|
188
|
+
assertDecimal(value);
|
|
189
|
+
return deepFreeze({ type: 'number', semantic, value });
|
|
190
|
+
}
|
|
191
|
+
createPercent(semantic, value) {
|
|
192
|
+
assertSemantic(semantic);
|
|
193
|
+
assertDecimal(value);
|
|
194
|
+
return deepFreeze({ type: 'percent', semantic, value });
|
|
195
|
+
}
|
|
196
|
+
createConstant(value) {
|
|
197
|
+
assertDecimal(value);
|
|
198
|
+
return deepFreeze({ type: 'constant', value });
|
|
199
|
+
}
|
|
200
|
+
createGroup(expression, options) {
|
|
201
|
+
if (options?.semantic !== undefined) {
|
|
202
|
+
assertSemantic(options.semantic);
|
|
203
|
+
}
|
|
204
|
+
if (options?.role !== undefined &&
|
|
205
|
+
!GROUP_ROLES.has(options.role)) {
|
|
206
|
+
throw new CalculationBreakdownError('INVALID_GROUP_ROLE', `Unsupported group role: ${String(options.role)}`);
|
|
207
|
+
}
|
|
208
|
+
if (options?.orientation !== undefined &&
|
|
209
|
+
!GROUP_ORIENTATIONS.has(options.orientation)) {
|
|
210
|
+
throw new CalculationBreakdownError('INVALID_GROUP_ORIENTATION', `Unsupported group orientation: ${String(options.orientation)}`);
|
|
211
|
+
}
|
|
212
|
+
return deepFreeze({
|
|
213
|
+
type: 'group',
|
|
214
|
+
expression,
|
|
215
|
+
...(options?.wrap === true ? { wrap: true } : {}),
|
|
216
|
+
...(options?.semantic !== undefined
|
|
217
|
+
? { semantic: options.semantic }
|
|
218
|
+
: {}),
|
|
219
|
+
...(options?.result !== undefined
|
|
220
|
+
? { result: options.result }
|
|
221
|
+
: {}),
|
|
222
|
+
...(options?.role !== undefined
|
|
223
|
+
? { role: options.role }
|
|
224
|
+
: {}),
|
|
225
|
+
...(options?.orientation !== undefined
|
|
226
|
+
? { orientation: options.orientation }
|
|
227
|
+
: {}),
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
createOperation(left, operator, right) {
|
|
231
|
+
const normalizedOperator = OPERATOR_ALIASES[operator] ?? operator;
|
|
232
|
+
if (!OPERATORS.has(normalizedOperator)) {
|
|
233
|
+
throw new CalculationBreakdownError('INVALID_OPERATOR', `Unsupported operator: ${operator}`);
|
|
234
|
+
}
|
|
235
|
+
return deepFreeze({
|
|
236
|
+
type: 'operation',
|
|
237
|
+
operator: normalizedOperator,
|
|
238
|
+
operands: [left, right],
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
//# sourceMappingURL=calculation-breakdown-builder.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"calculation-breakdown-builder.js","sourceRoot":"","sources":["../src/calculation-breakdown-builder.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ICalculationBreakdown } from './calculation-breakdown.interface.js';
|
|
2
|
+
/** Locales supported by the built-in human-readable renderer. */
|
|
3
|
+
export type TCalculationBreakdownLocale = 'en' | 'cs';
|
|
4
|
+
/** A semantic label with optional explanatory copy for richer UIs. */
|
|
5
|
+
export interface ICalculationBreakdownDictionaryEntry {
|
|
6
|
+
readonly name: string;
|
|
7
|
+
readonly description: string;
|
|
8
|
+
}
|
|
9
|
+
/** A short label or a label with explanatory copy. */
|
|
10
|
+
export type TCalculationBreakdownDictionaryEntry = string | ICalculationBreakdownDictionaryEntry;
|
|
11
|
+
/** Semantic labels grouped by supported locale. */
|
|
12
|
+
export type TCalculationBreakdownDictionary<Semantic extends string> = Partial<Record<TCalculationBreakdownLocale, Partial<Record<Semantic, TCalculationBreakdownDictionaryEntry>>>>;
|
|
13
|
+
/** Options for rendering a calculation breakdown as human-readable text. */
|
|
14
|
+
export interface ICalculationBreakdownHumanStringOptions<Semantic extends string> {
|
|
15
|
+
/** Language and decimal separator used by the renderer. */
|
|
16
|
+
readonly locale: TCalculationBreakdownLocale;
|
|
17
|
+
/** Localized labels for domain semantics. Missing labels use the semantic key. */
|
|
18
|
+
readonly dictionary: TCalculationBreakdownDictionary<Semantic>;
|
|
19
|
+
/** Whether semantic names are appended to values. Defaults to true. */
|
|
20
|
+
readonly includeNames?: boolean;
|
|
21
|
+
}
|
|
22
|
+
/** Renders a plain calculation breakdown without changing its serialized form. */
|
|
23
|
+
export declare function toHumanString<Semantic extends string>(calculation: ICalculationBreakdown<Semantic>, options: ICalculationBreakdownHumanStringOptions<Semantic>): string;
|
|
24
|
+
//# sourceMappingURL=calculation-breakdown-human-string.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"calculation-breakdown-human-string.d.ts","sourceRoot":"","sources":["../src/calculation-breakdown-human-string.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,sCAAsC,CAAC;AAOlF,iEAAiE;AACjE,MAAM,MAAM,2BAA2B,GAAG,IAAI,GAAG,IAAI,CAAC;AAEtD,sEAAsE;AACtE,MAAM,WAAW,oCAAoC;IACjD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAChC;AAED,sDAAsD;AACtD,MAAM,MAAM,oCAAoC,GAC1C,MAAM,GACN,oCAAoC,CAAC;AAE3C,mDAAmD;AACnD,MAAM,MAAM,+BAA+B,CAAC,QAAQ,SAAS,MAAM,IAAI,OAAO,CAC1E,MAAM,CACF,2BAA2B,EAC3B,OAAO,CAAC,MAAM,CAAC,QAAQ,EAAE,oCAAoC,CAAC,CAAC,CAClE,CACJ,CAAC;AAEF,4EAA4E;AAC5E,MAAM,WAAW,uCAAuC,CACpD,QAAQ,SAAS,MAAM;IAEvB,2DAA2D;IAC3D,QAAQ,CAAC,MAAM,EAAE,2BAA2B,CAAC;IAC7C,kFAAkF;IAClF,QAAQ,CAAC,UAAU,EAAE,+BAA+B,CAAC,QAAQ,CAAC,CAAC;IAC/D,uEAAuE;IACvE,QAAQ,CAAC,YAAY,CAAC,EAAE,OAAO,CAAC;CACnC;AAiID,kFAAkF;AAClF,wBAAgB,aAAa,CAAC,QAAQ,SAAS,MAAM,EACjD,WAAW,EAAE,qBAAqB,CAAC,QAAQ,CAAC,EAC5C,OAAO,EAAE,uCAAuC,CAAC,QAAQ,CAAC,GAC3D,MAAM,CAER"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
const INFIX_OPERATORS = {
|
|
2
|
+
add: '+',
|
|
3
|
+
subtract: '−',
|
|
4
|
+
multiply: '×',
|
|
5
|
+
divide: '÷',
|
|
6
|
+
};
|
|
7
|
+
function localizeDecimal(value, locale) {
|
|
8
|
+
return locale === 'cs' ? value.replace('.', ',') : value;
|
|
9
|
+
}
|
|
10
|
+
function decimalToPercent(value) {
|
|
11
|
+
const negative = value.startsWith('-');
|
|
12
|
+
const unsigned = negative ? value.slice(1) : value;
|
|
13
|
+
const [whole = '0', fraction = ''] = unsigned.split('.');
|
|
14
|
+
const digits = `${whole}${fraction}`;
|
|
15
|
+
const decimalIndex = whole.length + 2;
|
|
16
|
+
const padded = digits.padEnd(decimalIndex, '0');
|
|
17
|
+
const integer = padded
|
|
18
|
+
.slice(0, decimalIndex)
|
|
19
|
+
.replace(/^0+(?=\d)/, '');
|
|
20
|
+
const decimals = padded.slice(decimalIndex).replace(/0+$/, '');
|
|
21
|
+
return `${negative ? '-' : ''}${integer}${decimals ? `.${decimals}` : ''}`;
|
|
22
|
+
}
|
|
23
|
+
function localizeDurationUnit(amount, unit, locale) {
|
|
24
|
+
const singularUnit = unit.slice(0, -1);
|
|
25
|
+
const unitPart = new Intl.NumberFormat(locale, {
|
|
26
|
+
style: 'unit',
|
|
27
|
+
unit: singularUnit,
|
|
28
|
+
unitDisplay: 'long',
|
|
29
|
+
useGrouping: false,
|
|
30
|
+
})
|
|
31
|
+
.formatToParts(Number(amount))
|
|
32
|
+
.find((part) => part.type === 'unit');
|
|
33
|
+
if (!unitPart) {
|
|
34
|
+
throw new Error(`Unable to localize duration unit: ${unit}`);
|
|
35
|
+
}
|
|
36
|
+
return unitPart.value;
|
|
37
|
+
}
|
|
38
|
+
function labelValue(value, semantic, options) {
|
|
39
|
+
if (options.includeNames === false)
|
|
40
|
+
return value;
|
|
41
|
+
const entry = options.dictionary[options.locale]?.[semantic];
|
|
42
|
+
const label = typeof entry === 'string' ? entry : (entry?.name ?? semantic);
|
|
43
|
+
return `${value} ${label}`;
|
|
44
|
+
}
|
|
45
|
+
function renderValue(value, options) {
|
|
46
|
+
const rawDecimal = value.type === 'number' || value.type === 'percent'
|
|
47
|
+
? value.value
|
|
48
|
+
: value.amount;
|
|
49
|
+
const decimal = localizeDecimal(rawDecimal, options.locale);
|
|
50
|
+
if (value.type === 'money') {
|
|
51
|
+
return labelValue(`${decimal} ${value.currency}`, value.semantic, options);
|
|
52
|
+
}
|
|
53
|
+
if (value.type === 'duration') {
|
|
54
|
+
return labelValue(`${decimal} ${localizeDurationUnit(value.amount, value.unit, options.locale)}`, value.semantic, options);
|
|
55
|
+
}
|
|
56
|
+
if (value.type === 'percent') {
|
|
57
|
+
return labelValue(`${localizeDecimal(decimalToPercent(value.value), options.locale)}%`, value.semantic, options);
|
|
58
|
+
}
|
|
59
|
+
return labelValue(decimal, value.semantic, options);
|
|
60
|
+
}
|
|
61
|
+
function renderNode(node, options, nested = false) {
|
|
62
|
+
if (node.type === 'constant') {
|
|
63
|
+
return localizeDecimal(node.value, options.locale);
|
|
64
|
+
}
|
|
65
|
+
if (node.type === 'group') {
|
|
66
|
+
const result = node.result
|
|
67
|
+
? ` = ${renderValue(node.result, options)}`
|
|
68
|
+
: '';
|
|
69
|
+
return `(${renderNode(node.expression, options)}${result})`;
|
|
70
|
+
}
|
|
71
|
+
if (node.type !== 'operation') {
|
|
72
|
+
return renderValue(node, options);
|
|
73
|
+
}
|
|
74
|
+
const [left, right] = node.operands;
|
|
75
|
+
if (node.operator === 'minimum' || node.operator === 'maximum') {
|
|
76
|
+
const functionName = node.operator === 'minimum' ? 'min' : 'max';
|
|
77
|
+
return `${functionName}(${renderNode(left, options)}, ${renderNode(right, options)})`;
|
|
78
|
+
}
|
|
79
|
+
const rendered = `${renderNode(left, options, true)} ${INFIX_OPERATORS[node.operator]} ${renderNode(right, options, true)}`;
|
|
80
|
+
return nested ? `(${rendered})` : rendered;
|
|
81
|
+
}
|
|
82
|
+
/** Renders a plain calculation breakdown without changing its serialized form. */
|
|
83
|
+
export function toHumanString(calculation, options) {
|
|
84
|
+
return `${renderNode(calculation.breakdown, options)} = ${renderValue(calculation.result, options)}`;
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=calculation-breakdown-human-string.js.map
|