UNPKG

numfmt

Version:

Full Excel style number formatting

344 lines (325 loc) 13.6 kB
import { TOKEN_GENERAL, TOKEN_HASH, TOKEN_ZERO, TOKEN_QMARK, TOKEN_SLASH, TOKEN_GROUP, TOKEN_SCALE, TOKEN_COMMA, TOKEN_BREAK, TOKEN_TEXT, TOKEN_PLUS, TOKEN_MINUS, TOKEN_POINT, TOKEN_SPACE, TOKEN_PERCENT, TOKEN_DIGIT, TOKEN_CALENDAR, TOKEN_ERROR, TOKEN_DATETIME, TOKEN_DURATION, TOKEN_CONDITION, TOKEN_DBNUM, TOKEN_NATNUM, TOKEN_LOCALE, TOKEN_COLOR, TOKEN_MODIFIER, TOKEN_AMPM, TOKEN_ESCAPED, TOKEN_STRING, TOKEN_SKIP, TOKEN_EXP, TOKEN_FILL, TOKEN_PAREN, TOKEN_CHAR } from './constants.js'; export { getLocale, parseLocale, addLocale } from './locale.js'; import { defaultOptions } from './options.js'; export { round } from './round.js'; export { dec2frac } from './dec2frac.js'; import { dateToSerial as handleDates } from './serialDate.js'; export { dateToSerial, dateFromSerial } from './serialDate.js'; export { parseNumber, parseDate, parseTime, parseBool, parseValue } from './parseValue.js'; import { formatColor as fmtColor, formatValue as fmtValue } from './formatNumber.js'; import { info, dateInfo, isDate, isPercent, isText } from './formatInfo.js'; import { parsePattern } from './parsePattern.js'; export { tokenize } from './tokenize.js'; const _parseDataCache = Object.create({}); function prepareFormatterData (pattern, shouldThrow = false) { if (!pattern) { pattern = 'General'; } let parseData = _parseDataCache[pattern]; if (!parseData) { try { parseData = parsePattern(pattern); _parseDataCache[pattern] = parseData; } catch (err) { // if the options say to throw errors, then do so if (shouldThrow) { throw err; } // else we set the parsedata to error const errPart = { tokens: [ { type: 'error' } ], error: err.message }; parseData = { pattern: pattern, partitions: [ errPart, errPart, errPart, errPart ], error: err.message, locale: null }; } } return parseData; } /** * Formats a value as a string and returns the result. * * - Dates are normalized to spreadsheet style serial dates and then formatted. * - Booleans are emitted as uppercase "TRUE" or "FALSE". * - Null and Undefined will return an empty string "". * - Any non number values will be stringified and passed through the text section of the format pattern. * - NaNs and infinites will use the corresponding strings from the active locale. * * @param {string} pattern - A format pattern in the ECMA-376 number format. * @param {*} value - The value to format. * @param {object} [options={}] Options * @param {string} [options.locale=""] * A BCP 47 string tag. Locale default is english with a `\u00a0` * grouping symbol (see [addLocale](#addLocale)) * @param {string} [options.overflow="######"] * The string emitted when a formatter fails to format a date that is out * of bounds. * @param {string} [options.invalid="######"] * The string emitted when no-throw mode fails to parse a pattern. * @param {boolean} [options.throws=true] * Should the formatter throw an error if a provided pattern is invalid. * If false, a formatter will be constructed which instead outputs an error * string (see _invalid_ in this table). * @param {boolean} [options.nbsp=false] * By default the output will use a regular space, but in many cases you * may desire a non-breaking-space instead. * @param {boolean} [options.leap1900=true] * Simulate the Lotus 1-2-3 [1900 leap year bug](https://docs.microsoft.com/en-us/office/troubleshoot/excel/wrongly-assumes-1900-is-leap-year). * It is a requirement in the Ecma OOXML specification so it is on by default. * @param {boolean} [options.dateErrorThrows=false] * Should the formatter throw an error when trying to format a date that is * out of bounds? * @param {boolean} [options.dateErrorNumber=true] * Should the formatter switch to a General number format when trying to * format a date that is out of bounds? * @param {boolean} [options.bigintErrorNumber=false] * Should the formatter switch to a plain string number format when trying to * format a bigint that is out of bounds? * @param {boolean} [options.dateSpanLarge=true] * Extends the allowed range of dates from Excel bounds (1900–9999) to * Google Sheet bounds (0–99999). * @param {boolean} [options.ignoreTimezone=false] * Normally when date objects are used with the formatter, time zone is taken * into account. This makes the formatter ignore the timezone offset. * @param {boolean} [options.skipChar=''] * When the formatter encounters `_` it normally emits a single space instead * of the `_` and the next character (like Excel TEXT function does). Setting * this to a character will make the formatter emit that followed by the next * one. * @param {boolean} [options.fillChar=''] * When the formatter encounters `*` it normally emits nothing instead of the * `*` and the next character (like Excel TEXT function does). Setting this * to a character will make the formatter emit that followed by the next one. * @returns {string} A formatted value */ export function format (pattern, value, options = {}) { const opts = Object.assign({}, defaultOptions, options); const data = prepareFormatterData(pattern, opts.throws); const val = handleDates(value, opts) ?? value; return fmtValue(val, data, opts); } /** * Find the color appropriate to a value as dictated by a format pattern. * * If the pattern defines colors, this function will emit the color appropriate * to the value. If no colors were specified this function returns `undefined`. * * ```js * const color = formatColor("[green]#,##0;[red]-#,##0", -10); * console.log(color); // "red" * const color = formatColor("[green]#,##0;-#,##0", -10); * console.log(color); // null * ``` * * @param {string} pattern - A format pattern in the ECMA-376 number format. * @param {*} value - The value to format. * @param {object} [options={}] Options * @param {boolean} [options.throws=true] * Should the formatter throw an error if a provided pattern is invalid. * If false, a formatter will be constructed which instead outputs an error * string (see _invalid_ in this table). * @param {boolean} [options.ignoreTimezone=false] * Normally when date objects are used with the formatter, time zone is taken * into account. This makes the formatter ignore the timezone offset. * @param {boolean} [options.indexColors=true] * When indexed color modifiers are used (`[Color 1]`) the formatter will * convert the index into the corresponding hex color of the default palette. * When this option is set to false, the number will instead by emitted * allowing you to index against a custom palette. * @returns {string|number|null} * A string color value as described by the pattern or a number if the * indexColors option has been set to false. */ export function formatColor (pattern, value, options) { const opts = Object.assign({}, defaultOptions, options); const data = prepareFormatterData(pattern, opts.throws); const val = handleDates(value, opts) ?? value; return fmtColor(val, data, opts); } // FIXME: what is a a section?... /** * Determine if a given format pattern is a date pattern. * * The pattern is considered a date pattern if any of its sections contain a * date symbol (such as `Y` or `H`). Each section is restricted to be * _either_ a number or date format. * * @param {string} pattern - A format pattern in the ECMA-376 number format. * @returns {boolean} True if the specified pattern is date pattern, False otherwise. */ export function isDateFormat (pattern) { const data = prepareFormatterData(pattern, false); return isDate(data.partitions); } /** * Determine if a given format pattern is a percentage pattern. * * The pattern is considered a percentage pattern if any of its sections * contains an unescaped percentage symbol. * * @param {string} pattern - A format pattern in the ECMA-376 number format. * @returns {boolean} True if the specified pattern is date pattern, False otherwise. */ export function isPercentFormat (pattern) { const data = prepareFormatterData(pattern, false); return isPercent(data.partitions); } /** * Determine if a given format pattern is a text only pattern. * * The pattern is considered text only if its definition is composed of a single * section that includes that text symbol (`@`). * * For example `@` or `@" USD"` are text patterns but `#;@` is not. * * @param {string} pattern - A format pattern in the ECMA-376 number format. * @returns {boolean} True if the specified pattern is date pattern, False otherwise. */ export function isTextFormat (pattern) { const data = prepareFormatterData(pattern, false); return isText(data.partitions); } /** * Determine if a given format pattern is valid. * * @param {string} pattern - A format pattern in the ECMA-376 number format. * @returns {boolean} True if the specified pattern is valid, False otherwise. */ export function isValidFormat (pattern) { try { prepareFormatterData(pattern, true); return true; } catch (err) { return false; } } /** * Returns an object detailing the properties and internals of a format parsed * format pattern. * * @param {string} pattern - A format pattern in the ECMA-376 number format. * @param {object} [options={}] Options * @param {string} [options.currency] * Limit the patterns identified as currency to those that use the give string. * If nothing is provided, patterns will be tagged as currency if one of the * following currency symbols is used: ¤$£¥֏؋৳฿៛₡₦₩₪₫€₭₮₱₲₴₸₹₺₼₽₾₿ * @returns {FormatInfo} An object of format properties. */ export function getFormatInfo (pattern, options = {}) { const data = prepareFormatterData(pattern, false); if (!data.info) { data.info = info(data.partitions, options?.currency); } return data.info; } /** * Gets information about date codes use in a format string. * * @param {string} pattern - A format pattern in the ECMA-376 number format. * @returns {FormatDateInfo} An object of format date properties. */ export function getFormatDateInfo (pattern) { const data = prepareFormatterData(pattern, false); if (!data.dateInfo) { data.dateInfo = dateInfo(data.partitions); } return data.dateInfo; } /** * A dictionary of the types used to identify token variants. * * @readonly * @constant {Object<string>} tokenTypes * @property {string} AMPM - AM/PM operator (`AM/PM`, `A/P`) * @property {string} BREAK - Semicolon operator indicating a break between format sections (`;`) * @property {string} CALENDAR - Calendar modifier (`B2`) * @property {string} CHAR - Single non-operator character (`m`) * @property {string} COLOR - Color modifier (`[Black]`, `[color 5]`) * @property {string} COMMA - Plain non-operator comma (`,`) * @property {string} CONDITION - Condition modifier for a section (`[>=10]`) * @property {string} DATETIME - Date-time operator (`mmmm`, `YY`) * @property {string} DBNUM - Number display modifier (`[DBNum23]`) * @property {string} DIGIT - A digit between 1 and 9 (`3`) * @property {string} DURATION - Time duration (`[ss]`) * @property {string} ERROR - Unidentifiable or illegal character (`Ň`) * @property {string} ESCAPED - Escaped character (`\E`) * @property {string} EXP - Exponent operator (`E+`) * @property {string} FILL - Fill with char operator and operand (`*_`) * @property {string} GENERAL - General format operator (`General`) * @property {string} GROUP - Number grouping operator (`,`) * @property {string} HASH - Hash operator (digit if available) (`#`) * @property {string} LOCALE - Locale modifier (`[$-1E020404]`) * @property {string} MINUS - Minus sign (`-`) * @property {string} MODIFIER - An unidentified modifier (`[Schwarz]`) * @property {string} NATNUM - Number display modifier (`[NatNum3]`) * @property {string} PAREN - Parenthesis character (`)`) * @property {string} PERCENT - Percent operator (`%`) * @property {string} PLUS - Plus sign (`+`) * @property {string} POINT - Decimal point operator (`.`) * @property {string} QMARK - Question mark operator (digit or space if not available) (`?`) * @property {string} SCALE - Scaling operator (`,`) * @property {string} SKIP - Skip with char operator and operand (`*_`) * @property {string} SLASH - Slash operator (`/`) * @property {string} SPACE - Space (` `) * @property {string} STRING - Quoted string (`"days"`) * @property {string} TEXT - Text output operator (`@`) * @property {string} ZERO - Zero operator (digit or zero if not available) (`0`) * @see tokenize */ export const tokenTypes = Object.freeze({ AMPM: TOKEN_AMPM, BREAK: TOKEN_BREAK, CALENDAR: TOKEN_CALENDAR, CHAR: TOKEN_CHAR, COLOR: TOKEN_COLOR, COMMA: TOKEN_COMMA, CONDITION: TOKEN_CONDITION, DATETIME: TOKEN_DATETIME, DBNUM: TOKEN_DBNUM, DIGIT: TOKEN_DIGIT, DURATION: TOKEN_DURATION, ERROR: TOKEN_ERROR, ESCAPED: TOKEN_ESCAPED, EXP: TOKEN_EXP, FILL: TOKEN_FILL, GENERAL: TOKEN_GENERAL, GROUP: TOKEN_GROUP, HASH: TOKEN_HASH, LOCALE: TOKEN_LOCALE, MINUS: TOKEN_MINUS, MODIFIER: TOKEN_MODIFIER, NATNUM: TOKEN_NATNUM, PAREN: TOKEN_PAREN, PERCENT: TOKEN_PERCENT, PLUS: TOKEN_PLUS, POINT: TOKEN_POINT, QMARK: TOKEN_QMARK, SCALE: TOKEN_SCALE, SKIP: TOKEN_SKIP, SLASH: TOKEN_SLASH, SPACE: TOKEN_SPACE, STRING: TOKEN_STRING, TEXT: TOKEN_TEXT, ZERO: TOKEN_ZERO });