1. After the write-back, 12.30 is never 12.30 again
Last week I set a reconciliation sheet to a uniform 2 decimal places and switched off zero padding on the way out. The next day a colleague asked why the March row that had been 12.30 was now only 12.3, and the two-place book precision no longer matched. The loss happened at the moment of formatting, and once the file was written back there was no original string left to recover anywhere.
The page is thin: one input box, one slider for retained decimal places, a rounding-mode dropdown, two checkboxes for zero padding and thousands separators, plus two counters labelled processed values and skipped invalid items. The slider can only stop on 0 to 10, eleven detents in total, and it defaults to 2 places. Beyond that, the page carries no text about rounding direction, precision loss, or scientific notation.
2. Four rounding options collapse into two directions on negatives
The dropdown lists four entries: round half up, floor, ceil, and truncate (drop the rest outright). Take -2.5 to 0 places and the four give -3, -2, -3, -2: four names, two results. Floor and trunc agree entry by entry, round and ceil agree on .5, so the claim that four modes give four results does not hold here.
The reason sits in one formula: all four branches round the absolute value first, then reattach the sign. Rounding happens on the absolute-value axis, so going down on a negative really means going toward zero, and the value lands on -2. Comparing against the native functions makes it clearer: Math.floor(-2.5) is -3 while the page's floor gives -2; Math.round(-2.5) is -2 while the page's round half up gives -3. With -2.4 it is starker: -2, -2, -3, -2, three against one.
Fig. 1: -2.5 taken to 0 places leaves only two landing points
3. Why 1.005 kept to 2 places comes out as 1.00
This part has no direction to argue about, only which side the binary approximation lands on. Eight samples whose third decimal is exactly 5, taken to 2 places with round half up: five were dropped, three were raised.
| Input | Intermediate value after multiplying by 100 | Output |
|---|---|---|
| 1.005 | 100.49999999999999 | 1.00 |
| 8.995 | 899.4999999999999 | 8.99 |
| 1.015 | 101.49999999999999 | 1.01 |
| 1.045 | 104.5 | 1.05 |
| 1.055 | 105.5 | 1.06 |
| 2.675 | 267.5 | 2.68 |
The boundary is not "see a 5 and go up" but whether that binary approximation, once multiplied by 100, strictly clears .5: 100.49999999999999 sits under 100.5 and is dropped, while 104.5 lands exactly on .5 and is raised. The whole chain is one multiplication by 10 to the power of the place count, a rounding step, and a division back. There is no string shifting and no decimal library involved, and the comment in the code about correcting negative rounding repairs the sign only, never the precision. For money, as long as the third place can be a 5, the direction is not mine to pick.
4. Once zero padding is off, trailing zeros are lost for good
Back to the failure from the opening. Zero padding is checked by default, and the label reads "pad zeros (3.5 → 3.50)". With it on, 12.3 to 2 places gives 12.30; uncheck it and only 12.3 comes out. In the same batch 100 falls back from 100.00 to 100, and 1.005 is even blunter: no decimal left at all, the output is 1. Trailing zeros do not exist at the numeric level, what comes out is a string, and after the write-back nothing anywhere still records that the row had been kept at 2 places.
At 0 places padding never intervenes: 5 is still 5, and 5.5 truncated is still 5, however firmly the box is checked, no .00 appears. The place detent itself is not responsible for padding either: 12.3 to 4 places with padding off still outputs 12.3.
5. Batch paste: three version numbers all "succeed", and one of them is now an integer
The batch entry is real. Input is split into tokens on newline, half-width comma and full-width comma, the whole batch runs in one pass, and full width and half width behave identically. I pasted a column of version numbers, asked for 1 place and chose truncate.
Fig. 2: three version rows give count=3 and skipped=0, and one row has been cut down to an integer
Not one row errored and not one row was marked out. The skipped invalid counter sat obediently at 0, while the second entry had shrunk to a bare 10. The counters only answer whether something can be read as a number, never what it was read as.
A mixed batch says it better: 5 text rows give tokens=6, count=4, skipped=2. The row carrying a thousands separator is cut by its own commas into 2 tokens, so the labels #1 through #6 outnumber the rows, because they are subscripts after splitting rather than row numbers. In the same batch 2024.01.15 is written as 2024.01 and 1.2.3 is written as 1.20, while a row such as "price 3.5 yuan" and v1.2.3 are judged invalid for the whole line: only the one number at the head of each token is recognised, so the 3.5 in the middle of a row is never extracted.
"Copy all" is a second pipeline, and invalid items are written back as they came in. That batch pastes out as 1.00|234.56|价格 3.5 元|v1.2.3|2024.01|1.20, and the 2 rows highlighted in red on screen become 2 rows of untouched text mixed into the same data. The other way round, when a whole batch is invalid the result panel does not fall back to an empty state: three rows of abc give tokens=3 and skipped=3, and the list is still displayed. Only a completely empty input returns to the prompt asking for values before formatting.
6. The other end of the range: 1e21 and long integers
1e21 taken to 2 places outputs 999999999999999900000.00, which is only a multiplication by 100 and a division by 100, and the original value is short by 100000000000. At 0 places nothing is multiplied or divided, and scientific notation comes through unchanged as 1e+21. 1e-7 taken to 10 places and then padded yields the malformed string 1e-7.0000000000; the thousands separator cannot add a single comma to an exponent string like that, and the same number to 2 places is simply 0.00.
Long integers are quieter: 9007199254740993 enters parseFloat and comes out as 9007199254740992, the last digit changes while the interface shows no difference, and 123456789012345.67 taken to 2 places is written as 123456789012345.69 because the integer part has already eaten 15 significant digits. Order numbers and card numbers should not go down this road, and reading them in another base belongs to the base conversion article, since for the same string of characters this page does not even accept 0x10.
All of these switches can be reproduced on the decimal places formatter page: keep one input, move a single checkbox, and read the two counters together. After a batch I paste the input and the output into the two-text comparison to confirm that the differences fall only on the rows where they belong.
FAQ
Q: Which way does floor actually go on a negative number?
Toward zero. All four branches round the absolute value and then reattach the minus sign, so -2.5 to 0 places gives -2, the same as trunc, while the real downward value is -3 and that one comes from the ceil entry instead.
Q: Can it standardize money to 2 decimal places?
Not when the third place can be a 5: 1.005 → 1.00 and 8.995 → 8.99, while 1.045 → 1.05 and 2.675 → 2.68. As a display layer it is fine, as a settlement layer it is not.
Q: The whole batch was invalid, so why is the list still there?
Returning to the empty state requires both counters to be 0 at once, and three rows of abc are count=0, skipped=3. A list that is not empty is not a run that worked; those two numbers are the thing to read.