Comparing version 2.5.2 to 3.0.0-rc1
@@ -1,2 +0,2 @@ | ||
export default function clamp (number) { | ||
export function clamp (number) { | ||
if (number === 0) { | ||
@@ -7,3 +7,3 @@ return number; | ||
const mag = 10 ** (16 - Math.floor(d)); | ||
return Math.round(number * mag) / mag; | ||
return isFinite(mag) ? Math.round(number * mag) / mag : 0; | ||
} |
/* eslint-disable indent, no-multi-spaces */ | ||
// https://docs.microsoft.com/en-us/office/vba/api/office.msolanguageid | ||
export default { | ||
export default Object.freeze({ | ||
1078: 'af', // Afrikaans | ||
@@ -190,2 +190,2 @@ 1052: 'sq', // Albanian | ||
1077: 'zu' // Zulu | ||
}; | ||
}); |
@@ -0,1 +1,2 @@ | ||
/* eslint-disable array-element-newline */ | ||
export const u_YEAR = 2; | ||
@@ -24,14 +25,37 @@ export const u_MONTH = 2 ** 2; | ||
export const _numchars = { | ||
'#': '', | ||
'0': '0', | ||
'?': '\u00a0' | ||
}; | ||
export const TOKEN_GENERAL = 'general'; | ||
export const TOKEN_HASH = 'hash'; | ||
export const TOKEN_ZERO = 'zero'; | ||
export const TOKEN_QMARK = 'qmark'; | ||
export const TOKEN_SLASH = 'slash'; | ||
export const TOKEN_GROUP = 'group'; | ||
export const TOKEN_SCALE = 'scale'; | ||
export const TOKEN_COMMA = 'comma'; | ||
export const TOKEN_BREAK = 'break'; | ||
export const TOKEN_TEXT = 'text'; | ||
export const TOKEN_PLUS = 'plus'; | ||
export const TOKEN_MINUS = 'minus'; | ||
export const TOKEN_POINT = 'point'; | ||
export const TOKEN_SPACE = 'space'; | ||
export const TOKEN_PERCENT = 'percent'; | ||
export const TOKEN_DIGIT = 'digit'; | ||
export const TOKEN_CALENDAR = 'calendar'; | ||
export const TOKEN_ERROR = 'error'; | ||
export const TOKEN_DATETIME = 'datetime'; | ||
export const TOKEN_DURATION = 'duration'; | ||
export const TOKEN_CONDITION = 'condition'; | ||
export const TOKEN_DBNUM = 'dbnum'; | ||
export const TOKEN_NATNUM = 'natnum'; | ||
export const TOKEN_LOCALE = 'locale'; | ||
export const TOKEN_COLOR = 'color'; | ||
export const TOKEN_MODIFIER = 'modifier'; | ||
export const TOKEN_AMPM = 'ampm'; | ||
export const TOKEN_ESCAPED = 'escaped'; | ||
export const TOKEN_STRING = 'string'; | ||
export const TOKEN_SKIP = 'skip'; | ||
export const TOKEN_EXP = 'exp'; | ||
export const TOKEN_FILL = 'fill'; | ||
export const TOKEN_PAREN = 'paren'; | ||
export const TOKEN_CHAR = 'char'; | ||
export const _sp_chars = { | ||
'@': 'text', | ||
'-': 'minus', | ||
'+': 'plus' | ||
}; | ||
export const indexColors = [ | ||
@@ -38,0 +62,0 @@ '#000', '#FFF', '#F00', '#0F0', '#00F', '#FF0', '#F0F', '#0FF', '#000', '#FFF', |
@@ -1,9 +0,19 @@ | ||
// http://homepage.smc.edu/kennedy_john/DEC2FRAC.PDF | ||
const PRECISION = 1e-10; | ||
// https://web.archive.org/web/20110813042636/http://homepage.smc.edu/kennedy_john/DEC2FRAC.PDF | ||
const PRECISION = 1e-13; | ||
export default function dec2frac (val, maxdigits_num, maxdigits_de) { | ||
const sign = (val < 0) ? -1 : 1; | ||
const maxdigits_n = 10 ** (maxdigits_num || 2); | ||
const maxdigits_d = 10 ** (maxdigits_de || 2); | ||
let z = Math.abs(val); | ||
/** | ||
* Split a fractional number into a numerator and denominator for display as | ||
* vulgar fractions. | ||
* | ||
* @ignore | ||
* @param {number} number The value to split | ||
* @param {number} [numeratorMaxDigits=2] The maxdigits number | ||
* @param {number} [denominatorMaxDigits=2] The maxdigits de | ||
* @returns {Array<number>} Array of two numbers, numerator and denominator. | ||
*/ | ||
export function dec2frac (number, numeratorMaxDigits = 2, denominatorMaxDigits = 2) { | ||
const sign = (number < 0) ? -1 : 1; | ||
const maxdigits_n = 10 ** (numeratorMaxDigits || 2); | ||
const maxdigits_d = 10 ** (denominatorMaxDigits || 2); | ||
let z = Math.abs(number); | ||
let last_d = 0; | ||
@@ -15,11 +25,11 @@ let last_n = 0; | ||
let r; | ||
val = z; | ||
if (val % 1 === 0) { | ||
number = z; | ||
if (number % 1 === 0) { | ||
// handles exact integers including 0 | ||
r = [ val * sign, 1 ]; | ||
r = [ number * sign, 1 ]; | ||
} | ||
else if (val < 1e-19) { | ||
else if (number < 1e-19) { | ||
r = [ sign, 1e+19 ]; | ||
} | ||
else if (val > 1e+19) { | ||
else if (number > 1e+19) { | ||
r = [ 1e+19 * sign, 1 ]; | ||
@@ -34,3 +44,3 @@ } | ||
last_n = curr_n; | ||
curr_n = Math.floor(val * curr_d + 0.5); // round | ||
curr_n = Math.floor(number * curr_d + 0.5); // round | ||
if (curr_n >= maxdigits_n || curr_d >= maxdigits_d) { | ||
@@ -40,3 +50,3 @@ return [ sign * last_n, last_d ]; | ||
} | ||
while (Math.abs(val - (curr_n / curr_d)) >= PRECISION && z !== Math.floor(z)); | ||
while (Math.abs(number - (curr_n / curr_d)) >= PRECISION && z !== Math.floor(z)); | ||
r = [ sign * curr_n, curr_d ]; | ||
@@ -43,0 +53,0 @@ } |
@@ -58,2 +58,42 @@ import { u_YEAR, u_MONTH, u_DAY, u_HOUR, u_MIN, u_SEC, reCurrencySymbols } from './constants.js'; | ||
/** | ||
* @typedef {object} FormatInfo | ||
* An object of information properties based on a format pattern. | ||
* @property {( | ||
* "currency" | "date" | "datetime" | | ||
* "error" | "fraction" | "general" | | ||
* "grouped" | "number" | "percent" | | ||
* "scientific" | "text" | "time" | ||
* )} type | ||
* A string identifier for the type of the number formatter. | ||
* @property {boolean} isDate | ||
* Corresponds to the output from isDateFormat. | ||
* @property {boolean} isText | ||
* Corresponds to the output from isTextFormat. | ||
* @property {boolean} isPercent | ||
* Corresponds to the output from isPercentFormat. | ||
* @property {number} maxDecimals | ||
* The maximum number of decimals this format will emit. | ||
* @property {0|1} color | ||
* 1 if the format uses color on the negative portion of the string, else | ||
* a 0. This replicates Excel's `CELL("color")` functionality. | ||
* @property {0|1} parentheses | ||
* 1 if the positive portion of the number format contains an open | ||
* parenthesis, else a 0. This is replicates Excel's `CELL("parentheses")` | ||
* functionality. | ||
* @property {0|1} grouped | ||
* 1 if the positive portion of the format uses a thousands separator, | ||
* else a 0. | ||
* @property {string} code | ||
* Corresponds to Excel's `CELL("format")` functionality. It should match | ||
* Excel's esoteric behaviour fairly well. | ||
* [See Microsoft's documentation.](https://support.microsoft.com/en-us/office/cell-function-51bd39a5-f338-4dbe-a33f-955d67c2b2cf) | ||
* @property {number} scale | ||
* The multiplier used when formatting the number (100 for percentages). | ||
* @property {number} level | ||
* An arbirarty number that represents the format's specificity if you want | ||
* to compare one to another. Integer comparisons roughly match Excel's | ||
* resolutions when it determines which format wins out. | ||
*/ | ||
export function info (partitions, currencyId = null) { | ||
@@ -71,4 +111,3 @@ const [ partPos, partNeg ] = partitions; | ||
parentheses: 0, | ||
grouped: partPos.grouping ? 1 : 0, | ||
_partitions: partitions | ||
grouped: partPos.grouping ? 1 : 0 | ||
}; | ||
@@ -132,3 +171,3 @@ | ||
} | ||
else if (type === 'hour' || type === 'min' || type === 'sec' || type === 'am') { | ||
else if (type === 'hour' || type === 'min' || type === 'sec' || type === 'ampm') { | ||
order += type[0]; | ||
@@ -192,2 +231,21 @@ haveTime++; | ||
/** | ||
* @typedef {object} FormatDateInfo | ||
* An object detailing which date specifiers are used in a format pattern. | ||
* @property {boolean} year | ||
* true if the pattern uses years else false. | ||
* @property {boolean} month | ||
* true if the pattern uses months else false. | ||
* @property {boolean} day | ||
* true if the pattern uses day of the month else false. | ||
* @property {boolean} hours | ||
* true if the pattern uses hours else false. | ||
* @property {boolean} minutes | ||
* true if the pattern uses minutes else false. | ||
* @property {boolean} seconds | ||
* true if the pattern uses seconds else false. | ||
* @property {12|24} clockType | ||
* 12 if the pattern uses AM/PM clock else 24. | ||
*/ | ||
export function dateInfo (partitions) { | ||
@@ -194,0 +252,0 @@ const [ partPos ] = partitions; |
@@ -0,5 +1,10 @@ | ||
import { TOKEN_TEXT, indexColors } from './constants.js'; | ||
import { defaultLocale, getLocale } from './locale.js'; | ||
import { parsePart } from './parsePart.js'; | ||
import { parseFormatSection } from './parseFormatSection.js'; | ||
import { runPart } from './runPart.js'; | ||
const default_text = parseFormatSection([ | ||
{ type: TOKEN_TEXT, value: '@', raw: '@' } | ||
]); | ||
function getPart (value, parts) { | ||
@@ -32,16 +37,21 @@ for (let pi = 0; pi < 3; pi++) { | ||
const default_text = parsePart('@'); | ||
const default_color = 'black'; | ||
export function color (value, parts) { | ||
if (typeof value !== 'number' || !isFinite(value)) { | ||
const nan_color = parts[3] ? parts[3].color : default_text.color; | ||
return nan_color || default_color; | ||
export function formatColor (value, parseData, opts) { | ||
const parts = parseData.partitions; | ||
let part = parts[3]; | ||
let color = null; | ||
if (typeof value === 'number' && isFinite(value)) { | ||
part = getPart(value, parts); | ||
} | ||
const part = getPart(value, parts); | ||
return part ? part.color || default_color : default_color; | ||
if (part && part.color) { | ||
color = part.color; | ||
} | ||
if (color && typeof color === 'number' && opts.indexColors) { | ||
color = indexColors[color - 1] || '#000'; | ||
} | ||
return color; | ||
} | ||
export function formatNumber (value, parts, opts) { | ||
const l10n = getLocale(opts.locale); | ||
export function formatValue (value, parseData, opts) { | ||
const parts = parseData.partitions; | ||
const l10n = getLocale(parseData.locale || opts.locale); | ||
// not a number? | ||
@@ -48,0 +58,0 @@ const text_part = parts[3] ? parts[3] : default_text; |
import numdec from './numdec.js'; | ||
import round from './round.js'; | ||
import { round } from './round.js'; | ||
@@ -8,3 +8,3 @@ const fixLocale = (s, l10n) => { | ||
export default function general (ret, part, value, l10n) { | ||
export function general (ret, part, value, l10n) { | ||
const int = value | 0; | ||
@@ -26,5 +26,3 @@ | ||
: 0; | ||
let n = (exp < 0) | ||
? v * (10 ** -exp) | ||
: v / (10 ** exp); | ||
let n = v * (10 ** -exp); | ||
if (n === 10) { | ||
@@ -31,0 +29,0 @@ n = 1; |
417
lib/index.js
@@ -1,122 +0,331 @@ | ||
import { getLocale, parseLocale, addLocale } from './locale.js'; | ||
import round from './round.js'; | ||
import dec2frac from './dec2frac.js'; | ||
import options from './options.js'; | ||
import codeToLocale from './codeToLocale.js'; | ||
import { parsePattern, parseCatch } from './parsePattern.js'; | ||
import { dateToSerial, dateFromSerial } from './serialDate.js'; | ||
import { info, dateInfo } from './formatInfo.js'; | ||
import { color, formatNumber } from './formatNumber.js'; | ||
import { parseNumber, parseDate, parseTime, parseBool, parseValue } from './parseValue.js'; | ||
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'; | ||
const _cache = {}; | ||
export { | ||
getLocale, | ||
parseLocale, | ||
addLocale | ||
} from './locale.js'; | ||
function getFormatter (parseData, initOpts) { | ||
const { pattern, partitions, locale } = parseData; | ||
import { defaultOptions } from './options.js'; | ||
const getRuntimeOptions = opts => { | ||
const runOpts = Object.assign({}, options(), initOpts, opts); | ||
if (locale) { | ||
runOpts.locale = locale; | ||
} | ||
return runOpts; | ||
}; | ||
export { round } from './round.js'; | ||
export { dec2frac } from './dec2frac.js'; | ||
const formatter = (value, opts) => { | ||
const o = getRuntimeOptions(opts); | ||
return formatNumber(dateToSerial(value, o), partitions, o); | ||
}; | ||
formatter.color = (value, opts) => { | ||
const o = getRuntimeOptions(opts); | ||
return color(dateToSerial(value, o), partitions); | ||
}; | ||
const _info = info(partitions, (initOpts || {}).currency) || new SyntaxError(); | ||
formatter.info = _info; | ||
formatter.dateInfo = dateInfo(partitions); | ||
formatter.isPercent = () => !!_info.isPercent; | ||
formatter.isDate = () => !!_info.isDate; | ||
formatter.isText = () => !!_info.isText; | ||
formatter.pattern = pattern; | ||
if (parseData.error) { | ||
formatter.error = parseData.error; | ||
} | ||
formatter.options = getRuntimeOptions; | ||
formatter.locale = locale || (initOpts && initOpts.locale) || ''; | ||
return Object.freeze(formatter); | ||
} | ||
import { dateToSerial as handleDates } from './serialDate.js'; | ||
export { dateToSerial, dateFromSerial } from './serialDate.js'; | ||
function numfmt (pattern, opts) { | ||
if (!pattern) { | ||
pattern = 'General'; | ||
} | ||
let parseData = null; | ||
if (_cache[pattern]) { | ||
parseData = _cache[pattern]; | ||
} | ||
else { | ||
const constructOpts = Object.assign({}, options(), opts); | ||
parseData = constructOpts.throws | ||
? parsePattern(pattern) | ||
: parseCatch(pattern); | ||
if (!parseData.error) { | ||
_cache[pattern] = parseData; | ||
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 getFormatter(parseData, opts); | ||
return parseData; | ||
} | ||
numfmt.isDate = d => { | ||
// run parser in robust mode: malformed format code is not a date | ||
return numfmt(d, { throws: false }).isDate(); | ||
}; | ||
/** | ||
* 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.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. | ||
* @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); | ||
} | ||
numfmt.isPercent = d => { | ||
// run parser in robust mode: malformed format code is not a percent | ||
return numfmt(d, { throws: false }).isPercent(); | ||
}; | ||
/** | ||
* 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); | ||
} | ||
numfmt.isText = d => { | ||
// run parser in robust mode: malformed format code is not a percent | ||
return numfmt(d, { throws: false }).isText(); | ||
}; | ||
// 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); | ||
} | ||
numfmt.getInfo = (d, opts) => { | ||
return numfmt(d, { ...opts, throws: false }).info; | ||
}; | ||
/** | ||
* 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); | ||
} | ||
numfmt.getDateInfo = (d, opts) => { | ||
return numfmt(d, { ...opts, throws: false }).dateInfo; | ||
}; | ||
/** | ||
* 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); | ||
} | ||
numfmt.dateToSerial = dateToSerial; | ||
numfmt.dateFromSerial = dateFromSerial; | ||
numfmt.options = options; | ||
numfmt.dec2frac = dec2frac; | ||
numfmt.round = round; | ||
/** | ||
* 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; | ||
} | ||
} | ||
numfmt.codeToLocale = codeToLocale; | ||
numfmt.parseLocale = parseLocale; | ||
numfmt.getLocale = getLocale; | ||
numfmt.addLocale = (options, l4e) => { | ||
const c = parseLocale(l4e); | ||
// when locale is changed, expire all cached patterns | ||
delete _cache[c.lang]; | ||
delete _cache[c.language]; | ||
return addLocale(options, c); | ||
}; | ||
/** | ||
* 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; | ||
} | ||
// SSF interface compatibility | ||
function format (pattern, value, l4e, noThrows = false) { | ||
const opts = (l4e && typeof l4e === 'object') ? l4e : { locale: l4e, throws: !noThrows }; | ||
return numfmt(pattern, opts)(dateToSerial(value, opts), opts); | ||
/** | ||
* 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; | ||
} | ||
numfmt.format = format; | ||
numfmt.is_date = numfmt.isDate; | ||
numfmt.parseNumber = parseNumber; | ||
numfmt.parseDate = parseDate; | ||
numfmt.parseTime = parseTime; | ||
numfmt.parseBool = parseBool; | ||
numfmt.parseValue = parseValue; | ||
export default numfmt; | ||
/** | ||
* 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 | ||
}); |
import codeToLocale from './codeToLocale.js'; | ||
// Locale: [language[_territory][.codeset][@modifier]] | ||
const re_locale = /^([a-z\d]+)(?:[_-]([a-z\d]+))?(?:\.([a-z\d]+))?(?:@([a-z\d]+))?$/i; | ||
const locales = {}; | ||
/** | ||
* @typedef {object} LocaleData | ||
* An object of properties used by a formatter when printing a number in a certain locale. | ||
* @property {string} group - Symbol used as a grouping separator (`1,000,000` uses `,`) | ||
* @property {string} decimal - Symbol used to separate integers from fractions (usually `.`) | ||
* @property {string} positive - Symbol used to indicate positive numbers (usually `+`) | ||
* @property {string} negative - Symbol used to indicate positive numbers (usually `-`) | ||
* @property {string} percent - Symbol used to indicate a percentage (usually `%`) | ||
* @property {string} exponent - Symbol used to indicate an exponent (usually `E`) | ||
* @property {string} nan - Symbol used to indicate NaN values (`NaN`) | ||
* @property {string} infinity - Symbol used to indicate infinite values (`∞`) | ||
* @property {Array<string>} ampm - How AM and PM should be presented | ||
* @property {Array<string>} mmmm6 - Long month names for the Islamic calendar (`Rajab`) | ||
* @property {Array<string>} mmm6 - Short month names for the Islamic calendar (`Raj.`) | ||
* @property {Array<string>} mmmm - Long month names for the Gregorian calendar (`November`) | ||
* @property {Array<string>} mmm - Short month names for the Gregorian calendar (`Nov`) | ||
* @property {Array<string>} dddd - Long day names (`Wednesday`) | ||
* @property {Array<string>} ddd - Shortened day names (`Wed`) | ||
*/ | ||
const defaultData = { | ||
@@ -24,7 +45,20 @@ group: ' ', | ||
// Locale: [language[_territory][.codeset][@modifier]] | ||
export function parseLocale (l4e) { | ||
const lm = re_locale.exec(l4e); | ||
/** | ||
* @typedef {object} LocaleToken - An object of properties for a locale tag. | ||
* @property {string} lang - The basic tag such as `zh_CN` or `fi` | ||
* @property {string} language - The language section (`zh` for `zh_CN`) | ||
* @property {string} territory - The territory section (`CN` for `zh_CN`) | ||
*/ | ||
/** | ||
* Parse a regular IETF BCP 47 locale tag and emit an object of its parts. | ||
* Irregular tags and subtags are not supported. | ||
* | ||
* @param {string} locale - A BCP 47 string tag of the locale. | ||
* @returns {LocaleToken} - An object describing the locale. | ||
*/ | ||
export function parseLocale (locale) { | ||
const lm = re_locale.exec(locale); | ||
if (!lm) { | ||
throw new SyntaxError(`Malformed locale: ${l4e}`); | ||
throw new SyntaxError(`Malformed locale: ${locale}`); | ||
} | ||
@@ -34,5 +68,3 @@ return { | ||
language: lm[1], | ||
territory: lm[2] || '', | ||
codeset: lm[3] || '', | ||
modifier: lm[4] || '' | ||
territory: lm[2] || '' | ||
}; | ||
@@ -59,5 +91,13 @@ } | ||
// return a locale object given a language tag tag | ||
export function getLocale (l4e) { | ||
const tag = resolveLocale(l4e); | ||
/** | ||
* Used by the formatter to pull a locate from its registered locales. If | ||
* subtag isn't available but the base language is, the base language is used. | ||
* So if `en-CA` is not found, the formatter tries to find `en` else it | ||
* returns a `null`. | ||
* | ||
* @param {string} locale - A BCP 47 string tag of the locale, or an Excel locale code. | ||
* @returns {LocaleData | null} - An object of format date properties. | ||
*/ | ||
export function getLocale (locale) { | ||
const tag = resolveLocale(locale); | ||
let obj = null; | ||
@@ -76,11 +116,50 @@ if (tag) { | ||
// adds a locale object to locale collection | ||
export function addLocale (options, id) { | ||
/** | ||
* Register locale data for a language so for use when formatting. | ||
* | ||
* Any partial set of properties may be returned to have the defaults used where properties are missing. | ||
* | ||
* @see {LocaleData} | ||
* @param {object} localeSettings - A collection of settings for a locale. | ||
* @param {string} [localeSettings.group="\u00a0"] | ||
* Symbol used as a grouping separator (`1,000,000` uses `,`) | ||
* @param {string} [localeSettings.decimal="."] | ||
* Symbol used to separate integers from fractions (usually `.`) | ||
* @param {string} [localeSettings.positive="+"] | ||
* Symbol used to indicate positive numbers (usually `+`) | ||
* @param {string} [localeSettings.negative="-"] | ||
* Symbol used to indicate positive numbers (usually `-`) | ||
* @param {string} [localeSettings.percent="%"] | ||
* Symbol used to indicate a percentage (usually `%`) | ||
* @param {string} [localeSettings.exponent="E"] | ||
* Symbol used to indicate an exponent (usually `E`) | ||
* @param {string} [localeSettings.nan="NaN"] | ||
* Symbol used to indicate NaN values (`NaN`) | ||
* @param {string} [localeSettings.infinity="∞"] | ||
* Symbol used to indicate infinite values (`∞`) | ||
* @param {Array<string>} [localeSettings.ampm=["AM","PM"]] | ||
* How AM and PM should be presented. | ||
* @param {Array<string>} [localeSettings.mmmm6=["Muharram", "Safar", "Rabiʻ I", "Rabiʻ II", "Jumada I", "Jumada II", "Rajab", "Shaʻban", "Ramadan", "Shawwal", "Dhuʻl-Qiʻdah", "Dhuʻl-Hijjah"]] | ||
* Long month names for the Islamic calendar (e.g. `Rajab`) | ||
* @param {Array<string>} [localeSettings.mmm6=["Muh.", "Saf.", "Rab. I", "Rab. II", "Jum. I", "Jum. II", "Raj.", "Sha.", "Ram.", "Shaw.", "Dhuʻl-Q.", "Dhuʻl-H."]] | ||
* Short month names for the Islamic calendar (e.g. `Raj.`) | ||
* @param {Array<string>} [localeSettings.mmmm=["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]] | ||
* Long month names for the Gregorian calendar (e.g. `November`) | ||
* @param {Array<string>} [localeSettings.mmm=["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]] | ||
* Short month names for the Gregorian calendar (e.g. `Nov`) | ||
* @param {Array<string>} [localeSettings.dddd=["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]] | ||
* Long day names (e.g. `Wednesday`) | ||
* @param {Array<string>} [localeSettings.ddd=["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]] | ||
* Shortened day names (e.g. `Wed`) | ||
* @param {string} l4e - A string BCP 47 tag of the locale. | ||
* @returns {LocaleData} - A full collection of settings for a locale | ||
*/ | ||
export function addLocale (localeSettings, l4e) { | ||
// parse language tag | ||
const c = typeof id === 'object' ? id : parseLocale(id); | ||
const c = typeof l4e === 'object' ? l4e : parseLocale(l4e); | ||
// add the language | ||
locales[c.lang] = createLocale(options); | ||
locales[c.lang] = createLocale(localeSettings); | ||
// if "xx_YY" is added also create "xx" if it is missing | ||
if (c.language !== c.lang && !locales[c.language]) { | ||
locales[c.language] = createLocale(options); | ||
locales[c.language] = createLocale(localeSettings); | ||
} | ||
@@ -87,0 +166,0 @@ return locales[c.lang]; |
@@ -1,2 +0,2 @@ | ||
import round from './round.js'; | ||
import { round } from './round.js'; | ||
@@ -31,8 +31,3 @@ const zero = { | ||
const n = String( | ||
round( | ||
(intSize < 0) | ||
? v * (10 ** -intSize) | ||
: v / (10 ** intSize), | ||
15 | ||
) | ||
round(v * (10 ** -intSize), 15) | ||
); | ||
@@ -39,0 +34,0 @@ let f = n.length; |
@@ -1,2 +0,2 @@ | ||
const defaultOptions = { | ||
export const defaultOptions = { | ||
// Overflow error string | ||
@@ -13,3 +13,3 @@ overflow: '######', // dateErrorThrow needs to be off! [prev in locale] | ||
// Emit regular vs. non-breaking spaces | ||
nbsp: true, | ||
nbsp: false, | ||
// Robust/throw mode | ||
@@ -22,26 +22,7 @@ throws: true, | ||
// Don't adjust dates to UTC when converting them to serial time | ||
ignoreTimezone: false | ||
ignoreTimezone: false, | ||
// integer digit grouping | ||
grouping: [ 3, 3 ], | ||
// resolve indexed colors to hex | ||
indexColors: true | ||
}; | ||
const globalOptions = Object.assign({}, defaultOptions); | ||
export default function options (opts) { | ||
// passing in a null will reset to defaults | ||
if (opts === null) { | ||
opts = defaultOptions; | ||
} | ||
if (opts) { | ||
for (const key in opts) { | ||
if (key in defaultOptions) { | ||
const value = opts[key]; | ||
if (value == null) { // set back to default | ||
globalOptions[key] = defaultOptions[key]; | ||
} | ||
else { | ||
globalOptions[key] = value; | ||
} | ||
} | ||
} | ||
} | ||
return { ...globalOptions }; | ||
} |
import { resolveLocale } from './locale.js'; | ||
import { parsePart } from './parsePart.js'; | ||
import { parseFormatSection } from './parseFormatSection.js'; | ||
import { tokenize } from './tokenize.js'; | ||
@@ -19,2 +20,19 @@ const maybeAddMinus = part => { | ||
const clonePart = (part, prefixToken = null) => { | ||
const r = {}; | ||
for (const key in part) { | ||
if (Array.isArray(part[key])) { | ||
r[key] = [ ...part[key] ]; | ||
} | ||
else { | ||
r[key] = part[key]; | ||
} | ||
} | ||
if (prefixToken) { | ||
r.tokens.unshift(prefixToken); | ||
} | ||
r.generated = true; | ||
return r; | ||
}; | ||
export function parsePattern (pattern) { | ||
@@ -25,3 +43,2 @@ const partitions = []; | ||
let text_partition = null; | ||
let p = pattern; | ||
let more = 0; | ||
@@ -31,4 +48,5 @@ let part = false; | ||
let conditions = 0; | ||
let tokens = tokenize(pattern); | ||
do { | ||
part = parsePart(p); | ||
part = parseFormatSection(tokens); | ||
// Dates cannot blend with non-date tokens | ||
@@ -61,4 +79,5 @@ // General cannot blend with non-date tokens | ||
partitions.push(part); | ||
more = (p.charAt(part.pattern.length) === ';') ? 1 : 0; | ||
p = p.slice(part.pattern.length + more); | ||
more = tokens[part.tokensUsed]?.type === 'break' ? 1 : 0; | ||
tokens = tokens.slice(part.tokensUsed + more); | ||
i++; | ||
@@ -87,3 +106,3 @@ } | ||
// provide a fallback pattern if there isn't one | ||
partitions[1] = parsePart('General'); | ||
partitions[1] = parseFormatSection(tokenize('General')); | ||
partitions[1].generated = true; | ||
@@ -137,3 +156,3 @@ } | ||
if (partitions.length < 1 && text_partition) { | ||
partitions[0] = parsePart('General'); | ||
partitions[0] = parseFormatSection(tokenize('General')); | ||
partitions[0].generated = true; | ||
@@ -143,16 +162,9 @@ } | ||
if (partitions.length < 2) { | ||
const part = parsePart(partitions[0].pattern); | ||
// the volatile minus only happens if there is a single pattern | ||
part.tokens.unshift({ | ||
type: 'minus', | ||
volatile: true | ||
}); | ||
part.generated = true; | ||
partitions.push(part); | ||
const volMinus = { type: 'minus', volatile: true }; | ||
partitions.push(clonePart(partitions[0], volMinus)); | ||
} | ||
// missing zero | ||
if (partitions.length < 3) { | ||
const part = parsePart(partitions[0].pattern); | ||
part.generated = true; | ||
partitions.push(part); | ||
partitions.push(clonePart(partitions[0])); | ||
} | ||
@@ -165,3 +177,3 @@ // missing text | ||
else { | ||
const part = parsePart('@'); | ||
const part = parseFormatSection(tokenize('@')); | ||
part.generated = true; | ||
@@ -168,0 +180,0 @@ partitions.push(part); |
@@ -0,3 +1,10 @@ | ||
/* eslint-disable array-element-newline */ | ||
import { currencySymbols, reCurrencySymbols } from './constants.js'; | ||
import { dateFromSerial } from './serialDate.js'; | ||
/** | ||
* @typedef {object} ParseData | ||
* @property {number | boolean} v - the value | ||
* @property {string} [z] - number format pattern | ||
*/ | ||
/* | ||
@@ -103,6 +110,22 @@ This is a list of the allowed date formats. The test file contains | ||
export function parseNumber (str) { | ||
/** | ||
* Parse a numeric string input and return its value and format. If the input | ||
* was not recognized or valid, the function returns a `null`, for valid input | ||
* it returns an object with two properties: | ||
* | ||
* * `v`: the parsed value. | ||
* * `z`: the number format of the input (if applicable). | ||
* | ||
* @see parseValue | ||
* @param {string} value The number to parse | ||
* @returns {ParseData | null} An object of the parsed value and a corresponding format string | ||
*/ | ||
export function parseNumber (value) { | ||
// this horrifying regular expression assumes that | ||
// we only need #,###.### and never #.###,### | ||
const parts = new RegExp('^([\\s+%' + currencySymbols.join('') + '(-]*)(((?:(?:\\d[\\d,]*)(?:\\.\\d*)?|(?:\\.\\d+)))([eE][+-]?\\d+)?)([\\s%' + currencySymbols.join('') + ')]*)$').exec(str); | ||
const parts = new RegExp( | ||
'^([\\s+%' + currencySymbols.join('') + | ||
'(-]*)(((?:(?:\\d[\\d,]*)(?:\\.\\d*)?|(?:\\.\\d+)))([eE][+-]?\\d+)?)([\\s%' + | ||
currencySymbols.join('') + ')]*)$' | ||
).exec(value); | ||
if (parts) { | ||
@@ -119,5 +142,5 @@ const [ , prefix, number, numpart, exp, suffix ] = parts; | ||
let currencyTrailing = false; | ||
let value = parseFloat(number.replace(/,/g, '')); | ||
let numberValue = parseFloat(number.replace(/,/g, '')); | ||
// is number ok? | ||
if (!isFinite(value)) { | ||
if (!isFinite(numberValue)) { | ||
return null; | ||
@@ -182,3 +205,3 @@ } | ||
format = numpart.includes('.') ? '0.00%' : '0%'; | ||
value *= 0.01; | ||
numberValue *= 0.01; | ||
} | ||
@@ -198,4 +221,4 @@ else if (currency) { | ||
} | ||
// we may want to lower the fidelity of the number: +p.value.toFixed(13) | ||
const ret = { v: value * sign }; | ||
// we may want to lower the fidelity of the number: +num.toFixed(13) | ||
const ret = { v: numberValue * sign }; | ||
if (format) { | ||
@@ -206,2 +229,3 @@ ret.z = format; | ||
} | ||
return null; | ||
} | ||
@@ -324,5 +348,17 @@ | ||
export function parseDate (str, opts) { | ||
/** | ||
* Parse a date or datetime string input and return its value and format. If | ||
* the input was not recognized or valid, the function returns a `null`, for | ||
* valid input it returns an object with two properties: | ||
* | ||
* - `v`: the parsed value. | ||
* - `z`: the number format of the input (if applicable). | ||
* | ||
* @see parseValue | ||
* @param {string} value The date to parse | ||
* @returns {ParseData | null} An object of the parsed value and a corresponding format string | ||
*/ | ||
export function parseDate (value) { | ||
// possible shortcut: quickly dismiss if there isn't a number? | ||
const date = nextToken(str.trim(), dateTrie, { path: '' }); | ||
const date = nextToken(value.trim(), dateTrie, { path: '' }); | ||
if (date) { | ||
@@ -351,4 +387,4 @@ // disallow matches where two tokens are separated by a period | ||
} | ||
const value = (Date.UTC(year, date.month - 1, date.day) / 864e5) + epoch + (date.time || 0); | ||
if (value >= 0 && value <= 2958465) { | ||
const dateValue = (Date.UTC(year, date.month - 1, date.day) / 864e5) + epoch + (date.time || 0); | ||
if (dateValue >= 0 && dateValue <= 2958465) { | ||
const lead0 = ( | ||
@@ -377,6 +413,3 @@ // either has a leading zero | ||
}); | ||
if (opts && opts.nativeDate) { | ||
return { v: dateFromSerial(value, opts), z: format }; | ||
} | ||
return { v: value, z: format }; | ||
return { v: dateValue, z: format }; | ||
} | ||
@@ -387,4 +420,16 @@ } | ||
export function parseTime (str) { | ||
const parts = /^\s*([10]?\d|2[0-4])(?::([0-5]\d|\d))?(?::([0-5]\d|\d))?(\.\d{1,10})?(?:\s*([AP])M?)?\s*$/i.exec(str); | ||
/** | ||
* Parse a time string input and return its value and format. If the input was | ||
* not recognized or valid, the function returns a `null`, for valid input it | ||
* returns an object with two properties: | ||
* | ||
* - `v`: the parsed value. | ||
* - `z`: the number format of the input (if applicable). | ||
* | ||
* @see parseValue | ||
* @param {string} value The date to parse | ||
* @returns {ParseData | null} An object of the parsed value and a corresponding format string | ||
*/ | ||
export function parseTime (value) { | ||
const parts = /^\s*([10]?\d|2[0-4])(?::([0-5]\d|\d))?(?::([0-5]\d|\d))?(\.\d{1,10})?(?:\s*([AP])M?)?\s*$/i.exec(value); | ||
if (parts) { | ||
@@ -427,7 +472,18 @@ const [ , h, m, s, f, am ] = parts; | ||
export function parseBool (str) { | ||
if (/^\s*true\s*$/i.test(str)) { | ||
/** | ||
* Parse a string input and return its boolean value. If the input was not | ||
* recognized or valid, the function returns a `null`, for valid input it | ||
* returns an object with one property: | ||
* | ||
* - `v`: the parsed value. | ||
* | ||
* @see parseValue | ||
* @param {string} value The supposed boolean to parse | ||
* @returns {ParseData | null} An object of the parsed value and a corresponding format string | ||
*/ | ||
export function parseBool (value) { | ||
if (/^\s*true\s*$/i.test(value)) { | ||
return { v: true }; | ||
} | ||
if (/^\s*false\s*$/i.test(str)) { | ||
if (/^\s*false\s*$/i.test(value)) { | ||
return { v: false }; | ||
@@ -438,4 +494,62 @@ } | ||
export function parseValue (s, opts) { | ||
return parseNumber(s) ?? parseDate(s, opts) ?? parseTime(s) ?? parseBool(s); | ||
/** | ||
* Attempt to parse a "spreadsheet input" string input and return its value and | ||
* format. If the input was not recognized or valid, the function returns a | ||
* `null`, for valid input it returns an object with two properties: | ||
* | ||
* - `v`: The parsed value. For dates, this will be an Excel style serial date. | ||
* - `z`: (Optionally) the number format string of the input. This property will | ||
* not be present if it amounts to the `General` format. | ||
* | ||
* `parseValue()` recognizes a wide range of dates and date-times, times, | ||
* numbers, and booleans. Some examples: | ||
* | ||
* ```js | ||
* // basic number | ||
* parseValue("-123");// { v: -123 } | ||
* // formatted number | ||
* parseValue("$1,234"); // { v: 1234, z: "$#,##0" } | ||
* // a percent | ||
* parseValue("12.3%"); // { v: 0.123, z: "0.00%" } | ||
* // a date | ||
* parseValue("07 October 1984"); // { v: 30962, z: 'dd mmmm yyyy' } | ||
* // an ISO formatted date-time | ||
* parseValue("1984-09-10 11:12:13.1234"); // { v: 30935.46681855787, z: "yyyy-mm-dd hh:mm:ss" } | ||
* // a boolean | ||
* parseValue("false"); // { v: false } | ||
* ``` | ||
* | ||
* The formatting string outputted may not correspond exactly to the input. | ||
* Rather, is it composed of certain elements which the input controls. This is | ||
* comparable to how Microsoft Excel and Google Sheets parse pasted input. Some | ||
* things you may expect: | ||
* | ||
* - Whitespace is ignored. | ||
* - Decimal fractions are always represented by `.00` regardless of how many | ||
* digits were shown in the input. | ||
* - Negatives denoted by parentheses [`(1,234)`] will not include the | ||
* parentheses in the format string (the value will still by negative.) | ||
* - All "scientific notation" returns the same format: `0.00E+00`. | ||
* | ||
* Internally the parser calls, `parseNumber`, `parseDate`, | ||
* `parseTime` and `parseBool`. They work in the same way except | ||
* with a more limited scope. You may want those function if you are limiting | ||
* input to a smaller scope. | ||
* | ||
* Be warned that the parser do not (yet) take locale into account so all input | ||
* is assumed to be in "en-US". This means that `1,234.5` will parse, but | ||
* `1.234,5` will not. Similarly, the order of date parts will be US centric. | ||
* This may change in the future so be careful what options you pass the | ||
* functions. | ||
* | ||
* @param {string} value The value to parse | ||
* @returns {ParseData | null} An object of the parsed value and a corresponding format string | ||
*/ | ||
export function parseValue (value) { | ||
return ( | ||
parseNumber(value) ?? | ||
parseDate(value) ?? | ||
parseTime(value) ?? | ||
parseBool(value) | ||
); | ||
} |
@@ -1,14 +0,22 @@ | ||
// Excel uses symmetric arithmetic rounding | ||
export default function round (value, places) { | ||
if (typeof value !== 'number') { | ||
return value; | ||
/** | ||
* Return a number rounded to the specified amount of places. This is the | ||
* rounding function used internally by the formatter (symmetric arithmetic | ||
* rounding). | ||
* | ||
* @param {number} number - The number to round. | ||
* @param {number} [places=0] - The number of decimals to round to. | ||
* @returns {number} A rounded number. | ||
*/ | ||
export function round (number, places = 0) { | ||
if (typeof number !== 'number') { | ||
return number; | ||
} | ||
if (value < 0) { | ||
return -round(-value, places); | ||
if (number < 0) { | ||
return -round(-number, places); | ||
} | ||
if (places) { | ||
const p = 10 ** (places || 0) || 1; | ||
return round(value * p, 0) / p; | ||
const p = 10 ** (places) || 1; | ||
return round(number * p, 0) / p; | ||
} | ||
return Math.round(value); | ||
return Math.round(number); | ||
} |
@@ -1,5 +0,5 @@ | ||
import round from './round.js'; | ||
import clamp from './clamp.js'; | ||
import dec2frac from './dec2frac.js'; | ||
import general from './general.js'; | ||
import { round } from './round.js'; | ||
import { clamp } from './clamp.js'; | ||
import { dec2frac } from './dec2frac.js'; | ||
import { general } from './general.js'; | ||
import { toYMD } from './toYMD.js'; | ||
@@ -11,16 +11,8 @@ import { defaultLocale } from './locale.js'; | ||
MIN_S_DATE, MAX_S_DATE, | ||
MIN_L_DATE, MAX_L_DATE, | ||
_numchars | ||
MIN_L_DATE, MAX_L_DATE | ||
} from './constants.js'; | ||
import { pad } from './pad.js'; | ||
const DAYSIZE = 86400; | ||
const short_to_long = { | ||
int: 'integer', | ||
frac: 'fraction', | ||
man: 'mantissa', | ||
num: 'numerator', | ||
den: 'denominator' | ||
}; | ||
const getExponent = (num, int_max = 0) => { | ||
@@ -42,2 +34,3 @@ const exp = Math.floor(Math.log10(num)); | ||
let mantissa = ''; | ||
let mantissa_sign = ''; | ||
let numerator = ''; | ||
@@ -66,2 +59,3 @@ let denominator = ''; | ||
} | ||
// calc exponent | ||
@@ -75,6 +69,11 @@ if (part.exponential) { | ||
} | ||
v = v / (10 ** exp); | ||
if (value && !part.integer) { | ||
// when there isn't an integer part, the exp gets shifted by 1 | ||
exp++; | ||
} | ||
v = v * (10 ** -exp); | ||
value = (value < 0) ? -v : v; | ||
mantissa += Math.abs(exp); | ||
} | ||
// integer to text | ||
@@ -85,18 +84,6 @@ if (part.integer) { | ||
} | ||
// integer grouping | ||
const group_pri = opts.grouping[0] ?? 3; | ||
const group_sec = opts.grouping[1] ?? group_pri; | ||
// grouping | ||
if (part.grouping) { | ||
let gtmp = ''; | ||
let ipos = integer.length; | ||
if (ipos > part.group_pri) { | ||
ipos -= part.group_pri; | ||
gtmp = l10n.group + integer.slice(ipos, ipos + part.group_pri) + gtmp; | ||
} | ||
while (ipos > part.group_sec) { | ||
ipos -= part.group_sec; | ||
gtmp = l10n.group + integer.slice(ipos, ipos + part.group_sec) + gtmp; | ||
} | ||
integer = ipos ? integer.slice(0, ipos) + gtmp : gtmp; | ||
} | ||
// fraction to text | ||
@@ -108,8 +95,11 @@ if (part.dec_fractions) { | ||
// using vulgar fractions | ||
let have_fraction = false; | ||
const fixed_slash = !part.error && (part.num_p.includes('0') || part.den_p.includes('0')); | ||
let have_fraction = fixed_slash; | ||
if (part.fractions) { | ||
have_fraction = fixed_slash || !!(value % 1); | ||
const _dec = Math.abs(part.integer ? value % 1 : value); | ||
if (_dec) { | ||
have_fraction = true; | ||
if (isFinite(part.denominator)) { | ||
if (part.denominator && isFinite(part.denominator)) { | ||
// predefined denominator | ||
@@ -121,25 +111,24 @@ denominator += part.denominator; | ||
denominator = ''; | ||
have_fraction = false; | ||
if (!integer) { | ||
integer = '0'; | ||
} | ||
have_fraction = fixed_slash; | ||
} | ||
} | ||
else { | ||
const nmax = (part.integer) ? part.num_max : Infinity; | ||
const frt = dec2frac(_dec, nmax, part.den_max); | ||
const frt = dec2frac(_dec, Infinity, part.den_max); | ||
numerator += frt[0]; | ||
denominator += frt[1]; | ||
if (part.integer) { | ||
if (numerator === '0') { | ||
if (!integer) { | ||
integer = '0'; | ||
} | ||
numerator = ''; | ||
denominator = ''; | ||
have_fraction = false; | ||
} | ||
if (part.integer && numerator === '0') { | ||
numerator = ''; | ||
denominator = ''; | ||
have_fraction = fixed_slash; | ||
} | ||
} | ||
} | ||
else if (!value && !part.integer) { | ||
have_fraction = true; | ||
numerator = '0'; | ||
denominator = '1'; | ||
} | ||
if (part.integer && !have_fraction && !Math.trunc(value)) { | ||
integer = '0'; | ||
} | ||
} | ||
@@ -208,36 +197,35 @@ | ||
// integer padding | ||
if (part.int_padding) { | ||
integer = (part.int_padding.length === 1) | ||
? integer || part.int_padding | ||
: part.int_padding.substring(0, part.int_padding.length - integer.length) + integer; | ||
const padQ = pad('?', opts.nbsp); | ||
// mantissa sign | ||
if (exp < 0) { | ||
mantissa_sign = '-'; | ||
} | ||
// numerator padding | ||
if (part.num_padding) { | ||
numerator = (part.num_padding.length === 1) | ||
? numerator || part.num_padding | ||
: part.num_padding.substring(0, part.num_padding.length - numerator.length) + numerator; | ||
else if (part.exp_plus) { | ||
mantissa_sign = '+'; | ||
} | ||
// denominator padding | ||
if (part.den_padding) { | ||
denominator = (part.den_padding.length === 1) | ||
? denominator || part.den_padding | ||
: denominator + part.den_padding.slice(denominator.length); | ||
} | ||
// mantissa padding | ||
if (part.man_padding) { | ||
const m_sign = (part.exp_plus) ? '+' : ''; | ||
mantissa = (part.man_padding.length === 1) | ||
? (exp < 0 ? '-' : m_sign) + (mantissa || part.man_padding) | ||
: (exp < 0 ? '-' : m_sign) + part.man_padding.slice(0, part.man_padding.length - mantissa.length) + mantissa; | ||
} | ||
const ret = []; | ||
let integer_bits_counter = 0; | ||
const digitsStart = (numstr, pattern, part, offset) => { | ||
const l = (!offset && numstr.length > pattern.length) | ||
? part.length + numstr.length - pattern.length | ||
: part.length; | ||
if (numstr.length < pattern.length) { | ||
offset += numstr.length - pattern.length; | ||
} | ||
for (let i = 0; i < l; i++) { | ||
ret.push(numstr[i + offset] || pad(part[i], opts.nbsp)); | ||
} | ||
return l; | ||
}; | ||
let denominator_fixed = false; | ||
const counter = { int: 0, frac: 0, man: 0, num: 0, den: 0 }; | ||
for (let ti = 0, tl = part.tokens.length; ti < tl; ti++) { | ||
const tok = part.tokens[ti]; | ||
const tokenType = tok.type; | ||
const len = tok.num ? tok.num.length : 0; | ||
if (tok.type === 'string') { | ||
if (tokenType === 'string') { | ||
// special rules may apply if next or prev is numerator or denominator | ||
@@ -247,6 +235,7 @@ if (tok.rule) { | ||
if (have_fraction) { | ||
ret.push(tok.value); | ||
ret.push(tok.value.replace(/ /g, padQ)); | ||
} | ||
else if (part.num_min > 0 || part.den_min > 0) { | ||
ret.push(tok.value.replace(/./g, _numchars['?'])); | ||
// FIXME: ret.push(''.repeat(tok.value.length)) | ||
ret.push(tok.value.replace(/./g, padQ)); | ||
} | ||
@@ -256,6 +245,6 @@ } | ||
if (have_fraction && integer) { | ||
ret.push(tok.value); | ||
ret.push(tok.value.replace(/ /g, padQ)); | ||
} | ||
else if ((part.den_min > 0) && (integer || part.num_min)) { | ||
ret.push(tok.value.replace(/./g, _numchars['?'])); | ||
ret.push(tok.value.replace(/./g, padQ)); | ||
} | ||
@@ -265,6 +254,6 @@ } | ||
if (have_fraction) { | ||
ret.push(tok.value); | ||
ret.push(tok.value.replace(/ /g, padQ)); | ||
} | ||
else if (part.den_min > 0 || part.den_min > 0) { | ||
ret.push(tok.value.replace(/./g, _numchars['?'])); | ||
ret.push(tok.value.replace(/./g, padQ)); | ||
} | ||
@@ -274,20 +263,33 @@ } | ||
else { | ||
ret.push(tok.value); | ||
ret.push(tok.value.replace(/ /g, padQ)); | ||
} | ||
} | ||
else if (tok.type === 'error') { | ||
else if (tokenType === 'space') { | ||
if (tok.rule === 'num+int') { | ||
if ( | ||
(have_fraction || part.num_min || part.den_min) && | ||
(integer || part.num_min) | ||
) { | ||
ret.push(padQ); | ||
} | ||
} | ||
else { | ||
ret.push(padQ); | ||
} | ||
} | ||
else if (tokenType === 'error') { | ||
// token used to define invalid pattern | ||
ret.push(opts.invalid); | ||
} | ||
else if (tok.type === 'point') { | ||
else if (tokenType === 'point') { | ||
// Excel always emits a period: TEXT(0, "#.#") => "." | ||
ret.push(part.date ? tok.value : l10n.decimal); | ||
} | ||
else if (tok.type === 'general') { | ||
else if (tokenType === 'general') { | ||
general(ret, part, value, l10n); | ||
} | ||
else if (tok.type === 'exp') { | ||
else if (tokenType === 'exp') { | ||
ret.push(l10n.exponent); | ||
} | ||
else if (tok.type === 'minus') { | ||
else if (tokenType === 'minus') { | ||
if (tok.volatile && part.date) { | ||
@@ -309,9 +311,9 @@ // don't emit the prepended minus if this is a date | ||
} | ||
else if (tok.type === 'plus') { | ||
else if (tokenType === 'plus') { | ||
ret.push(l10n.positive); | ||
} | ||
else if (tok.type === 'text') { | ||
else if (tokenType === 'text') { | ||
ret.push(value); | ||
} | ||
else if (tok.type === 'div') { | ||
else if (tokenType === 'div') { | ||
if (have_fraction) { | ||
@@ -321,67 +323,93 @@ ret.push('/'); | ||
else if (part.num_min > 0 || part.den_min > 0) { | ||
ret.push(_numchars['?']); | ||
ret.push(padQ); | ||
} | ||
else { | ||
ret.push(_numchars['#']); | ||
ret.push(pad('#', opts.nbsp)); | ||
} | ||
} | ||
else if (tok.type === 'int') { | ||
if (part.int_pattern.length === 1) { // number isn't fragmented | ||
ret.push(integer); | ||
else if (tokenType === 'int') { | ||
// number isn't fragmented | ||
if (part.int_pattern.length === 1) { | ||
const pt = part.int_p; | ||
const l = Math.max(part.int_min, integer.length); | ||
let digits = ''; | ||
for (let i = l; i > 0; i--) { | ||
const d = integer.charAt(integer.length - i); | ||
const p = d ? '' : pt.charAt(pt.length - i) || pt[0]; | ||
let sep = ''; | ||
if (part.grouping) { | ||
const n = (i - 1) - group_pri; | ||
if (n >= 0 && !(n % group_sec)) { | ||
sep = (d || p === '0') | ||
? l10n.group | ||
: pad('?', opts.nbsp); | ||
} | ||
} | ||
digits += (d || pad(p, opts.nbsp)) + sep; | ||
} | ||
ret.push(digits); | ||
} | ||
else { | ||
const c_s = (!integer_bits_counter) | ||
? Infinity | ||
: part.int_pattern.join('').length - counter.int; | ||
const c_e = (integer_bits_counter === part.int_pattern.length - 1) | ||
? 0 | ||
: part.int_pattern.join('').length - (counter.int + tok.num.length); | ||
ret.push(integer.substring(integer.length - c_s, integer.length - c_e)); | ||
integer_bits_counter++; | ||
counter.int += tok.num.length; | ||
counter.int += digitsStart(integer, part.int_p, tok.num, counter.int); | ||
} | ||
} | ||
else if (tok.type === 'frac') { | ||
else if (tokenType === 'frac') { | ||
const o = counter.frac; | ||
for (let i = 0; i < len; i++) { | ||
ret.push(fraction[i + o] || _numchars[tok.num[i]]); | ||
ret.push(fraction[i + o] || pad(tok.num[i], opts.nbsp)); | ||
} | ||
counter.frac += len; | ||
} | ||
else if (tok.type in short_to_long) { | ||
if (part[tok.type + '_pattern'].length === 1) { | ||
// number isn't fragmented | ||
if (tok.type === 'int') { | ||
ret.push(integer); | ||
else if (tokenType === 'man') { | ||
// mantissa sign is attached to the first digit, not the exponent symbol | ||
// "0E+ 0" will print as "1E +12" | ||
if (!counter[tokenType] && !counter.man) { | ||
ret.push(mantissa_sign); | ||
} | ||
counter.man += digitsStart(mantissa, part.man_p, tok.num, counter.man); | ||
} | ||
else if (tokenType === 'num') { | ||
counter.num += digitsStart(numerator, part.num_p, tok.num, counter.num); | ||
} | ||
else if (tokenType === 'den') { | ||
const o = counter.den; | ||
for (let i = 0; i < len; i++) { | ||
let digit = denominator[i + o]; | ||
if (!digit) { | ||
const ch = tok.num[i]; | ||
if ( | ||
'123456789'.includes(ch) || | ||
(denominator_fixed && ch === '0') | ||
) { | ||
denominator_fixed = true; | ||
digit = opts.nbsp ? '\u00a0' : ' '; | ||
} | ||
else if ( | ||
!denominator_fixed && | ||
(i === len - 1) && | ||
ch === '0' && | ||
!denominator | ||
) { | ||
digit = '1'; | ||
} | ||
else { | ||
digit = pad(ch, opts.nbsp); | ||
} | ||
} | ||
if (tok.type === 'frac') { | ||
ret.push(fraction); | ||
} | ||
if (tok.type === 'man') { | ||
ret.push(mantissa); | ||
} | ||
if (tok.type === 'num') { | ||
ret.push(numerator); | ||
} | ||
if (tok.type === 'den') { | ||
ret.push(denominator); | ||
} | ||
ret.push(digit); | ||
} | ||
else { | ||
ret.push(short_to_long[tok.type].slice(counter[tok.type], counter[tok.type] + len)); | ||
counter[tok.type] += len; | ||
} | ||
counter.den += len; | ||
} | ||
else if (tok.type === 'year') { | ||
else if (tokenType === 'year') { | ||
if (year < 0) { ret.push(l10n.negative); } | ||
ret.push(String(Math.abs(year)).padStart(4, '0')); | ||
} | ||
else if (tok.type === 'year-short') { | ||
else if (tokenType === 'year-short') { | ||
const y = year % 100; | ||
ret.push(y < 10 ? '0' : '', y); | ||
} | ||
else if (tok.type === 'month') { | ||
else if (tokenType === 'month') { | ||
ret.push((tok.pad && month < 10 ? '0' : ''), month); | ||
} | ||
else if (tok.type === 'monthname-single') { | ||
else if (tokenType === 'monthname-single') { | ||
// This is what Excel does. | ||
@@ -399,3 +427,3 @@ // The Vietnamese list goes: | ||
} | ||
else if (tok.type === 'monthname-short') { | ||
else if (tokenType === 'monthname-short') { | ||
if (part.date_system === EPOCH_1317) { | ||
@@ -408,3 +436,3 @@ ret.push(l10n.mmm6[month - 1]); | ||
} | ||
else if (tok.type === 'monthname') { | ||
else if (tokenType === 'monthname') { | ||
if (part.date_system === EPOCH_1317) { | ||
@@ -420,19 +448,19 @@ ret.push(l10n.mmmm6[month - 1]); | ||
} | ||
else if (tok.type === 'weekday') { | ||
else if (tokenType === 'weekday') { | ||
ret.push(l10n.dddd[weekday]); | ||
} | ||
else if (tok.type === 'day') { | ||
else if (tokenType === 'day') { | ||
ret.push((tok.pad && day < 10 ? '0' : ''), day); | ||
} | ||
else if (tok.type === 'hour') { | ||
else if (tokenType === 'hour') { | ||
const h = hour % part.clock || (part.clock < 24 ? part.clock : 0); | ||
ret.push((tok.pad && h < 10 ? '0' : ''), h); | ||
} | ||
else if (tok.type === 'min') { | ||
else if (tokenType === 'min') { | ||
ret.push((tok.pad && minute < 10 ? '0' : ''), minute); | ||
} | ||
else if (tok.type === 'sec') { | ||
else if (tokenType === 'sec') { | ||
ret.push((tok.pad && second < 10 ? '0' : ''), second); | ||
} | ||
else if (tok.type === 'subsec') { | ||
else if (tokenType === 'subsec') { | ||
ret.push(l10n.decimal); | ||
@@ -444,3 +472,3 @@ // decimals is pre-determined by longest subsec token | ||
} | ||
else if (tok.type === 'am') { | ||
else if (tokenType === 'ampm') { | ||
const idx = hour < 12 ? 0 : 1; | ||
@@ -454,3 +482,3 @@ if (tok.short && !l10n_) { | ||
} | ||
else if (tok.type === 'hour-elap') { | ||
else if (tokenType === 'hour-elap') { | ||
if (value < 0) { ret.push(l10n.negative); } | ||
@@ -460,3 +488,3 @@ const hh = (date * 24) + Math.floor(Math.abs(time) / (60 * 60)); | ||
} | ||
else if (tok.type === 'min-elap') { | ||
else if (tokenType === 'min-elap') { | ||
if (value < 0) { ret.push(l10n.negative); } | ||
@@ -466,3 +494,3 @@ const mm = (date * 1440) + Math.floor(Math.abs(time) / 60); | ||
} | ||
else if (tok.type === 'sec-elap') { | ||
else if (tokenType === 'sec-elap') { | ||
if (value < 0) { ret.push(l10n.negative); } | ||
@@ -472,6 +500,6 @@ const ss = (date * DAYSIZE) + Math.abs(time); | ||
} | ||
else if (tok.type === 'b-year') { | ||
else if (tokenType === 'b-year') { | ||
ret.push(year + 543); | ||
} | ||
else if (tok.type === 'b-year-short') { | ||
else if (tokenType === 'b-year-short') { | ||
const y = (year + 543) % 100; | ||
@@ -481,7 +509,3 @@ ret.push(y < 10 ? '0' : '', y); | ||
} | ||
if (opts.nbsp) { | ||
// can we detect ? or string tokens and only do this if needed? | ||
return ret.join(''); | ||
} | ||
return ret.join('').replace(/\u00a0/g, ' '); | ||
return ret.join(''); | ||
} |
@@ -5,12 +5,34 @@ import { toYMD } from './toYMD.js'; | ||
export function dateToSerial (value, opts) { | ||
/** | ||
* Convert a native JavaScript Date, or array to an spreadsheet serial date. | ||
* | ||
* Returns a serial date number if input was a Date object or an array of | ||
* numbers, a null. | ||
* | ||
* ```js | ||
* // input as Date | ||
* dateToSerial(new Date(1978, 5, 17)); // 28627 | ||
* // input as [ Y, M, D, h, m, s ] | ||
* dateToSerial([ 1978, 5, 17 ]); // 28627 | ||
* // other input | ||
* dateToSerial("something else"); // null | ||
* ```` | ||
* | ||
* @param {Date | Array<number>} date The date | ||
* @param {object} [options={}] Options | ||
* @param {boolean} [options.ignoreTimezone=false] | ||
* Normally time zone will be taken into account. This makes the conversion to | ||
* serial date ignore the timezone offset. | ||
* @returns {number | null} The date as a spreadsheet serial date, or null. | ||
*/ | ||
export function dateToSerial (date, options) { | ||
let ts = null; | ||
if (Array.isArray(value)) { | ||
const [ y, m, d, hh, mm, ss ] = value; | ||
if (Array.isArray(date)) { | ||
const [ y, m, d, hh, mm, ss ] = date; | ||
ts = Date.UTC(y, m == null ? 0 : m - 1, d ?? 1, hh || 0, mm || 0, ss || 0); | ||
} | ||
// dates are changed to serial | ||
else if (value instanceof Date) { | ||
ts = value * 1; | ||
if (!opts || !opts.ignoreTimezone) { | ||
else if (date instanceof Date) { | ||
ts = date * 1; | ||
if (!options || !options.ignoreTimezone) { | ||
// Many timezones are offset in seconds but getTimezoneOffset() returns | ||
@@ -20,11 +42,11 @@ // time "rounded" to minutes so it is basically usable. 😿 | ||
dt.setUTCFullYear( | ||
value.getFullYear(), | ||
value.getMonth(), | ||
value.getDate() | ||
date.getFullYear(), | ||
date.getMonth(), | ||
date.getDate() | ||
); | ||
dt.setUTCHours( | ||
value.getHours(), | ||
value.getMinutes(), | ||
value.getSeconds(), | ||
value.getMilliseconds() | ||
date.getHours(), | ||
date.getMinutes(), | ||
date.getSeconds(), | ||
date.getMilliseconds() | ||
); | ||
@@ -39,9 +61,23 @@ // timestamp | ||
} | ||
// everything else is passed through | ||
return value; | ||
return null; | ||
} | ||
export function dateFromSerial (value, opts) { | ||
let date = (value | 0); | ||
const t = DAYSIZE * (value - date); | ||
/** | ||
* Convert a spreadsheet serial date to an array of date parts. | ||
* Accurate to a second. | ||
* | ||
* ```js | ||
* // output as [ Y, M, D, h, m, s ] | ||
* dateFromSerial(28627); // [ 1978, 5, 17, 0, 0, 0 ] | ||
* ```` | ||
* | ||
* @param {number} serialDate The date | ||
* @param {object} [options={}] The options | ||
* @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). | ||
* @returns {Array<number>} returns an array of date parts | ||
*/ | ||
export function dateFromSerial (serialDate, options) { | ||
let date = (serialDate | 0); | ||
const t = DAYSIZE * (serialDate - date); | ||
let time = floor(t); // in seconds | ||
@@ -58,15 +94,8 @@ // date "epsilon" correction | ||
const x = (time < 0) ? DAYSIZE + time : time; | ||
const [ y, m, d ] = toYMD(value, 0, opts && opts.leap1900); | ||
const [ y, m, d ] = toYMD(serialDate, 0, options && options.leap1900); | ||
const hh = floor((x / 60) / 60) % 60; | ||
const mm = floor(x / 60) % 60; | ||
const ss = floor(x) % 60; | ||
// return it as a native date object | ||
if (opts && opts.nativeDate) { | ||
const dt = new Date(0); | ||
dt.setUTCFullYear(y, m - 1, d); | ||
dt.setUTCHours(hh, mm, ss); | ||
return dt; | ||
} | ||
// return the parts | ||
return [ y, m, d, hh, mm, ss ]; | ||
} |
@@ -5,14 +5,27 @@ { | ||
"author": "Borgar Þorsteinsson <borgar@borgar.net>", | ||
"version": "2.5.2", | ||
"version": "3.0.0-rc1", | ||
"scripts": { | ||
"start": "webpack --mode development --watch", | ||
"build": "webpack --mode production", | ||
"dev": "nodemon -w test -w lib -x 'SKIPTABLES=1 tape 'test/*-test.js'|tap-min'", | ||
"build": "NODE_ENV=production rollup -c", | ||
"build:types": "jsdoc -c tsd.json lib>dist/numfmt.d.ts", | ||
"build:docs": "echo '# Numfmt API\n'>API.md; jsdoc -t node_modules/@borgar/jsdoc-tsmd -d console lib>>API.md", | ||
"build:all": "npm run build && npm run build:types && npm run build:docs", | ||
"start": "nodemon -w test -w lib -x 'SKIPTABLES=1 tape 'test/*-test.js'|tap-min'", | ||
"lint": "eslint index.js lib test", | ||
"test-all": "tape 'test/*-test.js'", | ||
"test": "SKIPTABLES=1 tape 'test/*-test.js'|tap-min" | ||
"test": "SKIPTABLES=1 tape './{lib,test}/*{.spec,-test}.js'|tap-min" | ||
}, | ||
"main": "./index.js", | ||
"main": "dist/numfmt.js", | ||
"types": "dist/numfmt.d.ts", | ||
"module": "lib/index.js", | ||
"exports": { | ||
".": { | ||
"require": "./dist/numfmt.js", | ||
"default": "./lib/index.js" | ||
} | ||
}, | ||
"preferGlobal": false, | ||
"repository": "git://github.com/borgar/numfmt.git", | ||
"repository": { | ||
"type": "git", | ||
"url": "git://github.com/borgar/numfmt.git" | ||
}, | ||
"homepage": "https://github.com/borgar/numfmt", | ||
@@ -25,2 +38,4 @@ "bugs": { | ||
"spreadsheet", | ||
"xls", | ||
"xlsx", | ||
"number", | ||
@@ -33,17 +48,23 @@ "date", | ||
"devDependencies": { | ||
"@babel/core": "~7.21.0", | ||
"@babel/core": "~7.24.5", | ||
"@babel/plugin-proposal-class-properties": "~7.18.6", | ||
"@babel/plugin-proposal-export-default-from": "~7.18.10", | ||
"@babel/plugin-proposal-export-default-from": "~7.24.1", | ||
"@babel/polyfill": "~7.12.1", | ||
"@babel/preset-env": "~7.20.2", | ||
"@borgar/eslint-config": "~3.0.0", | ||
"babel-loader": "~9.1.2", | ||
"eslint": "~8.35.0", | ||
"eslint-plugin-import": "~2.27.5", | ||
"tap-min": "~2.0.0", | ||
"tape": "~5.6.3", | ||
"terser-webpack-plugin": "~5.3.6", | ||
"webpack": "~5.75.0", | ||
"webpack-cli": "~5.0.1" | ||
"@babel/preset-env": "~7.24.5", | ||
"@borgar/eslint-config": "~3.1.0", | ||
"@borgar/jsdoc-tsmd": "~0.2.2", | ||
"@rollup/plugin-babel": "~6.0.4", | ||
"@rollup/plugin-terser": "~0.4.4", | ||
"babel-loader": "~9.1.3", | ||
"eslint": "~8.57.0", | ||
"eslint-plugin-import": "~2.29.1", | ||
"eslint-plugin-jsdoc": "~48.2.4", | ||
"jsdoc": "~4.0.3", | ||
"rollup": "~4.12.0", | ||
"rollup-plugin-minification": "~0.2.0", | ||
"tap-min": "~3.0.0", | ||
"tape": "~5.7.5", | ||
"webpack": "~5.91.0", | ||
"webpack-cli": "~5.1.4" | ||
} | ||
} |
367
README.md
@@ -55,15 +55,11 @@ # numfmt – a spreadsheet number formatter | ||
```js | ||
import numfmt from "numfmt"; | ||
import { format } from "numfmt"; | ||
// reusable function | ||
const formatter = numfmt("#,##0.00"); | ||
const output = formatter(1234.56); | ||
const output = format("#,##0.00", 1234.56); | ||
console.log(output); | ||
// ... or just | ||
const output = numfmt.format("#,##0.00", 1234.56); | ||
console.log(output); | ||
``` | ||
The full API documentation is available under [API.md](API.md). | ||
## Format syntax | ||
@@ -85,3 +81,3 @@ | ||
| `%` | Percentage | Number is multiplied by 100 before it is shown. `.7` formatted with `0%` will emit `"70%"` | ||
| `e-`, `e+` | Exponential format | `12200000` formatted with `0.00E+00` will emit `"1.22E+07"` | ||
| `E-`, `E+` | Exponential format | `12200000` formatted with `0.00E+00` will emit `"1.22E+07"` | ||
| `$`, `-`, `+`, `/`, `(`, `)`, ` ` | Pass-through | The symbol is printed as-is. | ||
@@ -122,354 +118,1 @@ | `\` | Escape | Pass the the next character through as-is. | ||
## API Reference | ||
### **numfmt**(pattern[, options]]) | ||
Constructs a new _formatter_ function with the specified options. | ||
Pattern must be a string according to the [ECMA-376][ecma] number format. <a href="#the-options">Options</a> should be an object of options. You may change defaults once for all instances using <a href="#numfmtoptionsoptions">numfmt.options</a>. | ||
#### _formatter_(value[, options]) | ||
Returns a formatted string for the argument value. If <a href="#the-options">options</a> object is provided then it overrides the constructor options of those options provided. | ||
#### _formatter_.isDate() | ||
Returns a true or false depending on if the pattern is a date pattern. The pattern is considered a date pattern if any of its sections contain a date symbol (see table above). Each section is restricted to to be _either_ a number or date format. | ||
#### _formatter_.isPercent() | ||
Returns a true or false depending on if the pattern is a percentage pattern. The pattern is considered a percentage pattern if any of its sections contain a percentage symbol (see table above). | ||
#### _formatter_.isText() | ||
Returns a true or false depending on if the pattern is a text percentage pattern if its definition is composed of a single section that includes that text symbol (see table above). For example `@` or `@" USD"` are text patterns but `#;@` is not. | ||
#### _formatter_.color(value) | ||
If the pattern defines colors this function will emit the color appropriate to the value. If no colors were specified this function returns `"black"`. | ||
```js | ||
import numfmt from "numfmt"; | ||
const formatter = numfmt("[green]#,##0;[red]-#,##0"); | ||
const color = formatter.color(-10); | ||
console.log(color); // "red" | ||
``` | ||
#### _formatter_.info | ||
Stores information and internals of the parsed format string. | ||
```js | ||
import numfmt from "numfmt"; | ||
console.log(numfmt("#,##0.00").info); | ||
// will emit... | ||
{ | ||
type: 'number', | ||
isDate: false, | ||
isText: false, | ||
isPercent: false, | ||
maxDecimals: 0, | ||
color: 0, | ||
parentheses: 0, | ||
grouped: 1, | ||
code: ',2', | ||
scale: 1, | ||
level: 4, | ||
}; | ||
``` | ||
| Member | Note | ||
|-- |-- | ||
| `type` | A string identifier for the type of the number formatter. The possible values are: `currency` , `date`, `datetime`, `error`, `fraction`, `general`, `grouped`, `number`, `percent`, `scientific`, `text`, `time` | ||
| `isDate` , `isText`, `isPercent` | Correspond to the output from same named functions found on the formatters. | ||
| `maxDecimals` | The maximum number of decimals this format will emit. | ||
| `color` | 1 if the format uses color on the negative portion of the string, else a 0. This replicates Excel's `CELL("color")` functionality. | ||
| `parentheses` | 1 if the positive portion of the number format contains an open parenthesis, else a 0. This is replicates Excel's `CELL("parentheses")` functionality. | ||
| `grouped` | 1 if the positive portion of the format uses a thousands separator, else a 0. | ||
| `code` | Corresponds to Excel's `CELL("format")` functionality. It is should match Excel's quirky behaviour fairly well. [See, Microsoft's documentation.](https://support.microsoft.com/en-us/office/cell-function-51bd39a5-f338-4dbe-a33f-955d67c2b2cf) | ||
| `level` | An arbirarty number that represents the format's specificity if you want to compare one to another. Integer comparisons roughly match Excel's resolutions when it determines which format wins out. | ||
| `scale` | The multiplier used when formatting the number (100 for percentages). | ||
#### _formatter_.dateInfo | ||
Stores information about date code use in the format string. | ||
```js | ||
import numfmt from "numfmt"; | ||
console.log(numfmt("dd/mm/yyyy").dateInfo); | ||
// will emit... | ||
{ | ||
year: true, | ||
month: true, | ||
day: true, | ||
hours: false, | ||
minutes: false, | ||
seconds: false, | ||
clockType: 24 | ||
}; | ||
``` | ||
| Member | Note | ||
|-- |-- | ||
| year | If any `y` or `b` operator was found in the pattern. | ||
| month | If any `m` operator was found in the pattern. | ||
| day | If any `d` operator was found in the pattern (including ones that emit weekday). | ||
| hours | If any `h` operator was found in the pattern. | ||
| minutes | If any `:m` operator was found in the pattern. | ||
| seconds | If any `s` operator was found in the pattern. | ||
| clockType | Will be set to `12` if `AM/PM` operators are being used in the formatting string, else it will be set to `24`. | ||
### numfmt.**format**(pattern, value[, options]) | ||
Parses the format pattern and formats the value according to the pattern, and optionally, any <a href="#the-options">options</a>. See definition [above](#numfmt). | ||
### numfmt.**round**(number[, places]) | ||
Return a value rounded to the specified amount of places. This is the rounding function used by the formatter (symmetric arithmetic rounding). | ||
### numfmt.**parseLocale**(tag) | ||
Parse a BCP 47 locale tag and emit an object of its parts. Intended for internal use. | ||
### numfmt.**getLocale**(tag) | ||
Used by the formatter to pull a locate from its registered locales. If subtag isn't available but the base language is, the base language is used. So if `en-CA` is not found, the formatter tries to find `en` else it returns a `null`. | ||
### numfmt.**addLocale**(data, tag) | ||
Register locale data for a language. The full data object looks like this: | ||
```js | ||
{ | ||
group: "\u00a0", // non-breaking space | ||
decimal: ".", | ||
positive: "+", | ||
negative: "-", | ||
percent: "%", | ||
exponent: "E", | ||
nan: "NaN", | ||
infinity: "∞", | ||
ampm: [ "AM", "PM" ], | ||
// gregorian calendar | ||
mmmm: [ "January", "February", … ], | ||
mmm: [ "Jan", "Feb", … ], | ||
dddd: [ "Sunday", "Monday", … ], | ||
ddd: [ "Sun", "Mon", … ], | ||
// islamic calendar | ||
mmmm6: [ "Muharram", "Safar", … ], | ||
mmm6: [ "Muh.", "Saf.", … ], | ||
} | ||
``` | ||
The data object does not need to be complete, it will fall back to defaults (mostly English) for any missing properties. Adding support for Faroese you would require only passing the data different from the defaults: | ||
```js | ||
numfmt.addLocale({ | ||
group: ".", | ||
decimal: ",", | ||
mmmm: [ "januar", "februar", … ], | ||
mmm: [ "jan", "feb", … ], | ||
dddd: [ "sunnudagur", "mánadagur", … ], | ||
ddd: [ "sun", "mán", … ], | ||
}, "fo-FO"); | ||
``` | ||
If the language tag provided has a subtag and a base language does not exit, the locale is register to both. In the Faroese example above both `fo` and `fo-FO` will be created. | ||
### numfmt.**isDate**(format) | ||
Returns a true or false depending on if the pattern is a date pattern. The pattern is considered a date pattern if any of its sections contain a date symbol (see table above). Each section is restricted to to be _either_ a number or date format. | ||
For compatibility reasons, this function is also available as `numfmt.is_date(format)`. | ||
### numfmt.**isPercent**(format) | ||
Returns a true or false depending on if the pattern is a percentage pattern. The pattern is considered a percentage pattern if any of its sections contain a percentage symbol (see table above). | ||
### numfmt.**isText**(format) | ||
Returns a true or false depending on if the pattern is a text percentage pattern if its definition is composed of a single section that includes that text symbol (see table above). For example `@` or `@" USD"` are text patterns but `#;@` is not. | ||
### numfmt.**getInfo**(format) | ||
Returns an object detailing the properties and internals of the format. See [formatter.info](#formatter-info) for details. | ||
### numfmt.**parseValue**(value[, options]) | ||
Attempt to parse a "spreadsheet input" string input and return its value and format. If the input was not recognized or valid, the function returns a `null`, for valid input it returns an object with two properties: | ||
* `v`: The parsed value. For dates, this will be an Excel style serial date unless the `nativeDate` option is used. | ||
* `z`: (Optionally) the number format string of the input. This property will not be present if it amounts to the `General` format. | ||
`numfmt.parseValue()` recognizes a wide range of dates and date-times, times, numbers, and booleans. Some examples: | ||
```js | ||
// basic number | ||
numfmt.parseValue("-123"); // { v: -123 } | ||
// formatted number | ||
numfmt.parseValue("$1,234"); // { v: 1234, z: "$#,##0" } | ||
// a percent | ||
numfmt.parseValue("12.3%"); // { v: 0.123, z: "0.00%" } | ||
// a date | ||
numfmt.parseValue("07 October 1984"); // { v: 30962, z: 'dd mmmm yyyy' } | ||
// an ISO formatted date-time | ||
numfmt.parseValue("1984-09-10 11:12:13.1234"); // { v: 30935.46681855787, z: "yyyy-mm-dd hh:mm:ss" } | ||
// a boolean | ||
numfmt.parseValue("false"); // { v: false } | ||
``` | ||
The formatting string outputted may not correspond exactly to the input. Rather, is it composed of certain elements which the input controls. This is comparable to how Microsoft Excel and Google Sheets parse pasted input. Some things you may expect: | ||
* Whitespace is ignored. | ||
* Decimal fractions are always represented by `.00` regardless of how many digits were shown in the input. | ||
* Negatives denoted by parentheses [`(1,234)`] will not include the parentheses in the format string (the value will still by negative.) | ||
* All "scientific notation" returns the same format: `0.00E+00`. | ||
Internally the parser calls, `numfmt.parseNumber`, `numfmt.parseDate`, `numfmt.parseTime` and `numfmt.parseBool`. They work in the same way exept with a more limited scope. If you do not want to be this liberal then use those functions. | ||
Be warned that the parser do not (yet) take locale into account so all input is assumed to be in "en-US". This means that `1,234.5` will parse, but `1.234,5` will not. Similarily, the order of date parts will be US centeric. This may change in the future so be careful what options you pass the functions. | ||
The parser does not listen to globally set default options (as set with [numfmt.options](#numfmtoptionsoptions)). | ||
### numfmt.**parseNumber**(value[, options]) | ||
Parse a numeric string input and return its value and format. If the input was not recognized or valid, the function returns a `null`, for valid input it returns an object with two properties: | ||
* `v`: the parsed value. | ||
* `z`: the number format of the input (if applicable). | ||
See [numfmt.parseValue](#numfmt-parsevalue-value-options) for more details. | ||
### numfmt.**parseDate**(value[, options]) | ||
Parse a date or datetime string input and return its value and format. If the input was not recognized or valid, the function returns a `null`, for valid input it returns an object with two properties: | ||
* `v`: the parsed value (in Excel serial time). | ||
* `z`: the number format of the input. | ||
See [numfmt.parseValue](#numfmt-parsevalue-value-options) for more details. | ||
### numfmt.**parseTime**(value[, options]) | ||
Parse a time string input and return its value and format. If the input was not recognized or valid, the function returns a `null`, for valid input it returns an object with two properties: | ||
* `v`: the parsed value (in Excel serial time). | ||
* `z`: the number format of the input. | ||
See [numfmt.parseValue](#numfmt-parsevalue-value-options) for more details. | ||
### numfmt.**parseBool**(value[, options]) | ||
Parse a string input and return its boolean value. If the input was not recognized or valid, the function returns a `null`, for valid input it returns an object with one property: | ||
* `v`: the parsed value. | ||
See [numfmt.parseValue](#numfmt-parsevalue-value-options) for more details. | ||
### numfmt.**dateToSerial**(date[, options]) | ||
Convert a native JavaScript Date, or array to an spreadsheet serial date. Can be set to ignore timezone information with `{ ignoreTimezone: true }` if you are passing in Date objects. | ||
Returns a serial date number if input was a Date object or an array, otherwise it will pass the input through untouched. | ||
```js | ||
// input as Date | ||
numfmt.dateToSerial(new Date(1978, 5, 17)); // 28627 | ||
// input as [ Y, M, D, h, m, s ] | ||
numfmt.dateToSerial([ 1978, 5, 17 ]); // 28627 | ||
// other input | ||
numfmt.dateToSerial("something else"); // "something else" | ||
```` | ||
This function does not listen to globally set default options (as set with [numfmt.options](#numfmtoptionsoptions)). | ||
### numfmt.**dateFromSerial**(value[, options]) | ||
Convert a spreadsheet style serial date value to an Array of date parts (`[ Y, M, D, h, m, s ]`) or, if the `nativeDate` option is used, a native JavaScript Date. | ||
```js | ||
// output as [ Y, M, D, h, m, s ] | ||
numfmt.dateToSerial(28627); // [ 1978, 5, 17, 0, 0 ,0 ] | ||
// output as Date | ||
numfmt.dateFromSerial(28627, { nativeDate: true }); // new Date(1978, 5, 17) | ||
```` | ||
This function does not listen to globally set default options (as set with [numfmt.options](#numfmtoptionsoptions)). | ||
### numfmt.**options**(options) | ||
Set a default option or <a href="#the-options">options</a> for the formatter. This will affect all formatters unless they have overwritten options at construction time. Calling `numfmt.options(null)` will reset to internal defaults. | ||
```js | ||
// basic "default" formatter | ||
const weekdayEN = numfmt("dddd"); | ||
weekdayEN(1234); // "Monday" | ||
// setting a new default | ||
numfmt.options({ locale: "is" }); | ||
// call the same formatter | ||
weekdayEN(1234); // "mánudagur" | ||
// construct a new formatter with a locale | ||
const weekdayFR = numfmt("dddd", { locale: "fr", }); | ||
weekdayFR(1234); // "lundi" | ||
// override settings at call-time | ||
weekdayEN(1234, { locale: "pl" }); // "poniedziałek" | ||
weekdayFR(1234, { locale: "pl" }); // "poniedziałek" | ||
``` | ||
#### The **options** | ||
As well as allowing locale customization, numfmt behaviour can be controlled with other options: | ||
| Member | Type | Default | Note | ||
|-- |-- |-- |-- | ||
| locale | `string` | `""` | A [BCP 47][bcp] string tag. Locale default is english with a `\u00a0` grouping symbol (see <a href="#numfmt-addlocale-data-tag">numfmt.addLocale</a>). | ||
| throws | `boolean` | `true` | Should the formatter throw an error if a provided pattern is invalid. If not, a formatter will be constructed which only ever outputs an error string (see _invalid_ in this table). | ||
| invalid | `string` | `"######"` | The string emitted when no-throw mode fails to parse a pattern. | ||
| nbsp | `boolean` | `true` | By default the formatters will emit [non-breaking-space][nbsp] rather than a regular space when emitting the formatted number. Setting this to false will make it use regular spaces instead. | ||
| leap1900 | `boolean` | `true` | Simulate the Lotus 1-2-3 [1900 leap year bug][bug1900]. It is a requirement in the Ecma OOXML specification so it is on by default. | ||
| dateErrorThrows | `boolean` | `false` | Should the formatter throw an error when trying to format a date that is out of bounds? | ||
| dateErrorNumber | `boolean` | `true` | Should the formatter switch to a General number format when trying to format a date that is out of bounds? | ||
| overflow | `string` | `"######"` | The string emitted when a formatter fails to format a date that is out of bounds. | ||
| dateSpanLarge | `boolean` | `true` | Extends the allowed range of dates from Excel bounds (1900–9999) to Google Sheet bounds (0–99999). | ||
| ignoreTimezone | `boolean` | `false` | Normally when date objects are used with the formatter, time zone is taken into account. This makes the formatter ignore the timezone offset. | ||
| nativeDate | `boolean` | `false` | when using the [numfmt.parseDate](#numfmt-parsedate-value), [numfmt.parseValue](#numfmt-parsevalue-value-options) and [numfmt.dateFromSerial](#numfmt-datefromserial-value) functions, the output will be a Date object. | ||
[ecma]: https://www.ecma-international.org/publications/standards/Ecma-376.htm | ||
[ssf]: https://www.npmjs.com/package/ssf | ||
[bcp]: http://www.rfc-editor.org/rfc/bcp/bcp47.txt | ||
[nbsp]: https://en.wikipedia.org/wiki/Non-breaking_space | ||
[bug1900]: https://docs.microsoft.com/en-us/office/troubleshoot/excel/wrongly-assumes-1900-is-leap-year |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No repository
Supply chain riskPackage does not have a linked source code repository. Without this field, a package will have no reference to the location of the source code use to generate the package.
Found 1 instance in 1 package
498992
33
5365
20
1
116
2
1