Skip to content

Formatting Negative Numbers Correctly by Locale

8 min read · updated August 11, 2026

A negative amount can correctly appear as −1.234,56, as (1,234.56), as 1.234,56− or as − 1 234,56. Which one is right depends on two independent things, and code that treats them as one thing gets a predictable subset wrong.

The pattern grammar decides it

The authority here is the Unicode Common Locale Data Repository, whose number formatting is specified in UTS #35, Part 3: Numbers. A locale’s number format is a pattern string, and the pattern may contain an optional negative subpattern separated from the positive one by a semicolon:

#,##0.###              positive only; negative derived
#,##0.00;(#,##0.00)    explicit negative subpattern
¤#,##0.00;(¤#,##0.00)  the accounting currency pattern for en

The rule when the negative subpattern is absent is the one worth committing to memory, because it is where most locales sit: the negative form is the positive form with the locale’s minusSign symbol prefixed. That is a two-step lookup, not one — the pattern gives the grouping and the decimal separator, and a separate symbol table gives the sign character. Reading only the pattern tells you nothing about what character the sign is.

A second rule catches people out when they hand-write a pattern: in a two-part pattern, only the prefix and suffix of the negative subpattern are used. The number of digits, the grouping and the decimal places are always taken from the positive subpattern. So writing #,##0.00;-#,##0.0 does not produce one decimal place for negatives; it produces two, with a minus in front.

The minus sign is locale data

The minusSign symbol is not guaranteed to be U+002D HYPHEN-MINUS. CLDR carries it per locale precisely because it varies: some locales use U+2212 MINUS SIGN, the typographically correct character which is wider and aligns with the digits, and locales written in right-to-left scripts carry bidi control characters alongside the sign so that it renders on the expected side of the number rather than being reordered by the bidirectional algorithm.

That last case is the one that produces bug reports. A number formatted with a bare U+002D and dropped into an Arabic or Hebrew paragraph can have the sign moved to the other end of the number by the Unicode bidirectional algorithm, turning −1234 into something a reader parses as 1234−. The formatted string is not wrong; the surrounding directionality is. Formatting through the locale data rather than by string concatenation is what avoids it, because the locale data already contains the isolating characters.

CLDR publishes two releases a year and per-locale symbols do change between them. Do not copy a symbol table into your source. Read it from the ICU or CLDR build you ship, and if you need to see the current values, the CLDR number-symbol charts on unicode.org are the primary reference — this page names the mechanism and deliberately does not reproduce a table that would be stale within a release.

Parentheses are a document type, not a locale

The most common misconception is that parentheses are “the US convention”. They are not. US English has a perfectly ordinary minus-sign form, and it is what a US price, a US temperature and a US bank balance in an app all use. Parentheses are the accounting convention, used in financial statements, and they are a second format that exists alongside the standard one in the same locale.

CLDR models this explicitly with a separate accounting currency pattern, and ECMA-402 exposes the choice as an option rather than as a locale:

const n = -1234.5;

new Intl.NumberFormat("en-US", {
  style: "currency", currency: "USD",
}).format(n);
// "-$1,234.50"

new Intl.NumberFormat("en-US", {
  style: "currency", currency: "USD", currencySign: "accounting",
}).format(n);
// "($1,234.50)"

new Intl.NumberFormat("de-DE", {
  style: "currency", currency: "EUR",
}).format(n);
// "-1.234,50 EUR" rendered with the euro sign after the number

The currencySign option is the fact to take away. Locales that have no distinct accounting form simply return the standard one, so passing it is safe everywhere and asking for it is the correct way to express “this is a financial statement”. Hard-coding parentheses for en-US and a minus for everything else encodes the wrong axis, and it will put parentheses on a US weather app.

Where accounting formats are used, they are frequently paired with red text or a trailing CR/DR marker, and in a printed statement the parenthesis form is often the only negative indicator because the column header already says what the sign means. None of that is derivable from a locale tag; it comes from the document.

Trailing signs and where they come from

The pattern grammar permits a suffix, so #,##0.00- is a legal pattern and a trailing sign is a legal format. In modern locale data it is rare in general-purpose formats, but it is very much alive in two places a developer meets it.

  • Fixed-width interchange files. Banking, payroll and legacy accounting formats routinely carry the sign at the end of the field, sometimes as a literal trailing hyphen and sometimes as a “zoned decimal” or overpunch encoding inherited from COBOL, where the sign is folded into the last digit character. These are file formats, not locales, and the spec for the file is the only authority.
  • Spreadsheet imports. A CSV exported with a trailing sign will be read as text rather than as a number by most spreadsheets, which is how a column of negatives silently becomes a column of strings and every sum below it becomes wrong by exactly the negative total.

Formatting is not reversible

Every one of these forms is easy to produce and none of them is reliably parseable back without knowing the locale it was produced in. 1.234 is one thousand two hundred and thirty-four in German and one point two three four in English. Number.parseFloat("(1,234.56)") returns NaN, silently, and a naive fallback of || 0 turns a debit of 1,234.56 into zero.

The rule that follows is unglamorous and absolute: a formatted number is output. Store and transmit the raw value — a decimal string or a minor-unit integer — and format only at the edge where it is rendered. A number that has been through a locale formatter and back has lost information, and for money it has lost it in a direction that does not reconcile.

What to actually do

  • Never let a language model format a monetary figure. Have it produce a bare numeric value and the currency code, then format with Intl.NumberFormat or ICU. A model will produce a plausible format for the locale it infers, and it will not be consistent across a hundred rows of a table.
  • Pass currencySign: "accounting" when and only when the output is a financial statement. Do not choose between minus and parentheses by locale.
  • Never build a negative by concatenating "-" to a formatted positive. That skips the locale’s sign symbol, skips the bidi isolates, and puts the sign in the wrong position for any locale with a suffix pattern.
  • For accessibility, remember that a screen reader announces parentheses as punctuation or not at all. An accounting table needs the sign in the accessible name even when the visual form is parenthetical.

The sibling decisions are the decimal separator and where the currency symbol goes, both of which come out of the same pattern string and should be solved by the same call.