Avoid Floating‑Point Errors in PHP Money Calculations with TypePHP’s Decimal Types

This article explains how PHP's floating‑point precision pitfalls can be eliminated by using TypePHP's built‑in high‑precision types—Decimal, BigInt, and BigFloat—through a complete order‑total example, detailed type‑system overview, inference rules, conversion methods, and performance‑critical tips.

Open Source Tech Hub
Open Source Tech Hub
Open Source Tech Hub
Avoid Floating‑Point Errors in PHP Money Calculations with TypePHP’s Decimal Types

01. Practical Example: Order Amount Calculator

First, the full code for order.php demonstrates calculating an order total where price, quantity, discount, and tax are handled without losing precision. The file declares strict_types=1 and uses decimal_types, so all floating‑point literals become Decimal objects.

<?php
// order.php
declare(strict_types=1);
use decimal_types;   // all float literals become Decimal

function main(): void {
    $price = 19.9;            // Decimal, not float!
    $qty = 3;
    $discount = 0.85;         // Decimal

    $total = $price * $qty * $discount;
    $tax = $total * 0.06;

    echo "小计: " . $total->toString() . "
";
    echo "税费: " . $tax->toString() . "
";
    echo "应付: " . ($total + $tax)->toString() . "
";
}

Compile and run:

tpc order.php -O2 -o order
./order

Expected output:

小计: 50.745
税费: 3.0447
应付: 53.7897
Key point: the line use decimal_types makes every float literal a Decimal , eliminating binary floating‑point errors. Running the same logic with native PHP would produce a trailing "53.78970000000001".

02. Overview of TypePHP Types

TypePHP’s compiler maintains a set of C++ storage types (php::*) that map to PHP types. Important entries include: php::Varmixed (dynamic type, default behavior) php::Int / php::Float / php::Boolint / float / bool (native types, require use native_types) php::Str / php::Arraystring /

array
php::Object

object
php::Stream

resource (TypePHP‑specific stream) php::BigInt / php::Decimal / php::BigFloat → high‑precision trio (BigInt, Decimal, BigFloat) php::StdVector / php::StdMap → C++ standard library containers

Note: without any use statements, all variables default to php::Var , preserving PHP’s dynamic nature. Optimizations rely on explicit type declarations.

03. Three Declaration Traps that Revert to Dynamic Type

While most PHP type declarations map directly to static types, three declarations always degrade to php::Var: null

php::Var
callable

php::Var (compiler cannot track closures and callbacks) iterablephp::Var Hot‑path functions using any of these lose native‑call optimizations. Example of a degrading declaration:

<?php
use native_types;

// Bad: callable parameter forces dynamic handling
function apply(callable $fn, int $x): int {
    return $fn($x); // dynamic call, no optimization
}

// Good: pure int parameter retains static type
function square(int $x): int {
    return $x * $x;
}

04. Type Inference Rules

During compilation, TypePHP automatically promotes literals:

Integer literals with more than 19 digits become BigInt.

Floating‑point literals with more than 16 significant digits become Decimal.

$id = 12345678901234567890; // 19‑digit, auto BigInt
$pi = 0.1234567890123456;   // 16‑digit, auto Decimal

Binary‑operation precedence (high to low): BigFloat > Decimal > BigInt > Float > Int. If any operand is of a higher‑precision type, the result is promoted, except that integer division remains Int and falls back to Var when not exact.

Strict rule: a Float variable cannot be implicitly converted to Decimal; it must be constructed from a string to avoid hidden precision loss.

$a = std::bigInt("12345678901234567890"); // string construction, recommended
$b = std::decimal("0.01");
$c = std::bigFloat("1.2345e100");

05. to* Methods for Explicit Conversion

TypePHP provides a family of to* methods that are chainable and allow the compiler to infer exact return types, surpassing PHP’s cast operators.

declare(strict_types=1);
use native_types;

function convert_demo(mixed $input): void {
    $i = $input->toInt();   // equivalent to (int)$input
    $f = $input->toFloat();
    $s = $input->toString();
    $b = $input->toBool();
    $a = $input->toArray();
}

The toArray() method first checks whether the object defines its own toArray() method; if not, it falls back to converting public properties.

class User {
    public int $id;
    public string $name;

    public function toArray(): array {
        return ['uid' => $this->id, 'display_name' => $this->name];
    }
}

$user = new User(1, 'admin');
$arr = $user->toArray(); // ['uid' => 1, 'display_name' => 'admin']

06. Re‑attaching Types after Mixed Returns

When a value extracted from an array or a resource is typed as mixed, subsequent method calls become dynamic. Using toObject() or toStream() restores a static type, enabling native calls and improving performance.

$user = $array['user']->toObject(User::class);
echo $user->greet(); // compiler now knows $user is a User

$sockets = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, 0);
$client = $sockets[0]->toStream();
$client->write("hello");

In hot loops, adding the toObject() / toStream() line can make a noticeable difference, as discussed in the second article about "Native Call Conditions".

07. any() – Opt‑out of Type Tracking

The compiler infers a variable’s type from its first assignment; later assignments of a different type cause an error. When a variable truly needs to hold multiple possible types, any() disables compile‑time tracking.

function main(): void {
    $rand = random_int(0, 10000);
    if ($rand % 2) {
        $o = any(new Foo1());
    } else {
        $o = any(new Foo2()); // without any() this would be a type conflict
    }
    if (method_exists($o, 'run')) {
        $o->run(); // dynamic dispatch
    }
}
any()

incurs no runtime check overhead, but the variable becomes Var, losing native‑call optimizations. The performance‑flexibility trade‑off is explicit.

08. Summary

Use use decimal_types for monetary calculations; use use bigint_types for extremely large integers. Literals automatically upgrade to high‑precision types.

High‑precision types are immutable; each operation returns a new value. Float cannot be implicitly turned into Decimal; string construction is required.

Declarations of null, callable, and iterable degrade to dynamic type; avoid them in performance‑critical code. toObject() / toStream() reconnect types extracted from arrays or resources, a key performance detail.

When dynamic behavior is truly needed, use any(), but be aware of the loss of native‑call benefits.

The next article will explore compile‑time annotations such as NotNull, NotEmpty, and Validate, showing how they can eliminate null‑pointer and illegal‑argument errors at compile time.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

BigIntPHPType SystemDecimalTypePHPHigh-Precision Arithmetic
Open Source Tech Hub
Written by

Open Source Tech Hub

Sharing cutting-edge internet technologies and practical AI resources.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.