Multi-currency expense report
Requirement
A trip ledger (data in the code) mixes EUR, GBP, JPY, and USD amounts. Convert everything to USD with the fixed rates in the code, then report: per-category totals sorted by spend, the currencies seen, the largest single expense (with its original amount), and the grand total. Both versions must print the report under Expected output.
Expected output
Trip expenses in USD (currencies: EUR, GBP, JPY, USD) Lodging $240.55 Travel $164.82 Food $37.85 Transit $22.50 Largest single expense: Travel $130.80 (120.00 EUR) Total: $465.72
Side by side
Native Dart
FxDart
Why they differ
Normalizing first — map each transaction to a
(tx, usd) pair — lets every later question run over one
list: foldBy + sortBy for the breakdown,
uniq for the currency list, maxBy and
sumBy for the summary lines. Each report line is one short
pipeline that names its aggregation. The native version makes the
identical moves but without the vocabulary: the per-category totals are a
hand-rolled map accumulator, the sort needs a comparator spelled out, the
maximum is a reduce comparator, and the currency list needs
the toSet().toList()..sort() shuffle. Nothing is hard — there
is just more of it, and less of it says what it means.
The aggregation is foldBy rather than
groupBy + sumBy on purpose, and it is worth a
moment. The answer here is one number per category, so grouping
first would build a List of every transaction under each
category and then immediately fold it away — allocation proportional to the
input, for an answer proportional to the number of categories.
foldBy accumulates straight into the result map, which is
exactly what the native loop beside it does. On a million-row ledger that
single choice is worth roughly 2.5× on both sides; see
Writing fast pipelines. Reach
for groupBy when you actually want the members.
Benchmark
N = 100
Time Tie
Peak memory Tie
N = 10,000
Time Tie
Peak memory Tie
N = 1,000,000
Time FxDart wins
Peak memory Native wins
Bars are medians of repeated timed iterations in fresh processes per side (small N is batched for timer resolution). Sides within 5% of each other — or within 0.6 ms, a difference no person can perceive — count as a tie; close relative races are re-measured up to 5 runs. In an app, anything under a few milliseconds is invisible to the user regardless of which bar is shorter. Memory is peak process RSS. The Dart VM and the dataset are identical on both sides, so the difference between the two bars is what the pipeline itself holds onto.