Notes › Tech Notes › 3.7 becomes 3 and 1e3 becomes 1: this fraction calculator truncates at the input, then switches display rules at 14 characters

3.7 becomes 3 and 1e3 becomes 1: this fraction calculator truncates at the input, then switches display rules at 14 characters

A numerator of 3.7 is computed as 3, 1e3 as 1, 0x10 as 16: parseInt runs without a radix. The decimal cell only checks string length against 14 characters; 1/4096 passes intact, 1/8192 becomes 0.0001220703..., and 1e-7 prints as is.

I type 3.7 in the numerator box; what this page computes with is 3

The setup is ordinary: numerator 3.7, denominator 2, the other fraction stays 1/3, the operator sits on plus. The page hands back 11/6, the decimal cell shows 1.8333333333..., and nowhere does it say that 3.7 was swapped out on the way in.

I extracted the whole script block from the template (11,127 bytes / 231 lines), confirmed that the 7 function sources read back match the template character for character, then fed the block into a plain Node vm and ran calc(). Everything below describes only what this page's own code does to the text you typed; no browser was opened, and repaint accounting is not measured at all.

The input side: four number fields get read a second time by parseInt with no radix

All four boxes declare their type as number, yet the code pushes their values through parseInt once more, without a second argument. So 3.7 halts before the dot and becomes 3, and 3.99 likewise becomes 3 — this is truncation, not rounding; both inputs produce 11/6. Type -3.5 and it reads -3, the result is -7/6, and the decimal cell shows -1.1666666667....

The remaining cases are even easier to overlook. Type 1e3 and it reads as 1: the parse stops at the letter e, and the 5/6 that comes out is exactly the shape you get by typing 1 directly. Type 0x10 and it reads as 16, because the hexadecimal prefix is accepted by the radix-free call, and the interface shows 25/3; the path with radix 10 would only produce NaN. Type -0 and it reads as 0, leaving 1/3 in the result cell. All of these reproduce on the fraction calculator by changing only the numerator box and letting the other three sit at their defaults. The thread is that parseInt treats its input as text, not a number: parsing ends at the first character that stops making sense, and everything past that point disappears silently.

Walking toward the empty end is quieter still: if any box receives an empty string, abc, plain spaces, or Infinity, the isNaN guard returns immediately for all four values, both cards hide together, and nothing is thrown and nothing is cleared. Measured before and after such an input, the steps container keeps an innerHTML length of 338 — not a byte of the old expression or the old step text is dropped, and the next valid input simply overwrites the stale string in place. The two error exits do not wipe it either: denominator 0 writes the literal interface string 分母不能为 0,请检查输入, and together with the warning marker in front of it the run totals 21 bytes; in division, a 0 numerator in fraction B writes a different sentence, and after valid input returns the old string is still there — only its parent container is hidden.

Typed in numerator boxWhat parseInt readsFraction cellEqual-decimal cell
3.7311/61.8333333333...
3.99311/6Same as above
-3.5-3-7/6-1.1666666667...
1e315/60.8333333333...
0x101625/38.3333333333...
-001/30.3333333333...
Numerator box → parseInt without radix → value read You typed Reads as Stops at On screen 3.7 3 Before the dot 11/6 1e3 1 Before the letter e 5/6 0x10 16 Prefix accepted 25/3 -3.5 -3 Minus sign kept -7/6 Empty string NaN Early return Old string 338

Figure 1 draws only the reading step: whichever character stops the parse becomes the boundary, and the integer that survives up to that point is what enters the arithmetic.

The display side: the ruler here is 14 characters, not a count of decimal places

The equal-decimal cell branches on a single condition: whether the character count of String(val) exceeds 14. Past the line it calls toFixed(10) and appends three ASCII dots; at or below the line it prints the string verbatim. So 1/2048, at 13 characters, reaches the screen untouched; 1/4096, at exactly 14 characters, also prints as is; 1/8192, at 15 characters, is cut to 0.0001220703... — a perfectly terminating decimal that divides evenly and ends with three significant digits changes its display mode purely because the total length crossed the line. Integer results answer to the same ruler. Put differently, the switch measures how long the plain decimal spelling happens to be, not how precise the value is.

The verbatim branch means exactly what it says: the cell for 1/10000000 measures as 1e-7, only 4 characters, and prints that way; 1/1000000 prints as 0.000001. The line only measures decimal notation, so a 4-character exponential string never even arrives at the comparison.

On the past-the-line branch the 11th digit participates in rounding: the true value of 1/7 runs 0.142857142857..., its 11th digit is 5, so this page displays 0.1428571429, with the last place sitting 1 above the true value; for 1/11 the 11th digit is 0, so the cell reads 0.0909090909. The comment in the code claims that finite decimals show at most 12 digits, but the page neither caps output at 12 digits nor checks whether the decimal repeats.

Equal-decimal cell: character count against the line at 14 Fraction Chars Branch On screen 1/2048 13 Verbatim 0.00048828125 1/4096 Exactly 14 Verbatim 0.000244140625 1/8192 15 Over 0.0001220703... 1/10000000 4 Exponential 1e-7 Integer cell 16 Over 4503599627370496.0000000000...

Figure 2 states it bluntly: 14 characters squeeze through right at the line, 15 characters get cut, and the 4-character exponential string never touches this line at all.

In the negative-denominator step, one line carries two conventions

The common denominator is built from absolute values, while the scaling factors divide by the signed denominator. So the step line for 1/-2 plus 1/3 prints common denominator = LCM(-2, 3) = 6, the next line prints numerator ×-3, and the sum line reads -3/6 + 2/6 = -1/6: the sign in the result cell is right, yet the same -2 is displayed as a parameter on one side and swallowed by an absolute value on the other. Switch to 1/-2 plus 1/-3, both factors come out negative, and the sum lands on -5/6. The scaling line in the subtraction branch also omits the numerator-times-k half, so it is not shaped like the addition branch. Neither quirk changes the final answer; they only change how the working lines describe it.

Sign movement happens only inside reduceFrac. The moment the minus flips, a reduction line appears out of thin air: -1/2 times -1/-3 prints 1/-6 as its first step, then GCD(1, -6) = 1 followed by -1/6 as its second — the common divisor equals 1, yet the line is still there. The first argument of the reduction line is likewise a signed value: the steps for -1/2 plus -1/2 write, at step three, reduce GCD(-2, 2) = 2, and the minus sign survives only inside the label. Another page in the same family keeps its own sign-ownership ledger, and that account lives on the page that handles reduction on its own.

The reduction line is conditional: the step count jumps between 6 and 8

The step card for 1/2 plus 1/3 runs only 6 lines and contains no reduction line; 1/6 plus 1/3 stretches to 8 lines, the extra one reading GCD(3, 6) = 3; 0/3 plus 0/5 prints GCD(0, 15) = 15, and the result cell shows 0. The whole rule in one sentence: the common denominator is always positive, and the reduction line appears if and only if the numerator sum and that denominator share a common divisor greater than 1 — when the divisor equals 1 the block is never concatenated, and the step count falls back from 8 to 6. The multiplication branch yields 2 or 4 lines, the division branch 3 or 5 lines, and 3 divided by 1/3 gives 9, so no reduction line shows. With a denominator of 1 the fraction cell prints only the numerator, which is why 0/3 times 0/5 displays a bare 0 instead of 0/1. A missing reduction line is therefore a completed decision, not a rendering failure.

Beyond this ruler I probed exactly one precision point: 99999999 times 99999999 comes out as 9999999800000000 on this page, while the true BigInt value is 9999999800000001 — a gap of 1. The default first screen is 1/2 plus 1/3 giving 5/6, and running it twice in a row returns four byte-for-byte identical fields: expression 11 bytes, fraction 3 bytes, decimal 15 bytes, step source 157 bytes. How one should teach common denominators and reduction is not this article's ground — that belongs to the fraction-arithmetic article already on the site.

Q: I typed 3.7 — why was there no hint of any kind?

The truncation happens at the reading stage, not the validation stage: parseInt stops before the first character it cannot consume, and what remains — 3 — is a perfectly valid numerator, so the page has no reason to raise an error.

Q: I typed 1e3 meaning one thousand — why did it not work out that way?

The reading step halts at the letter e, keeps 1, and outputs 5/6 — in every respect the same result as typing 1 directly.

Q: The reduction line appears and disappears — is something failing to render?

No — it was a conditional line from the start. The step count jumping between 6 and 8 shows the decision ran to its end; the block simply was not assembled into the output.

📎 Related articles

The cut lands on the last dot only: archive.tar.gz yields gz, photo. and .gitignore share one cell, and the rebuilt column drops a dot 2026-11-14 The Regex Test Page Always Forces g: gg Errors Out, Uppercase Fails Silently, and 21 Grouped Matches List Only 20 Rows 2026-11-14 Star Run Length Is Hardcoded: the Masking Page Cuts a Fixed 4, 3, 11 or 12 Stars, an 18-Unit String Is Taken by the Phone Tier, an Empty Box Falls Back to * 2026-11-13 The Lock Is One-Way and Percent Is Two Rulers: pct 50 Gives 619x430, and All Four PNG Quality Stops Return 6971 B 2026-11-13 A Month on This Page Is Hard-Coded as 30 Days: the Six Fixed Divisors, 13-Digit Millisecond Input, and slice(0,16) Truncation Measured Under UTC+8 and UTC-5 2026-11-13