WEBVTT

00:00:00.000 --> 00:00:02.560
On the first of April, a new rate period

00:00:02.720 --> 00:00:04.320
switched on in a YAML file.

00:00:04.960 --> 00:00:07.440
One statutory payroll rate in it had gone from

00:00:07.600 --> 00:00:09.720
three to three point five.

00:00:10.080 --> 00:00:12.640
Every calculator kept giving the old answer.

00:00:13.120 --> 00:00:14.800
No error. Nothing red.

00:00:15.520 --> 00:00:17.600
So I loaded that file the way the app

00:00:17.760 --> 00:00:19.440
loads it, and asked for the rate.

00:00:20.000 --> 00:00:22.240
The file says three point five.

00:00:22.280 --> 00:00:24.320
The loaded constant says three.

00:00:24.600 --> 00:00:27.320
The half went missing here, in the routine that

00:00:27.400 --> 00:00:30.360
tidies each rate period before it is frozen at

00:00:30.440 --> 00:00:34.920
boot. Every line in this column is .to_f, except

00:00:35.000 --> 00:00:38.600
two. Those two are .to_i.

00:00:38.720 --> 00:00:41.960
Three to_i is three, so while every rate was

00:00:42.040 --> 00:00:44.880
whole, this line changed nothing.

00:00:44.920 --> 00:00:47.840
Three point five to_i is also three.

00:00:47.880 --> 00:00:50.480
to_i does not complain about a fraction.

00:00:50.520 --> 00:00:52.000
It drops it.

00:00:52.040 --> 00:00:54.240
That line was written in February.

00:00:54.280 --> 00:00:57.160
April handed it its first fraction.

00:00:57.367 --> 00:01:01.046
The fix for the loader was one letter, twice.

00:01:01.207 --> 00:01:04.406
.to_i became .to_f.

00:01:04.487 --> 00:01:07.046
My commit message says the old line had been

00:01:07.127 --> 00:01:11.566
under-reporting employer contributions across every calculator.

00:01:11.607 --> 00:01:14.086
The tempting shortcut was to put the rate back

00:01:14.167 --> 00:01:15.566
to a whole number.

00:01:15.607 --> 00:01:17.246
But the rate was right.

00:01:17.287 --> 00:01:20.006
The bug was every piece of code that assumed

00:01:20.087 --> 00:01:22.806
it never would, and the loader was only the

00:01:22.887 --> 00:01:23.766
first.

00:01:30.967 --> 00:01:33.446
I fixed the loader, and the page still showed

00:01:33.487 --> 00:01:34.526
the wrong rate.

00:01:34.807 --> 00:01:39.046
Three templates printed it through .to_f, then round(0), and

00:01:39.127 --> 00:01:41.526
the oldest of those lines was from twenty twenty

00:01:41.687 --> 00:01:45.126
three. For three, that rounds to three.

00:01:45.207 --> 00:01:47.686
For three point five, it rounds up to four.

00:01:48.167 --> 00:01:49.966
That is worse than stale.

00:01:50.247 --> 00:01:52.966
Four is a different rate, shown with confidence.

00:01:53.447 --> 00:01:57.126
Each one became format("%g"), which keeps the half and

00:01:57.207 --> 00:01:59.526
still prints four point oh as four.

00:01:59.800 --> 00:02:02.440
Then the same loss, one layer down, in the

00:02:02.680 --> 00:02:07.187
browser. The controller read the selected rate with parseInt.

00:02:07.293 --> 00:02:10.307
parseInt of three point five is three.

00:02:10.413 --> 00:02:12.680
So a user could pick three point five, and

00:02:12.760 --> 00:02:15.200
the form sent three upstream.

00:02:15.240 --> 00:02:18.333
Same bug, different language, different file.

00:02:18.387 --> 00:02:21.760
A grep found that exact line in three controllers.

00:02:21.800 --> 00:02:23.800
Each became parseFloat.

00:02:24.000 --> 00:02:28.160
The fourth costume, I wrote myself, that same morning.

00:02:28.320 --> 00:02:31.080
I gave each Stimulus value a default.

00:02:31.120 --> 00:02:33.000
Forty six working weeks.

00:02:33.040 --> 00:02:35.960
Forty hours. Fifteen percent.

00:02:36.000 --> 00:02:38.320
Each one a copy of a number the server

00:02:38.400 --> 00:02:42.480
already held. Twenty seven minutes later, I deleted all

00:02:42.560 --> 00:02:45.680
ten. A default in a lower layer is the

00:02:45.840 --> 00:02:48.040
same bet as .to_i.

00:02:48.080 --> 00:02:50.400
Without it, a view that forgets to wire a

00:02:50.480 --> 00:02:54.080
value shows zero, and zero is loud.

00:02:54.267 --> 00:02:57.146
The rate change was one new number, in two

00:02:57.227 --> 00:03:01.146
YAML files. The fix was sixty four files, and

00:03:01.187 --> 00:03:04.426
a lockfile, in five commits, in just over an

00:03:04.507 --> 00:03:08.626
hour. Twenty six lines with .to_i came out.

00:03:08.667 --> 00:03:10.546
Eight with parseInt.

00:03:10.587 --> 00:03:12.786
Three with round(0).

00:03:12.827 --> 00:03:15.226
Some held a copy of the old rate.

00:03:15.307 --> 00:03:17.906
Others had written down one assumption.

00:03:17.947 --> 00:03:20.026
This number is a whole number.

00:03:25.567 --> 00:03:28.606
Nearly every language ships two ways to make three

00:03:28.687 --> 00:03:30.446
point five a whole number.

00:03:30.767 --> 00:03:32.446
The lenient one shrugs.

00:03:32.767 --> 00:03:37.406
Ruby's .to_i and JavaScript's parseInt both say three.

00:03:37.647 --> 00:03:39.966
The strict one refuses the text.

00:03:40.207 --> 00:03:42.246
Ruby's Integer method raises.

00:03:42.447 --> 00:03:44.206
Python's int raises.

00:03:44.447 --> 00:03:46.526
Postgres will not cast it.

00:03:46.767 --> 00:03:49.046
But hand them the number instead of the string,

00:03:49.167 --> 00:03:50.926
and they answer without a word.

00:03:51.167 --> 00:03:52.966
Ruby and Python truncate.

00:03:53.167 --> 00:03:54.926
Postgres rounds it to four.

00:03:55.167 --> 00:03:57.326
A strict parser guards text.

00:03:57.487 --> 00:03:59.326
It does not guard a narrowing.

00:03:59.600 --> 00:04:02.520
Even rounding a half is not agreed.

00:04:02.560 --> 00:04:05.560
Ask Postgres to round two point five.

00:04:05.600 --> 00:04:07.880
As a numeric, it says three.

00:04:07.920 --> 00:04:10.680
As a double precision, it says two.

00:04:10.720 --> 00:04:14.720
Same database, same function, a different answer by declared

00:04:15.200 --> 00:04:18.440
type. Ruby rounds two point five to three.

00:04:18.480 --> 00:04:20.453
Python rounds it to two.

00:04:20.507 --> 00:04:23.120
There is no universal rule that a half goes

00:04:23.280 --> 00:04:23.920
up.

00:04:24.134 --> 00:04:26.773
This shape has a body count.

00:04:26.854 --> 00:04:29.973
In nineteen ninety one, a Patriot battery in Saudi

00:04:30.054 --> 00:04:33.653
Arabia kept time in tenths of a second, and

00:04:33.694 --> 00:04:36.453
the factor of one tenth was chopped to fit

00:04:36.534 --> 00:04:38.186
twenty four bits.

00:04:38.241 --> 00:04:41.133
The error only ever ran one way.

00:04:41.174 --> 00:04:44.453
As the government's auditors reported it, after a hundred

00:04:44.534 --> 00:04:47.333
hours of uptime it had moved the tracking window

00:04:47.414 --> 00:04:50.320
about six hundred and eighty seven metres.

00:04:50.347 --> 00:04:53.453
The battery never tracked the incoming Scud.

00:04:53.494 --> 00:04:55.573
Twenty eight soldiers died.

00:04:55.767 --> 00:04:56.886
It is a family.

00:04:57.367 --> 00:04:59.286
The figures are as reported.

00:04:59.607 --> 00:05:02.726
Ariane five's first flight forced a sixty four bit

00:05:02.887 --> 00:05:05.286
float into a sixteen bit integer.

00:05:05.847 --> 00:05:09.606
The rocket destroyed itself thirty seven seconds after launch,

00:05:09.767 --> 00:05:12.886
about three hundred and seventy million dollars lost.

00:05:13.200 --> 00:05:16.400
A stock index in Vancouver was truncated instead of

00:05:16.480 --> 00:05:19.600
rounded, thousands of times a day, until it read

00:05:19.720 --> 00:05:21.440
less than half its true value.

00:05:22.080 --> 00:05:25.040
A payroll that rounded clock punches the employer's way

00:05:25.200 --> 00:05:28.160
settled for one point five six million dollars.

00:05:28.720 --> 00:05:31.800
Error that always points one way compounds.

00:05:32.080 --> 00:05:34.640
.to_i always points down.

00:05:41.834 --> 00:05:45.033
The final design has no fallback anywhere.

00:05:45.274 --> 00:05:47.193
The rates live in the YAML.

00:05:47.434 --> 00:05:49.633
A frozen module loads them once.

00:05:49.834 --> 00:05:52.313
A before_action hands them to the view.

00:05:52.634 --> 00:05:55.393
The view writes them into data attributes, and the

00:05:55.434 --> 00:05:59.073
Stimulus controller reads those, and nothing else.

00:05:59.434 --> 00:06:01.513
Every layer reads the one above it.

00:06:01.754 --> 00:06:03.633
None keeps a copy of its own.

00:06:03.914 --> 00:06:06.553
There is one place the number lives, and everything

00:06:06.674 --> 00:06:08.553
else is a straw pointed at it.

00:06:08.834 --> 00:06:10.673
The rules I kept.

00:06:10.754 --> 00:06:13.673
Make every coercion the widest one.

00:06:13.714 --> 00:06:16.713
.to_f over .to_i.

00:06:16.754 --> 00:06:19.313
parseFloat over parseInt.

00:06:19.554 --> 00:06:22.566
format("%g") over round(0).

00:06:22.621 --> 00:06:25.660
And delete every default in a lower layer.

00:06:25.687 --> 00:06:27.993
The libraries already package this.

00:06:28.034 --> 00:06:30.233
dry-types calls it strict.

00:06:30.274 --> 00:06:33.033
anyway_config marks it with a bang.

00:06:33.074 --> 00:06:35.113
Neither is a silver bullet.

00:06:35.154 --> 00:06:38.233
A lenient mode makes exactly the same bet.

00:06:38.274 --> 00:06:40.753
The tool only helps if you declare the real

00:06:41.034 --> 00:06:43.153
type, and choose the strict one.

00:06:43.367 --> 00:06:46.006
Here is the same probe, run against the fixed

00:06:46.087 --> 00:06:49.366
loader. The file says three point five.

00:06:49.447 --> 00:06:52.006
The constant says three point five.

00:06:52.087 --> 00:06:54.726
And a view that forgets to wire a value

00:06:54.807 --> 00:06:57.366
now shows a zero, instead of an old number

00:06:57.447 --> 00:06:58.886
that looks fine.

00:06:58.967 --> 00:07:01.486
Fail loud, not stale.

00:07:01.527 --> 00:07:04.286
The half was never really about payroll.

00:07:04.327 --> 00:07:06.406
It was the code telling me which of its

00:07:06.487 --> 00:07:09.286
coercions had been guessing all along.

