Skip to content

Commit ef86b7b

Browse files
committed
Add support for compact notation
1 parent 914b73e commit ef86b7b

4 files changed

Lines changed: 70 additions & 34 deletions

File tree

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
"trailingComma": "none"
3232
},
3333
"dependencies": {
34-
"make-plural": "^8.0.0"
34+
"make-plural": "^8.1.0"
3535
},
3636
"devDependencies": {
3737
"@vitest/coverage-v8": "^4.0.16",

src/factory.ts

Lines changed: 47 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,11 @@ function toNumber(value: unknown) {
4646
}
4747

4848
export type Category = 'zero' | 'one' | 'two' | 'few' | 'many' | 'other'
49-
export type Selector = (n: number | string, ord?: boolean) => Category
49+
export type Selector = (
50+
n: number | string,
51+
ord?: boolean,
52+
compact?: number
53+
) => Category
5054
export type RangeSelector = (start: Category, end: Category) => Category
5155
export type PluralRuleType = 'cardinal' | 'ordinal'
5256

@@ -96,14 +100,8 @@ export type PluralRulesOptions = Partial<
96100
>
97101

98102
function readOptions(opt: PluralRulesOptions | undefined) {
99-
if (!opt)
100-
return {
101-
type: 'cardinal',
102-
compactDisplay: 'short',
103-
nfOpt: undefined
104-
} as const
105-
106103
const get = <T extends string>(name: string, values: T[]): T => {
104+
if (!opt) return values[0]
107105
const val = Object.prototype.hasOwnProperty.call(opt, name)
108106
? (opt as Record<string, unknown>)[name]
109107
: undefined
@@ -131,7 +129,7 @@ function readOptions(opt: PluralRulesOptions | undefined) {
131129
minimumSignificantDigits,
132130
maximumSignificantDigits,
133131
roundingIncrement
134-
} = opt
132+
} = opt ?? {}
135133
const roundingMode = get('roundingMode', [
136134
'halfExpand',
137135
'ceil',
@@ -154,12 +152,12 @@ function readOptions(opt: PluralRulesOptions | undefined) {
154152
])
155153

156154
return {
157-
type,
158-
compactDisplay,
155+
prOpt:
156+
notation === 'compact'
157+
? { type, notation, compactDisplay }
158+
: { type, notation },
159159
nfOpt: {
160160
localeMatcher,
161-
notation,
162-
compactDisplay: 'short',
163161
minimumIntegerDigits,
164162
minimumFractionDigits,
165163
maximumFractionDigits,
@@ -168,11 +166,14 @@ function readOptions(opt: PluralRulesOptions | undefined) {
168166
roundingIncrement,
169167
roundingMode,
170168
roundingPriority,
171-
trailingZeroDisplay
169+
trailingZeroDisplay,
170+
useGrouping: false
172171
}
173172
} as const
174173
}
175174

175+
const compactExponents: Record<string, number> = { K: 3, M: 6, B: 9, T: 12 }
176+
176177
export interface PluralRules {
177178
resolvedOptions(): ResolvedPluralRulesOptions
178179
select(n: number | string): Category
@@ -224,9 +225,14 @@ export default function getPluralRules(
224225
#locale: string
225226
#range: RangeSelector
226227
#select: Selector
227-
#type: 'cardinal' | 'ordinal'
228-
#compactDisplay: 'short' | 'long'
228+
#isOrdinal: boolean
229+
#opt: {
230+
compactDisplay?: 'short' | 'long'
231+
notation: 'standard' | 'scientific' | 'engineering' | 'compact'
232+
type: 'ordinal' | 'cardinal'
233+
}
229234
#nf: Intl.NumberFormat
235+
#nfCompact?: Intl.NumberFormat
230236

231237
constructor(
232238
locales: string | readonly string[] | undefined = undefined,
@@ -237,10 +243,17 @@ export default function getPluralRules(
237243
if (!this.#select)
238244
throw new Error(`Selector not found for locale: ${this.#locale}`)
239245
this.#range = getRangeSelector(this.#locale)
240-
const res = readOptions(opt)
241-
this.#nf = new NumberFormat('en', res.nfOpt) // make-plural expects latin digits with . decimal separator
242-
this.#type = res.type
243-
this.#compactDisplay = res.compactDisplay
246+
const { prOpt, nfOpt } = readOptions(opt)
247+
this.#isOrdinal = prOpt.type === 'ordinal'
248+
this.#opt = prOpt
249+
this.#nf = new NumberFormat('en', nfOpt) // make-plural expects latin digits with . decimal separator
250+
this.#nfCompact =
251+
prOpt.notation === 'compact'
252+
? new NumberFormat('en', {
253+
...nfOpt,
254+
notation: 'compact'
255+
})
256+
: undefined
244257
}
245258

246259
resolvedOptions(): ResolvedPluralRulesOptions {
@@ -250,23 +263,21 @@ export default function getPluralRules(
250263
maximumFractionDigits,
251264
minimumSignificantDigits,
252265
maximumSignificantDigits,
253-
notation,
254266
roundingIncrement,
255267
roundingMode,
256268
roundingPriority,
257269
trailingZeroDisplay
258270
} = this.#nf.resolvedOptions()
259271
const locale = this.#locale
260-
const type = this.#type
261272
return Object.assign(
262-
{ locale, type, notation },
263-
notation === 'compact' && { compactDisplay: this.#compactDisplay },
273+
{ locale },
274+
this.#opt,
264275
{ minimumIntegerDigits },
265276
typeof minimumSignificantDigits === 'number'
266277
? { minimumSignificantDigits, maximumSignificantDigits }
267278
: { minimumFractionDigits, maximumFractionDigits },
268279
{
269-
pluralCategories: getCategories(locale, type === 'ordinal').slice(0),
280+
pluralCategories: getCategories(locale, this.#isOrdinal).slice(0),
270281
roundingIncrement: roundingIncrement ?? 1,
271282
roundingMode: roundingMode ?? 'halfExpand',
272283
roundingPriority: roundingPriority ?? 'auto',
@@ -280,11 +291,18 @@ export default function getPluralRules(
280291
throw new TypeError(`select() called on incompatible ${this}`)
281292
if (typeof number !== 'number') number = Number(number)
282293
if (!isFinite(number)) return 'other'
283-
let fmt = ''
284-
for (const part of this.#nf.formatToParts(Math.abs(number))) {
285-
fmt += part.value
294+
let compact = 0
295+
if (this.#nfCompact) {
296+
for (const part of this.#nfCompact.formatToParts(Math.abs(number))) {
297+
if (part.type === 'compact') {
298+
compact = compactExponents[part.value]
299+
if (!compact)
300+
throw new RangeError(`Unsupported compact key: ${part.value}`)
301+
}
302+
}
286303
}
287-
return this.#select(fmt, this.#type === 'ordinal')
304+
const fmt = this.#nf.format(Math.abs(number))
305+
return this.#select(fmt, this.#isOrdinal, compact)
288306
}
289307

290308
selectRange(start: number | string, end: number | string): Category {

test/pluralrules.test.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,24 @@ describe('PluralRules', () => {
163163
expect(p0.select(10)).to.equal('other')
164164
expect(p1.select(10)).to.equal('many')
165165
})
166+
it('should work with French millions', () => {
167+
var prstandard = new PluralRules('fr', { notation: 'standard' })
168+
var prcompact = new PluralRules('fr', { notation: 'compact' })
169+
for (var { value, standard, compact } of [
170+
{ value: 1e6, standard: 'many', compact: 'many' },
171+
{ value: 1.5e6, standard: 'other', compact: 'many' },
172+
{ value: 1e-6, standard: 'one', compact: 'one' }
173+
]) {
174+
expect(prcompact.select(value)).to.equal(
175+
compact,
176+
`Compact notation: ${value}`
177+
)
178+
expect(prstandard.select(value)).to.equal(
179+
standard,
180+
`Standard notation: ${value}`
181+
)
182+
}
183+
})
166184
})
167185

168186
describe('#selectRange()', () => {

0 commit comments

Comments
 (0)