@amboras-dev/money 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +401 -2
- package/dist/ai.cjs +185 -0
- package/dist/ai.d.cts +97 -0
- package/dist/ai.d.ts +97 -0
- package/dist/ai.js +175 -0
- package/dist/allocate-CZRg4TOP.cjs +222 -0
- package/dist/allocate-X7_jAtOj.js +199 -0
- package/dist/amount-BV0O-aN8.cjs +241 -0
- package/dist/amount-BoYBMgK9.js +200 -0
- package/dist/arithmetic-BhNwisjb.js +132 -0
- package/dist/arithmetic-DjT4Kzgt.cjs +233 -0
- package/dist/billing.cjs +184 -0
- package/dist/billing.d.cts +81 -0
- package/dist/billing.d.ts +81 -0
- package/dist/billing.js +175 -0
- package/dist/commerce.cjs +536 -0
- package/dist/commerce.d.cts +293 -0
- package/dist/commerce.d.ts +293 -0
- package/dist/commerce.js +516 -0
- package/dist/format-BdPnijvn.js +99 -0
- package/dist/format-DLhno7eX.cjs +128 -0
- package/dist/fx.cjs +407 -0
- package/dist/fx.d.cts +157 -0
- package/dist/fx.d.ts +157 -0
- package/dist/fx.js +374 -0
- package/dist/index.cjs +352 -0
- package/dist/index.d.cts +300 -0
- package/dist/index.d.ts +300 -0
- package/dist/index.js +276 -0
- package/dist/input.cjs +163 -0
- package/dist/input.d.cts +203 -0
- package/dist/input.d.ts +203 -0
- package/dist/input.js +153 -0
- package/dist/iso-minor-CqkWDxHS.js +48 -0
- package/dist/iso-minor-wSwJN57T.cjs +65 -0
- package/dist/medusa-4K8N1pUc.d.cts +87 -0
- package/dist/medusa-DBXJXXvF.d.ts +87 -0
- package/dist/medusa-DqJY1qpj.js +116 -0
- package/dist/medusa-lRY9xlsf.cjs +175 -0
- package/dist/money-B-3aFjNJ.d.cts +248 -0
- package/dist/money-B-3aFjNJ.d.ts +248 -0
- package/dist/money-B3wH_Z4S.cjs +3990 -0
- package/dist/money-WhnGiI00.js +3583 -0
- package/dist/number-text-BWu67jUq.cjs +145 -0
- package/dist/number-text-DE-q4jO5.js +134 -0
- package/dist/own-D7544D3k.js +4 -0
- package/dist/own-oP9DXFWv.cjs +9 -0
- package/dist/percent-C0InC2t2.cjs +43 -0
- package/dist/percent-D1VB9xXG.js +20 -0
- package/dist/rate-CVeMii4a.cjs +217 -0
- package/dist/rate-DBPiYgT1.d.ts +63 -0
- package/dist/rate-DicStBue.js +152 -0
- package/dist/rate-tqK8ijG0.d.cts +63 -0
- package/dist/reporting-CG-ai2sM.d.cts +31 -0
- package/dist/reporting-tpGJbqYp.d.ts +31 -0
- package/dist/retail-1mu888J1.js +69 -0
- package/dist/retail-C_BPLvMj.d.cts +31 -0
- package/dist/retail-Cml8YSKg.cjs +86 -0
- package/dist/retail-DRUTTQoJ.d.ts +31 -0
- package/dist/rounding-DHBtNCHG.cjs +79 -0
- package/dist/rounding-g9rvye0k.js +50 -0
- package/dist/sql.cjs +290 -0
- package/dist/sql.d.cts +147 -0
- package/dist/sql.d.ts +147 -0
- package/dist/sql.js +271 -0
- package/dist/testing.cjs +61995 -0
- package/dist/testing.d.cts +125 -0
- package/dist/testing.d.ts +125 -0
- package/dist/testing.js +61986 -0
- package/dist/wire-Bhuut8cW.cjs +2097 -0
- package/dist/wire-CaUN9Vf_.d.ts +309 -0
- package/dist/wire-sW0yzqth.js +1888 -0
- package/dist/wire-xjYXv4go.d.cts +309 -0
- package/dist/wire.cjs +37 -0
- package/dist/wire.d.cts +2 -0
- package/dist/wire.d.ts +2 -0
- package/dist/wire.js +2 -0
- package/package.json +158 -3
package/dist/ai.d.cts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { R as DecimalText, g as CurrencyCode, i as Money } from "./money-B-3aFjNJ.cjs";
|
|
2
|
+
//#region src/ai/mentions.d.ts
|
|
3
|
+
/** One price found in text. `code` is set when the mark names one currency; `amount` when the number reads (see the separator engine's 'auto' rule). */
|
|
4
|
+
interface MoneyMention {
|
|
5
|
+
/** The matched text, mark and number. */
|
|
6
|
+
readonly raw: string;
|
|
7
|
+
/** UTF-16 offset of `raw` in the text. */
|
|
8
|
+
readonly index: number;
|
|
9
|
+
/** The currency mark as written (`'$'`, `'AED'`, `'Rp'`). */
|
|
10
|
+
readonly symbol?: string;
|
|
11
|
+
readonly code?: CurrencyCode;
|
|
12
|
+
/** Major units as canonical decimal text. */
|
|
13
|
+
readonly amount?: DecimalText;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Every spelling a store may print for `currency` besides its ISO code, from the registry:
|
|
17
|
+
* symbol, narrow symbol and aliases (`['₹', 'Rs.', 'Rs']` for INR, `['$', 'US$']` for USD).
|
|
18
|
+
*/
|
|
19
|
+
export declare function currencySymbolAliases(currency: string): readonly string[];
|
|
20
|
+
/**
|
|
21
|
+
* The money-literal regex as `{ source, flags }` (flags `'gu'`), a string so a browser script
|
|
22
|
+
* (`page.evaluate`) runs exactly what Node runs. It matches a currency mark next to a number,
|
|
23
|
+
* either order, at most one space between: the marks of `currency`, or of every active
|
|
24
|
+
* currency (single-letter marks such as `R` only when `currency` names them), plus any Unicode
|
|
25
|
+
* currency sign. Numbers group in threes or in Indian lakh (`1,00,000`) with `.`, `,`, space
|
|
26
|
+
* or apostrophe and carry up to `maxFractionDigits` (default 3) fraction digits. Groups: 1 mark and 2 number, or 3 number
|
|
27
|
+
* and 4 mark. A bare number never matches.
|
|
28
|
+
*/
|
|
29
|
+
export declare function moneyLiteralPattern(options?: {
|
|
30
|
+
currency?: string;
|
|
31
|
+
maxFractionDigits?: number;
|
|
32
|
+
}): {
|
|
33
|
+
source: string;
|
|
34
|
+
flags: string;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Every price in `text`, in order (see `moneyLiteralPattern`). With `currency`, its marks are
|
|
38
|
+
* matched (single letters too) and claim the mentions they appear in; a mark only one active
|
|
39
|
+
* currency prints names that currency; `$`, `kr` and other shared marks leave `code` unset.
|
|
40
|
+
* `amount` is read with the separator engine's `'auto'` rule in the named currency (one
|
|
41
|
+
* separator before exactly three digits groups, except a KWD-style decimal mark).
|
|
42
|
+
*/
|
|
43
|
+
export declare function moneyMentions(text: string, options?: {
|
|
44
|
+
currency?: string;
|
|
45
|
+
}): MoneyMention[];
|
|
46
|
+
//#endregion
|
|
47
|
+
//#region src/ai/model-money.d.ts
|
|
48
|
+
/** A stored amount as the model sees it: the major amount, the code and a display string. */
|
|
49
|
+
interface MoneyView {
|
|
50
|
+
readonly amount: number;
|
|
51
|
+
readonly currency: CurrencyCode;
|
|
52
|
+
readonly display: string;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* `{ amount: 19.99, currency: 'USD', display: '$19.99' }`. `amount` is exact; `display` shows the
|
|
56
|
+
* display digits, widened to keep a stored value finer than them (JPY 550 is `'¥5.5'`, HUF 435050
|
|
57
|
+
* `'HUF 4,350.5'`). capability-kit's `moneyView` uses the storage digits as the minimum instead
|
|
58
|
+
* (HUF 435000 is `'HUF 4,350.00'` there and `'HUF 4,350'` here, IQD 1500 `'IQD 1.500'` against
|
|
59
|
+
* `'IQD 1.5'`): a deliberate change, listed in the README's behaviour changes.
|
|
60
|
+
*/
|
|
61
|
+
export declare function moneyView(m: Money): MoneyView;
|
|
62
|
+
type ParseModelMoneyResult = {
|
|
63
|
+
readonly ok: true;
|
|
64
|
+
readonly money: Money;
|
|
65
|
+
} | {
|
|
66
|
+
readonly ok: false;
|
|
67
|
+
readonly error: string;
|
|
68
|
+
};
|
|
69
|
+
interface ParseModelMoneyOptions {
|
|
70
|
+
/** Default false. */
|
|
71
|
+
allowNegative?: boolean;
|
|
72
|
+
/** Default true. */
|
|
73
|
+
allowZero?: boolean;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* An amount from a model tool call (major units, as the merchant says it) as Money, or an
|
|
77
|
+
* error to send back to the model ending with `MONEY_UNIT_SENTENCE`. Numbers go through
|
|
78
|
+
* `fromMajor` (float noise such as 0.1 + 0.2 allowed); strings through `/input`'s entry rule
|
|
79
|
+
* in English (`'1,299.00'`, `'$1,299'`, `'99.000'` VND, `'1.500'` JPY). More fraction digits
|
|
80
|
+
* than the currency stores and negative amounts are refused, never rounded or flipped. An
|
|
81
|
+
* unknown currency throws `currency_invalid` (it is the tool's, not the model's).
|
|
82
|
+
*/
|
|
83
|
+
export declare function parseModelMoney(input: unknown, currency: string, options?: ParseModelMoneyOptions): ParseModelMoneyResult;
|
|
84
|
+
//#endregion
|
|
85
|
+
//#region src/ai/wording.d.ts
|
|
86
|
+
/** The one unit sentence every money field carries, for the model and for people. Verbatim. */
|
|
87
|
+
export declare const MONEY_UNIT_SENTENCE = "Amount in normal currency units as the merchant says it (99 means 99.00; 1.5 means 1.50). Never cents or minor units.";
|
|
88
|
+
/** JSON Schema keyword marking a money field. Verbatim. */
|
|
89
|
+
export declare const MONEY_META_KEY = "x-amboras-money";
|
|
90
|
+
/** How store API amounts are scaled, for tool descriptions that hand stored amounts to the model. */
|
|
91
|
+
export declare const STORAGE_UNIT_SENTENCE = "Store API amounts are storage units: major units x100, x1000 for KWD, BHD, OMR, TND, JOD, IQD and LYD, and x100 for zero-decimal currencies too. Render them with the money formatter; never divide or multiply by hand.";
|
|
92
|
+
/** What a manual exchange rate means, for rate fields. */
|
|
93
|
+
export declare const RATE_UNIT_SENTENCE = "1 unit of the store currency equals this many units of the other currency.";
|
|
94
|
+
/** A money field's description: `describe` (trimmed) followed by the unit sentence, or the sentence alone. */
|
|
95
|
+
export declare function moneyFieldDescription(describe?: string): string;
|
|
96
|
+
//#endregion
|
|
97
|
+
export type { MoneyMention, MoneyView, ParseModelMoneyOptions, ParseModelMoneyResult };
|
package/dist/ai.d.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { R as DecimalText, g as CurrencyCode, i as Money } from "./money-B-3aFjNJ.js";
|
|
2
|
+
//#region src/ai/mentions.d.ts
|
|
3
|
+
/** One price found in text. `code` is set when the mark names one currency; `amount` when the number reads (see the separator engine's 'auto' rule). */
|
|
4
|
+
interface MoneyMention {
|
|
5
|
+
/** The matched text, mark and number. */
|
|
6
|
+
readonly raw: string;
|
|
7
|
+
/** UTF-16 offset of `raw` in the text. */
|
|
8
|
+
readonly index: number;
|
|
9
|
+
/** The currency mark as written (`'$'`, `'AED'`, `'Rp'`). */
|
|
10
|
+
readonly symbol?: string;
|
|
11
|
+
readonly code?: CurrencyCode;
|
|
12
|
+
/** Major units as canonical decimal text. */
|
|
13
|
+
readonly amount?: DecimalText;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Every spelling a store may print for `currency` besides its ISO code, from the registry:
|
|
17
|
+
* symbol, narrow symbol and aliases (`['₹', 'Rs.', 'Rs']` for INR, `['$', 'US$']` for USD).
|
|
18
|
+
*/
|
|
19
|
+
export declare function currencySymbolAliases(currency: string): readonly string[];
|
|
20
|
+
/**
|
|
21
|
+
* The money-literal regex as `{ source, flags }` (flags `'gu'`), a string so a browser script
|
|
22
|
+
* (`page.evaluate`) runs exactly what Node runs. It matches a currency mark next to a number,
|
|
23
|
+
* either order, at most one space between: the marks of `currency`, or of every active
|
|
24
|
+
* currency (single-letter marks such as `R` only when `currency` names them), plus any Unicode
|
|
25
|
+
* currency sign. Numbers group in threes or in Indian lakh (`1,00,000`) with `.`, `,`, space
|
|
26
|
+
* or apostrophe and carry up to `maxFractionDigits` (default 3) fraction digits. Groups: 1 mark and 2 number, or 3 number
|
|
27
|
+
* and 4 mark. A bare number never matches.
|
|
28
|
+
*/
|
|
29
|
+
export declare function moneyLiteralPattern(options?: {
|
|
30
|
+
currency?: string;
|
|
31
|
+
maxFractionDigits?: number;
|
|
32
|
+
}): {
|
|
33
|
+
source: string;
|
|
34
|
+
flags: string;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Every price in `text`, in order (see `moneyLiteralPattern`). With `currency`, its marks are
|
|
38
|
+
* matched (single letters too) and claim the mentions they appear in; a mark only one active
|
|
39
|
+
* currency prints names that currency; `$`, `kr` and other shared marks leave `code` unset.
|
|
40
|
+
* `amount` is read with the separator engine's `'auto'` rule in the named currency (one
|
|
41
|
+
* separator before exactly three digits groups, except a KWD-style decimal mark).
|
|
42
|
+
*/
|
|
43
|
+
export declare function moneyMentions(text: string, options?: {
|
|
44
|
+
currency?: string;
|
|
45
|
+
}): MoneyMention[];
|
|
46
|
+
//#endregion
|
|
47
|
+
//#region src/ai/model-money.d.ts
|
|
48
|
+
/** A stored amount as the model sees it: the major amount, the code and a display string. */
|
|
49
|
+
interface MoneyView {
|
|
50
|
+
readonly amount: number;
|
|
51
|
+
readonly currency: CurrencyCode;
|
|
52
|
+
readonly display: string;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* `{ amount: 19.99, currency: 'USD', display: '$19.99' }`. `amount` is exact; `display` shows the
|
|
56
|
+
* display digits, widened to keep a stored value finer than them (JPY 550 is `'¥5.5'`, HUF 435050
|
|
57
|
+
* `'HUF 4,350.5'`). capability-kit's `moneyView` uses the storage digits as the minimum instead
|
|
58
|
+
* (HUF 435000 is `'HUF 4,350.00'` there and `'HUF 4,350'` here, IQD 1500 `'IQD 1.500'` against
|
|
59
|
+
* `'IQD 1.5'`): a deliberate change, listed in the README's behaviour changes.
|
|
60
|
+
*/
|
|
61
|
+
export declare function moneyView(m: Money): MoneyView;
|
|
62
|
+
type ParseModelMoneyResult = {
|
|
63
|
+
readonly ok: true;
|
|
64
|
+
readonly money: Money;
|
|
65
|
+
} | {
|
|
66
|
+
readonly ok: false;
|
|
67
|
+
readonly error: string;
|
|
68
|
+
};
|
|
69
|
+
interface ParseModelMoneyOptions {
|
|
70
|
+
/** Default false. */
|
|
71
|
+
allowNegative?: boolean;
|
|
72
|
+
/** Default true. */
|
|
73
|
+
allowZero?: boolean;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* An amount from a model tool call (major units, as the merchant says it) as Money, or an
|
|
77
|
+
* error to send back to the model ending with `MONEY_UNIT_SENTENCE`. Numbers go through
|
|
78
|
+
* `fromMajor` (float noise such as 0.1 + 0.2 allowed); strings through `/input`'s entry rule
|
|
79
|
+
* in English (`'1,299.00'`, `'$1,299'`, `'99.000'` VND, `'1.500'` JPY). More fraction digits
|
|
80
|
+
* than the currency stores and negative amounts are refused, never rounded or flipped. An
|
|
81
|
+
* unknown currency throws `currency_invalid` (it is the tool's, not the model's).
|
|
82
|
+
*/
|
|
83
|
+
export declare function parseModelMoney(input: unknown, currency: string, options?: ParseModelMoneyOptions): ParseModelMoneyResult;
|
|
84
|
+
//#endregion
|
|
85
|
+
//#region src/ai/wording.d.ts
|
|
86
|
+
/** The one unit sentence every money field carries, for the model and for people. Verbatim. */
|
|
87
|
+
export declare const MONEY_UNIT_SENTENCE = "Amount in normal currency units as the merchant says it (99 means 99.00; 1.5 means 1.50). Never cents or minor units.";
|
|
88
|
+
/** JSON Schema keyword marking a money field. Verbatim. */
|
|
89
|
+
export declare const MONEY_META_KEY = "x-amboras-money";
|
|
90
|
+
/** How store API amounts are scaled, for tool descriptions that hand stored amounts to the model. */
|
|
91
|
+
export declare const STORAGE_UNIT_SENTENCE = "Store API amounts are storage units: major units x100, x1000 for KWD, BHD, OMR, TND, JOD, IQD and LYD, and x100 for zero-decimal currencies too. Render them with the money formatter; never divide or multiply by hand.";
|
|
92
|
+
/** What a manual exchange rate means, for rate fields. */
|
|
93
|
+
export declare const RATE_UNIT_SENTENCE = "1 unit of the store currency equals this many units of the other currency.";
|
|
94
|
+
/** A money field's description: `describe` (trimmed) followed by the unit sentence, or the sentence alone. */
|
|
95
|
+
export declare function moneyFieldDescription(describe?: string): string;
|
|
96
|
+
//#endregion
|
|
97
|
+
export type { MoneyMention, MoneyView, ParseModelMoneyOptions, ParseModelMoneyResult };
|
package/dist/ai.js
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { $ as describeValue, E as resolveCurrency, Q as MoneyError, l as toMajorNumber, m as currencyMarks, n as fromMajor, pt as toDecimalText, s as storedDecimal, u as CURRENCIES } from "./money-WhnGiI00.js";
|
|
2
|
+
import { t as formatMoney } from "./format-BdPnijvn.js";
|
|
3
|
+
import { n as readNumberText, t as digitGroupingSource } from "./number-text-DE-q4jO5.js";
|
|
4
|
+
import { s as parseAmountInput } from "./amount-BoYBMgK9.js";
|
|
5
|
+
//#region src/ai/mentions.ts
|
|
6
|
+
const ACTIVE = CURRENCIES.filter((c) => c.status === "active");
|
|
7
|
+
const escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
8
|
+
const LETTER_START = /^\p{L}/u;
|
|
9
|
+
const LETTER_END = /\p{L}$/u;
|
|
10
|
+
const SINGLE_LETTER = /^\p{L}$/u;
|
|
11
|
+
const OWNERS = /* @__PURE__ */ new Map();
|
|
12
|
+
for (const info of ACTIVE) for (const mark of currencyMarks(info)) OWNERS.set(mark, [...OWNERS.get(mark) ?? [], info.code]);
|
|
13
|
+
/**
|
|
14
|
+
* Every spelling a store may print for `currency` besides its ISO code, from the registry:
|
|
15
|
+
* symbol, narrow symbol and aliases (`['₹', 'Rs.', 'Rs']` for INR, `['$', 'US$']` for USD).
|
|
16
|
+
*/
|
|
17
|
+
function currencySymbolAliases(currency) {
|
|
18
|
+
const info = resolveCurrency(currency, { allowHistoric: true });
|
|
19
|
+
return Object.freeze(currencyMarks(info).filter((mark) => mark !== info.code));
|
|
20
|
+
}
|
|
21
|
+
/** A mark as a regex alternative; a mark that starts or ends with a letter must not touch another letter. */
|
|
22
|
+
function markSource(mark) {
|
|
23
|
+
const before = LETTER_START.test(mark) ? "(?<!\\p{L})" : "";
|
|
24
|
+
const after = LETTER_END.test(mark) ? "(?!\\p{L})" : "";
|
|
25
|
+
return `${before}${escapeRegExp(mark)}${after}`;
|
|
26
|
+
}
|
|
27
|
+
function numberSource(maxFractionDigits) {
|
|
28
|
+
const fraction = maxFractionDigits === 0 ? "" : `(?:[.,]\\d{1,${maxFractionDigits}})?`;
|
|
29
|
+
return `(?:${digitGroupingSource("[ \\u00a0\\u2009\\u202f.,'’]")})${fraction}|\\d+${fraction}`;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The money-literal regex as `{ source, flags }` (flags `'gu'`), a string so a browser script
|
|
33
|
+
* (`page.evaluate`) runs exactly what Node runs. It matches a currency mark next to a number,
|
|
34
|
+
* either order, at most one space between: the marks of `currency`, or of every active
|
|
35
|
+
* currency (single-letter marks such as `R` only when `currency` names them), plus any Unicode
|
|
36
|
+
* currency sign. Numbers group in threes or in Indian lakh (`1,00,000`) with `.`, `,`, space
|
|
37
|
+
* or apostrophe and carry up to `maxFractionDigits` (default 3) fraction digits. Groups: 1 mark and 2 number, or 3 number
|
|
38
|
+
* and 4 mark. A bare number never matches.
|
|
39
|
+
*/
|
|
40
|
+
function moneyLiteralPattern(options = {}) {
|
|
41
|
+
const digits = options.maxFractionDigits ?? 3;
|
|
42
|
+
if (!Number.isInteger(digits) || digits < 0 || digits > 6) throw new MoneyError("amount_invalid", `maxFractionDigits must be an integer from 0 to 6, got ${describeValue(digits)}`);
|
|
43
|
+
const mark = `(${[...(options.currency === void 0 ? [...OWNERS.keys()].filter((mark) => !SINGLE_LETTER.test(mark)) : currencyMarks(resolveCurrency(options.currency, { allowHistoric: true }))).sort((a, b) => b.length - a.length || (a < b ? -1 : 1)).map(markSource), "\\p{Sc}"].join("|")})`;
|
|
44
|
+
const number = `(${numberSource(digits)})`;
|
|
45
|
+
return {
|
|
46
|
+
source: `${mark}\\s?${number}|${number}\\s?${mark}`,
|
|
47
|
+
flags: "gu"
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function codeOfMark(mark, currency) {
|
|
51
|
+
if (currency && currencyMarks(currency).includes(mark)) return currency.code;
|
|
52
|
+
const owners = OWNERS.get(mark);
|
|
53
|
+
return owners?.length === 1 ? owners[0] : void 0;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Every price in `text`, in order (see `moneyLiteralPattern`). With `currency`, its marks are
|
|
57
|
+
* matched (single letters too) and claim the mentions they appear in; a mark only one active
|
|
58
|
+
* currency prints names that currency; `$`, `kr` and other shared marks leave `code` unset.
|
|
59
|
+
* `amount` is read with the separator engine's `'auto'` rule in the named currency (one
|
|
60
|
+
* separator before exactly three digits groups, except a KWD-style decimal mark).
|
|
61
|
+
*/
|
|
62
|
+
function moneyMentions(text, options = {}) {
|
|
63
|
+
const currency = options.currency === void 0 ? null : resolveCurrency(options.currency, { allowHistoric: true });
|
|
64
|
+
const { source, flags } = moneyLiteralPattern({ currency: currency?.code });
|
|
65
|
+
const mentions = [];
|
|
66
|
+
for (const match of String(text).matchAll(new RegExp(source, flags))) {
|
|
67
|
+
const symbol = match[1] ?? match[4];
|
|
68
|
+
const code = codeOfMark(symbol, currency);
|
|
69
|
+
const read = readNumberText(match[2] ?? match[3], {
|
|
70
|
+
decimalSeparator: "auto",
|
|
71
|
+
currency: code
|
|
72
|
+
});
|
|
73
|
+
mentions.push({
|
|
74
|
+
raw: match[0],
|
|
75
|
+
index: match.index,
|
|
76
|
+
symbol,
|
|
77
|
+
...code === void 0 ? {} : { code },
|
|
78
|
+
..."reason" in read ? {} : { amount: toDecimalText({
|
|
79
|
+
units: read.units,
|
|
80
|
+
scale: read.scale
|
|
81
|
+
}) }
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
return mentions;
|
|
85
|
+
}
|
|
86
|
+
//#endregion
|
|
87
|
+
//#region src/ai/wording.ts
|
|
88
|
+
/** The one unit sentence every money field carries, for the model and for people. Verbatim. */
|
|
89
|
+
const MONEY_UNIT_SENTENCE = "Amount in normal currency units as the merchant says it (99 means 99.00; 1.5 means 1.50). Never cents or minor units.";
|
|
90
|
+
/** JSON Schema keyword marking a money field. Verbatim. */
|
|
91
|
+
const MONEY_META_KEY = "x-amboras-money";
|
|
92
|
+
/** How store API amounts are scaled, for tool descriptions that hand stored amounts to the model. */
|
|
93
|
+
const STORAGE_UNIT_SENTENCE = "Store API amounts are storage units: major units x100, x1000 for KWD, BHD, OMR, TND, JOD, IQD and LYD, and x100 for zero-decimal currencies too. Render them with the money formatter; never divide or multiply by hand.";
|
|
94
|
+
/** What a manual exchange rate means, for rate fields. */
|
|
95
|
+
const RATE_UNIT_SENTENCE = "1 unit of the store currency equals this many units of the other currency.";
|
|
96
|
+
/** A money field's description: `describe` (trimmed) followed by the unit sentence, or the sentence alone. */
|
|
97
|
+
function moneyFieldDescription(describe) {
|
|
98
|
+
const lead = typeof describe === "string" ? describe.trim() : "";
|
|
99
|
+
return lead ? `${lead} ${MONEY_UNIT_SENTENCE}` : MONEY_UNIT_SENTENCE;
|
|
100
|
+
}
|
|
101
|
+
//#endregion
|
|
102
|
+
//#region src/ai/model-money.ts
|
|
103
|
+
/**
|
|
104
|
+
* `{ amount: 19.99, currency: 'USD', display: '$19.99' }`. `amount` is exact; `display` shows the
|
|
105
|
+
* display digits, widened to keep a stored value finer than them (JPY 550 is `'¥5.5'`, HUF 435050
|
|
106
|
+
* `'HUF 4,350.5'`). capability-kit's `moneyView` uses the storage digits as the minimum instead
|
|
107
|
+
* (HUF 435000 is `'HUF 4,350.00'` there and `'HUF 4,350'` here, IQD 1500 `'IQD 1.500'` against
|
|
108
|
+
* `'IQD 1.5'`): a deliberate change, listed in the README's behaviour changes.
|
|
109
|
+
*/
|
|
110
|
+
function moneyView(m) {
|
|
111
|
+
const { info } = storedDecimal(m);
|
|
112
|
+
return {
|
|
113
|
+
amount: toMajorNumber(m),
|
|
114
|
+
currency: info.code,
|
|
115
|
+
display: formatMoney(m, {
|
|
116
|
+
locale: "en",
|
|
117
|
+
digits: "widen"
|
|
118
|
+
})
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
function failure(reason, input, code, digits) {
|
|
122
|
+
const shown = describeValue(input);
|
|
123
|
+
return {
|
|
124
|
+
ok: false,
|
|
125
|
+
error: `${{
|
|
126
|
+
empty: `Not an amount: ${shown}.`,
|
|
127
|
+
unparseable: `Not an amount: ${shown}.`,
|
|
128
|
+
too_many_decimals: `${shown} has more decimal places than ${code} uses (at most ${digits}).`,
|
|
129
|
+
negative: "The amount must not be negative.",
|
|
130
|
+
zero: "The amount must be more than zero.",
|
|
131
|
+
below_min: `${shown} is out of range for ${code}.`,
|
|
132
|
+
above_max: `${shown} is out of range for ${code}.`
|
|
133
|
+
}[reason]} ${MONEY_UNIT_SENTENCE}`
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
function fromNumber(input, currency, options) {
|
|
137
|
+
if (!Number.isFinite(input)) return "unparseable";
|
|
138
|
+
if (input < 0 && !options.allowNegative) return "negative";
|
|
139
|
+
if (input === 0 && options.allowZero === false) return "zero";
|
|
140
|
+
try {
|
|
141
|
+
return fromMajor(input, currency);
|
|
142
|
+
} catch (error) {
|
|
143
|
+
return error.code === "precision" ? "too_many_decimals" : "above_max";
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* An amount from a model tool call (major units, as the merchant says it) as Money, or an
|
|
148
|
+
* error to send back to the model ending with `MONEY_UNIT_SENTENCE`. Numbers go through
|
|
149
|
+
* `fromMajor` (float noise such as 0.1 + 0.2 allowed); strings through `/input`'s entry rule
|
|
150
|
+
* in English (`'1,299.00'`, `'$1,299'`, `'99.000'` VND, `'1.500'` JPY). More fraction digits
|
|
151
|
+
* than the currency stores and negative amounts are refused, never rounded or flipped. An
|
|
152
|
+
* unknown currency throws `currency_invalid` (it is the tool's, not the model's).
|
|
153
|
+
*/
|
|
154
|
+
function parseModelMoney(input, currency, options = {}) {
|
|
155
|
+
const info = resolveCurrency(currency, { allowHistoric: true });
|
|
156
|
+
if (typeof input === "number") {
|
|
157
|
+
const result = fromNumber(input, info.code, options);
|
|
158
|
+
return typeof result === "string" ? failure(result, input, info.code, info.storageDigits) : {
|
|
159
|
+
ok: true,
|
|
160
|
+
money: result
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
if (typeof input !== "string") return failure("unparseable", input, info.code, info.storageDigits);
|
|
164
|
+
const result = parseAmountInput(input, info.code, {
|
|
165
|
+
locale: "en",
|
|
166
|
+
allowNegative: options.allowNegative,
|
|
167
|
+
allowZero: options.allowZero
|
|
168
|
+
});
|
|
169
|
+
return result.ok ? {
|
|
170
|
+
ok: true,
|
|
171
|
+
money: result.money
|
|
172
|
+
} : failure(result.reason, input, info.code, info.storageDigits);
|
|
173
|
+
}
|
|
174
|
+
//#endregion
|
|
175
|
+
export { MONEY_META_KEY, MONEY_UNIT_SENTENCE, RATE_UNIT_SENTENCE, STORAGE_UNIT_SENTENCE, currencySymbolAliases, moneyFieldDescription, moneyLiteralPattern, moneyMentions, moneyView, parseModelMoney };
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
const require_money = require("./money-B3wH_Z4S.cjs");
|
|
2
|
+
const require_rounding = require("./rounding-DHBtNCHG.cjs");
|
|
3
|
+
//#region src/allocate.ts
|
|
4
|
+
const invalid = (message) => new require_money.MoneyError("amount_invalid", message);
|
|
5
|
+
const sumOf = (values) => values.reduce((a, b) => a + b, 0n);
|
|
6
|
+
const floorShares = (units, weights, total) => weights.map((w) => require_money.divRound(units * w, total, "floor"));
|
|
7
|
+
function largestRemainder(units, weights, total) {
|
|
8
|
+
const shares = floorShares(units, weights, total);
|
|
9
|
+
let rest = units - sumOf(shares);
|
|
10
|
+
const order = weights.map((w, i) => ({
|
|
11
|
+
i,
|
|
12
|
+
fraction: units * w % total
|
|
13
|
+
})).sort((a, b) => a.fraction === b.fraction ? a.i - b.i : a.fraction > b.fraction ? -1 : 1);
|
|
14
|
+
for (const { i } of order) {
|
|
15
|
+
if (rest === 0n) break;
|
|
16
|
+
shares[i] += 1n;
|
|
17
|
+
rest -= 1n;
|
|
18
|
+
}
|
|
19
|
+
return shares;
|
|
20
|
+
}
|
|
21
|
+
function largestWeight(units, weights, total) {
|
|
22
|
+
const top = weights.indexOf(weights.reduce((a, b) => b > a ? b : a));
|
|
23
|
+
const shares = weights.map((w, i) => i === top ? 0n : require_money.mulDiv(units, w, total, "half-up"));
|
|
24
|
+
let deficit = sumOf(shares) - units;
|
|
25
|
+
if (deficit <= 0n) {
|
|
26
|
+
shares[top] = -deficit;
|
|
27
|
+
return shares;
|
|
28
|
+
}
|
|
29
|
+
const byWeight = weights.map((w, i) => ({
|
|
30
|
+
w,
|
|
31
|
+
i
|
|
32
|
+
})).sort((a, b) => a.w === b.w ? a.i - b.i : a.w > b.w ? -1 : 1);
|
|
33
|
+
for (const { i } of byWeight) {
|
|
34
|
+
const take = shares[i] < deficit ? shares[i] : deficit;
|
|
35
|
+
shares[i] -= take;
|
|
36
|
+
deficit -= take;
|
|
37
|
+
}
|
|
38
|
+
return shares;
|
|
39
|
+
}
|
|
40
|
+
function lastAbsorbs(units, weights, total) {
|
|
41
|
+
const shares = floorShares(units, weights, total);
|
|
42
|
+
let last = weights.length - 1;
|
|
43
|
+
while (weights[last] === 0n) last -= 1;
|
|
44
|
+
shares[last] += units - sumOf(shares);
|
|
45
|
+
return shares;
|
|
46
|
+
}
|
|
47
|
+
function cumulative(units, weights, total) {
|
|
48
|
+
let running = 0n;
|
|
49
|
+
let previous = 0n;
|
|
50
|
+
return weights.map((w) => {
|
|
51
|
+
running += w;
|
|
52
|
+
const reached = require_money.mulDiv(units, running, total, "half-up");
|
|
53
|
+
const share = reached - previous;
|
|
54
|
+
previous = reached;
|
|
55
|
+
return share;
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
function lastFirst(amount, weights, total, step, caps) {
|
|
59
|
+
const capTotal = sumOf(caps);
|
|
60
|
+
const target = amount < capTotal ? amount : capTotal;
|
|
61
|
+
const shares = weights.map((w, i) => {
|
|
62
|
+
const share = require_money.mulDiv(target, w, total * step, "half-up") * step;
|
|
63
|
+
return share < caps[i] ? share : caps[i];
|
|
64
|
+
});
|
|
65
|
+
let rest = target - sumOf(shares);
|
|
66
|
+
for (let i = shares.length - 1; i >= 0 && rest !== 0n; i--) {
|
|
67
|
+
const wanted = shares[i] + rest;
|
|
68
|
+
const next = wanted < 0n ? 0n : wanted > caps[i] ? caps[i] : wanted;
|
|
69
|
+
rest -= next - shares[i];
|
|
70
|
+
shares[i] = next;
|
|
71
|
+
}
|
|
72
|
+
return shares;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Internal: `total` split across non-negative integer `weights` in integer units, exactly.
|
|
76
|
+
* Shares are multiples of `step`; a negative total is split by magnitude and negated;
|
|
77
|
+
* all-zero weights need a zero total. Throws `amount_invalid` (weights, caps, step) or
|
|
78
|
+
* `precision` (total not a multiple of step).
|
|
79
|
+
*/
|
|
80
|
+
function allocateUnits(total, weights, options = {}) {
|
|
81
|
+
const strategy = options.strategy ?? "largest-remainder";
|
|
82
|
+
const step = options.step ?? 1n;
|
|
83
|
+
if (step <= 0n) throw invalid(`Allocation step must be positive, got ${step}`);
|
|
84
|
+
if (weights.some((w) => w < 0n)) throw invalid("Allocation weights must not be negative");
|
|
85
|
+
if (total % step !== 0n) throw new require_money.MoneyError("precision", `${total} is not a whole number of ${step}-unit steps; round it first`);
|
|
86
|
+
const weightTotal = sumOf(weights);
|
|
87
|
+
const negative = total < 0n;
|
|
88
|
+
const magnitude = negative ? -total : total;
|
|
89
|
+
let shares;
|
|
90
|
+
if (weightTotal === 0n) {
|
|
91
|
+
if (magnitude !== 0n) throw invalid("Cannot allocate a non-zero total across zero weights");
|
|
92
|
+
shares = weights.map(() => 0n);
|
|
93
|
+
} else if (strategy === "last-first") {
|
|
94
|
+
const caps = options.caps;
|
|
95
|
+
if (!caps || caps.length !== weights.length || caps.some((c) => c < 0n)) throw invalid("'last-first' needs one non-negative cap per weight");
|
|
96
|
+
shares = lastFirst(magnitude, weights, weightTotal, step, caps);
|
|
97
|
+
} else {
|
|
98
|
+
const units = magnitude / step;
|
|
99
|
+
const split = strategy === "largest-weight" ? largestWeight : strategy === "last" ? lastAbsorbs : strategy === "cumulative" ? cumulative : strategy === "largest-remainder" ? largestRemainder : void 0;
|
|
100
|
+
if (!split) throw invalid(`Unknown allocation strategy: ${require_money.describeValue(strategy)}`);
|
|
101
|
+
shares = split(units, weights, weightTotal).map((s) => s * step);
|
|
102
|
+
}
|
|
103
|
+
return negative ? shares.map((s) => s === 0n ? 0n : -s) : shares;
|
|
104
|
+
}
|
|
105
|
+
/** Weights as integers on one common scale. Money weights must be in `currency` (when given). */
|
|
106
|
+
function weightUnits(weights, info) {
|
|
107
|
+
const values = weights.map((w) => {
|
|
108
|
+
if (typeof w !== "object" || w === null) return require_money.decimalOf(w, true, "weight");
|
|
109
|
+
const own = require_money.storedDecimal(w).info;
|
|
110
|
+
if (info && own.code !== info.code) throw new require_money.MoneyError("currency_mismatch", `Weight in ${own.code}, total in ${info.code}`);
|
|
111
|
+
return {
|
|
112
|
+
units: BigInt(w.amount),
|
|
113
|
+
scale: 0
|
|
114
|
+
};
|
|
115
|
+
});
|
|
116
|
+
const scale = Math.max(0, ...values.map((v) => v.scale));
|
|
117
|
+
return values.map((v) => require_money.roundToScale(v, scale, "down").units);
|
|
118
|
+
}
|
|
119
|
+
function stepOf(info, unit) {
|
|
120
|
+
if (typeof unit === "object" && unit !== null) {
|
|
121
|
+
if (!Number.isSafeInteger(unit.step) || unit.step < 1) throw invalid(`unit.step must be a positive integer, got ${require_money.describeValue(unit.step)}`);
|
|
122
|
+
return BigInt(unit.step);
|
|
123
|
+
}
|
|
124
|
+
return require_rounding.unitSize(info, unit ?? "storage");
|
|
125
|
+
}
|
|
126
|
+
function allocate(total, weights, options = {}) {
|
|
127
|
+
if (typeof total === "object" && total !== null) {
|
|
128
|
+
const { info } = require_money.storedDecimal(total);
|
|
129
|
+
const units = weightUnits(weights, info);
|
|
130
|
+
let caps;
|
|
131
|
+
if (options.strategy === "last-first") caps = (options.caps ?? (weights.every((w) => typeof w === "object" && w !== null) ? weights : void 0))?.map((c) => weightUnits([c], info)[0]);
|
|
132
|
+
return allocateUnits(BigInt(total.amount), units, {
|
|
133
|
+
strategy: options.strategy,
|
|
134
|
+
step: stepOf(info, options.unit),
|
|
135
|
+
caps
|
|
136
|
+
}).map((s) => require_money.boundedMoney(s, info));
|
|
137
|
+
}
|
|
138
|
+
const scale = options.scale;
|
|
139
|
+
if (scale === void 0 || !Number.isSafeInteger(scale) || scale < 0 || scale > 100) throw invalid(`scale must be an integer from 0 to 100, got ${require_money.describeValue(scale)}`);
|
|
140
|
+
if (options.strategy === "last-first") throw invalid("'last-first' needs caps: allocate Money instead");
|
|
141
|
+
const value = require_money.decimalOf(total, true, "total");
|
|
142
|
+
if (require_money.fractionDigitsOf(value) > scale) throw new require_money.MoneyError("precision", `${total} has more than ${scale} fraction digits`);
|
|
143
|
+
return allocateUnits(require_money.roundToScale(value, scale, "down").units, weightUnits(weights, null), { strategy: options.strategy }).map((s) => require_money.toDecimalText({
|
|
144
|
+
units: s,
|
|
145
|
+
scale
|
|
146
|
+
}));
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Greedy capped allocation in the given order: `take_i = min(left, cap_i)`; `shortfall` is
|
|
150
|
+
* what could not be placed. With `allOrNothing`, a shortfall allocates nothing (all zeros,
|
|
151
|
+
* `shortfall = amount`). Negative caps or amount throw `amount_invalid`; all one currency.
|
|
152
|
+
*/
|
|
153
|
+
function allocateWaterfall(caps, amount, options = {}) {
|
|
154
|
+
const { info } = require_money.storedDecimal(amount);
|
|
155
|
+
const limits = caps.map((c) => weightUnits([c], info)[0]);
|
|
156
|
+
let left = BigInt(amount.amount);
|
|
157
|
+
if (left < 0n || limits.some((c) => c < 0n)) throw invalid("Waterfall caps and amount must not be negative");
|
|
158
|
+
const takes = limits.map((cap) => {
|
|
159
|
+
const take = cap < left ? cap : left;
|
|
160
|
+
left -= take;
|
|
161
|
+
return take;
|
|
162
|
+
});
|
|
163
|
+
if (left > 0n && options.allOrNothing) return {
|
|
164
|
+
allocations: takes.map(() => require_money.boundedMoney(0n, info)),
|
|
165
|
+
shortfall: require_money.boundedMoney(BigInt(amount.amount), info)
|
|
166
|
+
};
|
|
167
|
+
return {
|
|
168
|
+
allocations: takes.map((t) => require_money.boundedMoney(t, info)),
|
|
169
|
+
shortfall: require_money.boundedMoney(left, info)
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* A line total charged at one unit price, or split in two parts one `unit` apart when it does
|
|
174
|
+
* not divide by `quantity`, so `sum(quantity x unitPrice) === net` exactly (JPY 100000 x 3
|
|
175
|
+
* gives `2 @ 33333`, `1 @ 33334`). Zero-quantity parts are omitted. `quantity` must be a
|
|
176
|
+
* positive integer; `net` a whole `unit` (else `precision`).
|
|
177
|
+
*/
|
|
178
|
+
function splitUnitPrice(net, quantity, options = {}) {
|
|
179
|
+
const { info } = require_money.storedDecimal(net);
|
|
180
|
+
if (!Number.isSafeInteger(quantity) || quantity < 1) throw invalid(`quantity must be a positive integer, got ${require_money.describeValue(quantity)}`);
|
|
181
|
+
const unit = require_rounding.unitSize(info, options.unit ?? "storage");
|
|
182
|
+
const amount = BigInt(net.amount);
|
|
183
|
+
if (amount % unit !== 0n) throw new require_money.MoneyError("precision", `${net.amount} is not a whole number of ${unit}-unit steps`);
|
|
184
|
+
const units = amount / unit;
|
|
185
|
+
const q = BigInt(quantity);
|
|
186
|
+
const per = require_money.divRound(units, q, "floor");
|
|
187
|
+
const extra = units - per * q;
|
|
188
|
+
const parts = [{
|
|
189
|
+
quantity: Number(q - extra),
|
|
190
|
+
unitPrice: require_money.boundedMoney(per * unit, info)
|
|
191
|
+
}];
|
|
192
|
+
if (extra > 0n) parts.push({
|
|
193
|
+
quantity: Number(extra),
|
|
194
|
+
unitPrice: require_money.boundedMoney((per + 1n) * unit, info)
|
|
195
|
+
});
|
|
196
|
+
return parts;
|
|
197
|
+
}
|
|
198
|
+
//#endregion
|
|
199
|
+
Object.defineProperty(exports, "allocate", {
|
|
200
|
+
enumerable: true,
|
|
201
|
+
get: function() {
|
|
202
|
+
return allocate;
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
Object.defineProperty(exports, "allocateUnits", {
|
|
206
|
+
enumerable: true,
|
|
207
|
+
get: function() {
|
|
208
|
+
return allocateUnits;
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
Object.defineProperty(exports, "allocateWaterfall", {
|
|
212
|
+
enumerable: true,
|
|
213
|
+
get: function() {
|
|
214
|
+
return allocateWaterfall;
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
Object.defineProperty(exports, "splitUnitPrice", {
|
|
218
|
+
enumerable: true,
|
|
219
|
+
get: function() {
|
|
220
|
+
return splitUnitPrice;
|
|
221
|
+
}
|
|
222
|
+
});
|