You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
342 lines
8.6 KiB
342 lines
8.6 KiB
import aliases from './aliases';
|
|
import parentLocales from './parentLocales';
|
|
import {invariant} from './invariant';
|
|
import {
|
|
NumberFormatDigitInternalSlots,
|
|
NumberFormatDigitOptions,
|
|
} from './number-types';
|
|
|
|
/**
|
|
* https://tc39.es/ecma262/#sec-toobject
|
|
* @param arg
|
|
*/
|
|
export function toObject<T>(
|
|
arg: T
|
|
): T extends null ? never : T extends undefined ? never : T {
|
|
if (arg == null) {
|
|
throw new TypeError('undefined/null cannot be converted to object');
|
|
}
|
|
return Object(arg);
|
|
}
|
|
|
|
/**
|
|
* https://tc39.es/ecma262/#sec-tostring
|
|
*/
|
|
export function toString(o: unknown): string {
|
|
// Only symbol is irregular...
|
|
if (typeof o === 'symbol') {
|
|
throw TypeError('Cannot convert a Symbol value to a string');
|
|
}
|
|
return String(o);
|
|
}
|
|
|
|
/**
|
|
* https://tc39.es/ecma402/#sec-getoption
|
|
* @param opts
|
|
* @param prop
|
|
* @param type
|
|
* @param values
|
|
* @param fallback
|
|
*/
|
|
export function getOption<T extends object, K extends keyof T, F>(
|
|
opts: T,
|
|
prop: K,
|
|
type: 'string' | 'boolean',
|
|
values: T[K][] | undefined,
|
|
fallback: F
|
|
): Exclude<T[K], undefined> | F {
|
|
// const descriptor = Object.getOwnPropertyDescriptor(opts, prop);
|
|
let value: any = opts[prop];
|
|
if (value !== undefined) {
|
|
if (type !== 'boolean' && type !== 'string') {
|
|
throw new TypeError('invalid type');
|
|
}
|
|
if (type === 'boolean') {
|
|
value = Boolean(value);
|
|
}
|
|
if (type === 'string') {
|
|
value = toString(value);
|
|
}
|
|
if (values !== undefined && !values.filter(val => val == value).length) {
|
|
throw new RangeError(`${value} is not within ${values.join(', ')}`);
|
|
}
|
|
return value;
|
|
}
|
|
return fallback;
|
|
}
|
|
|
|
/**
|
|
* https://tc39.es/ecma402/#sec-defaultnumberoption
|
|
* @param val
|
|
* @param min
|
|
* @param max
|
|
* @param fallback
|
|
*/
|
|
export function defaultNumberOption(
|
|
val: any,
|
|
min: number,
|
|
max: number,
|
|
fallback: number
|
|
) {
|
|
if (val !== undefined) {
|
|
val = Number(val);
|
|
if (isNaN(val) || val < min || val > max) {
|
|
throw new RangeError(`${val} is outside of range [${min}, ${max}]`);
|
|
}
|
|
return Math.floor(val);
|
|
}
|
|
return fallback;
|
|
}
|
|
|
|
/**
|
|
* https://tc39.es/ecma402/#sec-getnumberoption
|
|
* @param options
|
|
* @param property
|
|
* @param min
|
|
* @param max
|
|
* @param fallback
|
|
*/
|
|
|
|
export function getNumberOption<T extends object, K extends keyof T>(
|
|
options: T,
|
|
property: K,
|
|
minimum: number,
|
|
maximum: number,
|
|
fallback: number
|
|
): number {
|
|
const val = options[property];
|
|
return defaultNumberOption(val, minimum, maximum, fallback);
|
|
}
|
|
|
|
export function getAliasesByLang(lang: string): Record<string, string> {
|
|
return Object.keys(aliases).reduce((all: Record<string, string>, locale) => {
|
|
if (locale.split('-')[0] === lang) {
|
|
all[locale] = aliases[locale as 'zh-CN'];
|
|
}
|
|
return all;
|
|
}, {});
|
|
}
|
|
|
|
export function getParentLocalesByLang(lang: string): Record<string, string> {
|
|
return Object.keys(parentLocales).reduce(
|
|
(all: Record<string, string>, locale) => {
|
|
if (locale.split('-')[0] === lang) {
|
|
all[locale] = parentLocales[locale as 'en-150'];
|
|
}
|
|
return all;
|
|
},
|
|
{}
|
|
);
|
|
}
|
|
|
|
export function setInternalSlot<
|
|
Instance extends object,
|
|
Internal extends object,
|
|
Field extends keyof Internal
|
|
>(
|
|
map: WeakMap<Instance, Internal>,
|
|
pl: Instance,
|
|
field: Field,
|
|
value: NonNullable<Internal>[Field]
|
|
) {
|
|
if (!map.get(pl)) {
|
|
map.set(pl, Object.create(null));
|
|
}
|
|
const slots = map.get(pl)!;
|
|
slots[field] = value;
|
|
}
|
|
|
|
export function setMultiInternalSlots<
|
|
Instance extends object,
|
|
Internal extends object,
|
|
K extends keyof Internal
|
|
>(
|
|
map: WeakMap<Instance, Internal>,
|
|
pl: Instance,
|
|
props: Pick<NonNullable<Internal>, K>
|
|
) {
|
|
for (const k of Object.keys(props) as K[]) {
|
|
setInternalSlot(map, pl, k, props[k]);
|
|
}
|
|
}
|
|
|
|
export function getInternalSlot<
|
|
Instance extends object,
|
|
Internal extends object,
|
|
Field extends keyof Internal
|
|
>(
|
|
map: WeakMap<Instance, Internal>,
|
|
pl: Instance,
|
|
field: Field
|
|
): Internal[Field] {
|
|
return getMultiInternalSlots(map, pl, field)[field];
|
|
}
|
|
|
|
export function getMultiInternalSlots<
|
|
Instance extends object,
|
|
Internal extends object,
|
|
Field extends keyof Internal
|
|
>(
|
|
map: WeakMap<Instance, Internal>,
|
|
pl: Instance,
|
|
...fields: Field[]
|
|
): Pick<Internal, Field> {
|
|
const slots = map.get(pl);
|
|
if (!slots) {
|
|
throw new TypeError(`${pl} InternalSlot has not been initialized`);
|
|
}
|
|
return fields.reduce((all, f) => {
|
|
all[f] = slots[f];
|
|
return all;
|
|
}, Object.create(null) as Pick<Internal, Field>);
|
|
}
|
|
|
|
export interface LiteralPart {
|
|
type: 'literal';
|
|
value: string;
|
|
}
|
|
|
|
export function isLiteralPart(
|
|
patternPart: LiteralPart | {type: string; value?: string}
|
|
): patternPart is LiteralPart {
|
|
return patternPart.type === 'literal';
|
|
}
|
|
|
|
export function partitionPattern(pattern: string) {
|
|
const result = [];
|
|
let beginIndex = pattern.indexOf('{');
|
|
let endIndex = 0;
|
|
let nextIndex = 0;
|
|
const length = pattern.length;
|
|
while (beginIndex < pattern.length && beginIndex > -1) {
|
|
endIndex = pattern.indexOf('}', beginIndex);
|
|
invariant(endIndex > beginIndex, `Invalid pattern ${pattern}`);
|
|
if (beginIndex > nextIndex) {
|
|
result.push({
|
|
type: 'literal',
|
|
value: pattern.substring(nextIndex, beginIndex),
|
|
});
|
|
}
|
|
result.push({
|
|
type: pattern.substring(beginIndex + 1, endIndex),
|
|
value: undefined,
|
|
});
|
|
nextIndex = endIndex + 1;
|
|
beginIndex = pattern.indexOf('{', nextIndex);
|
|
}
|
|
if (nextIndex < length) {
|
|
result.push({
|
|
type: 'literal',
|
|
value: pattern.substring(nextIndex, length),
|
|
});
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* https://tc39.es/ecma402/#sec-setnfdigitoptions
|
|
* https://tc39.es/proposal-unified-intl-numberformat/section11/numberformat_diff_out.html#sec-setnfdigitoptions
|
|
* @param intlObj
|
|
* @param opts
|
|
* @param mnfdDefault
|
|
* @param mxfdDefault
|
|
*/
|
|
export function setNumberFormatDigitOptions<
|
|
TObject extends object,
|
|
TInternalSlots extends NumberFormatDigitInternalSlots
|
|
>(
|
|
internalSlotMap: WeakMap<TObject, TInternalSlots>,
|
|
intlObj: TObject,
|
|
opts: NumberFormatDigitOptions,
|
|
mnfdDefault: number,
|
|
mxfdDefault: number
|
|
) {
|
|
const mnid = getNumberOption(opts, 'minimumIntegerDigits', 1, 21, 1);
|
|
let mnfd = opts.minimumFractionDigits;
|
|
let mxfd = opts.maximumFractionDigits;
|
|
let mnsd = opts.minimumSignificantDigits;
|
|
let mxsd = opts.maximumSignificantDigits;
|
|
setInternalSlot(internalSlotMap, intlObj, 'minimumIntegerDigits', mnid);
|
|
if (mnsd !== undefined || mxsd !== undefined) {
|
|
setInternalSlot(
|
|
internalSlotMap,
|
|
intlObj,
|
|
'roundingType',
|
|
'significantDigits'
|
|
);
|
|
mnsd = defaultNumberOption(mnsd, 1, 21, 1);
|
|
mxsd = defaultNumberOption(mxsd, mnsd, 21, 21);
|
|
setInternalSlot(internalSlotMap, intlObj, 'minimumSignificantDigits', mnsd);
|
|
setInternalSlot(internalSlotMap, intlObj, 'maximumSignificantDigits', mxsd);
|
|
} else if (mnfd !== undefined || mxfd !== undefined) {
|
|
setInternalSlot(internalSlotMap, intlObj, 'roundingType', 'fractionDigits');
|
|
mnfd = defaultNumberOption(mnfd, 0, 20, mnfdDefault);
|
|
const mxfdActualDefault = Math.max(mnfd, mxfdDefault);
|
|
mxfd = defaultNumberOption(mxfd, mnfd, 20, mxfdActualDefault);
|
|
setInternalSlot(internalSlotMap, intlObj, 'minimumFractionDigits', mnfd);
|
|
setInternalSlot(internalSlotMap, intlObj, 'maximumFractionDigits', mxfd);
|
|
} else if (
|
|
getInternalSlot(internalSlotMap, intlObj, 'notation') === 'compact'
|
|
) {
|
|
setInternalSlot(
|
|
internalSlotMap,
|
|
intlObj,
|
|
'roundingType',
|
|
'compactRounding'
|
|
);
|
|
} else {
|
|
setInternalSlot(internalSlotMap, intlObj, 'roundingType', 'fractionDigits');
|
|
setInternalSlot(
|
|
internalSlotMap,
|
|
intlObj,
|
|
'minimumFractionDigits',
|
|
mnfdDefault
|
|
);
|
|
setInternalSlot(
|
|
internalSlotMap,
|
|
intlObj,
|
|
'maximumFractionDigits',
|
|
mxfdDefault
|
|
);
|
|
}
|
|
}
|
|
|
|
export function objectIs(x: any, y: any) {
|
|
if (Object.is) {
|
|
return Object.is(x, y);
|
|
}
|
|
// SameValue algorithm
|
|
if (x === y) {
|
|
// Steps 1-5, 7-10
|
|
// Steps 6.b-6.e: +0 != -0
|
|
return x !== 0 || 1 / x === 1 / y;
|
|
}
|
|
// Step 6.a: NaN == NaN
|
|
return x !== x && y !== y;
|
|
}
|
|
|
|
const NOT_A_Z_REGEX = /[^A-Z]/;
|
|
|
|
/**
|
|
* This follows https://tc39.es/ecma402/#sec-case-sensitivity-and-case-mapping
|
|
* @param str string to convert
|
|
*/
|
|
function toUpperCase(str: string): string {
|
|
return str.replace(/([a-z])/g, (_, c) => c.toUpperCase());
|
|
}
|
|
|
|
/**
|
|
* https://tc39.es/proposal-unified-intl-numberformat/section6/locales-currencies-tz_proposed_out.html#sec-iswellformedcurrencycode
|
|
* @param currency
|
|
*/
|
|
export function isWellFormedCurrencyCode(currency: string): boolean {
|
|
currency = toUpperCase(currency);
|
|
if (currency.length !== 3) {
|
|
return false;
|
|
}
|
|
if (NOT_A_Z_REGEX.test(currency)) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|