Repository navigation
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Migrating from numfmt 3.x to 4.0
Version 4.0 rewrites the library in TypeScript, and brings formatting and parsing closer to Excel. Most of the changes are to the interface: how absence is signalled, what token types and parse results look like, and how the package is published. Some output changes as well, mainly in rounding and locale data. Those are listed under Output changes.
Most projects need three changes: replace
nullchecks withundefinedchecks, uppercase any hardcoded token type strings, and read parse results fromvalueandformatIdinstead ofvandz. If your code never inspects tokens, never parses input, and never compares a return value tonull, you may need no code changes at all.Breaking changes
1. Functions return
undefinedinstead ofnullEvery function that signalled "no result" with
nullnow returnsundefined. This affects:parseValue(),parseNumber(),parseDate(),parseTime(),parseBool()formatColor()dateToSerial()getLocale()If you want code that works with both, use a loose check, which catches
nullandundefinedalike:TypeScript catches this for you:
color !== nullno longer narrows the type, so usingcolorreports that it is possiblyundefined. Plain JavaScript does not catch it — a=== nulltest now silently takes the wrong branch.The parse functions changed further: see item 10.
2. Token type values are uppercase
tokenize()emits the same tokens in the same order, but everytypevalue is now uppercase.Replace any hardcoded comparison, or better, import the constant:
3.
tokenTypesis gone; importTOKEN_*constants insteadThe
tokenTypesdictionary has been removed. The individual constants are now exported from the package root, and theTokenTypetype is exported for TypeScript users.The constant names map to the old dictionary keys directly:
tokenTypes.AMPMbecomesTOKEN_AMPM,tokenTypes.BREAKbecomesTOKEN_BREAK, and so on for all 34 types. There is also one new type,TOKEN_CURRENCY(see item 12).4. Condition tokens carry a string, not an array
This is the one change that fails silently in both JavaScript and TypeScript, so check for it explicitly. In 3.x a
CONDITIONtoken'svaluewas a two-element array of operator and operand. It is now the raw condition string.Indexing still "works" and still returns a string, which is why nothing warns you:
If you split conditions apart yourself, parse the string instead:
The upside is that
Token.valueis now uniformly astringfor every token type.5. The
repeatCharoption is nowfillChardefaultOptionsin 3.x declared an option namedrepeatChar, but the formatter readopts.fillChar— so settingrepeatCharnever did anything. In 4.0 the option is namedfillCharthroughout and is documented.If you passed
fillCharin 3.x it kept working and still works. Only the deadrepeatCharname is gone.6. Package entry points and deep imports
The package now ships only built output. In 3.x the
exportsmap resolved the default condition to./lib/index.js, so raw source was published and deep imports intonumfmt/lib/*.jsresolved. In 4.0 onlydistis published.Everything previously reachable by deep import is available from the root.
7. TypeScript: locale name arrays are fixed-length tuples
LocaleDatanow types its name lists as tuples, so the compiler enforces the correct count:MonthNamesis exactly 12 strings andDayNamesis exactly 7. Both types are exported.If your data is a plain
string[], assert it at the boundary:ampmandboolare likewise[ string, string ]. This only affects TypeScript callers; the runtime is unchanged.8. TypeScript:
ParseDatais now two typesThe single
ParseDatatypedef is replaced byParseDataNumandParseDataBool, matching what each parser can actually return:parseNumber,parseDate,parseTimeParseData | nullParseDataNum | undefinedparseBoolParseData | nullParseDataBool | undefinedparseValueParseData | nullParseDataNum | ParseDataBool | undefinedParseDataNum.valueis anumberandParseDataBool.valueis aboolean, so you no longer need to narrowvalueyourself. Both types carry an optionalformatId, so readingresult.formatIdoff aparseValue()result works without narrowing. Item 10 describes the renamed properties.9.
preferMDYis replaced byformat.dateOrderLocaleData.preferMDYhas been removed. A locale's date order now lives in the newformatproperty, alongside its other format conventions, asdateOrder:dateOrder012/31/2025)preferMDY: true131/12/2025)preferMDY: false22025/12/31)preferMDY: falseThe parsers read numeric dates in the locale's order: month first when
dateOrderis0, day first when it is1, and year first when it is2. In 3.x, every locale withoutpreferMDYread dates day first.When you register a locale, set
format.dateOrderexplicitly rather than relying on the default.In JavaScript, passing
preferMDYis now silently ignored, and reading it returnsundefined. In TypeScript, both are compile errors.One built-in locale changes as a result: Filipino (
fil) now parses dates month first, matching the Philippines' convention. In 3.x onlyenanden-USdid.10. Parse results are
{ value, formatId }parseValue(),parseNumber(),parseDate(),parseTime(), andparseBool()return{ value, formatId }instead of{ v, z }.formatIdis the number of one of Excel's built-in formats instead of a pattern string. To get the pattern, pass it to the newgetFormatFromId(), which builds it for the locale you give.Because the format is now an ID, a date parsed in one locale can be shown in that locale's own pattern:
Reading
.vor.znow givesundefinedwithout an error in JavaScript. TypeScript reports both as compile errors.11. Parsing follows Excel and the locale by default
The parse functions take a new
modeoption. The default,mode: 0, accepts what Excel accepts when the same text is typed into a cell, using the locale's settings:/and-always work, and.works where the locale's date separator is.(31.12.2024inde).€is accepted on either side.1 1/2.mode: 1accepts a curated set of inputs, closer to what 3.x accepted. It allows some obvious forms that Excel rejects, such as dates with a weekday, and rejects some unusual ones Excel allows, such assep10or1984/07-4.If you show parsed input in a user interface, check the results against inputs your users actually type, and consider
mode: 1.12. Currency symbols are
CURRENCYtokenstokenize()now emits aCURRENCYtoken for currency symbols such as$,€,£, and¥. In 3.x these werechartokens.If you look for currency symbols among
CHARtokens, checkTOKEN_CURRENCYas well.An empty locale tag,
[$], is now aLOCALEtoken with an empty value. In 3.x it was amodifiertoken, and the pattern failed to parse.Additions
FormatOptions,FormatInfo,FormatDateInfo,LocaleData,LocalFormat,LocaleToken,MonthNames,DayNames,Token,TokenType,ParseDataNum, andParseDataBoolare all exported.LocaleDatahas a newformatproperty, typed asLocalFormat. It holds a locale's date order, date and time separators, padding of date and time parts, currency symbol position, negative number style, and spacing before the percent sign. Every built-in locale defines it.listLocales()is now exported from the package root.getLocale()accepts an Excel locale code as a number, matching whatformat()already accepted for itslocaleoption:getLocale(0x0419).addLocale()accepts a parsedLocaleTokenas its second argument, not only a string tag.format()options are typed asPartial<FormatOptions>, so misspelled or wrongly typed options are compile errors.getFormatFromId(id, locale)returns the pattern for one of Excel's built-in format IDs, built from the locale's conventions:getFormatFromId(14, 'de')returns'dd.mm.yyyy'. It emulates Windows Excel.delocalize(pattern, locale)converts a pattern written in a locale's own syntax to the canonical en-US form:delocalize('[Rot]#.##0,00', 'de')returns'[Red]#,##0.00'.modeoption, typed asParseValueOptions(see item 11).TOKEN_CURRENCYis a new token type (see item 12).[$-F800]and[$-x-sysdate]format a date in the locale's long date format, and[$-F400]and[$-x-systime]format a time in its time format.[$-is-IS,6], alongside the numeric LCID form.LocaleDatahas new properties:currency,general,color,colors, andopcodes(used bydelocalize()), andsysdateandsystime(used by the system date and time codes). Every built-in locale defines them.LocalFormat.monthFirstsays whether a locale's long date puts the month before the day. It is worked out fromsysdatewhen the locale is created, unless you set it.getFormatInfo()reportsminDecimals, the number of decimals the pattern always shows, next tomaxDecimals.Fixed
Object.prototypeno longer crash. In 3.x the pattern cache inherited fromObject.prototype, soformat('toString', 1)threwTypeError: Cannot read properties of undefined. The cache is now aMap, and such patterns are handled like any other invalid pattern — throwing aSyntaxError, or emitting theinvalidstring under{ throws: false }.format('[$]0', 1)returns'1', as in Excel. In 3.x it threwUnknown token modifier.Output changes
These changes make output match Excel. They need no code changes, but stored or compared output can differ from 3.x.
Rounding
Numbers round as Excel does: first to 15 significant digits, a tie going toward zero, and then to the pattern's last digit, a tie going away from zero. Digits are always written out in full.
0.001.0051.001.01#,##02 ** 701,.18,059,162,071,741,13e,+211,180,591,620,717,410,000,00001e211e+211000000000000000000000##0.0E+0999.961000.0E+01.0E+3General4403928373.544039283734403928374General19147467.11519147467.1119147467.12round()follows the same rules, soround(1.005, 2)returns1.01instead of1, and it rounds to at most 15 significant digits. Vulgar fractions such as# ?/?keep their 3.x rounding.Times that round up to midnight
A time that rounds up to midnight moves the date to the next day, when the pattern shows a time.
m/d/yy h:mmformats45292.9999999as1/2/24 0:00, where 3.x showed1/1/24 0:00. A pattern that shows only a date is unchanged.System date and time codes
[$-F800]and[$-F400]now switch to the locale's long date and time formats, as in Excel. 3.x ignored the code and used the rest of the pattern.Locale data
The built-in locale data has been corrected against Windows Excel, so localized output differs for many locales. Most changes are to month and day names:
ismmmmar.mardemmmMärzMrzdadddtir.ties-MX#,##0.001.234,501,234.50svGeneral(fortrue)TRUESANTIn all, short month names changed in 47 locales, short day names in 40, long month names in 23, AM/PM names in 15, and long day names in 9. The group separator changed in
de-CH,it-CH, andpt, and a few other locales changed their decimal separator, percent sign, orTRUE/FALSEwords.What did not change
Pattern syntax is unchanged, so stored format strings need no migration. Apart from the changes listed above,
format()results,formatColor()colors,getFormatDateInfo(), and theis*Format()predicates match 3.2.6.Upgrade checklist
Run these against your codebase to find affected call sites:
Then read through any code that indexes into a token's
value, since the condition-token change (item 4 above) produces no error. Check code that looks for currency symbols amongCHARtokens, since they are nowCURRENCYtokens (item 12). And if you store or compare formatted output, expect differences from the rounding and locale data changes under Output changes.