PHP from the ground up

Numbers and money

Integer and float arithmetic, division that stays whole, powers and rounding, and the function that turns a raw number into money people can read.

Part 4 of 13

Beginner14 min readBack to Courses

Two kinds of number

PHP has integers and floats, and the difference is not cosmetic: intdiv(7, 2) is 3 because that is what whole-number division means, while 7 / 2 is 3.5 because the slash always produces a float when the numbers do not divide evenly. The modulo operator % answers the other half of the question — the remainder — which is how you tell even from odd.

Integers on a 64-bit machine hold around nine and a half quintillion; past that, PHP silently moves to float. For ordinary work this never comes up, but it is why a stray enormous number can start printing in scientific notation.

Two details catch people out. % follows the sign of the dividend, so -7 % 3 is -1 rather than 2; when you need a positive remainder, add the divisor and take the remainder again. And ** binds tighter than a leading minus, so -2 ** 2 is -4, not 4 — parentheses are the way to say which one you meant.

Rounding, on purpose

round($price, 2) rounds to two decimal places, floor() always goes down, ceil() always goes up, and they are not interchangeable: a shop that rounds a total up takes money it did not earn, and one that floors a shelf of half-items loses count. Decide which meaning you want, then pick the function.

Floats cannot store every decimal exactly — 0.1 + 0.2 is famously not 0.3 — so money in serious systems is often handled in the smallest unit (cents, as integers) and converted only for display. number_format($amount, 2) is that display: thousands separators, always two decimal places.

Money in cents

The safest way to handle money in PHP is to stop treating it as money. Store an amount as an integer number of cents, add and subtract those integers, and divide by 100 only when you print. 19.99 becomes 1999; two of them are 3998; number_format(3998 / 100, 2) prints 39.98. No binary fraction is involved until the final division, and that division is only for display.

A percentage needs one rounding decision, made explicitly. A 15 percent tip on 8475 cents is 8475 × 0.15 — a float — so round it to the nearest cent with (int) round(...) before adding it. Rounding at the end of a long chain of floats lets the error accumulate; rounding once, at the point the value becomes cents, keeps every later sum exact.

A little randomness, safely

random_int(1, 6) gives a fair die and is the function to reach for when the result should be hard to predict; the older rand() is not suitable for anything security-shaped. 2 ** 10 is 1024 — the power operator, not a caret — and is_int(), is_float() ask the type questions when a value has travelled a long way to reach you.

Precedence is ordinary arithmetic: multiplication and division first, then addition and subtraction, with parentheses available to say what you mean. When a formula matters, wrap it in parentheses anyway — the next reader will thank you, and so will you after the third edit.

Division, rounding, formatting

<?php
$price = 19.99;
$quantity = 63;
$total = $price * $quantity;              // 1259.37

echo 'Raw: ' . $total . PHP_EOL;
echo 'Whole: ' . intdiv((int) $total, 100) . PHP_EOL;
echo 'Money: ' . number_format($total, 2) . PHP_EOL;
echo 'Euro symbol: ' . number_format($total, 2, ',', '.') . PHP_EOL;

// Remainder: splitting 23 slices among 7 people.
$slices = 23;
$people = 7;
echo "Each gets " . intdiv($slices, $people) . ', left over ' . ($slices % $people) . PHP_EOL;

// Powers and rounding.
echo 'Two to the ten: ' . 2 ** 10 . PHP_EOL;
echo 'Down: ' . floor(4.9) . ' Up: ' . ceil(4.1) . ' Nearest: ' . round(4.5) . PHP_EOL;
echo 'A die: ' . random_int(1, 6) . PHP_EOL;

A till that adds up

Run it and read the formatting choices. Then add a 10 percent discount as a float, print the discounted total as money, and decide for yourself whether round or floor is the honest one for the shop.

Output
Click "Run" to execute your code

Never compare two floats with === — 0.1 + 0.2 is not 0.3 and the comparison will fail. Compare the difference against a small tolerance, or keep money in whole cents. This is not a PHP flaw; it is how every language stores binary fractions.

What a number can hold

An integer is exact until it reaches PHP_INT_MAX, and then PHP quietly switches the result to a float. That is convenient for a calculator and dangerous for an identifier: a number too large to fit loses precision without an error. is_int() tells you which side of the line a value is on, and PHP_INT_MAX is the boundary on the machine running the script.

A float is stored as a binary fraction, so most decimal prices are already an approximation before any arithmetic happens. round() returns a float, number_format() returns a string, and the difference matters: strings are for display, floats are for further sums. When the sums have to be exact — money is the usual case — keep whole cents in integers and divide by 100 only at the edge.

Three number habits prevent quiet errors: never compare floats with ===, never run arithmetic on a number_format() result, and never assume / gives an integer. Use a tolerance or integer cents for comparison, format only when printing, and reach for intdiv() when the whole part is what you mean.

Try these next

  • Split 17 sweets among 5 people and print each share and the remainder.
  • Compute a 15 percent tip on 84.60 and format it to two decimal places.
  • Convert 1,234,567 cents to a money string without ever putting the cents into a float.

A bill split in cents

Run it and check the arithmetic by hand. Then change the tip to 10 percent, split the bill among five people, and print the new remainder.

Output
Click "Run" to execute your code

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

What are 7 / 2 and intdiv(7, 2)?