Skip to content

Volume ITechnology

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:

signexponentmantissa (fraction)1 bit11 bits52 bitsIEEE 754 double — 64 bits total
Figure 1 — A 64-bit float. Value ≈ ±(1.mantissa) × 2^(exponent). The floating binary point is what lets one fixed-size box span from the subatomic to the astronomical — at the cost of exactness.

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.

To understand why 0.1 repeats forever in binary, think about how place values work. In our everyday decimal system, the places after the dot represent tenths, hundredths, thousandths, and so on. A fraction only stops cleanly if its denominator is built entirely from 2s and 5s, which are the building blocks of 10. But in binary, the places after the dot represent halves, quarters, eighths, and sixteenths. A fraction can only stop cleanly in binary if its denominator is built only from 2s. Since 0.1 is mathematically the fraction 1/10, and 10 is built from 2 times 5, that leftover 5 makes it impossible to build exactly 1/10 using only halves, quarters, and eighths. Just like trying to divide 1 by 3 in decimal, the binary division loops endlessly.

To see this mechanically, moving right from the dot in decimal gives you tenths (10^{-1}), hundredths (10^{-2}), and thousandths (10^{-3}). Moving right in binary gives you halves (2^{-1}), quarters (2^{-2}), eighths (2^{-3}), and sixteenths (2^{-4}). When you want to store 0.5, it perfectly matches the half slot (2^{-1}), so it is stored exactly as 0.1. When you want to store 0.75, it is a half plus a quarter (2^{-1} + 2^{-2}), so it stores exactly as 0.11.

But if you want to store 0.1 (one tenth), you have to build it by adding up these pieces. You skip the half and the quarter, and take the sixteenth (0.0625). Then you need a bit more, so you add a thirty-second (0.03125). You keep taking smaller and smaller binary fractions, trying to reach exactly 0.1. But because of that leftover prime factor of 5, the pieces can never perfectly sum to a tenth. The math loops as 0.0001100110011... forever. The computer has to chop this infinite loop off after 52 bits, leaving a tiny rounding error that exposes itself when you add two of them together.

211/21/41/82¹2⁰2⁻¹2⁻²2⁻³0.50.250.125binary point
Binary places. Moving right past the dot, each position is half of the previous one. You can only build fractions that are sums of halves, quarters, eighths, and so on. A tenth cannot be built exactly.

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.

Deeper Look: The Mechanics and History of Floating Point ​

How does the computer know if bits are a whole number or a fraction? It does not. If you look at the raw memory of a computer, you only see streams of 1s and 0s. There is no tag attached to a byte saying "I am a fraction" or "I am a whole number." The bits mean nothing on their own.

The magic happens entirely in the instructions the computer executes. If your code tells the processor, "add these 32 bits using integer math," the processor treats them as a simple whole number. If your code says, "add these 32 bits using floating-point math," the processor splits the exact same bits into a sign, exponent, and mantissa. High-level programming languages manage this for you by generating the right instructions based on the variables you declare.

A concrete example of bits changing identity Imagine this exact 32-bit sequence stored in memory: 01000000 01000000 00000000 00000000

If you read this sequence as a standard 32-bit whole integer, it represents the large number 1,077,936,128.

But if you feed that exact same sequence into a floating-point instruction, the computer slices it up according to the IEEE 754 rules. The first bit (0) means it is positive. The next 8 bits calculate the exponent. The remaining 23 bits form the mantissa. The math works out to exactly 3.0. The identical bits can be one billion or three, completely dependent on the instruction that reads them.

Here is exactly how that math works out step by step. The 8 exponent bits are 10000000, which equals 128 in decimal. The standard subtracts a fixed 127 from this to allow for negative exponents, leaving an actual exponent of 1. The remaining 23 bits form the mantissa, starting with 1000000. In binary scientific notation, there is always an assumed "1." at the front, so the actual binary fraction becomes 1.1. In binary, the first spot after the dot is a half, so 1.1 in binary is 1.5 in decimal. The final calculation is 1.5 \times 2^1, which equals exactly 3.0.

Where did this convention come from? In the 1960s and 70s, calculating with fractions was chaos. Every computer company — IBM, Cray, DEC — invented their own format for handling decimals. A physics program running on a Cray supercomputer might give a slightly different answer than the same program on an IBM machine.

To fix this, Intel hired a brilliant mathematician named William Kahan. Kahan gathered experts to design one universal system that balanced speed, accuracy, and predictability. They chose a binary version of scientific notation because it provided an enormous mathematical range within a very limited amount of memory. The result was published in 1985 as the IEEE 754 standard. Today, nearly every device on Earth uses Kahan's standard. That is why 0.1 + 0.2 gives the exact same quirky 0.30000000000000004 answer on an iPhone as it does on a massive cloud server.

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.

Why exactly 7 bits? In binary, 7 bits gives you 2^7, which is exactly 128 possible combinations (the numbers 0 to 127). This was precisely enough space for the American engineers who designed it in 1963 to fit the English alphabet, punctuation, and control codes for their teletype machines. You might wonder why the character '0' is not simply the number 0 in ASCII, but is instead 48. The number 0 was reserved for a "null" control code used to signal the end of a message or fill empty paper tape. The numbers 0 through 31 were all taken by these invisible machine commands, like "ring the bell" or "move carriage return." The printable characters started at 32 (a space), which pushed the digit '0' up to the number 48.

The driving force behind this standard was an IBM engineer named Bob Bemer, often called the "Father of ASCII." Before Bemer, every computer manufacturer used their own secret codes for letters, meaning an IBM computer literally could not send a text message to a computer made by a rival. Bemer spent years aggressively lobbying companies to agree on a single shared alphabet. He also invented the "Escape" key (ESC) as part of ASCII, originally designing it to let computers switch out of ASCII and into other alphabets — a humble key that ended up shaping how we interact with computers today.

01000001 (65)→A01100001 (97)→a
ASCII mapping. Flipping a single bit (the 32's place) changes uppercase to lowercase. A simple, direct lookup table for 128 characters.

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.

This garbage text, born from reading the right bytes with the wrong code page, was called mojibake (a Japanese word meaning "character transformation"). To fix this forever, the tech industry formed the Unicode Consortium. Today, representatives from technology companies and language experts from around the world vote on which new characters to add. This single body is the reason everyone agrees that U+1F600 is the grinning face emoji.

They also maintain the massive rulebooks that define how characters from complex scripts like Bengali or Odia should join together. When you type Bengali on a keyboard, the operating system assigns the correct Unicode code points. Then, a specialized piece of software called a "text shaping engine" reads those code points and applies the complex grammatical rules of Bengali to figure out exactly which shapes to draw. Finally, the graphics system looks up those shapes in a font file and paints the pixels on your screen. This pipeline from a raw number to a shaped character is what allows unpopular or highly complex languages to work universally on computers today.

Code PointU+1F600→Text Shaping EngineApplies grammar rules→Font RendererDraws pixels→😀
The rendering pipeline. Unicode only assigns the number. Complex software is required to read that number, calculate how it connects to neighboring letters, and look up the physical picture in a font file.

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.

You might wonder: won't variable-length bytes confuse the processor? How does it know where one character ends and the next begins in a long stream of 1s and 0s? The answer is a brilliant trick in the binary design of UTF-8.

If a byte starts with a 0 bit, the computer knows instantly that it is a single-byte ASCII character. It reads those 7 remaining bits (which can hold numbers up to 127) and is done. This is how UTF-8 maps perfectly to ASCII: any old ASCII file is just a stream of bytes starting with 0, so early web browsers didn't even know they were reading UTF-8.

If a character needs more room, UTF-8 uses a binary prefix system. If the first byte starts with the bits 110, the computer knows this character takes exactly two bytes. The second byte will always start with 10. If the first byte starts with 1110, the character takes three bytes, and the two following bytes will both start with 10. If it starts with 11110, it takes four bytes, and the three following bytes all start with 10. Because of these strict rules, the computer never gets lost. If it looks at a byte and sees it starts with 10, it knows it has been dropped in the middle of a character and simply looks backward for the starting byte.

0xxxxxxx (1 byte, ASCII)110xxxxx 10xxxxxx (2 bytes)1110xxxx 10xxxxxx 10xxxxxx (3 bytes)11110xxx 10xxxxxx 10xxxxxx 10xxxxxx (4 bytes)
UTF-8 structure. Bold bits are the prefix headers. The "x" bits are where the actual Unicode number is stored. A computer can jump into the middle of this data, see a `10`, and know to step back to find the start.

This elegant puzzle of prefix bits was not designed by a massive corporate committee over years. It was invented in 1992 by two legendary computer scientists, Ken Thompson and Rob Pike, sitting in a New Jersey diner. They were building a new operating system and realized the existing proposals for expanding text would break all their old ASCII files. Over a single dinner, they sketched the binary rules for UTF-8 on a paper placemat. By the time they returned to their lab, they had the logic figured out. They wrote the code to implement it over the next few days, creating the system that now runs 98% of the global internet from a diner placemat sketch.

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.

Where did UTF-16 come from, and why do JavaScript and Java use it if the web uses UTF-8? In the early 1990s, when Unicode was first created, engineers thought 65,536 characters (16 bits, or 2 bytes) would be enough to hold every language forever. Languages like Java and JavaScript were being designed exactly at this time, so they eagerly adopted 16-bit characters as their internal standard. Every letter would take exactly 2 bytes, making it wonderfully simple to count characters.

But a few years later, Unicode realized they needed far more space for historical scripts and emoji. 16 bits was not enough. They had to expand Unicode beyond 65,536 characters, which broke the core 16-bit assumption. To fix this without breaking every language that had already adopted 16-bit characters, they invented the surrogate pair.

They took a block of 2,048 unused 16-bit numbers and reserved them entirely. Unlike UTF-8, which uses binary prefixes like 1110 at the bit level, UTF-16 looks at the actual 16-bit number. If a JavaScript engine sees a number between 0xD800 and 0xDBFF (the "high surrogate"), it knows this is only half of a character. It must be followed immediately by a number between 0xDC00 and 0xDFFF (the "low surrogate"). Together, this pair of 16-bit numbers does the math to point to a single massive character, like the four-byte grinning emoji.

This is why JavaScript's "😀".length returns 2. JavaScript was hardwired in the 1990s to count 16-bit units, not human characters. The emoji is one human character, but it is built from a high surrogate and a low surrogate, so JavaScript counts it as two units. If you use a function to slice the first character off "😀", you will only chop off the high surrogate. You are left with an orphaned half-character that the graphics engine cannot render, resulting in a garbage question-mark box.

😀U+1F6000xD83D0xDE00High SurrogateLow Surrogate
UTF-16 surrogate pair. One single character (the emoji) is split into two 16-bit halves. JavaScript's `.length` counts the halves, which is why it reports the length as 2.

Today, this leaves us with a split world. When you save an HTML file or send text over the internet, it is almost universally encoded in UTF-8 to save space and remain backward compatible with ASCII. But when that UTF-8 text arrives in your browser, the JavaScript engine translates it internally into UTF-16 to run its code, because that is how it was built in 1995. The translation happens automatically at the boundary, ensuring everything remains interconnected despite using completely different rules under the hood.

To understand exactly how this boundary conversion works, think of the Unicode code point as a universal middleman. When the browser downloads an emoji like 😀 over the network, it arrives as four UTF-8 bytes (F0 9F 98 80). The browser's network layer looks at the binary prefixes (11110..., 10...) and strips them away. It extracts only the data bits stored inside and glues them back together. This reveals the raw, underlying Unicode number: U+1F600.

Once the raw code point is uncovered, the JavaScript engine takes over. It sees that U+1F600 is too big to fit in a single 16-bit space. It performs the UTF-16 math: subtracting a base number and slicing the remaining bits into two pieces. It packages the first piece into a high surrogate (0xD83D) and the second into a low surrogate (0xDE00). The original UTF-8 bytes are thrown away, and the emoji lives on inside the browser's active memory as a UTF-16 pair. The meaning never changes, only the packaging.

UTF-8 BytesF0 9F 98 80Network Arrival→Unicode U+1F600Raw NumberDecoding→UTF-16 PairD83D DE00JavaScript Engine
The browser boundary. Text is unpacked from UTF-8 into raw Unicode numbers, then immediately repacked into UTF-16. The pure code point acts as the universal translator in the middle.

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.

Think of your screen as millions of tiny, separate flashlights. Each pixel has a red, a green, and a blue flashlight pointing right at your eye. By turning the dials on those three flashlights up or down, your brain is tricked into seeing any color. If you turn all three dials to maximum (255), you get pure white light. If you turn them all off (0), you get black. The hex code #FF5C00 is simply a set of three instructions to those dials: turn the Red dial to maximum (FF or 255), turn the Green dial down to about a third (5C or 92), and turn the Blue dial completely off (00 or 0). Your eye mixes that bright red and dim green light together to see orange.

Red (FF)Green (5C)Blue (00)→Result (#FF5C00)
Additive Color. Screens emit light, so colors add up. Bright red and dim green mix to create orange. Turning all three to maximum creates white.

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.

To picture the difference, imagine drawing a circle. A bitmap image is like filling in squares on a piece of graph paper. If you zoom in on that graph paper by magnifying it, the squares just get bigger and the edges look like jagged stairs. This is why photos get blurry when you blow them up. A vector image (SVG), on the other hand, does not save squares. It saves a mathematical rule: "draw a circle with a radius of 5 inches." If you zoom in, the computer simply recalculates the math for a bigger radius and draws a brand new, perfectly smooth curve. That is why the diagrams on this page stay sharp no matter how close you zoom.

Bitmap (Pixels)vsVector (Math)
Zooming in. A bitmap is a fixed grid of colored tiles; zooming in makes the tiles large and jagged. A vector is a mathematical instruction; zooming in tells the computer to smoothly redraw the shape at a larger size.

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.

Think of a sound wave like a roller coaster track, smoothly curving up and down. A computer cannot store a smooth curve because it only speaks in discrete numbers. So, instead of storing the whole track, the computer measures the exact height of the track at regular intervals — thousands of times every second. These measurements are called "samples". When you play an MP3, the computer reads those thousands of height numbers and rapidly pushes your speaker cone to those exact positions. Because it happens 44,100 times a second, your slow human ear blends all those distinct jolts together into one continuous, smooth musical note.

Sampling a wave. The smooth background curve is the real analog sound in the air. The vertical bars are the digital samples—discrete height measurements taken thousands of times per second.

Video. If a photograph is a single grid of pixels, a video is simply a fast slideshow of many grids. A standard movie shows you 24 pictures (called frames) every single second. A 60-second video at 24 frames per second means your screen is drawing 1,440 completely separate images. Because human vision is relatively slow, your brain cannot see the gap between the pictures; it blurs them together into the illusion of continuous motion. However, storing 1,440 full-sized photographs for a one-minute clip takes a massive amount of memory, which makes video compression the most important math on the internet.

Frame 1Frame 2Frame 3Frame 4
Illusion of motion. Video is not a moving picture; it is a rapid sequence of still pictures. A circle drawn slightly further to the right in each consecutive frame creates the illusion that it is travelling.

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.

To make this concrete, imagine trying to send a large document to a friend. Lossless compression is like abbreviating words: you change "For your information" into "FYI". Your friend can perfectly expand "FYI" back into the exact original words. No meaning is lost. Lossy compression is like summarising the document: you delete whole paragraphs that aren't very important. Your friend gets the core message, but they can never perfectly reconstruct the original exact document because you threw those details away forever. A JPEG throws away tiny variations in the color of the sky that your eye wouldn't notice anyway. It shrinks the file massively, but that data is permanently gone.

Lossless (ZIP, PNG)A A A A A A A A8xAPerfectly reversibleLossy (JPEG, MP3)A A A B A A A C8xADetails discarded permanently
Types of compression. Lossless looks for patterns (like eight 'A's in a row) to shrink size without losing data. Lossy decides that 'B' and 'C' are close enough to 'A' that you won't notice the difference, replacing them all to save huge amounts of space.

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: 01000001 is 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.3 because 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.