R (4.6) Module 2 – Vectors, types and missing values
Numbers, floating point and rounding
Why 0.1 + 0.2 == 0.3 is FALSE in R, how to compare doubles with all.equal(), how round() treats halves, where integers overflow, and how to format numbers.
What you will learn
- Explain why 0.1 + 0.2 == 0.3 is FALSE and compare doubles safely
- Predict R's rounding of halves and of numbers that are not exactly representable
- Recognise integer overflow, the limits of doubles and platform-dependent results
- Format numbers for display without changing the stored values
Before you start
What you’ll learn · Before you start
What you will learn
- Explain why 0.1 + 0.2 == 0.3 is FALSE and compare doubles safely
- Predict R's rounding of halves and of numbers that are not exactly representable
- Recognise integer overflow, the limits of doubles and platform-dependent results
- Format numbers for display without changing the stored values
Before you start
On this page
Type 0.1 + 0.2 == 0.3 into R and the answer is FALSE, although 0.1 + 0.2 prints as 0.3. Nothing is broken:
R stores numbers as binary fractions, most decimal numbers have no exact binary form, and the same fact explains why
round(2.675, 2) is 2.67 and why three shares of 1,000.00, each rounded to 333.33, add up to 999.99.
On runs of R 4.6.1 you will see what R stores for a number and compare doubles safely, predict how round() treats
halves, recognise where integers overflow and doubles stop counting by ones, and print numbers in any format without
changing the values that calculations use.
Doubles are binary fractions, which is why 0.1 plus 0.2 is not 0.3
A number such as 0.1 is stored as a double: R's type for real numbers: a binary fraction with 53 bits of precision, about 15 significant decimal digits, in the IEC 60559 (IEEE 754) format. Most decimals, such as 0.1, are stored as the nearest such fraction.: a binary fraction with 53 bits of
precision, in the IEC 60559 format that the help page of double says every R platform uses. A fraction whose
denominator, in lowest terms, is not a power of 2, such as 1/10, has no exact binary form, just as 1/3 has no exact
decimal one, so R stores the double nearest to it. Step through the four numbers of the comparison:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- 0.1 in binary. A double is a binary fraction of 53 bits. 0.1 has no exact binary form, as 1/3 has no exact decimal one, so R stores the nearest double, a little above 0.1.
- 0.2. 0.2 is stored a little above 0.2 as well.
- The sum. Adding the two stored values and rounding to the nearest double gives a number a little above 0.3.
- 0.3 itself. Typed directly, 0.3 is stored a little below 0.3. The sum and this value are neighbouring doubles, 2^-54 apart, about 5.55e-17.
- The comparison. == compares the stored values, which differ, so 0.1 + 0.2 == 0.3 is FALSE. Printing rounds both to 7 digits, 0.3, which hides the difference.
- With a tolerance. all.equal() accepts differences smaller than about 1.5e-8, measured relative to the size of the numbers, so isTRUE(all.equal(0.1 + 0.2, 0.3)) is TRUE.
The run prints the same four values to twenty decimal places with sprintf(), and then the comparisons:
from session import run, version
version()
# R reads binary.R as if each line were typed at the console.
run("""
R --vanilla --quiet -f binary.R
""") Output
# R version 4.6.1 -- "Happy Hop"
$ R --vanilla --quiet -f binary.R
> # The sum prints as 0.3, but it is not 0.3.
> 0.1 + 0.2
[1] 0.3
> 0.1 + 0.2 == 0.3
[1] FALSE
> # Twenty decimal places show the stored values.
> sprintf("%.20f", c(0.1, 0.2, 0.1 + 0.2, 0.3))
[1] "0.10000000000000000555" "0.20000000000000001110" "0.30000000000000004441"
[4] "0.29999999999999998890"
> (0.1 + 0.2) - 0.3
[1] 5.551115e-17
> # They are neighbouring doubles, 2^-54 apart.
> (0.1 + 0.2) - 0.3 == 2^-54
[1] TRUE
> # The same happens inside a sequence.
> seq(0, 1, by = 0.1)[4] == 0.3
[1] FALSE
> # Compare with a tolerance instead.
> all.equal(0.1 + 0.2, 0.3)
[1] TRUE
> isTRUE(all.equal(0.1 + 0.2, 0.3))
[1] TRUE
> abs((0.1 + 0.2) - 0.3) < 1e-9
[1] TRUE
> # A real difference is reported, not hidden.
> all.equal(1, 1.1)
[1] "Mean relative difference: 0.1"
> isTRUE(all.equal(1, 1.1))
[1] FALSE
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 binary_session.py
The sum lands on the double just above 0.3 and the literal 0.3 on the one just below, 2^-54 apart, which the run
confirms. The fourth value of seq(0, 1, by = 0.1) carries the same kind of error, so comparing it with 0.3 fails
too.
The fix is to compare with a tolerance. all.equal(x, y) accepts differences smaller than about 1.5e-8, measured
relative to the size of the numbers, and returns TRUE or a sentence that describes the difference, so inside if ()
write isTRUE(all.equal(x, y)). When you know the scale of your data, abs(x - y) < tol with your own tol says the
same thing explicitly.
Testing doubles with ==
if (total == 0.3) can fail on a value that prints as 0.3. Use isTRUE(all.equal(total, 0.3)), or
abs(total - 0.3) < 1e-9 when you know the scale. Keep == for integers and for values you set yourself.
Checkpoint
Check: Doubles as binary fractions
round() goes to the even digit, but only for halves it can see
round(c(0.5, 1.5, 2.5)) returns 0 2 2. For a half, the help page of round() says R expects the IEC 60559 rule:
go to the even digit, so 0.5 rounds down to 0 and 1.5 up to 2. Halves then round down as often as up, so a long
column of rounded values does not drift upwards. But a decimal “half” such as 2.675 is usually not stored as a
half at all:
The drawing shows the whole scene; its 4 steps are listed below.
Every step as text
- Two neighbours. Rounding 2.675 to two decimals chooses between 2.67 and 2.68, and 2.675 is exactly halfway between them.
- What R stores. 2.675 has no exact binary form, and the double R stores for it is a little below halfway.
- round(2.675, 2). Since R 4.0.0, round() measures which neighbour the stored value is nearer, and it is nearer 2.67. So round(2.675, 2) is 2.67, and no rule about halves is involved.
- Only true halves go to even. The even-digit rule decides only halves that are stored exactly, such as 2.5 or 0.125. For a decimal like 2.675, the stored value decides.
The run puts true halves beside decimal ones, with the stored values printed to twenty places:
from session import run, version
version()
# R reads rounding.R as if each line were typed at the console.
run("""
R --vanilla --quiet -f rounding.R
""") Output
# R version 4.6.1 -- "Happy Hop"
$ R --vanilla --quiet -f rounding.R
> # A half that is stored exactly goes to the even digit.
> round(c(0.5, 1.5, 2.5, -0.5, -1.5))
[1] 0 2 2 0 -2
> round(0.125, 2)
[1] 0.12
> # Most decimal "halves" are stored a little off.
> round(2.675, 2)
[1] 2.67
> sprintf("%.20f", 2.675)
[1] "2.67499999999999982236"
> round(0.15, 1)
[1] 0.1
> sprintf("%.20f", 0.15)
[1] "0.14999999999999999445"
> # The other ways to round.
> ceiling(2.1)
[1] 3
> floor(-2.1)
[1] -3
> trunc(-2.7)
[1] -2
> signif(123456, 2)
[1] 120000
> round(123456, -3)
[1] 123000
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 rounding_session.py
0.125 is 1/8, a binary fraction, so it is stored exactly, it is a true half, and round(0.125, 2) goes to the even
digit, 0.12. 2.675 and 0.15 are stored a little below their printed values, so they
round down, to 2.67 and 0.1. The help page warns about exactly this: the rounding rule applies to the represented
number, not the printed one.
ceiling() rounds up, floor() down, trunc() towards zero, signif() to a number of significant digits, and a
negative digits rounds to tens, hundreds or thousands: round(123456, -3) is 123000.
Key idea
round() works on the stored value: a true half goes to the even digit, and a decimal that only looks like a half
rounds to whichever neighbour its stored value is nearer.
Checkpoint
Check: How round() treats halves
Integers overflow to NA, and doubles stop counting by ones at 2^53
R’s integers are 32-bit, so .Machine$integer.max is 2,147,483,647, and one more is out of range. The help page of
integer says that as.integer() turns a value beyond the range into NA, and integer arithmetic does the same;
both warn, as the run shows. Doubles have the opposite trade-off: a far larger range, about 2e+308, with 53 bits of precision, so whole
numbers are exact only up to 2^53, 9,007,199,254,740,992.
from session import run, version
version()
# R reads limits.R as if each line were typed at the console.
run("""
R --vanilla --quiet -f limits.R
""") Output
# R version 4.6.1 -- "Happy Hop" $ R --vanilla --quiet -f limits.R > # Integers are 32-bit: this is the largest one. > .Machine$integer.max [1] 2147483647 > .Machine$integer.max + 1L [1] NA Warning message: In .Machine$integer.max + 1L : NAs produced by integer overflow > as.integer(2^31) [1] NA Warning message: NAs introduced by coercion to integer range > # The same sum in doubles is no problem. > .Machine$integer.max + 1 [1] 2147483648 > # Doubles hold whole numbers exactly up to 2^53. > format(2^53, scientific = FALSE) [1] "9007199254740992" > 2^53 == 2^53 + 1 [1] TRUE > # The gap between neighbouring doubles grows with size. > 2^(floor(log2(c(1, 1e6, 2^53))) - 52) [1] 2.220446e-16 1.164153e-10 2.000000e+00 > .Machine$double.eps [1] 2.220446e-16
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 limits_session.py
The second-to-last line shows why. The gap between one double and the next grows with the size of the number: about
2.2e-16 near 1, about 1.2e-10 near a million, and 2 at 2^53, so 2^53 + 1 has nowhere to go and rounds back to 2^53.
.Machine$double.eps, 2.220446e-16, is that gap at 1: its help page defines it as the smallest positive number x
for which 1 + x != 1.
Use a double when a sum may pass two billion
Counts, ids and money stored as integers overflow at 2,147,483,647. .Machine$integer.max + 1, with a double 1,
gives 2147483648 without a warning, because the sum is a double. Convert with as.numeric() before summing large
integer columns.
Checkpoint
Check: Integer and double limits
The last digits can differ between computers, so never test them
sum(rep(0.1, 10)) == 1 is FALSE in the run below, on an Apple-silicon Mac. The help page of sum() says it uses
extended-precision accumulators where possible, which is platform-dependent, and capabilities("long.double") says
whether this build of R has a long double wider than a double. Writing R Extensions adds that current ARM CPUs have
neither extended precision nor long doubles:
from session import run, version
version()
# R reads platform.R as if each line were typed at the console.
run("""
R --vanilla --quiet -f platform.R
""") Output
# R version 4.6.1 -- "Happy Hop"
$ R --vanilla --quiet -f platform.R
> # Does this build of R have a long double wider than a double?
> capabilities("long.double")
long.double
FALSE
> # sum() adds in long double where there is one; here there is not.
> sum(rep(0.1, 10)) == 1
[1] FALSE
> print(sum(rep(0.1, 10)), digits = 17)
[1] 0.99999999999999989
> # A loop adds in plain doubles on every computer.
> total <- 0
> for (v in rep(0.1, 10)) total <- total + v
> print(total, digits = 17)
[1] 0.99999999999999989
> # A check that gives the same answer on every computer.
> isTRUE(all.equal(sum(rep(0.1, 10)), 1))
[1] TRUE
> # Exact at special values on every platform since R 4.6.0.
> exp(0) == 1
[1] TRUE
> log1p(0) == 0
[1] TRUE
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 platform_session.py
On this Apple-silicon build there is no long double, so sum() adds in plain doubles, exactly as the loop does, and
both reach 0.99999999999999989. On a build where capabilities("long.double") is TRUE, sum() adds the same ten
values in a wider format, and the same comparison can come out TRUE. Code whose result depends on that last bit
behaves differently from one computer to the next; code that compares with all.equal() does not.
New in R 4.6.0: exact results at special values
As an experiment, R 4.6.0 made several maths functions, among them exp() and log1p(), give fully accurate
results near special values on every platform, so exp(0) == 1 and log1p(0) == 0 hold even where the system’s
maths library is less accurate. Its NEWS entry points to the R blog post in the references, on how one-unit
differences between platforms’ maths libraries break tests whose tolerances are too tight.
Checkpoint
Check: Results that depend on the computer
format(), formatC() and sprintf() change the print, not the value
x <- 1234567.891 prints as 1234568, because R shows 7 significant digits by default. Printing is a view, never a
change: format(), formatC() and sprintf() return text for display, and options(digits = 4) changes how
numbers print, while x itself keeps every digit it had.
from session import run, version
version()
# R reads formatting.R as if each line were typed at the console.
run("""
R --vanilla --quiet -f formatting.R
""") Output
# R version 4.6.1 -- "Happy Hop"
$ R --vanilla --quiet -f formatting.R
> x <- 1234567.891
> # Printing shows 7 significant digits by default.
> x
[1] 1234568
> # format(), formatC() and sprintf() return text.
> format(x, nsmall = 2)
[1] "1234567.89"
> formatC(x, format = "f", digits = 2, big.mark = ",")
[1] "1,234,567.89"
> sprintf("%.2f", x)
[1] "1234567.89"
> # options(digits =) changes printing, nothing else.
> options(digits = 4)
> 1 / 3
[1] 0.3333
> options(digits = 7)
> # The stored value never changed.
> x == 1234567.891
[1] TRUE
> # An amount of 1,000.00 in its smallest unit, split three ways.
> total <- 100000L
> share <- rep(total %/% 3L, 3)
> extra <- seq_len(total %% 3L)
> share[extra] <- share[extra] + 1L
> share
[1] 33334 33333 33333
> sum(share) == total
[1] TRUE
> sprintf("%.2f", share / 100)
[1] "333.34" "333.33" "333.33"
> # Rounding each share instead loses one unit.
> naive <- rep(round(1000 / 3, 2), 3)
> naive
[1] 333.33 333.33 333.33
> sum(naive)
[1] 999.99
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 formatting_session.py
format(x, nsmall = 2)keeps at least 2 decimals,"1234567.89".formatC(x, format = "f", digits = 2, big.mark = ",")adds thousands separators,"1,234,567.89".sprintf("%.2f", x)uses C’s formats, the same as in many other languages,"1234567.89".options(digits = 4)changes every later print, here1 / 3as 0.3333, until you set it back.
The second half of the run shows how to keep money exact. An amount of 1,000.00 held as 100000 of its smallest unit,
an integer, splits three ways with integer division, %/%, and the remainder, %%, goes to the first share: 333.34,
333.33 and 333.33, which add up exactly. Rounding each share to two decimals gives 333.33 three times and loses one
unit. Keep amounts in whole minor units, compute with them, and format them only for display.
Paise, and the Indian digit grouping
The same split works in paise: ₹1,000 is 100000 paise. formatC(big.mark = ",") groups digits in threes, the
international way, so 1234567.89 prints as 1,234,567.89. The Indian grouping writes it as 12,34,567.89, which
format() and formatC() cannot produce, since their big.mark separates digits at one fixed interval, so build
that string yourself, or leave the display to a tool that knows it.
Checkpoint
Check: Formatting without changing values
Practise: predict, then check
The last set of lines mixes binary fractions, halves, integer limits and printing. Commit to an answer for each.
Predict the six results
With x <- 0.3 - 0.1, predict x == 0.2 and isTRUE(all.equal(x, 0.2)); then round(c(1.5, 2.5, 3.5)),
round(1.005, 2), .Machine$integer.max + 1 and format(2/3, digits = 3). Then run them and compare.
from session import run, version
version()
# R reads practice.R as if each line were typed at the console.
run("""
R --vanilla --quiet -f practice.R
""") Output
# R version 4.6.1 -- "Happy Hop" $ R --vanilla --quiet -f practice.R > x <- 0.3 - 0.1 > x == 0.2 [1] FALSE > isTRUE(all.equal(x, 0.2)) [1] TRUE > round(c(1.5, 2.5, 3.5)) [1] 2 2 4 > round(1.005, 2) [1] 1 > .Machine$integer.max + 1 [1] 2147483648 > format(2/3, digits = 3) [1] "0.667"
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 practice_session.py
0.3 - 0.1 misses 0.2 for the same reason 0.1 + 0.2 misses 0.3, and all.equal() accepts it; the halves went to the
even digits 2, 2 and 4; 1.005 is stored just below 1.005, so it rounds to 1; adding a double 1 to the largest
integer gives a double, with no overflow; and format() rounded the printed text, not the value.
Last, the ideas of this page meet in one adaptive round: the weakest idea is asked first, each answer comes with its reason, and a question you miss returns once more before the summary:
Practice that adapts to you
Practise this lesson
14 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up: why is 0.1 + 0.2 == 0.3 FALSE in R, and how should you compare such numbers?
R keeps a number such as 0.1 as a double, a binary fraction with 53 bits of precision. None of 0.1, 0.2 and 0.3 can
be written as such a fraction, so R keeps the closest double to each, and the three small errors do not cancel:
0.1 + 0.2 ends up as 0.30000000000000004441 and 0.3 as 0.29999999999999998890, neighbouring doubles about 5.55e-17
apart. ==
compares the stored values, so it says FALSE, while printing to 7 digits shows both as 0.3. Compare with a tolerance
instead: isTRUE(all.equal(a, b)), which accepts relative differences below about 1.5e-8, or abs(a - b) < tol with
a tol that suits the scale of the data. The same applies in every language that uses IEEE 754 doubles; it is a
property of binary floating point, not of R.
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Fresher to mid
An invoice of 1,000.00 is split equally among three people with round(1000 / 3, 2) each, and the shares add up to 999.99. Explain what went wrong, and how you would compute shares that always add up to the total, for any amount and any number of people.
-
Mid to senior
A data check, stopifnot(sum(weights) == 1), passes on a colleague's x86-64 Linux server but stops on your Apple-silicon Mac with the same weights. Explain why the two computers disagree, and how you would write the check so that it gives the same verdict everywhere.
Key takeaways
- A double is a binary fraction with 53 bits of precision, so most decimals are stored inexactly and
0.1 + 0.2 == 0.3isFALSE; compare withisTRUE(all.equal(x, y))orabs(x - y) < tol. round()sends a true half to the even digit,round(2.5)is 2, while a decimal like 2.675 rounds by its stored value, soround(2.675, 2)is 2.67.- Integers stop at 2,147,483,647 and overflow to
NAwith a warning, while doubles count whole numbers exactly only up to 2^53. - The last bits can differ between computers, as
sum()with and without a long double shows, so tests must never depend on them. format(),formatC(),sprintf()andoptions(digits)change only how numbers print; keep money in whole minor units and format it at the end.
Terms in this lesson
- double
- R's type for real numbers: a binary fraction with 53 bits of precision, about 15 significant decimal digits, in the IEC 60559 (IEEE 754) format. Most decimals, such as 0.1, are stored as the nearest such fraction.
References
- Double-Precision Vectors (double): IEC 60559 values (The R Core Team)
- Rounding of Numbers (Round) (The R Core Team)
- Test if Two Objects are (Nearly) Equal (all.equal) (The R Core Team)
- Numerical Characteristics of the Machine (.Machine) (The R Core Team)
- Integer Vectors (integer) (The R Core Team)
- Sum of Vector Elements (sum) (The R Core Team)
- Report Capabilities of this Build of R (capabilities) (The R Core Team)
- Writing R Extensions: Writing portable packages (long doubles) (The R Core Team)
- Formatting Using C-style Formats (formatC) (The R Core Team)
- Use C-style String Formatting Commands (sprintf) (The R Core Team)
- Encode in a Common Format (format) (The R Core Team)
- R News: changes in R 4.6.0 (exact math near special values) (The R Core Team)
- R Blog: Debugging sensitivity to the C math library on Linux (The R Core Team)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress