Skip to content

fix(tips): abbreviate tip presets that run past three digits - #704

Merged
bmc08gt merged 2 commits into
mainfrom
fix/tip-preset-amount-abbreviation
Aug 31, 2026
Merged

fix(tips): abbreviate tip presets that run past three digits#704
bmc08gt merged 2 commits into
mainfrom
fix/tip-preset-amount-abbreviation

Conversation

@bmc08gt

@bmc08gt bmc08gt commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Four chips share a row in the Send a Tip sheet, and a preset is a converted USD tier, so in a small-unit currency every tier is four digits or more. In ARS the three presets render $1,400 / $35,000 / $1,400,000, and Text's minimumScaleFactor(0.5) shrinks them to half size to fit the chip.

FiatAmount.formattedAbbreviated(maxDigits:) caps the figure at three digits. Under the first scale it is plain formatted(...); past that the value is scaled and suffixed K/M/B/T, keeping only the decimals the cap leaves room for.

Currency Presets before After
ARS $1,400 $35,000 $1,400,000 $1.4K $35K $1.4M
COP $97,500 $97.5K
VND ₫650,000 ₫650K
IDR 400,000 400K
USD $1 $5 $25 unchanged

It formats through the same NumberFormatter.fiat path as formatted, so the symbol, grouping, and the negative form (-$1.5K) come from one place.

The same rule as Android

code-payments/code-android-app#1381 does this on Android, and the first version here did not produce the same strings: it kept a decimal only while the scaled figure was a single digit, so it spent two digits of a three-digit budget — $1.2K against $1.23K, $12K against $12.3K, 26K against 25.5K for an ARS preset. Android's rule is the one that answers "three digits", so this takes it.

The implementation now mirrors Fiat.abbreviated(maxDigits:) step for step: round to maxDigits significant digits, pick the largest scale the rounded value clears, spend what is left on decimals. Rounding first is what makes 999,999 print $1M instead of $1,000K, and it is half-up on both sides. Two behaviours came along with it — the T scale, where 1e12 used to print $1,000B, and 999.99, which now carries up to $1K rather than printing five digits.

formattedDroppingZeroFraction() is Android's FormattingRule.Truncated: the fraction is dropped when the amount is whole and kept when it isn't. It formats the amounts under the first scale, and replaces the local customTitle helper the sheet had for the same job.

Two more things Android does that this now does:

  • The custom "…" chip abbreviates as well. Android's slot is the same composable for presets and the custom amount, so it always did.
  • Both chips hand VoiceOver the unabbreviated amount, matching Android's content description. The abbreviation is visual only.

One abbreviation rule, not two

CompactCurrencyFormatStyle — the market-cap and weekly-delta style in Currency Discovery — was a second abbreviation rule, built on ICU's .compactName with its own symbol and sign handling. It is now a two-line FormatStyle entry point onto formattedAbbreviated, for the Double figures SwiftUI formats inline.

Market caps therefore move with the shared rule, and being capped at three digits rather than two they gain a digit: $1,029,331 reads $1.03M rather than $1M, -12,400 reads -$12.4K. $10.5M no longer reads $10M — ICU compact notation rounds half-even, display rounding in this app is half-up.

That the row follows the tip chips is the intent, not a side effect. It is not width-constrained the way a chip is, and getting ICU's shape back would take a second digit budget that caps decimals without capping whole digits — maxDigits: 2 is not it, since it rounds $211K down to $210K. That is a third rule to keep in step with Android for the sake of one row.

The style's Int(value) truncation is kept, so a market cap still reads $200 rather than $200.17.

Two divergences this does not close

Both are older than either PR and need an Android-side call, so they are worth naming rather than papering over:

  • ARS and COP symbols. Both platforms take the first single-character symbol the locale table offers. Apple's data gives ARS and COP a $; Android's does not, so the same peso preset reads $7.5K here and 7.5K there. IDR (bare) and VND () agree.
  • Market caps. Android keeps two abbreviation rules — Fiat.abbreviated for the tip chips and the uncapped Number.abbreviated for TokenMetricsRow, which always prints two decimals. A $690,272 cap reads $690K here and $690.27K there.

The parameterized cases in FiatAmountAbbreviatedTests follow Android's FiatTest — the scale boundaries, the decimal cap, the carry into the next scale, and the ARS/COP/VND/IDR/JPY rows — plus the negatives and the lower-cap case.

Four chips share a row in the Send a Tip sheet, and a preset is a
converted USD tier, so in a small-unit currency every tier is four digits
or more: ARS renders $1,400 / $35,000 / $1,400,000, which `Text` then
shrinks to half size to fit.

`FiatAmount.formattedAbbreviated(minimumFractionDigits:)` keeps the
figure to three digits, scaling past 999 with a K/M/B suffix and keeping
a decimal only while the scaled figure is a single digit: $1.4K, $35K,
$1.4M. It routes through the same `NumberFormatter.fiat` path as
`formatted`, so the symbol, grouping, and negative form (-$1.5K) are
unchanged.

`CompactCurrencyFormatStyle` — the market-cap style in Currency
Discovery — was a second abbreviation rule built on ICU's `.compactName`.
It is now a `FormatStyle` entry point onto this one. That agreed on every
case its tests covered except an exact half: $10.5M reads $11M rather
than $10M, because display rounding here is half-up like everywhere else
in the app.
Both apps now abbreviate the tip presets, but not to the same string: 12,345
pesos read $12.3K on Android and $12K here, because this rule kept a decimal
only while the scaled figure was a single digit instead of spending the whole
three-digit budget. Android's is the rule that answers "three digits", so this
takes it.

formattedAbbreviated(maxDigits:) now mirrors Fiat.abbreviated(maxDigits:) step
for step — round to maxDigits significant digits, pick the largest scale the
result clears, spend what is left on decimals. That adds the T scale, where
1e12 used to print $1,000B, and carries 999.99 up to $1K rather than showing
five digits.

formattedDroppingZeroFraction() is Android's FormattingRule.Truncated: it
formats the amounts under the first scale and replaces the sheet's local
customTitle helper.

The custom chip abbreviates too, as Android's shared slot does, and both chips
hand VoiceOver the unabbreviated amount the way Android's content description
does.

Market caps move with the shared rule, since Currency Discovery formats through
it: $1,029,331 reads $1.03M rather than $1M.
@bmc08gt
bmc08gt merged commit 8637414 into main Aug 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant