skip to content

Numbers & Precision

PHP integers overflow to float at PHP_INT_MAX, floats lose precision, and round() has several modes. Interviewers ask how you compare floats and when to reach for bcmath or gmp.

part ofPHPoverview, primer and where to startread it →
on this pageshow

explore

questions

5

In PHP, why is 0.1 + 0.2 == 0.3 false, and how should you compare two floats?

level: juniorimportance: must knowfreq 64%

answer

  1. IEEE 754 binary double
  2. 0.1 has no exact binary form
  3. echo shows 0.3, var_dump does not
  4. abs difference below a tolerance
  5. PHP_FLOAT_EPSILON is relative to 1.0

basics

~20 s

PHP floats are IEEE 754 binary doubles, and 0.1, 0.2 and 0.3 have no exact binary form, so the sum is 0.30000000000000004. Compare floats with a tolerance, abs($a - $b) < $epsilon, or avoid floats where exact decimals matter.

solid answer

~40 s

A PHP `float` is an **IEEE 754 double**: a binary fraction with about 15-17 significant decimal digits. Decimal values like `0.1` and `0.2` are repeating fractions in binary, so each is stored slightly off, and the errors add up: `0.1 + 0.2` is `0.30000000000000004`, which is not the double nearest to `0.3`. `echo` hides it because it prints with 14 significant digits, while `var_dump()` prints the shortest exact round-trip form. The fix is to compare with a tolerance, `abs($a - $b) < $epsilon`, choosing the tolerance for the domain; `PHP_FLOAT_EPSILON` is the gap between `1.0` and the next float, so it only suits values near 1. Where exact decimals matter, such as money, avoid floats altogether and use integers or bcmath.

code

php · 9 lines
php
<?php
$a = 0.1 + 0.2;

var_dump($a);                        // float(0.30000000000000004)
var_dump($a == 0.3);                 // bool(false)
var_dump(abs($a - 0.3) < 1e-9);      // bool(true)

var_dump(floor((0.1 + 0.7) * 10));   // float(7), not 8
var_dump(PHP_FLOAT_EPSILON);         // float(2.220446049250313E-16)

go deeper

for a junior

Recall that floats are binary approximations, that 0.1 + 0.2 is not exactly 0.3, and that floats are compared with a tolerance, not with ==.

for a middle

Explain why decimal fractions are inexact in binary, why echo and var_dump print differently, and why PHP_FLOAT_EPSILON only fits values near 1.

for a senior

Pick tolerances per domain, reject NAN and INF at boundaries, and keep floats out of money and exact-equality logic entirely.

for a principal

Set numeric representation rules for a codebase: which domains may use floats, which must use integers or decimal libraries, and how comparisons are written.

## Why the sum is not 0.3 PHP's `float` type is almost always an **IEEE 754 double-precision** number: 64 bits holding a sign, an exponent and a 52-bit binary fraction. That gives roughly 15-17 significant decimal digits, and a maximum relative rounding error around `1.11e-16`, as the manual puts it. The catch is the word **binary**. Only fractions whose denominator is a power of two are exact: `0.5`, `0.25`, `0.375`. A decimal like `0.1` is `1/10`, and in base 2 it repeats forever, just as `1/3` repeats in base 10. PHP stores the nearest double, which is a little above `0.1`. The same happens to `0.2` and `0.3`, each with its own small error. Adding `0.1` and `0.2` combines two errors, and the result lands on a different double from the one nearest to `0.3`: ```php <?php var_dump(0.1 + 0.2); // float(0.30000000000000004) var_dump(0.1 + 0.2 == 0.3); // bool(false) echo 0.1 + 0.2, PHP_EOL; // 0.3 ``` ## Why echo lies and var_dump does not The two outputs above disagree because PHP formats floats in two ways: - `echo`, string interpolation and `(string)` use the `precision` setting, **14** significant digits by default, so the tiny error is rounded away and you see `0.3`. - `var_dump()`, `var_export()` and `json_encode()` use the shortest representation that converts back to exactly the same double, so the error is visible. When debugging a float, always look at `var_dump()` output; what `echo` prints is not the stored value. ## Comparing floats safely Do not compare floats with `==` or `===` after arithmetic. Compare the **difference** against a tolerance: ```php <?php function nearlyEqual(float $a, float $b, float $tolerance): bool { return abs($a - $b) < $tolerance; } var_dump(nearlyEqual(0.1 + 0.2, 0.3, 1e-9)); // bool(true) ``` Choosing the tolerance is the real work: | Choice | When it works | When it fails | |---|---|---| | fixed absolute tolerance, e.g. `1e-9` | values of known, moderate size | very large values (error exceeds it) or very small ones (tolerance swamps them) | | `PHP_FLOAT_EPSILON` as absolute tolerance | values near `1.0` | almost everywhere else | | relative tolerance, `abs($a - $b) <= $rel * max(abs($a), abs($b))` | values of any magnitude | values near zero, which need an absolute floor too | `PHP_FLOAT_EPSILON` (PHP 7.2+) is the smallest positive number such that `1.0 + PHP_FLOAT_EPSILON != 1.0`, about `2.22e-16`. It describes the spacing of floats **at 1.0**. At `1e6` the spacing is a million times larger, so using the constant directly as a tolerance there is the same as using `==`. ## Special values Floats also include values that break ordinary comparison: - `NAN` (not a number), produced by operations like `fmod(1, 0)`, is not equal to anything, **including itself**. Test it with `is_nan()`. - `INF` and `-INF` come from overflowing the float range or from `fdiv(1, 0)`. Test with `is_infinite()`, or `is_finite()` for "an ordinary number". ## When not to use floats at all Floats are right for measurements, statistics and geometry, where a relative error in the 16th digit is irrelevant. They are wrong wherever a decimal must be **exact**: 1. **Money.** Cents drift when prices are summed or multiplied by tax rates; store integer minor units or use bcmath. 2. **Identifiers and counters** that exceed 2^53, beyond which a float cannot represent every integer. 3. **Equality-driven logic**, such as "is the balance exactly zero?", which a tolerance makes fuzzy. The manual's own warning sums it up: never trust floating-point results to the last digit, and do not compare floats directly for equality. ## Common mistakes - **Switching to `===`.** Strict comparison checks type as well as value, but two floats of the same type still differ in the last bit; it fixes nothing here. - **Rounding both sides before `==`.** `round($a, 2) == round($b, 2)` works most of the time but fails when the two values straddle a rounding boundary, such as `0.004999` and `0.005001`. - **Debugging with `echo`.** The 14-digit output hides exactly the error being investigated. - **Casting to `int` to "clean up".** `(int)` truncates, so a value stored as `7.9999999999999991` becomes `7`.

  • In PHP, why does floor((0.1 + 0.7) * 10) return 7 instead of 8?
    Because `0.1 + 0.7` is stored as a double slightly below `0.8`, so multiplying by 10 gives about `7.9999999999999991`, and `floor()` cuts it to `7`. The error is invisible with `echo`, which rounds to 14 digits. Rounding with `round()` before truncating, or doing the arithmetic in integers, avoids it.
  • In PHP, how do you test whether a float result is NAN?
    Use `is_nan($x)`. `NAN` compares unequal to every value, itself included, so `$x == NAN` and `$x === NAN` are always false. `is_finite()` is the broader check when both `NAN` and the infinities must be rejected.

Storing 0.1 as a float is like writing one third as 0.333 on a receipt: each entry is close, but add three of them and you get 0.999, not 1, so you check whether the total is within a small margin rather than exactly equal.

saying these in an interview costs you the question

  • Says 0.1 + 0.2 == 0.3 is false because of a bug in PHP.
  • Trusts echo output to show the exact stored float value.
  • Uses PHP_FLOAT_EPSILON as the tolerance for any magnitude of value.
  • Believes === compares floats more safely than == after arithmetic.
  • Checks for NAN with $x === NAN.
open as a page

In PHP, what happens when an integer calculation exceeds PHP_INT_MAX, and how do you work with integers that large?

level: middleimportance: should knowfreq 40%

basics

~20 s

PHP does not throw or wrap on integer overflow: the result silently becomes a float. On 64-bit builds PHP_INT_MAX is 9223372036854775807, and floats above 2^53 cannot represent every integer, so precision is lost; use GMP or bcmath strings for larger values.

open as a page

In PHP 8.4+, how does round() handle halves by default, and what does the RoundingMode enum add?

level: middleimportance: should knowfreq 30%

basics

~20 s

round() rounds halves away from zero by default, so round(2.5) is 3.0 and round(-2.5) is -3.0, and it always returns a float. PHP 8.4 added the RoundingMode enum: the four half modes plus TowardsZero, AwayFromZero, PositiveInfinity and NegativeInfinity.

open as a page

A PHP point-of-sale system's float currency totals drift by a cent; what causes it, and how do you fix the money arithmetic?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Floats cannot store most decimal prices exactly, so sums, tax multiplications and (int) conversions drift by a cent. Keep money as integer cents or as bcmath decimal strings (BcMath\Number since PHP 8.4), and round once, at a defined step, with an explicit RoundingMode.

open as a page

In PHP, how do intdiv(), the % operator, fmod() and fdiv() differ in result type, sign and division by zero?

level: middleimportance: nice to knowfreq 24%

basics

~20 s

intdiv() and % work on ints, truncate toward zero and throw DivisionByZeroError for a zero divisor. fmod() gives a float remainder and returns NAN for zero. fdiv() (PHP 8.0) performs IEEE float division, returning INF, -INF or NAN instead of throwing.

open as a page