Andre
Andre

Reputation: 1385

Unexpected rounding with money_format, sample code included

I'm using money_format, with format of %.2n but am getting a weird result.

I've put together sample code, so others may test for themselves.

<?php
    setlocale(LC_MONETARY, 'en_US');
    $deal = array(
        array(
            'amt' => 1350,
            'rate' => .75,
            'lod' => 47
        ),
        array(
            'amt' => 990,
            'rate' => .75,
            'lod' => 27
        ),
        array(
            'amt' => 4180,
            'rate' => .75,
            'lod' => 65
        ),
        array(
            'amt' => 2370,
            'rate' => .75,
            'lod' => 26
        )
    );
    foreach ($deal as $value) {
        $fee = (($value['amt'] / 1000) * $value['rate']) * $value['lod'];
        var_dump($fee);
        echo '<br />money_format: ', money_format('%.2n', $fee), '<br />number_format: ', number_format($fee, 2), '<br /><br />';
    }

Output:

float(47.5875)
money_format: $47.59
number_format: 47.59

float(20.0475)
money_format: $20.05
number_format: 20.05

float(203.775)
money_format: $203.77
number_format: 203.78

float(46.215)
money_format: $46.22
number_format: 46.22

You'll notice for the third result, 203.775 appears as $203.77 as opposed to $203.78.

Am I missing something in my understanding of money_format?

phpfiddle link: http://phpfiddle.io/fiddle/1909516587

Upvotes: 3

Views: 932

Answers (2)

Will B.
Will B.

Reputation: 18416

This is a common issue with floating point math operations

The reason for the confusion is that by default precision in PHP is set too low and is displaying a rounded value for the floating point when displayed from var_dump.

Source: http://php.net/manual/en/language.types.float.php

To see the actual number you will need to set a higher precision or define the precision using printf, which will display more of the float value from $fee.

ini_set('precision',32);
var_dump($fee);
//or
printf("%01.32f", $fee);

Precision Example: https://3v4l.org/ue0Th

float(47.587500000000005684341886080801)
money_format: 47.59
number_format: 47.59

float(20.04749999999999943156581139192)
money_format: 20.05
number_format: 20.05

float(203.77499999999997726263245567679)
money_format: 203.77
number_format: 203.78

float(46.215000000000003410605131648481)
money_format: 46.22
number_format: 46.22

As a result we can see that number_format is incorrectly rounding. When given the same decimal value, we get one number that is rounded up and one that is not: https://3v4l.org/pgKDs

echo number_format(23.77499999999997, 2); //returns 23.78
echo number_format(2.77499999999997, 2); //returns 2.77

As a general rule, try to not use floating point math where precision is involved and favoring the lowest possible denomination (in the sense of money a cent), or rely on the bcmath functions and using a multiplier to retrieve the fractional value. http://php.net/manual/en/book.bc.php


Changing the calculations to use bcmath will resolve the issue with both money_format and number_format:

BC Math Example: https://3v4l.org/JEZds

bcscale(4);
foreach ($deal as $value) {
    $fee = (float) bcmul(bcmul(bcdiv($value['amt'], 1000), $value['rate']), $value['lod']);
    echo 'money_format: ' . money_format('%.2n', $fee) . PHP_EOL;
    echo 'number_format: ' . number_format($fee, 2) . PHP_EOL . PHP_EOL;
}

Results:

string(7) "47.5875"
money_format: 47.59
number_format: 47.59

string(7) "20.0475"
money_format: 20.05
number_format: 20.05

string(8) "203.7750"
money_format: 203.78
number_format: 203.78

string(7) "46.2150"
money_format: 46.22
number_format: 46.22

Upvotes: 5

iautomation
iautomation

Reputation: 1084

A solution to international support would be something like the following:

<?php

setlocale(LC_MONETARY, 'en_US');

function _money_format($number, $decimals=2){
    $number = number_format($number, $decimals);
    $local_settings = localeconv();
    $currency_symbol = $local_settings['currency_symbol'];
    return $currency_symbol . $number;
}

echo _money_format('23.4567');

?>

Still getting the currency symbol + using number_format which should get what you need.

Demo here: http://phpfiddle.io/fiddle/772115651

Upvotes: 0

Related Questions