Skip to content

[plugin-dashboard] recordFields re-introduces the divide-by-100 round trip #4590 removed: 1.605 renders 1.60% where half-up is 1.61% #5607

Description

@os-sales

Found while implementing objectui#4596 (comment repair in packages/core/src/utils). Out of that card's surface — recorded rather than folded in.

The site

packages/plugin-dashboard/src/recordFields.tsx:189-192, the percent branch of the record-field formatter:

if (typeof fmt === 'string' && /%/.test(fmt) && typeof value === 'number') {
  const decimals = (fmt.match(/0\.(0+)%/) || [undefined, ''] as any)[1].length;
  const normalized = value > 1 ? value / 100 : value;
  return formatPercent(normalized * 100, decimals, displayLocale);
}

For value <= 1 the pair is a correct fraction-to-points scaling (value * 100). For value > 1 it is (value / 100) * 100 — an identity that is not value-preserving in binary floating point, and the result is then handed to formatPercent, whose own percentDisplayValue passes a magnitude at or above 1 through unchanged.

This is the same artefact objectui#4590 (PR #4595) removed from inside formatPercent, re-introduced one call frame upstream. formatPercent now renders percentage points directly via style: 'percentPoints' with no division, so the division here is both lossy and pointless.

Measured

Values v = i/1000 for i in 1001..200000 (all v > 1, so all take the divided branch), rendered through the same Intl options formatPercent uses (style: 'unit' / unit: 'percent' / unitDisplay: 'narrow') at precisions 0/1/2:

  • 19,978 of 199,000 values have (v / 100) * 100 !== v.
  • 1,108 rendered strings actually move, every one a last-digit off-by-one.

First movers, all at 2 decimals — [value, direct, via the round trip]:

1.605  ->  "1.61%"   vs   "1.60%"
1.655  ->  "1.66%"   vs   "1.65%"
1.705  ->  "1.71%"   vs   "1.70%"
1.785  ->  "1.79%"   vs   "1.78%"
1.835  ->  "1.84%"   vs   "1.83%"

Half-up on the authored decimal is the left column, so the round trip is the artefact. Same failure mode objectui#4590 recorded: a last-digit off-by-one, which is the shape least likely to be noticed and most likely to be trusted.

Suggested shape (not a ruling)

The division has no work to do — the branch only needs to decide whether the stored number is a fraction or already points:

return formatPercent(value > 1 ? value : value * 100, decimals, displayLocale);

Worth checking against percentDisplayValue first: formatPercent already applies that helper, so this call site may be duplicating a scaling decision that @object-ui/core owns as its single source of truth (percentDisplayValue's doc comment states exactly that intent). If so, the right fix may be to drop the local normalisation entirely rather than to repair it. That is a design question for whoever picks this up, not something to guess at here.

Scope note

Not a duplicate of objectui#4590, which closed the division inside formatPercent. This is a separate caller doing its own division before the call. Nothing in objectui#4596's fenced surface touches plugin-dashboard.

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpm:dispatched

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions