Skip to content

formatCurrency drops a real cents digit: an amount of 1234.5 renders $1,234.5, not the $1,234.50 its own contract promises #4332

Description

@yinlianghui

Summary

formatCurrency in packages/fields/src/index.tsx renders a currency amount whose fractional part has one significant digit with one decimal place instead of two. 1234.5 renders as $1,234.5.

Its own doc comment states the opposite contract:

Trailing .00 is dropped when the value is a whole number — Salesforce convention: $1,234.50 keeps cents; $1,234 does not.

So the intended behaviour is documented, and the code does not deliver it.

Mechanism

const isWhole = Number.isFinite(value) && value === Math.trunc(value);
const maxFrac = isWhole ? 0 : 2;
// ...
minimumFractionDigits: 0,      // ← the defect
maximumFractionDigits: maxFrac,

maximumFractionDigits is correctly switched on wholeness, but minimumFractionDigits is a constant 0. For a non-whole amount that gives the range [0, 2], and Intl then emits the shortest representation — so a genuine cents value of .50 is printed as .5. The intended pairing is minimumFractionDigits: isWhole ? 0 : 2.

Measured directly against the construction:

1234.5   =>  $1,234.5      (expected $1,234.50)
1234.56  =>  $1,234.56     (correct)
1234     =>  $1,234        (correct — the whole-number case the comment describes)

Only the "non-whole, one significant decimal digit" case is wrong, which is why it survived: the whole-number and two-digit cases both look right.

Impact

Reachable on any currency-typed field whose amount ends in a zero cent digit — prices like 19.90, 1234.50, 0.50. Those render as $19.9, $1,234.5, $0.5. This is money on a record page and in grid cells, so it reads as a data error rather than a formatting one.

Consumers of the same function inherit it: CurrencyCellRenderer (packages/fields/src/index.tsx), ObjectGrid (packages/plugin-grid/src/ObjectGrid.tsx), recordFields (packages/plugin-dashboard), and ObjectGantt (packages/plugin-gantt).

Not fixed here, and why

Found while implementing #4033 (number renderers hardcoding Intl.NumberFormat('en-US')). It is out of that card's scope — it is neither a locale defect nor a grouping defect, and #4033's ruling is deliberately narrow — so it is filed rather than folded in.

PR for #4033 pins the current behaviour in a control test (packages/fields/src/__tests__/NumberCellRenderer.grouping.test.tsx, "keeps grouping and the fractional part when the amount is not whole") with a comment pointing here, so this issue's fix will flip a test that is deliberately red-on-fix rather than silently changing an unwatched surface.

Suggested fix

minimumFractionDigits: isWhole ? 0 : 2, plus a pin for the .50 case. Worth checking CurrencyField's formatAmount (packages/fields/src/widgets/CurrencyField.tsx) at the same time — it uses precision for both bounds and so does not have this bug, which makes the two paths disagree today.


Generated by Claude Code

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions