Numbers & Bits
By the end of this lesson you will be able to read and write numbers in decimal, binary and hex; explain exactly what happens inside the CPU when an int overflows or a double loses precision; predict the output of any bitwise expression bit by bit; and solve the classic bit-manipulation interview problems (set bits, XOR tricks, bit masks, integer division without /) with confidence.
1. Number systems: decimal, binary, hex
Decimal (base 10) uses digits 0–9; each position is worth a power of 10 (…, 100, 10, 1). Binary (base 2) uses only 0 and 1; each position is worth a power of 2 (…, 8, 4, 2, 1). Hexadecimal (base 16) uses 0–9 then A–F for 10–15; each position is worth a power of 16 — and conveniently, one hex digit represents exactly 4 bits, so hex is the notation programmers use to write binary compactly (e.g. a whole byte 1111 1111 is just FF).
The same thing as runnable Dart — decimalToBinary is the repeated-division loop from the animation and binaryToDecimal is the place-value loop of the next two animations (exact output in the comments, verified):
Input size → what's feasible: decimalToBinary runs ⌊log2 n⌋ + 1 passes: the player's limit n ≤ 65 535 means ≤ 16 passes, and even the largest native int (n < 263) needs only ≤ 63. Converting is never the bottleneck; use toRadixString(2) in real code.
Going the other direction — binary to decimal — you add up the value of every position that has a 1, using the place values 1, 2, 4, 8, 16, 32…
Edge case: a byte (8 bits) of all 1s is the largest value that fits in 8 bits.
The same thing as runnable Dart — the all-ones byte, written four ways, plus the identity 20 + 21 + … + 2n−1 = 2n − 1 checked for n = 1…62 (exact output in the comments, verified):
Dart lets you write integers directly in hex (0xFF) — but, unlike many languages, it has no 0b… binary literal — and it converts between any bases from 2 to 36 with library functions:
0x before a number means "the digits that follow are hex" — it is just a different way to type the same integer; 0xFF and 255 are the exact same value in memory. int.parse(text, radix: b) reads a string written in base b. n.toRadixString(b) converts n back into a string in base b.
int: 64-bit two's complement, range, overflow
int is like a car odometer with exactly 64 wheels, each showing 0 or 1. There is no 65th wheel. If a calculation needs a 65th wheel to represent the true answer, the extra digit has nowhere to go — it is dropped, and the odometer silently shows the wrong number. That is overflow.On the Dart VM and in AOT-compiled apps (the normal case: dart run, compiled Flutter/CLI apps — the VM is Dart's own runner that executes your code directly, AOT means “ahead-of-time”: compiled to machine code before the app ships; both use real 64-bit integers), int is a genuine 64-bit signed integer stored in two's complement form. That gives a range of exactly:
Two's complement is the encoding almost every real CPU uses for signed integers: the top ("sign") bit is 1 for negative numbers, and a negative number
−n is stored as "invert every bit of n, then add 1". It is popular because addition/subtraction hardware doesn't need a separate code path for negative numbers — the same adder circuit works for both.
The same thing as runnable Dart — the exact range, the 63-bit magnitude, and the asymmetric minimum (exact output in the comments, verified):
The same thing as runnable Dart — two's-complement encoding of −5: invert, add 1, mask to 8 bits, and −n == ~n + 1 checked for n = −1000…1000 (exact output in the comments, verified):
Because there are only 64 wheels, adding 1 to the biggest possible int doesn't throw an error in Dart — it silently wraps around to the smallest possible (most negative) int. This is the two's-complement overflow rule.
The same thing as runnable Dart — silent wrap-around, a multiplication overflow, and a cheap overflow detector built from the sign-bit rule (exact output in the comments, verified):
Input size → what's feasible: int add/sub/mul are single CPU steps (~108–109 per second), so 108 additions are fine; once a true result can exceed 263 − 1 ≈ 9.22·1018 (e.g. a product of two numbers above 3.04·109) switch to BigInt.
int using JavaScript's double (a 64-bit float), which can only represent whole numbers exactly up to 253 (about 9 quadrillion). Below that, arithmetic behaves as you'd expect; above it, and for the overflow-wraps-around behaviour above, web and native Dart can disagree. If your code depends on exact 64-bit wraparound (crypto, hashing, bit-twiddling puzzles), test it on the platform you actually ship to.
The same thing as runnable Dart — how to detect the web at compile time and why 253 is the limit there (exact output in the comments, verified):
double: IEEE-754, precision, NaN/Infinity
double is like writing a number in scientific notation with a fixed number of decimal places — say, always 15 significant digits, e.g. 6.02214076 × 10²³. Numbers that need more digits than that to write exactly (like most fractions in binary) get quietly rounded to the nearest value that fits. Almost every language's double does this the same way, because they all follow the same standard: IEEE-754.A Dart double is a 64-bit IEEE-754 "double precision" float, split into three fields: 1 sign bit, 11 exponent bits, and 52 mantissa (fraction) bits. At a high level: the sign says positive/negative, the exponent says roughly "how big", and the mantissa stores the precise digits.
The same thing as runnable Dart — pulling the sign, exponent and mantissa fields out of the double 0.1 (exact output in the comments, verified):
The same thing as runnable Dart — the exact decimal value a double really stores (the long numbers quoted in the animation) (exact output in the comments, verified):
Input size → what's feasible: a double holds ~15–17 significant decimal digits and counts whole numbers exactly only up to 253 ≈ 9.0·1015; summing 106 money values as doubles drifts, so keep money as integer minor units (paise) up to 9.2·1018.
== after arithmetic. Because most decimal fractions (like 0.1) cannot be written exactly in binary — just like ⅓ cannot be written exactly in decimal — tiny rounding errors accumulate. Compare with a tolerance instead:
double also has two special values that aren't ordinary numbers: double.infinity (what you get from dividing a positive number by 0.0) and double.nan ("Not a Number" — the result of an undefined operation like 0.0/0.0). NaN has one famously strange property: it is never equal to anything, including itself.
The same thing as runnable Dart — num as the parent of int and double, and what / returns (exact output in the comments, verified):
num is the common parent type of both int and double — a variable typed num can hold either, and Dart infers which one from the value assigned. Use it for parameters that should accept "any number, I don't care which kind".4. Arithmetic operators
Dart has more division-related operators than most languages, because "divide two integers" is genuinely ambiguous (do you want the exact fractional answer, or a whole-number answer?) and Dart lets you say precisely which one you mean.
| Operator | Meaning | Example | Result |
|---|---|---|---|
/ | Division — always returns a double, even for whole-number results | 7 / 2 | 3.5 |
~/ | Integer (truncating) division — returns an int, drops the fractional part toward zero | -7 ~/ 3 | -2 |
% | Euclidean modulo — the result is never negative: always 0 ≤ r < |divisor| | -7 % 3 | 2 |
.remainder() | Truncating remainder — the result has the same sign as the dividend (matches C/Java's %) | (-7).remainder(3) | -1 |
% is not the same as those languages' %. Their % behaves like Dart's .remainder(). If you need "always non-negative, wraps around a fixed size" (e.g. indexing into a circular buffer), Dart's % is exactly what you want and you don't need to add correction code the way C programmers do.The same thing as runnable Dart — every operator in the table with negative operands (exact output in the comments, verified):
The same thing as runnable Dart — the identities behind them: a = (a ~/ b)·b + a.remainder(b) and 0 ≤ a % b < |b|, checked for a = −20…20 and b ∈ {−7, −3, 3, 7} (exact output in the comments, verified):
Other everyday numeric methods, all on num/int/double:
| Method | What it does | Example |
|---|---|---|
.abs() | Absolute value (drop the sign) | (-5).abs() == 5 |
.round() | Nearest integer; exact halves round away from zero | 2.5.round() == 3, (-2.5).round() == -3 |
.floor() | Round toward negative infinity | 2.7.floor() == 2 |
.ceil() | Round toward positive infinity | 2.1.ceil() == 3 |
.truncate() | Drop the fractional part (round toward zero) | 2.9.truncate() == 2 |
.clamp(lo, hi) | Force a value into a range | 10.clamp(0, 5) == 5 |
math.pow(b, e) | b raised to the power e (dart:math) | pow(2, 10) == 1024 |
.isEven / .isOdd | Parity check on an int | 4.isEven == true |
The same thing as runnable Dart — each method in the table (exact output in the comments, verified):
Input size → what's feasible: every operator and method here is O(1); even 108 of them per second is fine. The full list (gcd, modPow, toStringAsExponential, toRadixString, bitLength …) is in D23 · Number methods.
5. Bitwise operators
& (AND) turns a light on only if both of two switches say on. | (OR) turns it on if either says on. ^ (XOR, "exclusive or") turns it on if exactly one says on. ~ (NOT) flips every switch. <</>>/>>> slide the whole row of switches left or right.The same thing as runnable Dart — & | ^ ~ on 12 and 10, plus the laws ~n == −n − 1, De Morgan, XOR from AND/OR, and a + b == (a ^ b) + ((a & b) << 1) checked for n = −100…100 (exact output in the comments, verified):
Shifting a bit pattern left or right is the same as multiplying/dividing by a power of 2 — but the two right-shift operators disagree about what to do with negative numbers:
The same thing as runnable Dart — all three shifts, and the laws n << k == n·2k and n >> k == ⌊n / 2k⌋ (exact output in the comments, verified):
>> (arithmetic shift) copies the sign bit into the vacated slots, so a negative number stays negative — this matches "divide by 2, rounding toward negative infinity". >>> (logical shift, added in Dart 2.14) always fills with 0, treating the bits as a plain unsigned pattern — so shifting a negative int right with >>> can produce a huge positive number. Use >>> when you specifically want to manipulate raw bit patterns (hashing, checksums) regardless of sign.Bitwise operators are also how you use an int as a compact set of true/false flags, called a bit mask: pick a bit position for each flag, then set/clear/toggle/check it directly.
The same thing as runnable Dart — set / clear / toggle / check a bit, n & (n − 1), and permission flags stored in one int (exact output in the comments, verified):
Input size → what's feasible: a bit mask over n items has 2n states: n ≤ 20 → 106 states (instant), n = 25 → 3.4·107 (about a second), n = 40 → 1012 (impossible) — and a native int holds at most 64 flags.
n & (n - 1) clears the lowest set bit (used to count set bits and to test powers of two); x ^ x == 0 for any x (used to find a "lone" value via XOR); and n > 0 && (n & (n - 1)) == 0 tests whether n is an exact power of two (a power of two has exactly one set bit, so clearing the lowest set bit leaves nothing).
The same thing as runnable Dart — the three tricks above plus the lowest-set-bit isolator n & −n (exact output in the comments, verified):
6. Parsing, formatting & BigInt
Turning text into numbers is a common source of crashes if you're not careful: int.parse throws a FormatException on invalid input, while int.tryParse returns null instead — always prefer tryParse when the input isn't guaranteed to be a clean number (user input, network data).
To control how a double is displayed (say, always 2 decimal places for money), use toStringAsFixed. And when even a 64-bit int isn't big enough — factorials, cryptography, huge counters — Dart's BigInt grows to hold numbers of any size, limited only by available memory.
int (max ~19 digits) could ever hold. BigInt represents numbers as an internal array of digit-chunks and does arithmetic chunk by chunk, the same way you'd do long multiplication on paper with more digits than fit on one line.Input size → what's feasible: factorial(n) does n BigInt multiplications on numbers of up to ≈ n·log10 n digits: n = 1000 gives 2568 digits and is instant, while n = 105 (456 574 digits) takes far longer — big-number work grows much faster than native int work.
Every num, int, double, BigInt and bool member (rounding variants, toStringAsPrecision, gcd, modPow, modInverse, toSigned/toUnsigned, BigInt conversions …) is catalogued with examples in D23 · Number methods.
Quiz
Interview questions
Cheat sheet
| Topic | Key facts |
|---|---|
| int | 64-bit two's complement (native); range ±263; overflow wraps silently; web uses JS doubles (exact only up to 253) |
| double | IEEE-754 64-bit; never compare with == after math; double.nan != double.nan; double.infinity |
/ ~/ % remainder | / always double; ~/ truncates; % Euclidean (always ≥ 0); remainder() sign of dividend |
| Bitwise | & | ^ ~ per-bit logic; << shift left; >> arithmetic (sign-preserving); >>> logical (zero-fill) |
| Tricks | n & (n-1) clears lowest bit; x^x==0; power-of-two test via n&(n-1)==0 |
| Parsing | parse throws, tryParse returns null; BigInt for arbitrary precision |