2020-07-21 13:20:51 +00:00
|
|
|
/**
|
|
|
|
* Here are the most relevant formats we want to support.
|
|
|
|
* -1234.56 Plain
|
|
|
|
* -1,234.56 Number (with separators)
|
|
|
|
* 12.34% Percent
|
|
|
|
* 1.23E3 Scientific
|
|
|
|
* $(1,234.56) Accounting
|
|
|
|
* (1,234.56) Financial
|
|
|
|
* -$1,234.56 Currency
|
|
|
|
*
|
|
|
|
* We implement a button-based UI, using one selector button to choose mode:
|
|
|
|
* none = NumMode undefined (plain number, no thousand separators)
|
|
|
|
* `$` = NumMode 'currency'
|
|
|
|
* `,` = NumMode 'decimal' (plain number, with thousand separators)
|
|
|
|
* `%` = NumMode 'percent'
|
|
|
|
* `Exp` = NumMode 'scientific'
|
|
|
|
* A second toggle button is `(-)` for Sign, to use parentheses rather than "-" for negative
|
|
|
|
* numbers. It is Ignored and disabled when mode is 'scientific'.
|
|
|
|
*/
|
|
|
|
|
|
|
|
import {clamp} from 'app/common/gutil';
|
2022-03-03 18:48:25 +00:00
|
|
|
import {StringUnion} from 'app/common/StringUnion';
|
2021-08-26 16:35:11 +00:00
|
|
|
import * as LocaleCurrency from "locale-currency";
|
|
|
|
import {FormatOptions} from 'app/common/ValueFormatter';
|
|
|
|
import {DocumentSettings} from 'app/common/DocumentSettings';
|
2020-07-21 13:20:51 +00:00
|
|
|
|
|
|
|
// Options for number formatting.
|
2022-03-03 18:48:25 +00:00
|
|
|
export const NumMode = StringUnion('currency', 'decimal', 'percent', 'scientific');
|
|
|
|
export type NumMode = typeof NumMode.type;
|
2020-07-21 13:20:51 +00:00
|
|
|
export type NumSign = 'parens';
|
|
|
|
|
2021-08-26 16:35:11 +00:00
|
|
|
export interface NumberFormatOptions extends FormatOptions {
|
2022-10-14 10:07:19 +00:00
|
|
|
numMode?: NumMode|null;
|
|
|
|
numSign?: NumSign|null;
|
|
|
|
decimals?: number|null; // aka minimum fraction digits
|
|
|
|
maxDecimals?: number|null;
|
|
|
|
currency?: string|null;
|
2020-07-21 13:20:51 +00:00
|
|
|
}
|
|
|
|
|
2021-10-19 18:59:13 +00:00
|
|
|
export function getCurrency(options: NumberFormatOptions, docSettings: DocumentSettings): string {
|
2022-04-27 17:46:24 +00:00
|
|
|
return options.currency || docSettings.currency || LocaleCurrency.getCurrency(docSettings.locale ?? 'en-US');
|
2021-10-19 18:59:13 +00:00
|
|
|
}
|
|
|
|
|
2021-08-26 16:35:11 +00:00
|
|
|
export function buildNumberFormat(options: NumberFormatOptions, docSettings: DocumentSettings): Intl.NumberFormat {
|
2021-10-19 18:59:13 +00:00
|
|
|
const currency = getCurrency(options, docSettings);
|
2021-08-26 16:35:11 +00:00
|
|
|
const nfOptions: Intl.NumberFormatOptions = parseNumMode(options.numMode, currency);
|
2020-07-21 13:20:51 +00:00
|
|
|
// numSign is implemented outside of Intl.NumberFormat since the latter's similar 'currencySign'
|
|
|
|
// option is not well-supported, and doesn't apply to non-currency formats.
|
|
|
|
|
2022-10-14 10:07:19 +00:00
|
|
|
if (options.decimals !== undefined && options.decimals !== null) {
|
2020-07-21 13:20:51 +00:00
|
|
|
// Should be at least 0
|
|
|
|
nfOptions.minimumFractionDigits = clamp(Number(options.decimals), 0, 20);
|
|
|
|
}
|
|
|
|
|
|
|
|
// maximumFractionDigits must not be less than the minimum, so we need to know the minimum
|
|
|
|
// implied by numMode.
|
2021-08-26 16:35:11 +00:00
|
|
|
const tmp = new Intl.NumberFormat(docSettings.locale, nfOptions).resolvedOptions();
|
2020-07-21 13:20:51 +00:00
|
|
|
|
2022-10-14 10:07:19 +00:00
|
|
|
if (options.maxDecimals !== undefined && options.maxDecimals !== null) {
|
2020-07-21 13:20:51 +00:00
|
|
|
// Should be at least 0 and at least minimumFractionDigits.
|
|
|
|
nfOptions.maximumFractionDigits = clamp(Number(options.maxDecimals), tmp.minimumFractionDigits || 0, 20);
|
|
|
|
} else if (!options.numMode) {
|
|
|
|
// For the default format, keep max digits at 10 as we had before.
|
|
|
|
nfOptions.maximumFractionDigits = clamp(10, tmp.minimumFractionDigits || 0, 20);
|
|
|
|
}
|
|
|
|
|
2021-08-26 16:35:11 +00:00
|
|
|
return new Intl.NumberFormat(docSettings.locale, nfOptions);
|
2020-07-21 13:20:51 +00:00
|
|
|
}
|
|
|
|
|
2021-11-04 09:01:37 +00:00
|
|
|
// Safari 13 and some other browsers don't support narrowSymbol option:
|
|
|
|
// https://github.com/mdn/browser-compat-data/issues/8985
|
|
|
|
// https://caniuse.com/?search=currencyDisplay
|
|
|
|
const currencyDisplay = (function(){
|
|
|
|
try {
|
|
|
|
new Intl.NumberFormat('en-US', {style: 'currency', currency: 'USD', currencyDisplay: 'narrowSymbol'});
|
|
|
|
return 'narrowSymbol';
|
|
|
|
} catch(err) {
|
|
|
|
return 'symbol';
|
|
|
|
}
|
|
|
|
})();
|
|
|
|
|
2022-10-14 10:07:19 +00:00
|
|
|
export function parseNumMode(numMode?: NumMode|null, currency?: string): Intl.NumberFormatOptions {
|
2020-07-21 13:20:51 +00:00
|
|
|
switch (numMode) {
|
2021-11-04 09:01:37 +00:00
|
|
|
case 'currency': return {style: 'currency', currency, currencyDisplay};
|
2020-07-21 13:20:51 +00:00
|
|
|
case 'decimal': return {useGrouping: true};
|
|
|
|
case 'percent': return {style: 'percent'};
|
|
|
|
// TODO 'notation' option (and therefore numMode 'scientific') works on recent Firefox and
|
|
|
|
// Chrome, not on Safari or Node 10.
|
|
|
|
case 'scientific': return {notation: 'scientific'} as Intl.NumberFormatOptions;
|
|
|
|
default: return {useGrouping: false};
|
|
|
|
}
|
|
|
|
}
|