Appearance
1.4 — Representing Everything
Integers were the easy part (1.3). But your screen shows photographs, your speakers play music, this very sentence is text — and underneath, it's all still just bytes. The miracle of the computer is that one substance, the bit, can impersonate everything, because meaning isn't in the bits — it's in the agreement about how to read them. The same byte 01000001 is the number 65, the letter 'A', a shade of dark red, or one slice of a sound wave, depending entirely on the code we've agreed to apply. This chapter walks the great encodings: fractions (and the famous 0.1 + 0.2 scandal), text from ASCII to the whole world's writing, and how bits become images and sound.
1. Fractions — and why 0.1 + 0.2 ≠ 0.3
Two's complement handled whole numbers. But how do you store 3.14, or 0.001, or Avogadro's number? You can't give every real number its own bit pattern — there are infinitely many, even between 0 and 1. So computers use the same trick as scientific notation, in binary.
In school you write huge or tiny decimals as 6.022 \times 10^{23}: a mantissa (the significant digits, 6.022) times ten to an exponent (23). Floating point does exactly this in base 2: store a sign, a mantissa, and an exponent, and let the "binary point" float to wherever the exponent puts it — which is how one 64-bit box can hold both 10^{-300} and 10^{300}. The universal standard, obeyed by essentially every CPU and language, is IEEE 754 (1985). Its 64-bit "double" splits as 1 sign bit + 11 exponent bits + 52 mantissa bits:
Now the scandal. Type 0.1 + 0.2 into any JavaScript console, Python shell, or C program and you get 0.30000000000000004, not 0.3. This is not a bug — it's mathematics meeting finite storage, and understanding why separates engineers who trust their tools blindly from those who know where the floor is.
The reason: 0.1 has no exact representation in binary. Just as \frac{1}{3} is 0.333\ldots forever in decimal (base 10 can't express thirds exactly), \frac{1}{10} is 0.0001100110011\ldots forever in base 2 — because 10's prime factors include 5, which is not a power of 2. The computer stores the closest 52-bit approximation, a hair off. Add two hairs-off values and the tiny errors surface in the 17th digit. The lesson is permanent and it matters: never use floating point for money. $0.10 + $0.20 must not wobble. Represent currency as an integer number of cents (or use a decimal type), so 10 + 20 = 30, exactly, with the integer math from 1.3. Every payment system in Part 11 obeys this rule. And never compare floats with ==; check whether they're within a tiny epsilon of each other.
The trade you're making
Floating point trades exactness for range and speed. For physics, graphics, ML, and science — where you want to represent wildly different magnitudes and a rounding error in the 15th digit is irrelevant — it's perfect, and it's what GPUs are built to crunch. For counting money or comparing for exact equality, its approximate nature is a liability. Knowing which situation you're in is the whole skill.
2. Text — from ASCII to the whole world
A computer stores no letters, only numbers, so text needs a character encoding: an agreed table mapping each character to a number (its code point) and then to bytes. The first widely-adopted table was ASCII (1963): 128 characters — the English uppercase and lowercase letters, digits, punctuation, and invisible control codes — numbered 0–127, fitting in 7 bits. In ASCII, 'A' is 65, 'a' is 97 (exactly 32 more — flip one bit to change case, a deliberate convenience), '0' the character is 48. ASCII is why the byte won: 128 characters plus room to spare fits neatly in 8 bits.
But 128 slots can't hold é, ñ, ß, let alone Bengali অ, Chinese 中, Arabic ع, or 😀. The 1980s–90s saw a chaos of incompatible regional "code pages," where the same byte meant different letters in different countries — open a document with the wrong page and it turned to garbage (the notorious mojibake). The fix was Unicode: one gigantic table assigning a unique code point to every character in every human writing system — over 150,000 of them and counting, including historical scripts and emoji. 'A' is U+0041 (still 65, for ASCII compatibility), 中 is U+4E2D, 😀 is U+1F600.
But Unicode is just the numbering; you still must turn a code point like U+1F600 into bytes. The dominant answer — over 98% of the web — is UTF-8, and its design is a small masterpiece worth admiring:
- ASCII characters (U+0000–007F) encode as one byte, identical to old ASCII. So every ASCII file ever written is already valid UTF-8 — perfect backward compatibility, the reason UTF-8 won.
- Characters beyond ASCII use 2, 3, or 4 bytes, and the leading bits of the first byte announce how many bytes follow, so a decoder is never lost.
- It's variable-width: common characters stay compact, rare ones cost more. English text is essentially free; the whole world is still expressible.
This variable width is a real gotcha for programmers: in UTF-8 the number of bytes is not the number of characters. The emoji 😀 is one character but four bytes. UTF-16 (used internally by JavaScript and Java strings — Part 3) makes it worse: it stores most characters in two bytes but encodes 😀 as a surrogate pair of two 16-bit units, so JavaScript's "😀".length returns 2, not 1. Truncating a string at a fixed byte or unit count can slice a character in half and produce garbage. This is why "just count the characters" is a landmine, and why proper text handling (Part 3.6) respects code-point boundaries.
3. Images, color, and sound — same bits, new agreements
Everything visual and audible is the same story: agree on how numbers map to the thing, then store the numbers.
Color. A screen pixel is three tiny lights — red, green, blue. Give each a brightness from 0 to 255 (one byte, 2^8 levels) and you can mix 256^3 \approx 16.7 million colors. That's the RGB model, and its hex form is exactly the nibbles from 1.3: #FF5C00 means red = 0xFF (255, full), green = 0x5C (92), blue = 0x00 (0) — an orange. A fourth byte, alpha, adds transparency (RGBA). This is why web colors are six hex digits: two per channel.
Images. A bitmap (raster) image is a grid of pixels, each a color — so a plain, uncompressed image is literally width × height × 3 bytes. A 4000×3000 photo is ~36 million bytes — far too large to store or send raw, which is why compression exists (below, and 1.8). The alternative, vector images (SVG — the very diagrams in this book), store not pixels but instructions ("line from here to there, circle of radius r"), so they scale to any size with no blur and often far fewer bytes — brilliant for logos and diagrams, wrong for photographs.
Sound. Sound is a continuous wave of air pressure — an analog signal, exactly the kind 1.1 warned about. To store it, we sample: measure the wave's height many thousands of times per second and store each measurement as a number. CD audio takes 44,100 samples per second (44.1 kHz); each sample is a 16-bit integer. Why 44,100 and not more or less is one of the most beautiful results in engineering — the Nyquist–Shannon sampling theorem — and it earns a full treatment in Volume III (Electronics), Chapter 4.19, where signals get the mathematics they deserve. Here, hold the intuition: enough snapshots per second, and the ear can't tell the staircase of samples from the original smooth wave.
Compression. Raw media is enormous, so we shrink it two ways. Lossless compression (PNG, ZIP, FLAC) removes only redundancy — patterns that can be perfectly reconstructed — so you get every bit back. Lossy compression (JPEG, MP3, H.264 video) goes further by throwing away detail humans barely perceive — subtle color gradients the eye skips, frequencies the ear misses — achieving 10× or 100× smaller files at the cost of exactness you'll never notice. The deep why of compression — the theoretical floor on how small data can go — is Shannon's information theory, waiting in 1.8. The inside of an MP3 specifically (the psychoacoustics of what it discards) is Volume III, 5.4.
4. The expert lens
Bits are meaningless without an agreed interpretation — and confusing the agreements is a whole bug class. The byte 01000001 is 65, 'A', or part of a color, and nothing in the byte says which. Reading bytes with the wrong encoding is the root of mojibake (wrong text codec), the endianness bugs of 1.3, deserialization exploits (Part 8), and half of all "why is my data garbage" incidents. Senior engineers develop a reflex: what is the exact format of these bytes, and does every party agree? — at file boundaries, network boundaries, and language boundaries.
"Character," "code point," "byte," and "grapheme" are four different things. In UTF-8/UTF-16 they routinely disagree — 😀 is 1 character, 1 code point, 4 UTF-8 bytes, 2 UTF-16 units; a flag emoji or an accented letter can be several code points forming one visible symbol (grapheme). Code that assumes "1 character = 1 byte" (or slices strings by index) breaks on the first non-English input. This is why internationalization (i18n) is hard and why it must be designed in, not bolted on.
Every media format is a point on the exactness-vs-size curve. Integer-cents vs float for money, PNG vs JPEG, FLAC vs MP3, lossless vs lossy — they're all the same decision you first met with two's complement and noise margins: choose the representation whose trade-offs fit the job. You'll make it consciously for the rest of your career.
Next chapter: we can now represent numbers, text, and media as bits, and 1.2 gave us circuits that transform them. 1.5 assembles it all into the CPU — and traces a single line of source code, sum(a, b), all the way down to the transistors switching.
Recall
- Meaning lives in the agreed interpretation, not the bits:
01000001is 65, 'A', or a color depending on the code applied. - Floating point (IEEE 754) stores sign + mantissa + exponent for huge range at the cost of exactness;
0.1 + 0.2 ≠ 0.3because 0.1 has no exact binary form — so never use floats for money (use integer cents). - ASCII (128 chars, 7 bits) → Unicode (a code point for every character) → UTF-8 (variable-width bytes, ASCII-compatible, ~98% of the web). Byte count ≠ character count; UTF-16 makes
"😀".length= 2. - Color is RGB bytes (
#RRGGBB= the nibbles of 1.3); images are pixel grids (raster) or instructions (vector); sound is sampled thousands of times per second. - Lossless compression (PNG/ZIP/FLAC) is perfectly reversible; lossy (JPEG/MP3/H.264) discards imperceptible detail for far smaller files. The theory of the limit is 1.8.
Self-test: Why is 0.1 + 0.2 not 0.3, and what should you use for currency? What does UTF-8 do that made it win over rival encodings? What do the six digits of #FF5C00 mean? Lossy vs lossless — when each?
Quiz Bank
FoundationalWhy does 0.1 + 0.2 equal 0.30000000000000004 in most languages?
Because 0.1 cannot be represented exactly in binary floating point. Base 2 can only exactly represent fractions whose denominators are powers of 2; 1/10 is a non-terminating binary fraction (0.0001100110011\ldots), so it's stored as the nearest 52-bit approximation. Two such tiny errors surface when added, appearing around the 17th digit. It's not a bug — it's IEEE 754 meeting finite precision. Fixes: use integer cents for money, and compare floats within an epsilon, never with ==.
FoundationalWhat is the difference between ASCII and Unicode?
ASCII is a 128-character table (7 bits) covering English letters, digits, punctuation, and control codes. Unicode is a vastly larger table assigning a unique code point to every character in every writing system (150,000+, including emoji), with ASCII as its first 128 code points for compatibility. Crucially, Unicode is only the numbering — turning code points into bytes needs an encoding like UTF-8 or UTF-16.
AppliedWhy did UTF-8 win over other Unicode encodings?
Chiefly backward compatibility and efficiency. UTF-8 encodes the ASCII range (U+0000–007F) as single bytes identical to ASCII, so every existing ASCII file is already valid UTF-8 and English text costs no extra space. It's self-synchronizing (the first byte announces the sequence length) and variable-width (1–4 bytes), keeping common text compact while still expressing all of Unicode. Those properties made adoption frictionless — it's now ~98% of the web.
AppliedIn JavaScript, why is the length of the 😀 emoji string 2?
JavaScript strings are UTF-16, which stores most characters in one 16-bit unit but encodes code points above U+FFFF (like 😀, U+1F600) as a surrogate pair — two 16-bit units. .length counts units, not characters, so it returns 2. This is why naive indexing/slicing/.length on strings with emoji or many non-Latin scripts is buggy; correct handling iterates by code point (e.g. [..."😀"].length is 1) or uses a grapheme-aware library.
InterviewA colleague stores prices as JavaScript numbers (floats). What goes wrong and what's the fix?
Floats can't exactly represent most decimal fractions, so arithmetic drifts: 0.1 + 0.2 → 0.30000000000000004, and repeated sums (taxes, totals, currency conversion) accumulate error and fail exact comparisons — unacceptable for money. Fix: store currency as an integer number of minor units (cents), doing all math in integers (1.3), and format for display only at the edge; or use a dedicated decimal/BigInt type. Every serious payment system (Part 11) follows this.
InterviewExplain lossy vs lossless compression and give the right use case for each.
Lossless (PNG, ZIP, FLAC, gzip) removes only redundancy and reconstructs the original bit-for-bit — use it for text, code, archives, and any data where every bit matters. Lossy (JPEG, MP3, H.264) additionally discards information humans barely perceive (subtle color/frequency detail), reaching far higher ratios (10–100×) at the cost of exact fidelity — use it for photos, audio, and video destined for human senses, where the loss is imperceptible and the size savings are decisive. The choice is an exactness-vs-size trade-off; the theoretical limit of lossless is Shannon entropy (1.8).
StaffYour API truncates user 'bios' to 100 characters with substring(0,100) on the byte length, and non-English users report corrupted text and occasional crashes. Diagnose and prescribe.
The code conflates bytes/units with characters. In UTF-8 a character can be up to 4 bytes; cutting at a fixed byte offset can slice through the middle of a multi-byte sequence, producing an invalid byte (mojibake) or, in strict decoders, an exception — hence corruption and crashes for non-ASCII input. Prescription: define the limit in terms of code points (or better, user-perceived graphemes), and truncate on those boundaries using a Unicode-aware routine (e.g. iterate the string's code points, or an Intl.Segmenter/grapheme library), never a raw byte or UTF-16-unit index. Add tests with emoji and combining characters. The deeper lesson: text length is ambiguous — always specify which unit — and i18n correctness must be designed in.
Flashcards
FlashIEEE 754 double — three fields
Sign (1 bit) + exponent (11 bits) + mantissa/fraction (52 bits) = 64 bits.
FlashWhy 0.1 + 0.2 ≠ 0.3
0.1 has no exact binary representation (10's factor of 5 isn't a power of 2), so it's stored approximately; errors surface when added.
FlashHow to store money
As integers (e.g. cents) or a decimal type — never binary floating point.
FlashASCII 'A' and 'a'
'A' = 65, 'a' = 97 (differ by 32 — a single bit).
FlashUTF-8 key win
ASCII range encodes as identical single bytes → full backward compatibility; variable 1–4 bytes covers all of Unicode.
FlashWhat #FF5C00 means
RGB: red 0xFF (255), green 0x5C (92), blue 0x00 (0) — two hex digits (one nibble pair) per channel.
FlashLossy vs lossless
Lossless = perfectly reversible (PNG/ZIP/FLAC). Lossy = discards imperceptible detail for smaller files (JPEG/MP3/H.264).
Scenario Drill
DrillA data pipeline sums millions of floating-point sensor readings and the total disagrees with a hand check by a surprising amount — larger than any single rounding error. What's happening, and what's one mitigation that doesn't abandon floats?
This is accumulated floating-point error, amplified by a specific hazard: adding many numbers of very different magnitudes. When a tiny reading is added to a large running total, the small value's low bits fall off the end of the mantissa and are lost — repeated millions of times, the dropped bits sum to a visible discrepancy (and order-of-addition changes the result, since float addition isn't associative). Mitigations that keep floats: use a compensated summation algorithm (e.g. Kahan summation) that tracks and re-adds the lost low-order bits; sum in a higher-precision accumulator; or sort/group so like magnitudes are added together. If exactness is truly required, switch to integers or a decimal type. The staff insight: float addition is not associative, so "just sum them" has a precision cost that grows with count and magnitude spread.