Math #
PHP has a rich math library — from basic operations like abs() and round(), to trigonometric functions, logarithms, and two high-precision libraries: BCMath for arbitrary-precision decimals and GMP for very large integers. What PHP developers often overlook is floating-point precision — 0.1 + 0.2 !== 0.3 isn’t a PHP bug, it’s a fundamental property of the IEEE 754 representation that applies in every language. Knowing when to use regular floats and when to switch to BCMath is the difference between a correct financial application and one with subtle, very expensive bugs.
Basic Operations #
<?php
// Absolute value
echo abs(-42); // 42
echo abs(-3.14); // 3.14
echo abs(42); // 42
// Modulo — already covered in operators, but fmod() exists for floats
echo 10 % 3; // 1 (integer modulo)
echo fmod(10.5, 3); // 1.5 (float modulo)
echo fmod(-10, 3); // -1 (sign follows the dividend)
// Integer division (PHP 7+)
echo intdiv(17, 5); // 3 (rounded down)
echo intdiv(-17, 5); // -3 (not -4!)
// Powers
echo pow(2, 10); // 1024
echo 2 ** 10; // 1024 (operator, more idiomatic)
echo sqrt(144); // 12.0
echo sqrt(2); // 1.4142135623731
// Logarithms
echo log(M_E); // 1.0 — natural log
echo log(100, 10); // 2.0 — log base 10
echo log10(1000); // 3.0 — log base 10 shorthand
echo log(8, 2); // 3.0 — log base 2
echo log2(8); // 3.0 — log base 2 shorthand (PHP 5.6+)
// Min and max
echo min(3, 1, 4, 1, 5, 9, 2, 6); // 1
echo max(3, 1, 4, 1, 5, 9, 2, 6); // 9
// From arrays
$numbers = [5, 3, 8, 1, 9, 2, 7];
echo min($numbers); // 1
echo max($numbers); // 9
// Array sums
echo array_sum($numbers); // 35
echo array_product([1,2,3,4,5]); // 120 (1×2×3×4×5)
Rounding #
PHP has four different rounding strategies — choosing the wrong one can produce inaccurate calculations:
<?php
// round() — round to a specific number of decimals
echo round(3.4); // 3.0 — down (< 0.5)
echo round(3.5); // 4.0 — up (>= 0.5)
echo round(3.55, 1); // 3.6
echo round(3.45, 1); // 3.5 — can surprise because 3.45 is actually 3.4499...
// Decimal precision
echo round(1234.5678, 2); // 1234.57
echo round(1234.5678, -2); // 1200.0 — round to hundreds
// Rounding modes
echo round(2.5, 0, PHP_ROUND_HALF_UP); // 3 — standard (default)
echo round(2.5, 0, PHP_ROUND_HALF_DOWN); // 2 — down at exactly 0.5
echo round(2.5, 0, PHP_ROUND_HALF_EVEN); // 2 — banker's rounding (to even)
echo round(3.5, 0, PHP_ROUND_HALF_EVEN); // 4
echo round(2.5, 0, PHP_ROUND_HALF_ODD); // 3 — to odd
// ceil() — always up (ceiling)
echo ceil(4.1); // 5.0
echo ceil(4.9); // 5.0
echo ceil(-4.1); // -4.0 (up = toward 0 for negatives)
echo ceil(-4.9); // -4.0
// floor() — always down (floor)
echo floor(4.9); // 4.0
echo floor(4.1); // 4.0
echo floor(-4.1); // -5.0 (down = away from 0 for negatives)
echo floor(-4.9); // -5.0
// intval() / (int) — truncate (cut off, not round)
echo (int) 4.9; // 4 (decimal cut off)
echo (int) -4.9; // -4 (cut toward 0)
// Difference table for negative values
$value = -4.5;
echo round($value); // -5.0 (round half away from zero)
echo ceil($value); // -4.0 (up = toward 0)
echo floor($value); // -5.0 (down = away from 0)
echo (int) $value; // -4 (truncate toward 0)
Floating-Point Precision Traps #
<?php
// The famous IEEE 754 precision problem
var_dump(0.1 + 0.2 == 0.3); // bool(false) — surprising!
var_dump(0.1 + 0.2); // float(0.30000000000000004)
// This isn't a PHP bug — every language using IEEE 754 has this problem
// Solution 1: use an epsilon for float comparisons
function floatEqual(float $a, float $b, float $epsilon = PHP_FLOAT_EPSILON): bool
{
return abs($a - $b) < $epsilon;
}
var_dump(floatEqual(0.1 + 0.2, 0.3)); // bool(true)
// Solution 2: round before comparing
var_dump(round(0.1 + 0.2, 10) === round(0.3, 10)); // bool(true)
// Solution 3: use BCMath for important calculations
echo bcadd('0.1', '0.2', 10); // "0.3000000000" — perfect precision
// A real case that often happens
$price = 19.99;
$qty = 3;
echo $price * $qty; // 59.97 — correct by coincidence
echo $price * $qty == 59.97; // might be false!
// Safe: BCMath or integer cents
$priceCents = 1999; // 19.99 in cents
$total = $priceCents * $qty; // 5997 cents = 59.97 — always exact
// PHP_FLOAT_EPSILON — the smallest value such that 1.0 + EPSILON !== 1.0
echo PHP_FLOAT_EPSILON; // 2.2204460492503E-16
Random Numbers #
PHP has two kinds of random functions: regular ones (for simulations/games) and cryptographic ones (for security tokens):
<?php
// rand() and mt_rand() — NOT for security purposes!
echo rand(); // random integer between 0 and getrandmax()
echo rand(1, 100); // random integer between 1 and 100
echo mt_rand(1, 100); // faster with a better distribution
// random_int() — cryptographic, for security purposes
// Use this for tokens, OTPs, password resets, etc.
echo random_int(100000, 999999); // a secure 6-digit OTP
echo random_int(1, PHP_INT_MAX); // a secure random integer
// random_bytes() — random bytes for tokens
$token = bin2hex(random_bytes(32)); // 64 hex characters
// Useful for: CSRF tokens, session tokens, password reset links
// Shuffle an array with good distribution
$cards = range(1, 52);
shuffle($cards); // in-place, uses a good RNG
// array_rand — pick random keys from an array
$fruits = ['apple', 'mango', 'orange', 'grape'];
$key = array_rand($fruits); // one random key
$two = array_rand($fruits, 2); // two random keys
// Random float from 0.0 to 1.0
echo mt_rand() / mt_getrandmax(); // float between 0.0 and 1.0
// Random float within a range
function randomFloat(float $min, float $max): float
{
return $min + mt_rand() / mt_getrandmax() * ($max - $min);
}
echo randomFloat(1.5, 9.9); // float between 1.5 and 9.9
Trigonometry #
<?php
// All trig functions use RADIANS, not degrees
$degrees = 45;
$radians = deg2rad($degrees); // convert degrees to radians
echo sin(deg2rad(30)); // 0.5 — sin 30°
echo cos(deg2rad(60)); // 0.5 — cos 60°
echo tan(deg2rad(45)); // 1.0 — tan 45°
// Inverse
echo rad2deg(asin(0.5)); // 30.0 — arcsin(0.5) = 30°
echo rad2deg(acos(0.5)); // 60.0 — arccos(0.5) = 60°
echo rad2deg(atan(1.0)); // 45.0 — arctan(1) = 45°
// atan2 — two-argument arctan (more robust than atan)
$y = 1.0; $x = 1.0;
echo rad2deg(atan2($y, $x)); // 45.0
// Hyperbolic
echo sinh(1); // 1.1752011936438
echo cosh(1); // 1.5430806348152
echo tanh(1); // 0.76159415595576
// Calculate the distance between two coordinate points (Euclidean)
function euclideanDistance(float $x1, float $y1, float $x2, float $y2): float
{
return sqrt(($x2 - $x1) ** 2 + ($y2 - $y1) ** 2);
}
// Calculate the distance between two GPS coordinates (Haversine formula)
function haversineDistance(float $lat1, float $lng1, float $lat2, float $lng2): float
{
$r = 6371; // Earth's radius in km
$dLat = deg2rad($lat2 - $lat1);
$dLng = deg2rad($lng2 - $lng1);
$a = sin($dLat / 2) ** 2
+ cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * sin($dLng / 2) ** 2;
return $r * 2 * atan2(sqrt($a), sqrt(1 - $a));
}
$jakarta = [-6.2088, 106.8456];
$surabaya = [-7.2575, 112.7521];
echo round(haversineDistance(...$jakarta, ...$surabaya)) . " km\n"; // ~666 km
Math Constants #
<?php
echo M_PI; // 3.1415926535898 — π
echo M_E; // 2.718281828459 — Euler's number (e)
echo M_LOG2E; // 1.4426950408890 — log₂(e)
echo M_LOG10E; // 0.43429448190325 — log₁₀(e)
echo M_LN2; // 0.69314718055995 — ln(2)
echo M_LN10; // 2.302585092994 — ln(10)
echo M_SQRT2; // 1.4142135623731 — √2
echo M_SQRT3; // 1.7320508075689 — √3
echo M_1_PI; // 0.31830988618379 — 1/π
echo M_2_PI; // 0.63661977236758 — 2/π
echo M_SQRT1_2; // 0.70710678118655 — 1/√2
echo PHP_INT_MAX; // 9223372036854775807
echo PHP_INT_MIN; // -9223372036854775808
echo PHP_INT_SIZE; // 8 (bytes)
echo PHP_FLOAT_MAX; // 1.7976931348623E+308
echo PHP_FLOAT_MIN; // 2.2250738585072E-308
echo PHP_FLOAT_EPSILON;// 2.2204460492503E-16
echo INF; // INF
echo NAN; // NAN
echo -INF; // -INF
// Check special values
var_dump(is_finite(42.0)); // bool(true)
var_dump(is_infinite(INF)); // bool(true)
var_dump(is_nan(NAN)); // bool(true)
var_dump(is_nan(sqrt(-1))); // bool(true)
var_dump(is_numeric("42")); // bool(true)
var_dump(is_numeric("42.5")); // bool(true)
var_dump(is_numeric("0x1A")); // bool(false) in PHP 7+
BCMath — Arbitrary Precision #
BCMath (Binary Calculator) is a library for decimal calculations with arbitrarily selectable precision — a must for financial applications:
<?php
// Arguments are STRINGS for full precision
// The third argument is the number of decimals in the result
// Basic BCMath operations
echo bcadd('10.1', '20.2', 2); // "30.30" — addition
echo bcsub('100.00', '30.75', 2); // "69.25" — subtraction
echo bcmul('19.99', '3', 2); // "59.97" — multiplication
echo bcdiv('100.00', '3', 10); // "33.3333333333" — division
echo bcmod('100', '7'); // "2" — modulo (integers only)
echo bcpow('2', '32', 0); // "4294967296" — powers
echo bcsqrt('2', 10); // "1.4142135624" — square root
// bccomp — comparison (returns -1, 0, or 1)
echo bccomp('10.5', '10.50', 2); // 0 — equal
echo bccomp('10.6', '10.5', 1); // 1 — greater
echo bccomp('10.4', '10.5', 1); // -1 — smaller
// Set the default precision
bcscale(4); // all subsequent BC operations use 4 decimals
echo bcadd('1', '2'); // "3.0000"
// Real example: discount and VAT calculations
function calculateTotal(string $unitPrice, string $qty, string $discount, string $vat = '0.11'): array
{
$subtotal = bcmul($unitPrice, $qty, 4);
$deduction = bcmul($subtotal, $discount, 4);
$afterDiscount = bcsub($subtotal, $deduction, 4);
$tax = bcmul($afterDiscount, $vat, 4);
$total = bcadd($afterDiscount, $tax, 4);
return [
'subtotal' => $subtotal,
'deduction' => $deduction,
'after_discount' => $afterDiscount,
'tax' => $tax,
'total' => $total,
];
}
$result = calculateTotal('19999.99', '3', '0.10');
// subtotal: 59999.9700
// deduction: 5999.9970
// after_discount: 53999.9730
// tax: 5939.9970
// total: 59939.9700
// Format the output
function formatRupiah(string $value, int $decimals = 0): string
{
// Convert the BC string to a float for number_format (safe because it's only for display)
return 'Rp ' . number_format((float) $value, $decimals, ',', '.');
}
echo formatRupiah($result['total']); // "Rp 59.940"
GMP — Very Large Integers #
GMP (GNU Multiple Precision) handles integers beyond PHP_INT_MAX:
<?php
// Large factorials — PHP_INT_MAX isn't enough
function factorialGmp(int $n): \GMP
{
$result = gmp_init(1);
for ($i = 2; $i <= $n; $i++) {
$result = gmp_mul($result, gmp_init($i));
}
return $result;
}
$factorial100 = factorialGmp(100);
echo gmp_strval($factorial100);
// 93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000
// GMP operations
$a = gmp_init('123456789012345678901234567890');
$b = gmp_init('987654321098765432109876543210');
$sum = gmp_add($a, $b);
$diff = gmp_sub($b, $a);
$product = gmp_mul($a, $b);
$quotient = gmp_div_q($b, $a); // quotient
$remainder = gmp_mod($b, $a); // modulo
echo gmp_strval($sum); // "1111111110111111111011111111100"
// Comparison
$cmp = gmp_cmp($a, $b); // -1 (a < b)
// GMP is useful for cryptography — RSA and Diffie-Hellman
// Modular exponentiation (used in RSA)
$base = gmp_init('2');
$exp = gmp_init('100');
$mod = gmp_init('1000000007'); // a large prime number
$result = gmp_powm($base, $exp, $mod); // (2^100) mod 1000000007
echo gmp_strval($result); // "976371285"
// GCD (Greatest Common Divisor)
$gcd = gmp_gcd(gmp_init(48), gmp_init(18));
echo gmp_strval($gcd); // "6"
// Check whether a number is prime
$p = gmp_init('104729'); // a prime number
echo gmp_prob_prime($p) > 0 ? "probably prime" : "definitely not prime";
// Value 0: not prime, 1: probably prime, 2: definitely prime
Correct Financial Calculation Patterns #
<?php
// ANTI-PATTERN: using floats for money
$price = 19.99;
$qty = 100;
$total = $price * $qty; // might not be exactly 1999.00
$vat = $total * 0.11;
$totalVat = $total + $vat; // might have small precision errors
// CORRECT Option 1: Store in the smallest unit (cents/whole rupiah)
$priceCents = 1999; // 19.99 in cents
$qty = 100;
$total = $priceCents * $qty; // 199900 cents = 1999.00
$vat = intdiv($total * 11, 100); // 21989 cents = 219.89
$totalVat = $total + $vat; // 221889 cents = 2218.89
// Display
echo number_format($totalVat / 100, 2, ',', '.'); // "2.218,89"
// CORRECT Option 2: BCMath for calculations
$price = '19.99';
$qty = '100';
$total = bcmul($price, $qty, 4); // "1999.0000"
$vat = bcmul($total, '0.11', 4); // "219.8900"
$totalVat = bcadd($total, $vat, 2); // "2218.89"
echo 'Rp ' . number_format((float) $totalVat, 2, ',', '.'); // "Rp 2.218,89"
// Financial rounding — PHP_ROUND_HALF_UP for consistency
$value = 2218.885;
$rounded = round($value, 2, PHP_ROUND_HALF_UP); // 2218.89
$bcRounded = bcadd(bcsub(bcadd($value, '0.005', 3), '0', 2), '0', 2); // via BCMath
Summary #
0.1 + 0.2 !== 0.3— this isn’t a bug, it’s the nature of IEEE 754. Use an epsilon (PHP_FLOAT_EPSILON) for float comparisons, or avoid floats for important values.- BCMath for finance —
bcadd,bcsub,bcmul,bcdivwith string arguments give exact decimal precision without floating-point errors.- Store money as integers (cents/whole rupiah without decimals) —
$price = 1999(not19.99) is the safest and most efficient approach for monetary calculations.random_int()andrandom_bytes()for security — never userand()ormt_rand()for tokens, OTPs, or password resets. Neither is cryptographically secure.round()has four modes —PHP_ROUND_HALF_EVEN(banker’s rounding) reduces accumulation bias in calculations involving many roundings.- All trigonometric functions use radians — use
deg2rad()andrad2deg()for conversion.atan2($y, $x)is more robust thanatan($y/$x)because it handles all quadrants.- GMP for numbers above PHP_INT_MAX — cryptography, large factorials, and number-theory operations need precision beyond 64-bit integers.
intdiv()for safe integer division (PHP 7+) — it doesn’t produce a float like the/operator, and its intent is clearer than(int)($a / $b).