1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167 |
- 'use strict';
- var parser = require('@messageformat/parser');
- var Runtime = require('@messageformat/runtime');
- var Formatters = require('@messageformat/runtime/lib/formatters');
- var safeIdentifier = require('safe-identifier');
- function _interopNamespaceDefault(e) {
- var n = Object.create(null);
- if (e) {
- Object.keys(e).forEach(function (k) {
- if (k !== 'default') {
- var d = Object.getOwnPropertyDescriptor(e, k);
- Object.defineProperty(n, k, d.get ? d : {
- enumerable: true,
- get: function () { return e[k]; }
- });
- }
- });
- }
- n.default = e;
- return Object.freeze(n);
- }
- var Runtime__namespace = /*#__PURE__*/_interopNamespaceDefault(Runtime);
- var Formatters__namespace = /*#__PURE__*/_interopNamespaceDefault(Formatters);
- /**
- * Parent class for errors.
- *
- * @remarks
- * Errors with `type: "warning"` do not necessarily indicate that the parser
- * encountered an error. In addition to a human-friendly `message`, may also
- * includes the `token` at which the error was encountered.
- *
- * @public
- */
- class DateFormatError extends Error {
- /** @internal */
- constructor(msg, token, type) {
- super(msg);
- this.token = token;
- this.type = type || 'error';
- }
- }
- const alpha = (width) => width < 4 ? 'short' : width === 4 ? 'long' : 'narrow';
- const numeric = (width) => (width % 2 === 0 ? '2-digit' : 'numeric');
- function yearOptions(token, onError) {
- switch (token.char) {
- case 'y':
- return { year: numeric(token.width) };
- case 'r':
- return { calendar: 'gregory', year: 'numeric' };
- case 'u':
- case 'U':
- case 'Y':
- default:
- onError(`${token.desc} is not supported; falling back to year:numeric`, DateFormatError.WARNING);
- return { year: 'numeric' };
- }
- }
- function monthStyle(token, onError) {
- switch (token.width) {
- case 1:
- return 'numeric';
- case 2:
- return '2-digit';
- case 3:
- return 'short';
- case 4:
- return 'long';
- case 5:
- return 'narrow';
- default:
- onError(`${token.desc} is not supported with width ${token.width}`);
- return undefined;
- }
- }
- function dayStyle(token, onError) {
- const { char, desc, width } = token;
- if (char === 'd')
- return numeric(width);
- else {
- onError(`${desc} is not supported`);
- return undefined;
- }
- }
- function weekdayStyle(token, onError) {
- const { char, desc, width } = token;
- if ((char === 'c' || char === 'e') && width < 3) {
- // ignoring stand-alone-ness
- const msg = `Numeric value is not supported for ${desc}; falling back to weekday:short`;
- onError(msg, DateFormatError.WARNING);
- }
- // merging narrow styles
- return alpha(width);
- }
- function hourOptions(token) {
- const hour = numeric(token.width);
- let hourCycle;
- switch (token.char) {
- case 'h':
- hourCycle = 'h12';
- break;
- case 'H':
- hourCycle = 'h23';
- break;
- case 'k':
- hourCycle = 'h24';
- break;
- case 'K':
- hourCycle = 'h11';
- break;
- }
- return hourCycle ? { hour, hourCycle } : { hour };
- }
- function timeZoneNameStyle(token, onError) {
- // so much fallback behaviour here
- const { char, desc, width } = token;
- switch (char) {
- case 'v':
- case 'z':
- return width === 4 ? 'long' : 'short';
- case 'V':
- if (width === 4)
- return 'long';
- onError(`${desc} is not supported with width ${width}`);
- return undefined;
- case 'X':
- onError(`${desc} is not supported`);
- return undefined;
- }
- return 'short';
- }
- function compileOptions(token, onError) {
- switch (token.field) {
- case 'era':
- return { era: alpha(token.width) };
- case 'year':
- return yearOptions(token, onError);
- case 'month':
- return { month: monthStyle(token, onError) };
- case 'day':
- return { day: dayStyle(token, onError) };
- case 'weekday':
- return { weekday: weekdayStyle(token, onError) };
- case 'period':
- return undefined;
- case 'hour':
- return hourOptions(token);
- case 'min':
- return { minute: numeric(token.width) };
- case 'sec':
- return { second: numeric(token.width) };
- case 'tz':
- return { timeZoneName: timeZoneNameStyle(token, onError) };
- case 'quarter':
- case 'week':
- case 'sec-frac':
- case 'ms':
- onError(`${token.desc} is not supported`);
- }
- return undefined;
- }
- function getDateFormatOptions(tokens, onError = error => {
- throw error;
- }) {
- const options = {};
- const fields = [];
- for (const token of tokens) {
- const { error, field, str } = token;
- if (error) {
- const dte = new DateFormatError(error.message, token);
- dte.stack = error.stack;
- onError(dte);
- }
- if (str) {
- const msg = `Ignoring string part: ${str}`;
- onError(new DateFormatError(msg, token, DateFormatError.WARNING));
- }
- if (field) {
- if (fields.indexOf(field) === -1)
- fields.push(field);
- else
- onError(new DateFormatError(`Duplicate ${field} token`, token));
- }
- const opt = compileOptions(token, (msg, isWarning) => onError(new DateFormatError(msg, token, isWarning)));
- if (opt)
- Object.assign(options, opt);
- }
- return options;
- }
- const fields = {
- G: { field: 'era', desc: 'Era' },
- y: { field: 'year', desc: 'Year' },
- Y: { field: 'year', desc: 'Year of "Week of Year"' },
- u: { field: 'year', desc: 'Extended year' },
- U: { field: 'year', desc: 'Cyclic year name' },
- r: { field: 'year', desc: 'Related Gregorian year' },
- Q: { field: 'quarter', desc: 'Quarter' },
- q: { field: 'quarter', desc: 'Stand-alone quarter' },
- M: { field: 'month', desc: 'Month in year' },
- L: { field: 'month', desc: 'Stand-alone month in year' },
- w: { field: 'week', desc: 'Week of year' },
- W: { field: 'week', desc: 'Week of month' },
- d: { field: 'day', desc: 'Day in month' },
- D: { field: 'day', desc: 'Day of year' },
- F: { field: 'day', desc: 'Day of week in month' },
- g: { field: 'day', desc: 'Modified julian day' },
- E: { field: 'weekday', desc: 'Day of week' },
- e: { field: 'weekday', desc: 'Local day of week' },
- c: { field: 'weekday', desc: 'Stand-alone local day of week' },
- a: { field: 'period', desc: 'AM/PM marker' },
- b: { field: 'period', desc: 'AM/PM/noon/midnight marker' },
- B: { field: 'period', desc: 'Flexible day period' },
- h: { field: 'hour', desc: 'Hour in AM/PM (1~12)' },
- H: { field: 'hour', desc: 'Hour in day (0~23)' },
- k: { field: 'hour', desc: 'Hour in day (1~24)' },
- K: { field: 'hour', desc: 'Hour in AM/PM (0~11)' },
- j: { field: 'hour', desc: 'Hour in preferred cycle' },
- J: { field: 'hour', desc: 'Hour in preferred cycle without marker' },
- C: { field: 'hour', desc: 'Hour in preferred cycle with flexible marker' },
- m: { field: 'min', desc: 'Minute in hour' },
- s: { field: 'sec', desc: 'Second in minute' },
- S: { field: 'sec-frac', desc: 'Fractional second' },
- A: { field: 'ms', desc: 'Milliseconds in day' },
- z: { field: 'tz', desc: 'Time Zone: specific non-location' },
- Z: { field: 'tz', desc: 'Time Zone' },
- O: { field: 'tz', desc: 'Time Zone: localized' },
- v: { field: 'tz', desc: 'Time Zone: generic non-location' },
- V: { field: 'tz', desc: 'Time Zone: ID' },
- X: { field: 'tz', desc: 'Time Zone: ISO8601 with Z' },
- x: { field: 'tz', desc: 'Time Zone: ISO8601' }
- };
- const isLetter = (char) => (char >= 'A' && char <= 'Z') || (char >= 'a' && char <= 'z');
- function readFieldToken(src, pos) {
- const char = src[pos];
- let width = 1;
- while (src[++pos] === char)
- ++width;
- const field = fields[char];
- if (!field) {
- const msg = `The letter ${char} is not a valid field identifier`;
- return { char, error: new Error(msg), width };
- }
- return { char, field: field.field, desc: field.desc, width };
- }
- function readQuotedToken(src, pos) {
- let str = src[++pos];
- let width = 2;
- if (str === "'")
- return { char: "'", str, width };
- while (true) {
- const next = src[++pos];
- ++width;
- if (next === undefined) {
- const msg = `Unterminated quoted literal in pattern: ${str || src}`;
- return { char: "'", error: new Error(msg), str, width };
- }
- else if (next === "'") {
- if (src[++pos] !== "'")
- return { char: "'", str, width };
- else
- ++width;
- }
- str += next;
- }
- }
- function readToken(src, pos) {
- const char = src[pos];
- if (!char)
- return null;
- if (isLetter(char))
- return readFieldToken(src, pos);
- if (char === "'")
- return readQuotedToken(src, pos);
- let str = char;
- let width = 1;
- while (true) {
- const next = src[++pos];
- if (!next || isLetter(next) || next === "'")
- return { char, str, width };
- str += next;
- width += 1;
- }
- }
- /**
- * Parse an {@link http://userguide.icu-project.org/formatparse/datetime | ICU
- * DateFormat skeleton} string into a {@link DateToken} array.
- *
- * @remarks
- * Errors will not be thrown, but if encountered are included as the relevant
- * token's `error` value.
- *
- * @public
- * @param src - The skeleton string
- *
- * @example
- * ```js
- * import { parseDateTokens } from '@messageformat/date-skeleton'
- *
- * parseDateTokens('GrMMMdd', console.error)
- * // [
- * // { char: 'G', field: 'era', desc: 'Era', width: 1 },
- * // { char: 'r', field: 'year', desc: 'Related Gregorian year', width: 1 },
- * // { char: 'M', field: 'month', desc: 'Month in year', width: 3 },
- * // { char: 'd', field: 'day', desc: 'Day in month', width: 2 }
- * // ]
- * ```
- */
- function parseDateTokens(src) {
- const tokens = [];
- let pos = 0;
- while (true) {
- const token = readToken(src, pos);
- if (!token)
- return tokens;
- tokens.push(token);
- pos += token.width;
- }
- }
- /**
- * Returns a date formatter function for the given locales and date skeleton
- *
- * @remarks
- * Uses `Intl.DateTimeFormat` internally.
- *
- * @public
- * @param locales - One or more valid BCP 47 language tags, e.g. `fr` or `en-CA`
- * @param tokens - An ICU DateFormat skeleton string, or an array or parsed
- * `DateToken` tokens
- * @param onError - If defined, will be called separately for each encountered
- * parsing error and unsupported feature.
- * @example
- * ```js
- * import { getDateFormatter } from '@messageformat/date-skeleton'
- *
- * // 2006 Jan 2, 15:04:05.789 in local time
- * const date = new Date(2006, 0, 2, 15, 4, 5, 789)
- *
- * let fmt = getDateFormatter('en-CA', 'GrMMMdd', console.error)
- * fmt(date) // 'Jan. 02, 2006 AD'
- *
- * fmt = getDateFormatter('en-CA', 'hamszzzz', console.error)
- * fmt(date) // '3:04:05 p.m. Newfoundland Daylight Time'
- * ```
- */
- function getDateFormatter(locales, tokens, onError) {
- if (typeof tokens === 'string')
- tokens = parseDateTokens(tokens);
- const opt = getDateFormatOptions(tokens, onError);
- const dtf = new Intl.DateTimeFormat(locales, opt);
- return (date) => dtf.format(date);
- }
- /**
- * Returns a string of JavaScript source that evaluates to a date formatter
- * function with the same `(date: Date | number) => string` signature as the
- * function returned by {@link getDateFormatter}.
- *
- * @remarks
- * The returned function will memoize an `Intl.DateTimeFormat` instance.
- *
- * @public
- * @param locales - One or more valid BCP 47 language tags, e.g. `fr` or `en-CA`
- * @param tokens - An ICU DateFormat skeleton string, or an array or parsed
- * `DateToken` tokens
- * @param onError - If defined, will be called separately for each encountered
- * parsing error and unsupported feature.
- * @example
- * ```js
- * import { getDateFormatterSource } from '@messageformat/date-skeleton'
- *
- * getDateFormatterSource('en-CA', 'GrMMMdd', console.error)
- * // '(function() {\n' +
- * // ' var opt = {"era":"short","calendar":"gregory","year":"numeric",' +
- * // '"month":"short","day":"2-digit"};\n' +
- * // ' var dtf = new Intl.DateTimeFormat("en-CA", opt);\n' +
- * // ' return function(value) { return dtf.format(value); }\n' +
- * // '})()'
- *
- * const src = getDateFormatterSource('en-CA', 'hamszzzz', console.error)
- * // '(function() {\n' +
- * // ' var opt = {"hour":"numeric","hourCycle":"h12","minute":"numeric",' +
- * // '"second":"numeric","timeZoneName":"long"};\n' +
- * // ' var dtf = new Intl.DateTimeFormat("en-CA", opt);\n' +
- * // ' return function(value) { return dtf.format(value); }\n' +
- * // '})()'
- *
- * const fmt = new Function(`return ${src}`)()
- * const date = new Date(2006, 0, 2, 15, 4, 5, 789)
- * fmt(date) // '3:04:05 p.m. Newfoundland Daylight Time'
- * ```
- */
- function getDateFormatterSource(locales, tokens, onError) {
- if (typeof tokens === 'string')
- tokens = parseDateTokens(tokens);
- const opt = getDateFormatOptions(tokens, onError);
- const lines = [
- `(function() {`,
- `var opt = ${JSON.stringify(opt)};`,
- `var dtf = new Intl.DateTimeFormat(${JSON.stringify(locales)}, opt);`,
- `return function(value) { return dtf.format(value); }`
- ];
- return lines.join('\n ') + '\n})()';
- }
- /**
- * Base class for errors. In addition to a `code` and a human-friendly
- * `message`, may also includes the token `stem` as well as other fields.
- *
- * @public
- */
- class NumberFormatError extends Error {
- /** @internal */
- constructor(code, msg) {
- super(msg);
- this.code = code;
- }
- }
- /** @internal */
- class BadOptionError extends NumberFormatError {
- constructor(stem, opt) {
- super('BAD_OPTION', `Unknown ${stem} option: ${opt}`);
- this.stem = stem;
- this.option = opt;
- }
- }
- /** @internal */
- class BadStemError extends NumberFormatError {
- constructor(stem) {
- super('BAD_STEM', `Unknown stem: ${stem}`);
- this.stem = stem;
- }
- }
- /** @internal */
- class MaskedValueError extends NumberFormatError {
- constructor(type, prev) {
- super('MASKED_VALUE', `Value for ${type} is set multiple times`);
- this.type = type;
- this.prev = prev;
- }
- }
- /** @internal */
- class MissingOptionError extends NumberFormatError {
- constructor(stem) {
- super('MISSING_OPTION', `Required option missing for ${stem}`);
- this.stem = stem;
- }
- }
- /** @internal */
- class PatternError extends NumberFormatError {
- constructor(char, msg) {
- super('BAD_PATTERN', msg);
- this.char = char;
- }
- }
- /** @internal */
- class TooManyOptionsError extends NumberFormatError {
- constructor(stem, options, maxOpt) {
- const maxOptStr = maxOpt > 1 ? `${maxOpt} options` : 'one option';
- super('TOO_MANY_OPTIONS', `Token ${stem} only supports ${maxOptStr} (got ${options.length})`);
- this.stem = stem;
- this.options = options;
- }
- }
- /** @internal */
- class UnsupportedError extends NumberFormatError {
- constructor(stem, source) {
- super('UNSUPPORTED', `The stem ${stem} is not supported`);
- this.stem = stem;
- if (source) {
- this.message += ` with value ${source}`;
- this.source = source;
- }
- }
- }
- /**
- * Add
- * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl#Locale_identification_and_negotiation | numbering-system tags}
- * to locale identifiers
- *
- * @internal
- */
- function getNumberFormatLocales(locales, { numberingSystem }) {
- if (!Array.isArray(locales))
- locales = [locales];
- return numberingSystem
- ? locales
- .map(lc => {
- const ext = lc.indexOf('-u-') === -1 ? 'u-nu' : 'nu';
- return `${lc}-${ext}-${numberingSystem}`;
- })
- .concat(locales)
- : locales;
- }
- // from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/round
- function round(x, precision) {
- const y = +x + precision / 2;
- return y - (y % +precision);
- }
- function getNumberFormatMultiplier({ scale, unit }) {
- let mult = typeof scale === 'number' && scale >= 0 ? scale : 1;
- if (unit && unit.style === 'percent')
- mult *= 0.01;
- return mult;
- }
- /**
- * Determine a modifier for the input value to account for any `scale`,
- * `percent`, and `precision-increment` tokens in the skeleton.
- *
- * @internal
- * @remarks
- * With ICU NumberFormatter, the `percent` skeleton would style `25` as "25%".
- * To achieve the same with `Intl.NumberFormat`, the input value must be `0.25`.
- */
- function getNumberFormatModifier(skeleton) {
- const mult = getNumberFormatMultiplier(skeleton);
- const { precision } = skeleton;
- if (precision && precision.style === 'precision-increment') {
- return (n) => round(n, precision.increment) * mult;
- }
- else {
- return (n) => n * mult;
- }
- }
- /**
- * Returns a string of JavaScript source that evaluates to a modifier for the
- * input value to account for any `scale`, `percent`, and `precision-increment`
- * tokens in the skeleton.
- *
- * @internal
- * @remarks
- * With ICU NumberFormatter, the `percent` skeleton would style `25` as "25%".
- * To achieve the same with `Intl.NumberFormat`, the input value must be `0.25`.
- */
- function getNumberFormatModifierSource(skeleton) {
- const mult = getNumberFormatMultiplier(skeleton);
- const { precision } = skeleton;
- if (precision && precision.style === 'precision-increment') {
- // see round() above for source
- const setX = `+n + ${precision.increment / 2}`;
- let res = `x - (x % +${precision.increment})`;
- if (mult !== 1)
- res = `(${res}) * ${mult}`;
- return `function(n) { var x = ${setX}; return ${res}; }`;
- }
- return mult !== 1 ? `function(n) { return n * ${mult}; }` : null;
- }
- /**
- * Given an input ICU NumberFormatter skeleton, does its best to construct a
- * corresponding `Intl.NumberFormat` options structure.
- *
- * @remarks
- * Some features depend on `Intl.NumberFormat` features defined in ES2020.
- *
- * @internal
- * @param onUnsupported - If defined, called when encountering unsupported (but
- * valid) tokens, such as `decimal-always` or `permille`. The error `source`
- * may specify the source of an unsupported option.
- *
- * @example
- * ```js
- * import {
- * getNumberFormatOptions,
- * parseNumberSkeleton
- * } from '@messageformat/number-skeleton'
- *
- * const src = 'currency/CAD unit-width-narrow'
- * const skeleton = parseNumberSkeleton(src, console.error)
- * // {
- * // unit: { style: 'currency', currency: 'CAD' },
- * // unitWidth: 'unit-width-narrow'
- * // }
- *
- * getNumberFormatOptions(skeleton, console.error)
- * // {
- * // style: 'currency',
- * // currency: 'CAD',
- * // currencyDisplay: 'narrowSymbol',
- * // unitDisplay: 'narrow'
- * // }
- *
- * const sk2 = parseNumberSkeleton('group-min2')
- * // { group: 'group-min2' }
- *
- * getNumberFormatOptions(sk2, console.error)
- * // Error: The stem group-min2 is not supported
- * // at UnsupportedError.NumberFormatError ... {
- * // code: 'UNSUPPORTED',
- * // stem: 'group-min2'
- * // }
- * // {}
- * ```
- */
- function getNumberFormatOptions(skeleton, onUnsupported) {
- const { decimal, group, integerWidth, notation, precision, roundingMode, sign, unit, unitPer, unitWidth } = skeleton;
- const fail = (stem, source) => {
- if (onUnsupported)
- onUnsupported(new UnsupportedError(stem, source));
- };
- const opt = {};
- if (unit) {
- switch (unit.style) {
- case 'base-unit':
- opt.style = 'decimal';
- break;
- case 'currency':
- opt.style = 'currency';
- opt.currency = unit.currency;
- break;
- case 'measure-unit':
- opt.style = 'unit';
- opt.unit = unit.unit.replace(/.*-/, '');
- if (unitPer)
- opt.unit += '-per-' + unitPer.replace(/.*-/, '');
- break;
- case 'percent':
- opt.style = 'percent';
- break;
- case 'permille':
- fail('permille');
- break;
- }
- }
- switch (unitWidth) {
- case 'unit-width-full-name':
- opt.currencyDisplay = 'name';
- opt.unitDisplay = 'long';
- break;
- case 'unit-width-hidden':
- fail(unitWidth);
- break;
- case 'unit-width-iso-code':
- opt.currencyDisplay = 'code';
- break;
- case 'unit-width-narrow':
- opt.currencyDisplay = 'narrowSymbol';
- opt.unitDisplay = 'narrow';
- break;
- case 'unit-width-short':
- opt.currencyDisplay = 'symbol';
- opt.unitDisplay = 'short';
- break;
- }
- switch (group) {
- case 'group-off':
- opt.useGrouping = false;
- break;
- case 'group-auto':
- opt.useGrouping = true;
- break;
- case 'group-min2':
- case 'group-on-aligned':
- case 'group-thousands':
- fail(group);
- opt.useGrouping = true;
- break;
- }
- if (precision) {
- switch (precision.style) {
- case 'precision-fraction': {
- const { minFraction: minF, maxFraction: maxF, minSignificant: minS, maxSignificant: maxS, source } = precision;
- if (typeof minF === 'number') {
- opt.minimumFractionDigits = minF;
- if (typeof minS === 'number')
- fail('precision-fraction', source);
- }
- if (typeof maxF === 'number')
- opt.maximumFractionDigits = maxF;
- if (typeof minS === 'number')
- opt.minimumSignificantDigits = minS;
- if (typeof maxS === 'number')
- opt.maximumSignificantDigits = maxS;
- break;
- }
- case 'precision-integer':
- opt.maximumFractionDigits = 0;
- break;
- case 'precision-unlimited':
- opt.maximumFractionDigits = 20;
- break;
- case 'precision-increment':
- break;
- case 'precision-currency-standard':
- opt.trailingZeroDisplay = precision.trailingZero;
- break;
- case 'precision-currency-cash':
- fail(precision.style);
- break;
- }
- }
- if (notation) {
- switch (notation.style) {
- case 'compact-short':
- opt.notation = 'compact';
- opt.compactDisplay = 'short';
- break;
- case 'compact-long':
- opt.notation = 'compact';
- opt.compactDisplay = 'long';
- break;
- case 'notation-simple':
- opt.notation = 'standard';
- break;
- case 'scientific':
- case 'engineering': {
- const { expDigits, expSign, source, style } = notation;
- opt.notation = style;
- if ((expDigits && expDigits > 1) ||
- (expSign && expSign !== 'sign-auto'))
- fail(style, source);
- break;
- }
- }
- }
- if (integerWidth) {
- const { min, max, source } = integerWidth;
- if (min > 0)
- opt.minimumIntegerDigits = min;
- if (Number(max) > 0) {
- const hasExp = opt.notation === 'engineering' || opt.notation === 'scientific';
- if (max === 3 && hasExp)
- opt.notation = 'engineering';
- else
- fail('integer-width', source);
- }
- }
- switch (sign) {
- case 'sign-auto':
- opt.signDisplay = 'auto';
- break;
- case 'sign-always':
- opt.signDisplay = 'always';
- break;
- case 'sign-except-zero':
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
- // @ts-ignore https://github.com/microsoft/TypeScript/issues/46712
- opt.signDisplay = 'exceptZero';
- break;
- case 'sign-never':
- opt.signDisplay = 'never';
- break;
- case 'sign-accounting':
- opt.currencySign = 'accounting';
- break;
- case 'sign-accounting-always':
- opt.currencySign = 'accounting';
- opt.signDisplay = 'always';
- break;
- case 'sign-accounting-except-zero':
- opt.currencySign = 'accounting';
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
- // @ts-ignore https://github.com/microsoft/TypeScript/issues/46712
- opt.signDisplay = 'exceptZero';
- break;
- }
- if (decimal === 'decimal-always')
- fail(decimal);
- if (roundingMode)
- fail(roundingMode);
- return opt;
- }
- function parseAffixToken(src, pos, onError) {
- const char = src[pos];
- switch (char) {
- case '%':
- return { char: '%', style: 'percent', width: 1 };
- case '‰':
- return { char: '%', style: 'permille', width: 1 };
- case '¤': {
- let width = 1;
- while (src[++pos] === '¤')
- ++width;
- switch (width) {
- case 1:
- return { char, currency: 'default', width };
- case 2:
- return { char, currency: 'iso-code', width };
- case 3:
- return { char, currency: 'full-name', width };
- case 5:
- return { char, currency: 'narrow', width };
- default: {
- const msg = `Invalid number (${width}) of ¤ chars in pattern`;
- onError(new PatternError('¤', msg));
- return null;
- }
- }
- }
- case '*': {
- const pad = src[pos + 1];
- if (pad)
- return { char, pad, width: 2 };
- break;
- }
- case '+':
- case '-':
- return { char, width: 1 };
- case "'": {
- let str = src[++pos];
- let width = 2;
- if (str === "'")
- return { char, str, width };
- while (true) {
- const next = src[++pos];
- ++width;
- if (next === undefined) {
- const msg = `Unterminated quoted literal in pattern: ${str}`;
- onError(new PatternError("'", msg));
- return { char, str, width };
- }
- else if (next === "'") {
- if (src[++pos] !== "'")
- return { char, str, width };
- else
- ++width;
- }
- str += next;
- }
- }
- }
- return null;
- }
- const isDigit = (char) => char >= '0' && char <= '9';
- function parseNumberToken(src, pos) {
- const char = src[pos];
- if (isDigit(char)) {
- let digits = char;
- while (true) {
- const next = src[++pos];
- if (isDigit(next))
- digits += next;
- else
- return { char: '0', digits, width: digits.length };
- }
- }
- switch (char) {
- case '#': {
- let width = 1;
- while (src[++pos] === '#')
- ++width;
- return { char, width };
- }
- case '@': {
- let min = 1;
- while (src[++pos] === '@')
- ++min;
- let width = min;
- pos -= 1;
- while (src[++pos] === '#')
- ++width;
- return { char, min, width };
- }
- case 'E': {
- const plus = src[pos + 1] === '+';
- if (plus)
- ++pos;
- let expDigits = 0;
- while (src[++pos] === '0')
- ++expDigits;
- const width = (plus ? 2 : 1) + expDigits;
- if (expDigits)
- return { char, expDigits, plus, width };
- else
- break;
- }
- case '.':
- case ',':
- return { char, width: 1 };
- }
- return null;
- }
- function parseSubpattern(src, pos, onError) {
- let State;
- (function (State) {
- State[State["Prefix"] = 0] = "Prefix";
- State[State["Number"] = 1] = "Number";
- State[State["Suffix"] = 2] = "Suffix";
- })(State || (State = {}));
- const prefix = [];
- const number = [];
- const suffix = [];
- let state = State.Prefix;
- let str = '';
- while (pos < src.length) {
- const char = src[pos];
- if (char === ';') {
- pos += 1;
- break;
- }
- switch (state) {
- case State.Prefix: {
- const token = parseAffixToken(src, pos, onError);
- if (token) {
- if (str) {
- prefix.push({ char: "'", str, width: str.length });
- str = '';
- }
- prefix.push(token);
- pos += token.width;
- }
- else {
- const token = parseNumberToken(src, pos);
- if (token) {
- if (str) {
- prefix.push({ char: "'", str, width: str.length });
- str = '';
- }
- state = State.Number;
- number.push(token);
- pos += token.width;
- }
- else {
- str += char;
- pos += 1;
- }
- }
- break;
- }
- case State.Number: {
- const token = parseNumberToken(src, pos);
- if (token) {
- number.push(token);
- pos += token.width;
- }
- else {
- state = State.Suffix;
- }
- break;
- }
- case State.Suffix: {
- const token = parseAffixToken(src, pos, onError);
- if (token) {
- if (str) {
- suffix.push({ char: "'", str, width: str.length });
- str = '';
- }
- suffix.push(token);
- pos += token.width;
- }
- else {
- str += char;
- pos += 1;
- }
- break;
- }
- }
- }
- if (str)
- suffix.push({ char: "'", str, width: str.length });
- return { pattern: { prefix, number, suffix }, pos };
- }
- function parseTokens(src, onError) {
- const { pattern, pos } = parseSubpattern(src, 0, onError);
- if (pos < src.length) {
- const { pattern: negative } = parseSubpattern(src, pos, onError);
- return { tokens: pattern, negative };
- }
- return { tokens: pattern };
- }
- function parseNumberAsSkeleton(tokens, onError) {
- const res = {};
- let hasGroups = false;
- let hasExponent = false;
- let intOptional = 0;
- let intDigits = '';
- let decimalPos = -1;
- let fracDigits = '';
- let fracOptional = 0;
- for (let pos = 0; pos < tokens.length; ++pos) {
- const token = tokens[pos];
- switch (token.char) {
- case '#': {
- if (decimalPos === -1) {
- if (intDigits) {
- const msg = 'Pattern has # after integer digits';
- onError(new PatternError('#', msg));
- }
- intOptional += token.width;
- }
- else {
- fracOptional += token.width;
- }
- break;
- }
- case '0': {
- if (decimalPos === -1) {
- intDigits += token.digits;
- }
- else {
- if (fracOptional) {
- const msg = 'Pattern has digits after # in fraction';
- onError(new PatternError('0', msg));
- }
- fracDigits += token.digits;
- }
- break;
- }
- case '@': {
- if (res.precision)
- onError(new MaskedValueError('precision', res.precision));
- res.precision = {
- style: 'precision-fraction',
- minSignificant: token.min,
- maxSignificant: token.width
- };
- break;
- }
- case ',':
- hasGroups = true;
- break;
- case '.':
- if (decimalPos === 1) {
- const msg = 'Pattern has more than one decimal separator';
- onError(new PatternError('.', msg));
- }
- decimalPos = pos;
- break;
- case 'E': {
- if (hasExponent)
- onError(new MaskedValueError('exponent', res.notation));
- if (hasGroups) {
- const msg = 'Exponential patterns may not contain grouping separators';
- onError(new PatternError('E', msg));
- }
- res.notation = { style: 'scientific' };
- if (token.expDigits > 1)
- res.notation.expDigits = token.expDigits;
- if (token.plus)
- res.notation.expSign = 'sign-always';
- hasExponent = true;
- }
- }
- }
- // imprecise mapping due to paradigm differences
- if (hasGroups)
- res.group = 'group-auto';
- else if (intOptional + intDigits.length > 3)
- res.group = 'group-off';
- const increment = Number(`${intDigits || '0'}.${fracDigits}`);
- if (increment)
- res.precision = { style: 'precision-increment', increment };
- if (!hasExponent) {
- if (intDigits.length > 1)
- res.integerWidth = { min: intDigits.length };
- if (!res.precision && (fracDigits.length || fracOptional)) {
- res.precision = {
- style: 'precision-fraction',
- minFraction: fracDigits.length,
- maxFraction: fracDigits.length + fracOptional
- };
- }
- }
- else {
- if (!res.precision || increment) {
- res.integerWidth = intOptional
- ? { min: 1, max: intOptional + intDigits.length }
- : { min: Math.max(1, intDigits.length) };
- }
- if (res.precision) {
- if (!increment)
- res.integerWidth = { min: 1, max: 1 };
- }
- else {
- const dc = intDigits.length + fracDigits.length;
- if (decimalPos === -1) {
- if (dc > 0)
- res.precision = { style: 'precision-fraction', maxSignificant: dc };
- }
- else {
- res.precision = {
- style: 'precision-fraction',
- maxSignificant: Math.max(1, dc) + fracOptional
- };
- if (dc > 1)
- res.precision.minSignificant = dc;
- }
- }
- }
- return res;
- }
- function handleAffix(affixTokens, res, currency, onError, isPrefix) {
- let inFmt = false;
- let str = '';
- for (const token of affixTokens) {
- switch (token.char) {
- case '%':
- res.unit = { style: token.style };
- if (isPrefix)
- inFmt = true;
- else
- str = '';
- break;
- case '¤':
- if (!currency) {
- const msg = `The ¤ pattern requires a currency`;
- onError(new PatternError('¤', msg));
- break;
- }
- res.unit = { style: 'currency', currency };
- switch (token.currency) {
- case 'iso-code':
- res.unitWidth = 'unit-width-iso-code';
- break;
- case 'full-name':
- res.unitWidth = 'unit-width-full-name';
- break;
- case 'narrow':
- res.unitWidth = 'unit-width-narrow';
- break;
- }
- if (isPrefix)
- inFmt = true;
- else
- str = '';
- break;
- case '*':
- // TODO
- break;
- case '+':
- if (!inFmt)
- str += '+';
- break;
- case "'":
- if (!inFmt)
- str += token.str;
- break;
- }
- }
- return str;
- }
- function getNegativeAffix(affixTokens, isPrefix) {
- let inFmt = false;
- let str = '';
- for (const token of affixTokens) {
- switch (token.char) {
- case '%':
- case '¤':
- if (isPrefix)
- inFmt = true;
- else
- str = '';
- break;
- case '-':
- if (!inFmt)
- str += '-';
- break;
- case "'":
- if (!inFmt)
- str += token.str;
- break;
- }
- }
- return str;
- }
- /**
- * Parse an {@link
- * http://unicode.org/reports/tr35/tr35-numbers.html#Number_Format_Patterns |
- * ICU NumberFormatter pattern} string into a {@link Skeleton} structure.
- *
- * @public
- * @param src - The pattern string
- * @param currency - If the pattern includes ¤ tokens, their skeleton
- * representation requires a three-letter currency code.
- * @param onError - Called when the parser encounters a syntax error. The
- * function will still return a {@link Skeleton}, but it will be incomplete
- * and/or inaccurate. If not defined, the error will be thrown instead.
- *
- * @remarks
- * Unlike the skeleton parser, the pattern parser is not able to return partial
- * results on error, and will instead throw. Output padding is not supported.
- *
- * @example
- * ```js
- * import { parseNumberPattern } from '@messageformat/number-skeleton'
- *
- * parseNumberPattern('#,##0.00 ¤', 'EUR', console.error)
- * // {
- * // group: 'group-auto',
- * // precision: {
- * // style: 'precision-fraction',
- * // minFraction: 2,
- * // maxFraction: 2
- * // },
- * // unit: { style: 'currency', currency: 'EUR' }
- * // }
- * ```
- */
- function parseNumberPattern(src, currency, onError = error => {
- throw error;
- }) {
- const { tokens, negative } = parseTokens(src, onError);
- const res = parseNumberAsSkeleton(tokens.number, onError);
- const prefix = handleAffix(tokens.prefix, res, currency, onError, true);
- const suffix = handleAffix(tokens.suffix, res, currency, onError, false);
- if (negative) {
- const negPrefix = getNegativeAffix(negative.prefix, true);
- const negSuffix = getNegativeAffix(negative.suffix, false);
- res.affix = { pos: [prefix, suffix], neg: [negPrefix, negSuffix] };
- res.sign = 'sign-never';
- }
- else if (prefix || suffix) {
- res.affix = { pos: [prefix, suffix] };
- }
- return res;
- }
- /** @internal */
- function isNumberingSystem(ns) {
- const systems = [
- 'arab',
- 'arabext',
- 'bali',
- 'beng',
- 'deva',
- 'fullwide',
- 'gujr',
- 'guru',
- 'hanidec',
- 'khmr',
- 'knda',
- 'laoo',
- 'latn',
- 'limb',
- 'mlym',
- 'mong',
- 'mymr',
- 'orya',
- 'tamldec',
- 'telu',
- 'thai',
- 'tibt'
- ];
- return systems.indexOf(ns) !== -1;
- }
- // FIXME: subtype is not checked
- /** @internal */
- function isUnit(unit) {
- const types = [
- 'acceleration',
- 'angle',
- 'area',
- 'concentr',
- 'consumption',
- 'digital',
- 'duration',
- 'electric',
- 'energy',
- 'force',
- 'frequency',
- 'graphics',
- 'length',
- 'light',
- 'mass',
- 'power',
- 'pressure',
- 'speed',
- 'temperature',
- 'torque',
- 'volume'
- ];
- const [type] = unit.split('-', 1);
- return types.indexOf(type) !== -1;
- }
- const maxOptions = {
- 'compact-short': 0,
- 'compact-long': 0,
- 'notation-simple': 0,
- scientific: 2,
- engineering: 2,
- percent: 0,
- permille: 0,
- 'base-unit': 0,
- currency: 1,
- 'measure-unit': 1,
- 'per-measure-unit': 1,
- 'unit-width-narrow': 0,
- 'unit-width-short': 0,
- 'unit-width-full-name': 0,
- 'unit-width-iso-code': 0,
- 'unit-width-hidden': 0,
- 'precision-integer': 0,
- 'precision-unlimited': 0,
- 'precision-currency-standard': 1,
- 'precision-currency-cash': 0,
- 'precision-increment': 1,
- 'rounding-mode-ceiling': 0,
- 'rounding-mode-floor': 0,
- 'rounding-mode-down': 0,
- 'rounding-mode-up': 0,
- 'rounding-mode-half-even': 0,
- 'rounding-mode-half-down': 0,
- 'rounding-mode-half-up': 0,
- 'rounding-mode-unnecessary': 0,
- 'integer-width': 1,
- scale: 1,
- 'group-off': 0,
- 'group-min2': 0,
- 'group-auto': 0,
- 'group-on-aligned': 0,
- 'group-thousands': 0,
- latin: 0,
- 'numbering-system': 1,
- 'sign-auto': 0,
- 'sign-always': 0,
- 'sign-never': 0,
- 'sign-accounting': 0,
- 'sign-accounting-always': 0,
- 'sign-except-zero': 0,
- 'sign-accounting-except-zero': 0,
- 'decimal-auto': 0,
- 'decimal-always': 0
- };
- const minOptions = {
- currency: 1,
- 'integer-width': 1,
- 'measure-unit': 1,
- 'numbering-system': 1,
- 'per-measure-unit': 1,
- 'precision-increment': 1,
- scale: 1
- };
- function hasMaxOption(stem) {
- return stem in maxOptions;
- }
- function hasMinOption(stem) {
- return stem in minOptions;
- }
- /** @internal */
- function validOptions(stem, options, onError) {
- if (hasMaxOption(stem)) {
- const maxOpt = maxOptions[stem];
- if (options.length > maxOpt) {
- if (maxOpt === 0) {
- for (const opt of options)
- onError(new BadOptionError(stem, opt));
- }
- else {
- onError(new TooManyOptionsError(stem, options, maxOpt));
- }
- return false;
- }
- else if (hasMinOption(stem) && options.length < minOptions[stem]) {
- onError(new MissingOptionError(stem));
- return false;
- }
- }
- return true;
- }
- function parseBlueprintDigits(src, style) {
- const re = style === 'fraction' ? /^\.(0*)(\+|#*)$/ : /^(@+)(\+|#*)$/;
- const match = src && src.match(re);
- if (match) {
- const min = match[1].length;
- switch (match[2].charAt(0)) {
- case '':
- return { min, max: min };
- case '+':
- return { min, max: null };
- case '#': {
- return { min, max: min + match[2].length };
- }
- }
- }
- return null;
- }
- function parsePrecisionBlueprint(stem, options, onError) {
- const fd = parseBlueprintDigits(stem, 'fraction');
- if (fd) {
- if (options.length > 1)
- onError(new TooManyOptionsError(stem, options, 1));
- const res = {
- style: 'precision-fraction',
- source: stem,
- minFraction: fd.min
- };
- if (fd.max != null)
- res.maxFraction = fd.max;
- const option = options[0];
- const sd = parseBlueprintDigits(option, 'significant');
- if (sd) {
- res.source = `${stem}/${option}`;
- res.minSignificant = sd.min;
- if (sd.max != null)
- res.maxSignificant = sd.max;
- }
- else if (option)
- onError(new BadOptionError(stem, option));
- return res;
- }
- const sd = parseBlueprintDigits(stem, 'significant');
- if (sd) {
- for (const opt of options)
- onError(new BadOptionError(stem, opt));
- const res = {
- style: 'precision-fraction',
- source: stem,
- minSignificant: sd.min
- };
- if (sd.max != null)
- res.maxSignificant = sd.max;
- return res;
- }
- return null;
- }
- /** @internal */
- class TokenParser {
- constructor(onError) {
- this.skeleton = {};
- this.onError = onError;
- }
- badOption(stem, opt) {
- this.onError(new BadOptionError(stem, opt));
- }
- assertEmpty(key) {
- const prev = this.skeleton[key];
- if (prev)
- this.onError(new MaskedValueError(key, prev));
- }
- parseToken(stem, options) {
- if (!validOptions(stem, options, this.onError))
- return;
- const option = options[0];
- const res = this.skeleton;
- switch (stem) {
- // notation
- case 'compact-short':
- case 'compact-long':
- case 'notation-simple':
- this.assertEmpty('notation');
- res.notation = { style: stem };
- break;
- case 'scientific':
- case 'engineering': {
- let expDigits = null;
- let expSign = undefined;
- for (const opt of options) {
- switch (opt) {
- case 'sign-auto':
- case 'sign-always':
- case 'sign-never':
- case 'sign-accounting':
- case 'sign-accounting-always':
- case 'sign-except-zero':
- case 'sign-accounting-except-zero':
- expSign = opt;
- break;
- default:
- if (/^\+e+$/.test(opt))
- expDigits = opt.length - 1;
- else {
- this.badOption(stem, opt);
- }
- }
- }
- this.assertEmpty('notation');
- const source = options.join('/');
- res.notation =
- expDigits && expSign
- ? { style: stem, source, expDigits, expSign }
- : expDigits
- ? { style: stem, source, expDigits }
- : expSign
- ? { style: stem, source, expSign }
- : { style: stem, source };
- break;
- }
- // unit
- case 'percent':
- case 'permille':
- case 'base-unit':
- this.assertEmpty('unit');
- res.unit = { style: stem };
- break;
- case 'currency':
- if (/^[A-Z]{3}$/.test(option)) {
- this.assertEmpty('unit');
- res.unit = { style: stem, currency: option };
- }
- else
- this.badOption(stem, option);
- break;
- case 'measure-unit': {
- if (isUnit(option)) {
- this.assertEmpty('unit');
- res.unit = { style: stem, unit: option };
- }
- else
- this.badOption(stem, option);
- break;
- }
- // unitPer
- case 'per-measure-unit': {
- if (isUnit(option)) {
- this.assertEmpty('unitPer');
- res.unitPer = option;
- }
- else
- this.badOption(stem, option);
- break;
- }
- // unitWidth
- case 'unit-width-narrow':
- case 'unit-width-short':
- case 'unit-width-full-name':
- case 'unit-width-iso-code':
- case 'unit-width-hidden':
- this.assertEmpty('unitWidth');
- res.unitWidth = stem;
- break;
- // precision
- case 'precision-integer':
- case 'precision-unlimited':
- case 'precision-currency-cash':
- this.assertEmpty('precision');
- res.precision = { style: stem };
- break;
- case 'precision-currency-standard':
- this.assertEmpty('precision');
- if (option === 'w') {
- res.precision = { style: stem, trailingZero: 'stripIfInteger' };
- }
- else {
- res.precision = { style: stem };
- }
- break;
- case 'precision-increment': {
- const increment = Number(option);
- if (increment > 0) {
- this.assertEmpty('precision');
- res.precision = { style: stem, increment };
- }
- else
- this.badOption(stem, option);
- break;
- }
- // roundingMode
- case 'rounding-mode-ceiling':
- case 'rounding-mode-floor':
- case 'rounding-mode-down':
- case 'rounding-mode-up':
- case 'rounding-mode-half-even':
- case 'rounding-mode-half-odd':
- case 'rounding-mode-half-ceiling':
- case 'rounding-mode-half-floor':
- case 'rounding-mode-half-down':
- case 'rounding-mode-half-up':
- case 'rounding-mode-unnecessary':
- this.assertEmpty('roundingMode');
- res.roundingMode = stem;
- break;
- // integerWidth
- case 'integer-width': {
- if (/^\+0*$/.test(option)) {
- this.assertEmpty('integerWidth');
- res.integerWidth = { source: option, min: option.length - 1 };
- }
- else {
- const m = option.match(/^#*(0*)$/);
- if (m) {
- this.assertEmpty('integerWidth');
- res.integerWidth = {
- source: option,
- min: m[1].length,
- max: m[0].length
- };
- }
- else
- this.badOption(stem, option);
- }
- break;
- }
- // scale
- case 'scale': {
- const scale = Number(option);
- if (scale > 0) {
- this.assertEmpty('scale');
- res.scale = scale;
- }
- else
- this.badOption(stem, option);
- break;
- }
- // group
- case 'group-off':
- case 'group-min2':
- case 'group-auto':
- case 'group-on-aligned':
- case 'group-thousands':
- this.assertEmpty('group');
- res.group = stem;
- break;
- // numberingSystem
- case 'latin':
- this.assertEmpty('numberingSystem');
- res.numberingSystem = 'latn';
- break;
- case 'numbering-system': {
- if (isNumberingSystem(option)) {
- this.assertEmpty('numberingSystem');
- res.numberingSystem = option;
- }
- else
- this.badOption(stem, option);
- break;
- }
- // sign
- case 'sign-auto':
- case 'sign-always':
- case 'sign-never':
- case 'sign-accounting':
- case 'sign-accounting-always':
- case 'sign-except-zero':
- case 'sign-accounting-except-zero':
- this.assertEmpty('sign');
- res.sign = stem;
- break;
- // decimal
- case 'decimal-auto':
- case 'decimal-always':
- this.assertEmpty('decimal');
- res.decimal = stem;
- break;
- // precision blueprint
- default: {
- const precision = parsePrecisionBlueprint(stem, options, this.onError);
- if (precision) {
- this.assertEmpty('precision');
- res.precision = precision;
- }
- else {
- this.onError(new BadStemError(stem));
- }
- }
- }
- }
- }
- /**
- * Parse an {@link
- * https://github.com/unicode-org/icu/blob/master/docs/userguide/format_parse/numbers/skeletons.md
- * | ICU NumberFormatter skeleton} string into a {@link Skeleton} structure.
- *
- * @public
- * @param src - The skeleton string
- * @param onError - Called when the parser encounters a syntax error. The
- * function will still return a {@link Skeleton}, but it may not contain
- * information for all tokens. If not defined, the error will be thrown
- * instead.
- *
- * @example
- * ```js
- * import { parseNumberSkeleton } from '@messageformat/number-skeleton'
- *
- * parseNumberSkeleton('compact-short currency/GBP', console.error)
- * // {
- * // notation: { style: 'compact-short' },
- * // unit: { style: 'currency', currency: 'GBP' }
- * // }
- * ```
- */
- function parseNumberSkeleton(src, onError = error => {
- throw error;
- }) {
- const tokens = [];
- for (const part of src.split(' ')) {
- if (part) {
- const options = part.split('/');
- const stem = options.shift() || '';
- tokens.push({ stem, options });
- }
- }
- const parser = new TokenParser(onError);
- for (const { stem, options } of tokens) {
- parser.parseToken(stem, options);
- }
- return parser.skeleton;
- }
- /**
- * Returns a number formatter function for the given locales and number skeleton
- *
- * @remarks
- * Uses `Intl.NumberFormat` (ES2020) internally.
- *
- * @public
- * @param locales - One or more valid BCP 47 language tags, e.g. `fr` or `en-CA`
- * @param skeleton - An ICU NumberFormatter pattern or `::`-prefixed skeleton
- * string, or a parsed `Skeleton` structure
- * @param currency - If `skeleton` is a pattern string that includes ¤ tokens,
- * their skeleton representation requires a three-letter currency code.
- * @param onError - If defined, will be called separately for each encountered
- * parsing error and unsupported feature.
- * @example
- * ```js
- * import { getNumberFormatter } from '@messageformat/number-skeleton'
- *
- * let src = ':: currency/CAD unit-width-narrow'
- * let fmt = getNumberFormatter('en-CA', src, console.error)
- * fmt(42) // '$42.00'
- *
- * src = '::percent scale/100'
- * fmt = getNumberFormatter('en', src, console.error)
- * fmt(0.3) // '30%'
- * ```
- */
- function getNumberFormatter(locales, skeleton, currency, onError) {
- if (typeof skeleton === 'string') {
- skeleton =
- skeleton.indexOf('::') === 0
- ? parseNumberSkeleton(skeleton.slice(2), onError)
- : parseNumberPattern(skeleton, currency, onError);
- }
- const lc = getNumberFormatLocales(locales, skeleton);
- const opt = getNumberFormatOptions(skeleton, onError);
- const mod = getNumberFormatModifier(skeleton);
- const nf = new Intl.NumberFormat(lc, opt);
- if (skeleton.affix) {
- const [p0, p1] = skeleton.affix.pos;
- const [n0, n1] = skeleton.affix.neg || ['', ''];
- return (value) => {
- const n = nf.format(mod(value));
- return value < 0 ? `${n0}${n}${n1}` : `${p0}${n}${p1}`;
- };
- }
- return (value) => nf.format(mod(value));
- }
- /**
- * Returns a string of JavaScript source that evaluates to a number formatter
- * function with the same `(value: number) => string` signature as the function
- * returned by {@link getNumberFormatter}.
- *
- * @remarks
- * The returned function will memoize an `Intl.NumberFormat` instance.
- *
- * @public
- * @param locales - One or more valid BCP 47 language tags, e.g. `fr` or `en-CA`
- * @param skeleton - An ICU NumberFormatter pattern or `::`-prefixed skeleton
- * string, or a parsed `Skeleton` structure
- * @param currency - If `skeleton` is a pattern string that includes ¤ tokens,
- * their skeleton representation requires a three-letter currency code.
- * @param onError - If defined, will be called separately for each encountered
- * parsing error and unsupported feature.
- * @example
- * ```js
- * import { getNumberFormatterSource } from '@messageformat/number-skeleton'
- *
- * getNumberFormatterSource('en', '::percent', console.error)
- * // '(function() {\n' +
- * // ' var opt = {"style":"percent"};\n' +
- * // ' var nf = new Intl.NumberFormat(["en"], opt);\n' +
- * // ' var mod = function(n) { return n * 0.01; };\n' +
- * // ' return function(value) { return nf.format(mod(value)); }\n' +
- * // '})()'
- *
- * const src = getNumberFormatterSource('en-CA', ':: currency/CAD unit-width-narrow', console.error)
- * // '(function() {\n' +
- * // ' var opt = {"style":"currency","currency":"CAD","currencyDisplay":"narrowSymbol","unitDisplay":"narrow"};\n' +
- * // ' var nf = new Intl.NumberFormat(["en-CA"], opt);\n'
- * // ' return function(value) { return nf.format(value); }\n' +
- * // '})()'
- * const fmt = new Function(`return ${src}`)()
- * fmt(42) // '$42.00'
- * ```
- */
- function getNumberFormatterSource(locales, skeleton, currency, onError) {
- if (typeof skeleton === 'string') {
- skeleton =
- skeleton.indexOf('::') === 0
- ? parseNumberSkeleton(skeleton.slice(2), onError)
- : parseNumberPattern(skeleton, currency, onError);
- }
- const lc = getNumberFormatLocales(locales, skeleton);
- const opt = getNumberFormatOptions(skeleton, onError);
- const modSrc = getNumberFormatModifierSource(skeleton);
- const lines = [
- `(function() {`,
- `var opt = ${JSON.stringify(opt)};`,
- `var nf = new Intl.NumberFormat(${JSON.stringify(lc)}, opt);`
- ];
- let res = 'nf.format(value)';
- if (modSrc) {
- lines.push(`var mod = ${modSrc};`);
- res = 'nf.format(mod(value))';
- }
- if (skeleton.affix) {
- const [p0, p1] = skeleton.affix.pos.map(s => JSON.stringify(s));
- if (skeleton.affix.neg) {
- const [n0, n1] = skeleton.affix.neg.map(s => JSON.stringify(s));
- res = `value < 0 ? ${n0} + ${res} + ${n1} : ${p0} + ${res} + ${p1}`;
- }
- else {
- res = `${p0} + ${res} + ${p1}`;
- }
- }
- lines.push(`return function(value) { return ${res}; }`);
- return lines.join('\n ') + '\n})()';
- }
- const rtlLanguages = [
- 'ar',
- 'ckb',
- 'fa',
- 'he',
- 'ks($|[^bfh])',
- 'lrc',
- 'mzn',
- 'pa-Arab',
- 'ps',
- 'ug',
- 'ur',
- 'uz-Arab',
- 'yi'
- ];
- const rtlRegExp = new RegExp('^' + rtlLanguages.join('|^'));
- function biDiMarkText(text, locale) {
- const isLocaleRTL = rtlRegExp.test(locale);
- const mark = JSON.stringify(isLocaleRTL ? '\u200F' : '\u200E');
- return `${mark} + ${text} + ${mark}`;
- }
- const RUNTIME_MODULE = '@messageformat/runtime';
- const CARDINAL_MODULE = '@messageformat/runtime/lib/cardinals';
- const PLURAL_MODULE = '@messageformat/runtime/lib/plurals';
- const FORMATTER_MODULE = '@messageformat/runtime/lib/formatters';
- class Compiler {
- constructor(options) {
- this.arguments = [];
- this.runtime = {};
- this.options = options;
- }
- compile(src, plural, plurals) {
- const { localeCodeFromKey, requireAllArguments, strict, strictPluralKeys } = this.options;
- if (typeof src === 'object') {
- const result = {};
- for (const key of Object.keys(src)) {
- const lc = localeCodeFromKey ? localeCodeFromKey(key) : key;
- const pl = (plurals && lc && plurals[lc]) || plural;
- result[key] = this.compile(src[key], pl, plurals);
- }
- return result;
- }
- this.plural = plural;
- const parserOptions = {
- cardinal: plural.cardinals,
- ordinal: plural.ordinals,
- strict,
- strictPluralKeys
- };
- this.arguments = [];
- const r = parser.parse(src, parserOptions).map(token => this.token(token, null));
- const hasArgs = this.arguments.length > 0;
- const res = this.concatenate(r, true);
- if (requireAllArguments && hasArgs) {
- this.setRuntimeFn('reqArgs');
- const reqArgs = JSON.stringify(this.arguments);
- return `(d) => { reqArgs(${reqArgs}, d); return ${res}; }`;
- }
- return `(${hasArgs ? 'd' : ''}) => ${res}`;
- }
- cases(token, pluralToken) {
- let needOther = true;
- const r = token.cases.map(({ key, tokens }) => {
- if (key === 'other')
- needOther = false;
- const s = tokens.map(tok => this.token(tok, pluralToken));
- return `${safeIdentifier.property(null, key.replace(/^=/, ''))}: ${this.concatenate(s, false)}`;
- });
- if (needOther) {
- const { type } = token;
- const { cardinals, ordinals } = this.plural;
- if (type === 'select' ||
- (type === 'plural' && cardinals.includes('other')) ||
- (type === 'selectordinal' && ordinals.includes('other')))
- throw new Error(`No 'other' form found in ${JSON.stringify(token)}`);
- }
- return `{ ${r.join(', ')} }`;
- }
- concatenate(tokens, root) {
- const asValues = this.options.returnType === 'values';
- return asValues && (root || tokens.length > 1)
- ? '[' + tokens.join(', ') + ']'
- : tokens.join(' + ') || '""';
- }
- token(token, pluralToken) {
- if (token.type === 'content')
- return JSON.stringify(token.value);
- const { id, lc } = this.plural;
- let args, fn;
- if ('arg' in token) {
- this.arguments.push(token.arg);
- args = [safeIdentifier.property('d', token.arg)];
- }
- else
- args = [];
- switch (token.type) {
- case 'argument':
- return this.options.biDiSupport
- ? biDiMarkText(String(args[0]), lc)
- : String(args[0]);
- case 'select':
- fn = 'select';
- if (pluralToken && this.options.strict)
- pluralToken = null;
- args.push(this.cases(token, pluralToken));
- this.setRuntimeFn('select');
- break;
- case 'selectordinal':
- fn = 'plural';
- args.push(token.pluralOffset || 0, id, this.cases(token, token), 1);
- this.setLocale(id, true);
- this.setRuntimeFn('plural');
- break;
- case 'plural':
- fn = 'plural';
- args.push(token.pluralOffset || 0, id, this.cases(token, token));
- this.setLocale(id, false);
- this.setRuntimeFn('plural');
- break;
- case 'function':
- if (!this.options.customFormatters[token.key]) {
- if (token.key === 'date') {
- fn = this.setDateFormatter(token, args, pluralToken);
- break;
- }
- else if (token.key === 'number') {
- fn = this.setNumberFormatter(token, args, pluralToken);
- break;
- }
- }
- args.push(JSON.stringify(this.plural.locale));
- if (token.param) {
- if (pluralToken && this.options.strict)
- pluralToken = null;
- const arg = this.getFormatterArg(token, pluralToken);
- if (arg)
- args.push(arg);
- }
- fn = token.key;
- this.setFormatter(fn);
- break;
- case 'octothorpe':
- if (!pluralToken)
- return '"#"';
- args = [
- JSON.stringify(this.plural.locale),
- safeIdentifier.property('d', pluralToken.arg),
- pluralToken.pluralOffset || 0
- ];
- if (this.options.strict) {
- fn = 'strictNumber';
- args.push(JSON.stringify(pluralToken.arg));
- this.setRuntimeFn('strictNumber');
- }
- else {
- fn = 'number';
- this.setRuntimeFn('number');
- }
- break;
- }
- if (!fn)
- throw new Error('Parser error for token ' + JSON.stringify(token));
- return `${fn}(${args.join(', ')})`;
- }
- runtimeIncludes(key, type) {
- if (safeIdentifier.identifier(key) !== key)
- throw new SyntaxError(`Reserved word used as ${type} identifier: ${key}`);
- const prev = this.runtime[key];
- if (!prev || prev.type === type)
- return prev;
- throw new TypeError(`Cannot override ${prev.type} runtime function as ${type}: ${key}`);
- }
- setLocale(key, ord) {
- const prev = this.runtimeIncludes(key, 'locale');
- const { getCardinal, getPlural, isDefault } = this.plural;
- let pf, module, toString;
- if (!ord && isDefault && getCardinal) {
- if (prev)
- return;
- pf = (n) => getCardinal(n);
- module = CARDINAL_MODULE;
- toString = () => String(getCardinal);
- }
- else {
- if (prev && (!isDefault || prev.module === PLURAL_MODULE))
- return;
- pf = (n, ord) => getPlural(n, ord);
- module = isDefault ? PLURAL_MODULE : getPlural.module || null;
- toString = () => String(getPlural);
- }
- this.runtime[key] = Object.assign(pf, {
- id: key,
- module,
- toString,
- type: 'locale'
- });
- }
- setRuntimeFn(key) {
- if (this.runtimeIncludes(key, 'runtime'))
- return;
- this.runtime[key] = Object.assign(Runtime__namespace[key], {
- id: key,
- module: RUNTIME_MODULE,
- type: 'runtime'
- });
- }
- getFormatterArg({ key, param }, pluralToken) {
- const fmt = this.options.customFormatters[key] ||
- (isFormatterKey(key) && Formatters__namespace[key]);
- if (!fmt || !param)
- return null;
- const argShape = ('arg' in fmt && fmt.arg) || 'string';
- if (argShape === 'options') {
- let value = '';
- for (const tok of param) {
- if (tok.type === 'content')
- value += tok.value;
- else
- throw new SyntaxError(`Expected literal options for ${key} formatter`);
- }
- const options = {};
- for (const pair of value.split(',')) {
- const keyEnd = pair.indexOf(':');
- if (keyEnd === -1)
- options[pair.trim()] = null;
- else {
- const k = pair.substring(0, keyEnd).trim();
- const v = pair.substring(keyEnd + 1).trim();
- if (v === 'true')
- options[k] = true;
- else if (v === 'false')
- options[k] = false;
- else if (v === 'null')
- options[k] = null;
- else {
- const n = Number(v);
- options[k] = Number.isFinite(n) ? n : v;
- }
- }
- }
- return JSON.stringify(options);
- }
- else {
- const parts = param.map(tok => this.token(tok, pluralToken));
- if (argShape === 'raw')
- return `[${parts.join(', ')}]`;
- const s = parts.join(' + ');
- return s ? `(${s}).trim()` : '""';
- }
- }
- setFormatter(key) {
- if (this.runtimeIncludes(key, 'formatter'))
- return;
- let cf = this.options.customFormatters[key];
- if (cf) {
- if (typeof cf === 'function')
- cf = { formatter: cf };
- this.runtime[key] = Object.assign(cf.formatter, { type: 'formatter' }, 'module' in cf && cf.module && cf.id
- ? { id: safeIdentifier.identifier(cf.id), module: cf.module }
- : { id: null, module: null });
- }
- else if (isFormatterKey(key)) {
- this.runtime[key] = Object.assign(Formatters__namespace[key], { type: 'formatter' }, { id: key, module: FORMATTER_MODULE });
- }
- else {
- throw new Error(`Formatting function not found: ${key}`);
- }
- }
- setDateFormatter({ param }, args, plural) {
- const { locale } = this.plural;
- const argStyle = param && param.length === 1 && param[0];
- if (argStyle &&
- argStyle.type === 'content' &&
- /^\s*::/.test(argStyle.value)) {
- const argSkeletonText = argStyle.value.trim().substr(2);
- const key = safeIdentifier.identifier(`date_${locale}_${argSkeletonText}`, true);
- if (!this.runtimeIncludes(key, 'formatter')) {
- const fmt = getDateFormatter(locale, argSkeletonText);
- this.runtime[key] = Object.assign(fmt, {
- id: key,
- module: null,
- toString: () => getDateFormatterSource(locale, argSkeletonText),
- type: 'formatter'
- });
- }
- return key;
- }
- args.push(JSON.stringify(locale));
- if (param && param.length > 0) {
- if (plural && this.options.strict)
- plural = null;
- const s = param.map(tok => this.token(tok, plural));
- args.push('(' + (s.join(' + ') || '""') + ').trim()');
- }
- this.setFormatter('date');
- return 'date';
- }
- setNumberFormatter({ param }, args, plural) {
- const { locale } = this.plural;
- if (!param || param.length === 0) {
- args.unshift(JSON.stringify(locale));
- args.push('0');
- this.setRuntimeFn('number');
- return 'number';
- }
- args.push(JSON.stringify(locale));
- if (param.length === 1 && param[0].type === 'content') {
- const fmtArg = param[0].value.trim();
- switch (fmtArg) {
- case 'currency':
- args.push(JSON.stringify(this.options.currency));
- this.setFormatter('numberCurrency');
- return 'numberCurrency';
- case 'integer':
- this.setFormatter('numberInteger');
- return 'numberInteger';
- case 'percent':
- this.setFormatter('numberPercent');
- return 'numberPercent';
- }
- const cm = fmtArg.match(/^currency:([A-Z]+)$/);
- if (cm) {
- args.push(JSON.stringify(cm[1]));
- this.setFormatter('numberCurrency');
- return 'numberCurrency';
- }
- const key = safeIdentifier.identifier(`number_${locale}_${fmtArg}`, true);
- if (!this.runtimeIncludes(key, 'formatter')) {
- const { currency } = this.options;
- const fmt = getNumberFormatter(locale, fmtArg, currency);
- this.runtime[key] = Object.assign(fmt, {
- id: null,
- module: null,
- toString: () => getNumberFormatterSource(locale, fmtArg, currency),
- type: 'formatter'
- });
- }
- return key;
- }
- if (plural && this.options.strict)
- plural = null;
- const s = param.map(tok => this.token(tok, plural));
- args.push('(' + (s.join(' + ') || '""') + ').trim()');
- args.push(JSON.stringify(this.options.currency));
- this.setFormatter('numberFmt');
- return 'numberFmt';
- }
- }
- function isFormatterKey(key) {
- return key in Formatters__namespace;
- }
- module.exports = Compiler;
|