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.