Shipmind Labs

A ticket came in saying our minus sign sat in the wrong place. Not missing. Wrong place.

Finance exported a report, and the negative amounts showed up wrapped in parentheses on one view and with a leading minus on another. Same number, same currency, two screens. Assuming a bug was reasonable.

It was not a bug. There were two different formatting paths, both of them ours, and each one made its own small decision about what a negative amount looks like.

That is when we stopped treating display as a cosmetic layer.

In our open-source amountfield project the display module is now the showing side of a value, and it has no reading side on purpose. You give it a money value and formatting options, and it formats. That is all it does.

What we pushed into it:

The locale decides the symbol, we do not. If you pass none, it falls back to a single default. Accounting style negatives happen only when the caller asks for accounting explicitly, and otherwise you get a standard minus. One switch, one owner.

The amount's own exponent wins. We pin minimum and maximum fraction digits to the exponent the value carries, instead of letting the formatter use whatever it keeps for that currency code. The value knows how precise it is, while the currency table knows only the general case.

Grouping separators are on by default, because a displayed amount has stopped moving. Typing a number and reading a number are different jobs.

Where the currency mark goes is not a table in our code. We build a formatter, format the number one, read the resulting parts, see where the currency sits relative to the integer, and report before or after, plus whatever literal sits between them, usually a no-break space rather than a plain one. We would rather ask the platform than keep a list we would probably get wrong for some locale we have never shipped to.

Formatters are memoized by the whole combination that produced them: locale, currency, display style, sign style, grouping, exponent. Building a formatter is not free, and this code runs in lists.

The rule that actually saved us is the boring one: display reads a decimal string rather than a float, and nothing takes its output back in. A formatted string offered to the field gets refused outright instead of half-parsed. Parentheses, no-break spaces and locale separators are not input. They are the end of the road.

The real cost of getting this wrong shows up as a support thread where two people look at the same balance and disagree about the sign, which is a good deal worse than a rendering glitch.

If you run a multi-currency product, the same choice lands on you: let the locale decide negatives, or force one house style everywhere.

Was this useful?

Building something similar?

or email hello@shipmindlabs.com