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.
Files changed (55) hide show
  1. package/CHANGELOG-ARCHIVE.md +3 -0
  2. package/CHANGELOG.md +19 -0
  3. package/LICENSE +21 -0
  4. package/README.md +179 -2
  5. package/dist/calculation-breakdown-builder.d.ts +56 -0
  6. package/dist/calculation-breakdown-builder.d.ts.map +1 -0
  7. package/dist/calculation-breakdown-builder.js +242 -0
  8. package/dist/calculation-breakdown-builder.js.map +1 -0
  9. package/dist/calculation-breakdown-human-string.d.ts +24 -0
  10. package/dist/calculation-breakdown-human-string.d.ts.map +1 -0
  11. package/dist/calculation-breakdown-human-string.js +86 -0
  12. package/dist/calculation-breakdown-human-string.js.map +1 -0
  13. package/dist/calculation-breakdown.error.d.ts +14 -0
  14. package/dist/calculation-breakdown.error.d.ts.map +1 -0
  15. package/dist/calculation-breakdown.error.js +16 -0
  16. package/dist/calculation-breakdown.error.js.map +1 -0
  17. package/dist/calculation-breakdown.interface.d.ts +134 -0
  18. package/dist/calculation-breakdown.interface.d.ts.map +1 -0
  19. package/dist/calculation-breakdown.interface.js +2 -0
  20. package/dist/calculation-breakdown.interface.js.map +1 -0
  21. package/dist/calculation-breakdown.type.d.ts +20 -0
  22. package/dist/calculation-breakdown.type.d.ts.map +1 -0
  23. package/dist/calculation-breakdown.type.js +2 -0
  24. package/dist/calculation-breakdown.type.js.map +1 -0
  25. package/dist/cjs/calculation-breakdown-builder.d.ts +55 -0
  26. package/dist/cjs/calculation-breakdown-builder.js +245 -0
  27. package/dist/cjs/calculation-breakdown-human-string.d.ts +23 -0
  28. package/dist/cjs/calculation-breakdown-human-string.js +88 -0
  29. package/dist/cjs/calculation-breakdown.error.d.ts +13 -0
  30. package/dist/cjs/calculation-breakdown.error.js +19 -0
  31. package/dist/cjs/calculation-breakdown.interface.d.ts +133 -0
  32. package/dist/cjs/calculation-breakdown.interface.js +2 -0
  33. package/dist/cjs/calculation-breakdown.type.d.ts +19 -0
  34. package/dist/cjs/calculation-breakdown.type.js +2 -0
  35. package/dist/cjs/index.d.ts +5 -0
  36. package/dist/cjs/index.js +9 -0
  37. package/dist/cjs/package.json +3 -0
  38. package/dist/index.d.ts +6 -0
  39. package/dist/index.d.ts.map +1 -0
  40. package/dist/index.js +4 -0
  41. package/dist/index.js.map +1 -0
  42. package/docs/_config.yml +19 -0
  43. package/docs/assets/calculation-breakdown/calculation-breakdown-builder.js +242 -0
  44. package/docs/assets/calculation-breakdown/calculation-breakdown-human-string.js +86 -0
  45. package/docs/assets/calculation-breakdown/calculation-breakdown.error.js +16 -0
  46. package/docs/assets/calculation-breakdown/calculation-breakdown.interface.js +2 -0
  47. package/docs/assets/calculation-breakdown/calculation-breakdown.type.js +2 -0
  48. package/docs/assets/calculation-breakdown/index.js +4 -0
  49. package/docs/changelog-archive.md +9 -0
  50. package/docs/changelog.md +25 -0
  51. package/docs/index.md +13 -0
  52. package/docs/playground/index.html +110 -0
  53. package/docs/playground/playground.css +407 -0
  54. package/docs/playground/playground.js +1303 -0
  55. package/package.json +66 -4
@@ -0,0 +1,88 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.toHumanString = toHumanString;
4
+ const INFIX_OPERATORS = {
5
+ add: '+',
6
+ subtract: '−',
7
+ multiply: '×',
8
+ divide: '÷',
9
+ };
10
+ function localizeDecimal(value, locale) {
11
+ return locale === 'cs' ? value.replace('.', ',') : value;
12
+ }
13
+ function decimalToPercent(value) {
14
+ const negative = value.startsWith('-');
15
+ const unsigned = negative ? value.slice(1) : value;
16
+ const [whole = '0', fraction = ''] = unsigned.split('.');
17
+ const digits = `${whole}${fraction}`;
18
+ const decimalIndex = whole.length + 2;
19
+ const padded = digits.padEnd(decimalIndex, '0');
20
+ const integer = padded
21
+ .slice(0, decimalIndex)
22
+ .replace(/^0+(?=\d)/, '');
23
+ const decimals = padded.slice(decimalIndex).replace(/0+$/, '');
24
+ return `${negative ? '-' : ''}${integer}${decimals ? `.${decimals}` : ''}`;
25
+ }
26
+ function localizeDurationUnit(amount, unit, locale) {
27
+ const singularUnit = unit.slice(0, -1);
28
+ const unitPart = new Intl.NumberFormat(locale, {
29
+ style: 'unit',
30
+ unit: singularUnit,
31
+ unitDisplay: 'long',
32
+ useGrouping: false,
33
+ })
34
+ .formatToParts(Number(amount))
35
+ .find((part) => part.type === 'unit');
36
+ if (!unitPart) {
37
+ throw new Error(`Unable to localize duration unit: ${unit}`);
38
+ }
39
+ return unitPart.value;
40
+ }
41
+ function labelValue(value, semantic, options) {
42
+ if (options.includeNames === false)
43
+ return value;
44
+ const entry = options.dictionary[options.locale]?.[semantic];
45
+ const label = typeof entry === 'string' ? entry : (entry?.name ?? semantic);
46
+ return `${value} ${label}`;
47
+ }
48
+ function renderValue(value, options) {
49
+ const rawDecimal = value.type === 'number' || value.type === 'percent'
50
+ ? value.value
51
+ : value.amount;
52
+ const decimal = localizeDecimal(rawDecimal, options.locale);
53
+ if (value.type === 'money') {
54
+ return labelValue(`${decimal} ${value.currency}`, value.semantic, options);
55
+ }
56
+ if (value.type === 'duration') {
57
+ return labelValue(`${decimal} ${localizeDurationUnit(value.amount, value.unit, options.locale)}`, value.semantic, options);
58
+ }
59
+ if (value.type === 'percent') {
60
+ return labelValue(`${localizeDecimal(decimalToPercent(value.value), options.locale)}%`, value.semantic, options);
61
+ }
62
+ return labelValue(decimal, value.semantic, options);
63
+ }
64
+ function renderNode(node, options, nested = false) {
65
+ if (node.type === 'constant') {
66
+ return localizeDecimal(node.value, options.locale);
67
+ }
68
+ if (node.type === 'group') {
69
+ const result = node.result
70
+ ? ` = ${renderValue(node.result, options)}`
71
+ : '';
72
+ return `(${renderNode(node.expression, options)}${result})`;
73
+ }
74
+ if (node.type !== 'operation') {
75
+ return renderValue(node, options);
76
+ }
77
+ const [left, right] = node.operands;
78
+ if (node.operator === 'minimum' || node.operator === 'maximum') {
79
+ const functionName = node.operator === 'minimum' ? 'min' : 'max';
80
+ return `${functionName}(${renderNode(left, options)}, ${renderNode(right, options)})`;
81
+ }
82
+ const rendered = `${renderNode(left, options, true)} ${INFIX_OPERATORS[node.operator]} ${renderNode(right, options, true)}`;
83
+ return nested ? `(${rendered})` : rendered;
84
+ }
85
+ /** Renders a plain calculation breakdown without changing its serialized form. */
86
+ function toHumanString(calculation, options) {
87
+ return `${renderNode(calculation.breakdown, options)} = ${renderValue(calculation.result, options)}`;
88
+ }
@@ -0,0 +1,13 @@
1
+ /** Machine-readable codes emitted when breakdown construction fails. */
2
+ export type TCalculationBreakdownErrorCode = 'INVALID_SEMANTIC' | 'INVALID_DECIMAL' | 'INVALID_CURRENCY' | 'INVALID_DURATION_UNIT' | 'INVALID_OPERATOR' | 'INVALID_ROUNDING' | 'INVALID_ROUNDING_STEP' | 'INVALID_GROUP_ROLE' | 'INVALID_GROUP_ORIENTATION' | 'MISSING_BREAKDOWN' | 'MISSING_RESULT';
3
+ /** Error thrown when a calculation breakdown contains invalid or incomplete input. */
4
+ export declare class CalculationBreakdownError extends Error {
5
+ readonly code: TCalculationBreakdownErrorCode;
6
+ /**
7
+ * Creates a calculation breakdown error.
8
+ *
9
+ * @param code - Stable code suitable for programmatic error handling.
10
+ * @param message - Human-readable explanation of the failure.
11
+ */
12
+ constructor(code: TCalculationBreakdownErrorCode, message: string);
13
+ }
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CalculationBreakdownError = void 0;
4
+ /** Error thrown when a calculation breakdown contains invalid or incomplete input. */
5
+ class CalculationBreakdownError extends Error {
6
+ code;
7
+ /**
8
+ * Creates a calculation breakdown error.
9
+ *
10
+ * @param code - Stable code suitable for programmatic error handling.
11
+ * @param message - Human-readable explanation of the failure.
12
+ */
13
+ constructor(code, message) {
14
+ super(message);
15
+ this.code = code;
16
+ this.name = 'CalculationBreakdownError';
17
+ }
18
+ }
19
+ exports.CalculationBreakdownError = CalculationBreakdownError;
@@ -0,0 +1,133 @@
1
+ import type { TCalculationBreakdownDurationUnit, TCalculationBreakdownGroupRole, TCalculationBreakdownGroupOrientation, TCalculationBreakdownNode, TCalculationBreakdownOperator, TCalculationBreakdownOperatorInput, TCalculationBreakdownRounding, TCalculationBreakdownValue } from './calculation-breakdown.type.js';
2
+ /** Optional rounding rules for a duration value. */
3
+ export interface ICalculationBreakdownDurationOptions {
4
+ /** Direction in which the duration is rounded. Must be used with `step`. */
5
+ rounding?: TCalculationBreakdownRounding;
6
+ /** Positive interval to which the duration is rounded. Must be used with `rounding`. */
7
+ step?: number;
8
+ }
9
+ /** A monetary value represented by an exact decimal string and currency code. */
10
+ export interface ICalculationBreakdownMoneyValue<Semantic extends string> {
11
+ /** Discriminator for a monetary value. */
12
+ readonly type: 'money';
13
+ /** Domain-specific meaning of the value. */
14
+ readonly semantic: Semantic;
15
+ /** Exact decimal amount. */
16
+ readonly amount: string;
17
+ /** Three-letter uppercase currency code, such as `USD`. */
18
+ readonly currency: string;
19
+ }
20
+ /** A duration represented by an exact decimal string and unit. */
21
+ export interface ICalculationBreakdownDurationValue<Semantic extends string> extends ICalculationBreakdownDurationOptions {
22
+ /** Discriminator for a duration value. */
23
+ readonly type: 'duration';
24
+ /** Domain-specific meaning of the value. */
25
+ readonly semantic: Semantic;
26
+ /** Exact decimal duration. */
27
+ readonly amount: string;
28
+ /** Unit in which the duration is expressed. */
29
+ readonly unit: TCalculationBreakdownDurationUnit;
30
+ }
31
+ /** A dimensionless semantic number represented by an exact decimal string. */
32
+ export interface ICalculationBreakdownNumberValue<Semantic extends string> {
33
+ /** Discriminator for a semantic number. */
34
+ readonly type: 'number';
35
+ /** Domain-specific meaning of the value. */
36
+ readonly semantic: Semantic;
37
+ /** Exact decimal value. */
38
+ readonly value: string;
39
+ }
40
+ /** A percentage represented as an exact fractional decimal string. */
41
+ export interface ICalculationBreakdownPercentValue<Semantic extends string> {
42
+ /** Discriminator for a percentage value. */
43
+ readonly type: 'percent';
44
+ /** Domain-specific meaning of the value. */
45
+ readonly semantic: Semantic;
46
+ /** Exact fractional value, where `0.20` represents 20%. */
47
+ readonly value: string;
48
+ }
49
+ /** A dimensionless numeric literal used inside an expression. */
50
+ export interface ICalculationBreakdownConstantValue {
51
+ /** Discriminator for a constant. */
52
+ readonly type: 'constant';
53
+ /** Exact decimal value. */
54
+ readonly value: string;
55
+ }
56
+ /** A binary operation recorded in a calculation breakdown. */
57
+ export interface ICalculationBreakdownOperation<Semantic extends string> {
58
+ /** Discriminator for an operation. */
59
+ readonly type: 'operation';
60
+ /** Operation applied to the two operands. */
61
+ readonly operator: TCalculationBreakdownOperator;
62
+ /** Left and right operands, in evaluation order. */
63
+ readonly operands: readonly [
64
+ TCalculationBreakdownNode<Semantic>,
65
+ TCalculationBreakdownNode<Semantic>
66
+ ];
67
+ }
68
+ /** Optional presentation hints for an explicitly grouped expression. */
69
+ export interface ICalculationBreakdownGroupOptions<Semantic extends string = string> {
70
+ /** Suggests that renderers place this group on its own visual line. */
71
+ readonly wrap?: boolean;
72
+ /** Domain-specific meaning of the grouped expression. */
73
+ readonly semantic?: Semantic;
74
+ /** Authoritative result of the grouped expression, such as a line subtotal. */
75
+ readonly result?: TCalculationBreakdownValue<Semantic>;
76
+ /** Identifies a roll-up whose authoritative result may be emphasized by renderers. */
77
+ readonly role?: TCalculationBreakdownGroupRole;
78
+ /** Preferred flow for detail values. Omitted groups default to horizontal. */
79
+ readonly orientation?: TCalculationBreakdownGroupOrientation;
80
+ }
81
+ /** An explicitly grouped subexpression. */
82
+ export interface ICalculationBreakdownGroup<Semantic extends string> extends ICalculationBreakdownGroupOptions<Semantic> {
83
+ /** Discriminator for a group. */
84
+ readonly type: 'group';
85
+ /** Expression enclosed by the group. */
86
+ readonly expression: TCalculationBreakdownNode<Semantic>;
87
+ }
88
+ /** Serializable, immutable record of how an externally calculated result was derived. */
89
+ export interface ICalculationBreakdown<Semantic extends string> {
90
+ /** Root node describing the operations and values in the calculation. */
91
+ readonly breakdown: TCalculationBreakdownNode<Semantic>;
92
+ /** Authoritative result supplied by the calling application. */
93
+ readonly result: TCalculationBreakdownValue<Semantic>;
94
+ }
95
+ /** Factory methods available inside breakdown and result callbacks. */
96
+ export interface ICalculationBreakdownNodeBuilder<Semantic extends string> {
97
+ /** Creates a monetary value. */
98
+ money(semantic: Semantic, amount: string, currency: string): ICalculationBreakdownMoneyValue<Semantic>;
99
+ /** Creates a duration value, optionally with a rounding rule. */
100
+ duration(semantic: Semantic, amount: string, unit: TCalculationBreakdownDurationUnit, options?: ICalculationBreakdownDurationOptions): ICalculationBreakdownDurationValue<Semantic>;
101
+ /** Creates a semantic, dimensionless number. */
102
+ number(semantic: Semantic, value: string): ICalculationBreakdownNumberValue<Semantic>;
103
+ /** Creates a percentage from a fractional decimal, such as `0.20` for 20%. */
104
+ percent(semantic: Semantic, value: string): ICalculationBreakdownPercentValue<Semantic>;
105
+ /** Creates a dimensionless numeric literal. */
106
+ constant(value: string): ICalculationBreakdownConstantValue;
107
+ /** Preserves an explicit grouping and optional presentation hints. */
108
+ group(expression: TCalculationBreakdownNode<Semantic>, options?: ICalculationBreakdownGroupOptions<Semantic>): ICalculationBreakdownGroup<Semantic>;
109
+ /** Creates a binary operation using any supported operator. */
110
+ operator(left: TCalculationBreakdownNode<Semantic>, operator: TCalculationBreakdownOperatorInput, right: TCalculationBreakdownNode<Semantic>): ICalculationBreakdownOperation<Semantic>;
111
+ /** Adds two expressions. */
112
+ add(left: TCalculationBreakdownNode<Semantic>, right: TCalculationBreakdownNode<Semantic>): ICalculationBreakdownOperation<Semantic>;
113
+ /** Subtracts the right expression from the left expression. */
114
+ subtract(left: TCalculationBreakdownNode<Semantic>, right: TCalculationBreakdownNode<Semantic>): ICalculationBreakdownOperation<Semantic>;
115
+ /** Decimal.js-style alias for `subtract`. */
116
+ sub(left: TCalculationBreakdownNode<Semantic>, right: TCalculationBreakdownNode<Semantic>): ICalculationBreakdownOperation<Semantic>;
117
+ /** Multiplies two expressions. */
118
+ multiply(left: TCalculationBreakdownNode<Semantic>, right: TCalculationBreakdownNode<Semantic>): ICalculationBreakdownOperation<Semantic>;
119
+ /** Decimal.js-style alias for `multiply`. */
120
+ mul(left: TCalculationBreakdownNode<Semantic>, right: TCalculationBreakdownNode<Semantic>): ICalculationBreakdownOperation<Semantic>;
121
+ /** Divides the left expression by the right expression. */
122
+ divide(left: TCalculationBreakdownNode<Semantic>, right: TCalculationBreakdownNode<Semantic>): ICalculationBreakdownOperation<Semantic>;
123
+ /** Decimal.js-style alias for `divide`. */
124
+ div(left: TCalculationBreakdownNode<Semantic>, right: TCalculationBreakdownNode<Semantic>): ICalculationBreakdownOperation<Semantic>;
125
+ /** Selects the smaller of two expressions. */
126
+ minimum(left: TCalculationBreakdownNode<Semantic>, right: TCalculationBreakdownNode<Semantic>): ICalculationBreakdownOperation<Semantic>;
127
+ /** Decimal.js-style alias for `minimum`. */
128
+ min(left: TCalculationBreakdownNode<Semantic>, right: TCalculationBreakdownNode<Semantic>): ICalculationBreakdownOperation<Semantic>;
129
+ /** Selects the larger of two expressions. */
130
+ maximum(left: TCalculationBreakdownNode<Semantic>, right: TCalculationBreakdownNode<Semantic>): ICalculationBreakdownOperation<Semantic>;
131
+ /** Decimal.js-style alias for `maximum`. */
132
+ max(left: TCalculationBreakdownNode<Semantic>, right: TCalculationBreakdownNode<Semantic>): ICalculationBreakdownOperation<Semantic>;
133
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,19 @@
1
+ import type { ICalculationBreakdownConstantValue, ICalculationBreakdownDurationValue, ICalculationBreakdownGroup, ICalculationBreakdownMoneyValue, ICalculationBreakdownNumberValue, ICalculationBreakdownOperation, ICalculationBreakdownPercentValue } from './calculation-breakdown.interface.js';
2
+ /** Binary operations that can be recorded in a calculation breakdown. */
3
+ export type TCalculationBreakdownOperator = 'add' | 'subtract' | 'multiply' | 'divide' | 'minimum' | 'maximum';
4
+ /** Decimal.js-style aliases accepted when constructing an operation. */
5
+ export type TCalculationBreakdownOperatorAlias = 'sub' | 'mul' | 'div' | 'min' | 'max';
6
+ /** Any operator name accepted by the builder. */
7
+ export type TCalculationBreakdownOperatorInput = TCalculationBreakdownOperator | TCalculationBreakdownOperatorAlias;
8
+ /** Units available for duration values. */
9
+ export type TCalculationBreakdownDurationUnit = 'milliseconds' | 'seconds' | 'minutes' | 'hours' | 'days';
10
+ /** Direction used when rounding a duration to a step. */
11
+ export type TCalculationBreakdownRounding = 'up' | 'down' | 'nearest';
12
+ /** Semantic role of a grouped expression. */
13
+ export type TCalculationBreakdownGroupRole = 'detail' | 'summary';
14
+ /** Preferred flow of a detail group in structured renderers. */
15
+ export type TCalculationBreakdownGroupOrientation = 'horizontal' | 'vertical';
16
+ /** A semantic value that can be recorded as the authoritative result. */
17
+ export type TCalculationBreakdownValue<Semantic extends string> = ICalculationBreakdownMoneyValue<Semantic> | ICalculationBreakdownDurationValue<Semantic> | ICalculationBreakdownNumberValue<Semantic> | ICalculationBreakdownPercentValue<Semantic>;
18
+ /** Any node that can appear in a calculation breakdown tree. */
19
+ export type TCalculationBreakdownNode<Semantic extends string> = TCalculationBreakdownValue<Semantic> | ICalculationBreakdownConstantValue | ICalculationBreakdownOperation<Semantic> | ICalculationBreakdownGroup<Semantic>;
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,5 @@
1
+ export { CalculationBreakdownBuilder } from './calculation-breakdown-builder.js';
2
+ export { toHumanString, type ICalculationBreakdownDictionaryEntry, type ICalculationBreakdownHumanStringOptions, type TCalculationBreakdownDictionary, type TCalculationBreakdownDictionaryEntry, type TCalculationBreakdownLocale, } from './calculation-breakdown-human-string.js';
3
+ export { CalculationBreakdownError, type TCalculationBreakdownErrorCode, } from './calculation-breakdown.error.js';
4
+ export type { ICalculationBreakdown, ICalculationBreakdownConstantValue, ICalculationBreakdownDurationOptions, ICalculationBreakdownDurationValue, ICalculationBreakdownNodeBuilder, ICalculationBreakdownGroup, ICalculationBreakdownGroupOptions, ICalculationBreakdownMoneyValue, ICalculationBreakdownNumberValue, ICalculationBreakdownOperation, ICalculationBreakdownPercentValue, } from './calculation-breakdown.interface.js';
5
+ export type { TCalculationBreakdownDurationUnit, TCalculationBreakdownGroupRole, TCalculationBreakdownGroupOrientation, TCalculationBreakdownNode, TCalculationBreakdownOperator, TCalculationBreakdownOperatorAlias, TCalculationBreakdownOperatorInput, TCalculationBreakdownRounding, TCalculationBreakdownValue, } from './calculation-breakdown.type.js';
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CalculationBreakdownError = exports.toHumanString = exports.CalculationBreakdownBuilder = void 0;
4
+ var calculation_breakdown_builder_js_1 = require("./calculation-breakdown-builder.js");
5
+ Object.defineProperty(exports, "CalculationBreakdownBuilder", { enumerable: true, get: function () { return calculation_breakdown_builder_js_1.CalculationBreakdownBuilder; } });
6
+ var calculation_breakdown_human_string_js_1 = require("./calculation-breakdown-human-string.js");
7
+ Object.defineProperty(exports, "toHumanString", { enumerable: true, get: function () { return calculation_breakdown_human_string_js_1.toHumanString; } });
8
+ var calculation_breakdown_error_js_1 = require("./calculation-breakdown.error.js");
9
+ Object.defineProperty(exports, "CalculationBreakdownError", { enumerable: true, get: function () { return calculation_breakdown_error_js_1.CalculationBreakdownError; } });
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "commonjs"
3
+ }
@@ -0,0 +1,6 @@
1
+ export { CalculationBreakdownBuilder } from './calculation-breakdown-builder.js';
2
+ export { toHumanString, type ICalculationBreakdownDictionaryEntry, type ICalculationBreakdownHumanStringOptions, type TCalculationBreakdownDictionary, type TCalculationBreakdownDictionaryEntry, type TCalculationBreakdownLocale, } from './calculation-breakdown-human-string.js';
3
+ export { CalculationBreakdownError, type TCalculationBreakdownErrorCode, } from './calculation-breakdown.error.js';
4
+ export type { ICalculationBreakdown, ICalculationBreakdownConstantValue, ICalculationBreakdownDurationOptions, ICalculationBreakdownDurationValue, ICalculationBreakdownNodeBuilder, ICalculationBreakdownGroup, ICalculationBreakdownGroupOptions, ICalculationBreakdownMoneyValue, ICalculationBreakdownNumberValue, ICalculationBreakdownOperation, ICalculationBreakdownPercentValue, } from './calculation-breakdown.interface.js';
5
+ export type { TCalculationBreakdownDurationUnit, TCalculationBreakdownGroupRole, TCalculationBreakdownGroupOrientation, TCalculationBreakdownNode, TCalculationBreakdownOperator, TCalculationBreakdownOperatorAlias, TCalculationBreakdownOperatorInput, TCalculationBreakdownRounding, TCalculationBreakdownValue, } from './calculation-breakdown.type.js';
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,2BAA2B,EAAE,MAAM,oCAAoC,CAAC;AACjF,OAAO,EACH,aAAa,EACb,KAAK,oCAAoC,EACzC,KAAK,uCAAuC,EAC5C,KAAK,+BAA+B,EACpC,KAAK,oCAAoC,EACzC,KAAK,2BAA2B,GACnC,MAAM,yCAAyC,CAAC;AACjD,OAAO,EACH,yBAAyB,EACzB,KAAK,8BAA8B,GACtC,MAAM,kCAAkC,CAAC;AAC1C,YAAY,EACR,qBAAqB,EACrB,kCAAkC,EAClC,oCAAoC,EACpC,kCAAkC,EAClC,gCAAgC,EAChC,0BAA0B,EAC1B,iCAAiC,EACjC,+BAA+B,EAC/B,gCAAgC,EAChC,8BAA8B,EAC9B,iCAAiC,GACpC,MAAM,sCAAsC,CAAC;AAC9C,YAAY,EACR,iCAAiC,EACjC,8BAA8B,EAC9B,qCAAqC,EACrC,yBAAyB,EACzB,6BAA6B,EAC7B,kCAAkC,EAClC,kCAAkC,EAClC,6BAA6B,EAC7B,0BAA0B,GAC7B,MAAM,iCAAiC,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { CalculationBreakdownBuilder } from './calculation-breakdown-builder.js';
2
+ export { toHumanString, } from './calculation-breakdown-human-string.js';
3
+ export { CalculationBreakdownError, } from './calculation-breakdown.error.js';
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,2BAA2B,EAAE,MAAM,oCAAoC,CAAC;AACjF,OAAO,EACH,aAAa,GAMhB,MAAM,yCAAyC,CAAC;AACjD,OAAO,EACH,yBAAyB,GAE5B,MAAM,kCAAkC,CAAC"}
@@ -0,0 +1,19 @@
1
+ title: calculation-breakdown
2
+ description: Typed, serializable calculation breakdowns.
3
+ url: https://thismeansmore.github.io
4
+ baseurl: /calculation-breakdown
5
+ theme: minima
6
+ github_username: ThisMeansMore
7
+
8
+ header_pages:
9
+ - playground/index.html
10
+ - changelog.md
11
+
12
+ plugins:
13
+ - jekyll-relative-links
14
+
15
+ defaults:
16
+ - scope:
17
+ path: ""
18
+ values:
19
+ layout: default
@@ -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