This is the whole thing: a function that looks obvious, and the frozen table of what it actually does on eight tricky inputs — every value captured by running the code, every row green. The exercise is this same page with the answers blanked out — you predict them first.

Before you can trust an agent's code — or safely change it — you have to say what the code does. Not what it should do, not what its name suggests: what it actually does, pinned to a value you can check. Say it as a line that runs and that value becomes a small, exact spec of the real behaviour. That move has a name — a characterization test — and it is the first thing you reach for on legacy code in Module 4 — Refactoring.

The function

Nothing exotic — the rounding you have called a thousand times:

static double RoundTo(double value, int digits) => Math.Round(value, digits);

Ask ten engineers what RoundTo(2.5, 0) returns and ten say 3. Run it: you get 2. .NET's Math.Round rounds a halfway value to the nearest even number — banker's rounding, not "half up" — and because a double cannot hold every decimal exactly, even that rule has a twist. None of this is a bug. It is behaviour, and behaviour is what you pin.

The frozen table — prediction vs. actual

The Naive column is what "round half up" predicts; the Actual column is the frozen truth, captured by running RoundTo. Seven of the eight defy the naive guess:

Call Naive (half-up) Actual (.NET) Why
RoundTo(0.5, 0) 1 0 half to even — 0 is even
RoundTo(1.5, 0) 2 2 half to even — 2 is even (the one that agrees)
RoundTo(2.5, 0) 3 2 half to even — 2, not 3
RoundTo(4.5, 0) 5 4 half to even — 4, not 5
RoundTo(-2.5, 0) -3 -2 rounds toward even on both signs
RoundTo(0.125, 2) 0.13 0.12 0.125 is exact in binary; the even digit wins
RoundTo(0.625, 2) 0.63 0.62 0.625 is exact; the even digit wins again
RoundTo(1.005, 2) 1.01 1.00 1.005 is really 1.00499… — it can't be halved

The last row is a different lesson: it is not banker's rounding at all, it is the double itself. 1.005 cannot be stored exactly, so the value held is a hair below 1.005 and rounds down. Guessing was never going to reach it — only running it does.

Live and green

The function and the whole frozen battery, in one runnable cell. It feeds every input through RoundTo and checks the actual against the pinned prediction. Press Run — every row must say ✓.

Wrap Math.Round in a RoundTo helper and pin its real behaviour: feed it the eight tricky cases — 0.5, 1.5, 2.5, 4.5 and -2.5 at zero digits, plus 0.125, 0.625 and 1.005 at two — and render a grid comparing the naive half-up guess, the pinned prediction, and the actual result, with a green check per row.

Green all the way down means you have characterized the function: you no longer believe what it should do, you know what it does — written as a test that will shout the day the behaviour changes. That is the safety net every refactor stands on.


← Back to the exercise · Module 1 — Introduction · All solutions

Reconnecting…
The connection to the server was interrupted. Trying to restore it…
Trying again…
The connection could not be restored. Reloading the page…
The server was updated. Reloading the page to pick up the latest version.