Skip to content

Commit 8127579

Browse files
refactor(mf2): Add common readNumericOperand()
1 parent dc5601d commit 8127579

File tree

3 files changed

+60
-84
lines changed

3 files changed

+60
-84
lines changed

mf2/messageformat/src/functions/currency.ts

Lines changed: 8 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { MessageError, MessageResolutionError } from '../errors.js';
22
import type { MessageFunctionContext } from './index.js';
3-
import { type MessageNumber, number } from './number.js';
3+
import { type MessageNumber, number, readNumericOperand } from './number.js';
44
import { asPositiveInteger, asString } from './utils.js';
55

66
/**
@@ -12,32 +12,16 @@ import { asPositiveInteger, asString } from './utils.js';
1212
export function currency(
1313
ctx: MessageFunctionContext,
1414
exprOpt: Record<string | symbol, unknown>,
15-
input?: unknown
15+
operand?: unknown
1616
): MessageNumber {
1717
const { source } = ctx;
18+
const input = readNumericOperand(operand, source);
19+
const value = input.value;
1820
const options: Intl.NumberFormatOptions &
19-
Intl.PluralRulesOptions & { select?: 'exact' | 'cardinal' } = {
20-
localeMatcher: ctx.localeMatcher
21-
};
22-
let value = input;
23-
if (typeof value === 'object') {
24-
const valueOf = value?.valueOf;
25-
if (typeof valueOf === 'function') {
26-
Object.assign(options, (value as { options: unknown }).options);
27-
value = valueOf.call(value);
28-
}
29-
}
30-
if (typeof value === 'string') {
31-
try {
32-
value = JSON.parse(value);
33-
} catch {
34-
// handled below
35-
}
36-
}
37-
if (typeof value !== 'bigint' && typeof value !== 'number') {
38-
const msg = 'Input is not numeric';
39-
throw new MessageResolutionError('bad-operand', msg, source);
40-
}
21+
Intl.PluralRulesOptions & { select?: 'exact' | 'cardinal' } = Object.assign(
22+
{},
23+
input.options
24+
);
4125

4226
options.style = 'currency';
4327
for (const [name, optval] of Object.entries(exprOpt)) {

mf2/messageformat/src/functions/math.ts

Lines changed: 6 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { MessageResolutionError } from '../errors.js';
22
import type { MessageFunctionContext } from './index.js';
3-
import { MessageNumber, number } from './number.js';
3+
import { MessageNumber, number, readNumericOperand } from './number.js';
44
import { asPositiveInteger } from './utils.js';
55

66
/**
@@ -10,36 +10,17 @@ import { asPositiveInteger } from './utils.js';
1010
*/
1111
export function math(
1212
ctx: MessageFunctionContext,
13-
mathOpt: Record<string | symbol, unknown>,
14-
input?: unknown
13+
exprOpt: Record<string | symbol, unknown>,
14+
operand?: unknown
1515
): MessageNumber {
1616
const { source } = ctx;
17-
let options: unknown = undefined;
18-
let value = input;
19-
if (typeof value === 'object') {
20-
const valueOf = value?.valueOf;
21-
if (typeof valueOf === 'function') {
22-
options = (value as { options: unknown }).options;
23-
value = valueOf.call(value);
24-
}
25-
}
26-
if (typeof value === 'string') {
27-
try {
28-
value = JSON.parse(value);
29-
} catch {
30-
// handled below
31-
}
32-
}
33-
if (typeof value !== 'bigint' && typeof value !== 'number') {
34-
const msg = 'Input is not numeric';
35-
throw new MessageResolutionError('bad-operand', msg, source);
36-
}
17+
let { value, options } = readNumericOperand(operand, source);
3718

3819
let add: number;
3920
let sub: number;
4021
try {
41-
add = 'add' in mathOpt ? asPositiveInteger(mathOpt.add) : -1;
42-
sub = 'subtract' in mathOpt ? asPositiveInteger(mathOpt.subtract) : -1;
22+
add = 'add' in exprOpt ? asPositiveInteger(exprOpt.add) : -1;
23+
sub = 'subtract' in exprOpt ? asPositiveInteger(exprOpt.subtract) : -1;
4324
} catch (error) {
4425
throw new MessageResolutionError('bad-option', String(error), source);
4526
}

mf2/messageformat/src/functions/number.ts

Lines changed: 46 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -39,30 +39,15 @@ export interface MessageNumberPart extends MessageExpressionPart {
3939

4040
const INT = Symbol('INT');
4141

42-
/**
43-
* `number` accepts a number, BigInt or string representing a JSON number as input
44-
* and formats it with the same options as
45-
* {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat | Intl.NumberFormat}.
46-
* It also supports plural category selection via
47-
* {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules | Intl.PluralRules}.
48-
*
49-
* @beta
50-
*/
51-
export function number(
52-
ctx: MessageFunctionContext,
53-
options: Record<string | symbol, unknown>,
54-
input?: unknown
55-
): MessageNumber {
56-
const { source } = ctx;
57-
const opt: Intl.NumberFormatOptions &
58-
Intl.PluralRulesOptions & { select?: 'exact' | 'cardinal' | 'ordinal' } = {
59-
localeMatcher: ctx.localeMatcher
60-
};
61-
let value = input;
42+
export function readNumericOperand(
43+
value: unknown,
44+
source: string
45+
): { value: number | bigint; options: unknown } {
46+
let options: unknown = undefined;
6247
if (typeof value === 'object') {
6348
const valueOf = value?.valueOf;
6449
if (typeof valueOf === 'function') {
65-
Object.assign(opt, (value as { options: unknown }).options);
50+
options = (value as { options: unknown }).options;
6651
value = valueOf.call(value);
6752
}
6853
}
@@ -77,7 +62,33 @@ export function number(
7762
const msg = 'Input is not numeric';
7863
throw new MessageResolutionError('bad-operand', msg, source);
7964
}
80-
for (const [name, optval] of Object.entries(options)) {
65+
return { value, options };
66+
}
67+
68+
/**
69+
* `number` accepts a number, BigInt or string representing a JSON number as input
70+
* and formats it with the same options as
71+
* {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat | Intl.NumberFormat}.
72+
* It also supports plural category selection via
73+
* {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules | Intl.PluralRules}.
74+
*
75+
* @beta
76+
*/
77+
export function number(
78+
ctx: MessageFunctionContext,
79+
exprOpt: Record<string | symbol, unknown>,
80+
operand?: unknown
81+
): MessageNumber {
82+
const { source } = ctx;
83+
const options: Intl.NumberFormatOptions &
84+
Intl.PluralRulesOptions & { select?: 'exact' | 'cardinal' | 'ordinal' } = {
85+
localeMatcher: ctx.localeMatcher
86+
};
87+
const input = readNumericOperand(operand, source);
88+
const value = input.value;
89+
Object.assign(options, input.options);
90+
91+
for (const [name, optval] of Object.entries(exprOpt)) {
8192
if (optval === undefined) continue;
8293
try {
8394
switch (name) {
@@ -91,17 +102,17 @@ export function number(
91102
case 'maximumSignificantDigits':
92103
case 'roundingIncrement':
93104
// @ts-expect-error TS types don't know about roundingIncrement
94-
opt[name] = asPositiveInteger(optval);
105+
options[name] = asPositiveInteger(optval);
95106
break;
96107
case 'useGrouping': {
97108
const strval = asString(optval);
98109
// @ts-expect-error TS type is wrong
99-
opt[name] = strval === 'never' ? false : strval;
110+
options[name] = strval === 'never' ? false : strval;
100111
break;
101112
}
102113
default:
103114
// @ts-expect-error Unknown options will be ignored
104-
opt[name] = asString(optval);
115+
options[name] = asString(optval);
105116
}
106117
} catch {
107118
const msg = `Value ${optval} is not valid for :number option ${name}`;
@@ -110,11 +121,11 @@ export function number(
110121
}
111122

112123
const num =
113-
Number.isFinite(value) && options[INT]
124+
Number.isFinite(value) && exprOpt[INT]
114125
? Math.round(value as number)
115126
: value;
116127

117-
const lc = mergeLocales(ctx.locales, input, options);
128+
const lc = mergeLocales(ctx.locales, operand, exprOpt);
118129
let locale: string | undefined;
119130
let dir = ctx.dir;
120131
let nf: Intl.NumberFormat | undefined;
@@ -128,24 +139,24 @@ export function number(
128139
return dir;
129140
},
130141
get locale() {
131-
return (locale ??= Intl.NumberFormat.supportedLocalesOf(lc, opt)[0]);
142+
return (locale ??= Intl.NumberFormat.supportedLocalesOf(lc, options)[0]);
132143
},
133144
get options() {
134-
return { ...opt };
145+
return { ...options };
135146
},
136147
selectKey(keys) {
137148
const str = String(num);
138149
if (keys.has(str)) return str;
139-
if (opt.select === 'exact') return null;
140-
const pluralOpt = opt.select
141-
? { ...opt, select: undefined, type: opt.select }
142-
: opt;
150+
if (options.select === 'exact') return null;
151+
const pluralOpt = options.select
152+
? { ...options, select: undefined, type: options.select }
153+
: options;
143154
// Intl.PluralRules needs a number, not bigint
144155
cat ??= new Intl.PluralRules(lc, pluralOpt).select(Number(num));
145156
return keys.has(cat) ? cat : null;
146157
},
147158
toParts() {
148-
nf ??= new Intl.NumberFormat(lc, opt);
159+
nf ??= new Intl.NumberFormat(lc, options);
149160
const parts = nf.formatToParts(num);
150161
locale ??= nf.resolvedOptions().locale;
151162
dir ??= getLocaleDir(locale);
@@ -154,7 +165,7 @@ export function number(
154165
: [{ type: 'number', source, locale, parts }];
155166
},
156167
toString() {
157-
nf ??= new Intl.NumberFormat(lc, opt);
168+
nf ??= new Intl.NumberFormat(lc, options);
158169
str ??= nf.format(num);
159170
return str;
160171
},

0 commit comments

Comments
 (0)