1

I'm working on a system where I need to round down to the nearest penny financial payments. Naively I thought I would multiply up by 100, take the floor and then divide back down. However the following example is misbehaving:

echo 1298.34*100; 

correctly shows:

129834 

but

echo floor(1298.34*100); 

unexpectedly shows:

129833 

I get the same problem using intval for example.

I suspect the multiplication is falling foul of floating point rounding. But if I can't rely on multiplication, how can I do this? I always want to round down reliably, and I don't need to take negative amounts into consideration.

To be clear, I want any fractional penny amounts to be stripped off:

1298.345 should give 1298.34 1298.349 should give 1298.34 1298.342 should give 1298.34 
4
  • You could turn it into a string and truncate if all you want is to lose anything past the second decimal. Commented Jan 22, 2021 at 12:20
  • Yes, it's doable and I may do that. But amounts from the database are not necessarily formatted to two decimal places when I query them. So a numerical solution is preferable. Commented Jan 22, 2021 at 12:24
  • Not sure if I understood your formatting problem correctly, but I had something like this in mind. Commented Jan 22, 2021 at 12:33
  • @El_Vanja if you could post this as an answer, then I'll consider accepting it. Commented Jan 22, 2021 at 12:44

6 Answers 6

3

First of all you need to read RED section on official PHP page https://www.php.net/manual/en/language.types.float.php

Operations on floats are not precise, and we need to use special functions https://www.php.net/manual/en/ref.bc.php

Why? Because
var_dump(1298.34*100); // output float(129833.99999999999)

How to fix it with official math functions?

Short way - use number_format()

number_format(1298.34*100, 0, '.', '')); 

Long way - Use bc functions
bcmul(1298.34, 100); // output "129834"

If you want to set precision of decimals do:
bcmul(1298.34, 100, 2); // output "129834.00"

Finally you can cast it to float

$result = (float)bcmul(1298.34, 100, 2); 

Be careful with secound paramter in bcmul, it not round up or down, it cut numbers of decimals

ROUNDING:

number_format rounds automatically UP but if you want DOWN use

round($result,2,PHP_ROUND_HALF_DOWN) //round DOWN to 2 decimals 
Sign up to request clarification or add additional context in comments.

1 Comment

This is clear. However var_dump(1298.34*100) displays float(129834). I think both var_dump and echo may themselves round the internal representation of the float before displaying.
1

Since you mention you only use this for displaying purposes, you could take the amount, turn it into a string and truncate anything past the second decimal. A regular expression could do the job:

preg_match('/\d+\.{0,1}\d{0,2}/', (string) $amount, $matches); 

This expression works with any number of decimals (including zero). How it works in detail:

  • \d+ matches any number of digits
  • \.{0,1} matches 0 or 1 literal dot
  • \d{0,2} matches zero or two digits after the dot

You can run the following code to test it:

$amounts = [ 1298, 1298.3, 1298.34, 1298.341, 1298.349279745, ]; foreach ($amounts as $amount) { preg_match('/\d+\.{0,1}\d{0,2}/', (string) $amount, $matches); var_dump($matches[0]); } 

Also available as a live test in this fiddle.

1 Comment

Thanks. I've used this solution, as it's guaranteed to work reliably. And it's fairly simple to pad out to two decimal places afterwards.
1

You can use round() to round to the required precision, and with the expected behavior when rounding the final 5 (which is another financial hurdle you might encounter).

 $display = round(3895.0 / 3.0, 2); 

Also, as a reminder, I have the habit of always writing floating point integers with a final dot or a ".0". This prevents some languages from inferring the wrong type and doing, say, integer division, so that 5 / 3 will yield 1.

If you need a "custom rounding" and want to be sure, well, the reason it didn't work is because not all floating point numbers exist in machine representation. 1298.34 does not exist; what does exist (I'm making the precise numbers up!) in its place might be 1298.33999999999999124.

So when you multiply it by 100 and get 129833.999999999999124, of course truncating it will yield 129833.

What you need to do then is to add a small quantity that must be enough to cover the machine error but not enough to matter in the financial calculation. There is an algorithm to determine this quantity, but you can probably get away with "one thousandth after upscaling".

So:

 $display = floor((3895.0 / 3.0)*100.0 + 0.001); 

Please be aware that this number, which you will "see" as 1234.56, might again not exist precisely. It might really be 1234.5600000000000123 or 1234.559999999999876. This might have consequences in complex, composite calculations.

6 Comments

I agree that this should do the job.
Thanks, but I always want to round down. round() only rounds to the nearest, which is not what I want.
To be sure, if you have 12.339999, must that become 12.33?
Yes, I've modified my question.
@GuillermoPhillips I've added a possible solution.
|
0

Since You're working with financial, You should use some kind of Money library (https://github.com/moneyphp/money). Almost all other solutions are asking for trouble.


Other ways, which I don't recommend, are: a) use integers only, b) calculate with bcmath or c) use Number class from the Money library e.g.:

function getMoneyValue($value): string { if (!is_numeric($value)) { throw new \RuntimeException(sprintf('Money value has to be a numeric value, "%s" given', is_object($value) ? get_class($value) : gettype($value))); } $number = \Money\Number::fromNumber($value)->base10(-2); return $number->getIntegerPart(); } 

1 Comment

I'd prefer not to add another dependency if I can help it. But I may go this way if I can't find a 100% reliable solution.
0

Since I asked the question, I've been wondering about a purely numerical solution to the problem.

The answer is to add a small fraction to the multiplier before taking the floor:

echo floor(1298.349 * (100 + 1/10000)) / 100; 

Gives the expected answer:

1298.34 

How much of a fraction to add depends on the largest size of number you want to handle. For two decimal places the general formula is:

echo floor($amount * (100 + 1 / ($max_num * 10))) / 100; 

This works for any number of decimal places in $amount where $amount is a float.

To show that this works, here is a small test program. This will check all floats of up to three decimal places, truncated to two decimal places. It checks the numerical solution against a purely string solution and outputs the first failure.

$increment = 1/((float) 100000); $multiplier_float = (float) (100 + $increment); for ($integer_part = 1; $integer_part <= 20000; $integer_part++) { $integer_part_string = (string) $integer_part; for ($decimal_part = 0; $decimal_part <= 999; $decimal_part++) { $decimal_part_string = str_pad($decimal_part, 3, '0', STR_PAD_LEFT); $truncated_decimal_part_string = substr($decimal_part_string, 0, 2); $number_string = $integer_part_string . '.' . $decimal_part_string; $number_float = (float) $number_string; $number_truncated_string = $integer_part_string . '.' . $truncated_decimal_part_string; $number_truncated_float = floor($number_float * $multiplier_float)/(float) 100; $number_truncated_float_as_string = (string) $number_truncated_float; if ($number_truncated_string != $number_truncated_float_as_string) { echo $number_string . ' ' . $number_truncated_string . ' ' . $number_truncated_float_as_string . "\n"; exit; } } } 

I've explicitly cast to floats to make it clearer.

If you change the $increment you can check where the first failure occurs. The example is checking all numbers up to 20,000. And you will see that the first failure occurs with 10000.009.

Realistically, when working with monetary values, there will be a maximum size of number (say 6 significant places or less). So this solution may be more performant and is a one-liner.

Comments

-1

he other function available is round(), which takes two parameters - the number to round, and the number of decimal places to round to. If a number is exactly half way between two integers, round() will always round up.

use round :

echo round (1298.34*100); 

result :

129834 

2 Comments

This is not reliable. echo ceil(1298.38*100); gives 129839.
Don't do that! ceil makes exactly the same conceptual error as floor, just reversed. Use round or - if you really can't for some weird reason - at least add a negligible quantity for your scale (usually half of that) and use floor. Even this isn't safe because rounding behaviour is not standardized, you can have "financial rounding" and "strict rounding".

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.