13

As input, I want to accept any of the following: "$12.33", "14.92", "$13", "17", "14.00001". As output, I want 1233, 1492, 1300, 1700 and 1400 respectively. This is apparently not as easy as it looks:

<?php
$input = '$64.99';  // value is given via form submission
$dollars = str_replace('$', '', $input);  // get rid of the dollar sign
$cents = (int)($dollars * 100) // multiply by 100 and truncate
echo $cents;
?>

This outputs 6498 instead of 6499.

I assume this has to do with inaccuracies in floating point values, and avoiding these is the whole reason I'm converting to integer cents in the first place. I suppose I could use logic like "get rid of the $ sign, check if there's a decimal point, if so, check how many characters there are after it padding to two and truncating after that then remove the period, if there wasn't one append two zeros and hope for the best" but using string operations for this seems ridiculous.

Surely taking a monetary value from a form and storing it as cents in a database is a common use case. Surely there is a "reasonable" way of doing this.

Right? .....right? :<

Mala
  • 14,178
  • 25
  • 88
  • 119

7 Answers7

48

Consider using the BC Math extension, which does arbitrary-precision math. In particular, bcmul():

<?php
$input = '$64.99';
$dollars = str_replace('$', '', $input);
$cents = bcmul($dollars, 100);
echo $cents;
?>

Output:

6499
cdhowie
  • 158,093
  • 24
  • 286
  • 300
  • I'd prefer to avoid installing extensions for a task as simple as this. Upvoted because it works, though, and may be useful to someone who is already using BC Math. – Mala Feb 12 '14 at 17:59
  • 9
    @Mala If you are doing financial computations in PHP then this extension should be considered a must-have, as it avoids all of the pitfalls of rounding imprecise floating point numbers. – cdhowie Feb 12 '14 at 17:59
4
$input[] = "$12.33";
$input[] = "14.92";
$input[] = "$13";
$input[] = "17";
$input[] = "14.00001";
$input[] = "$64.99";

foreach($input as $number)
{
    $dollars = str_replace('$', '', $number);
    echo number_format((float)$dollars*100., 0, '.', '');
}

Gives:

1233
1492
1300
1700
1400
6499

Watch out for corner cases like "$0.125". I don't know how you would like to handle those.

sigy
  • 2,408
  • 1
  • 24
  • 55
  • Note type cast with (int) to return the number rather than a string. – Shane Aug 23 '18 at 12:30
  • Note that the question is about converting to an integer, and this answer never casts to an integer. However, after testing it, it looks like the number_format() function correctly rounds `(float)$dollars*100.`, which is equal to 6498.9999, into the string "6499", and this string can then be safely casted to an int if necessary. But all of this string -> float -> string -> int conversion is widely unnecessary. We can simply round the float and directly convert to an int (see my answer), no need to go back to a string using the number_format() function. – Boris Dalstein Jul 02 '21 at 13:28
3

Ah, I found out why. When you cast (int) on ($dollars*100) it drops a decimal. I'm not sure WHY, but remove the int cast and it's fixed.

Sterling Archer
  • 22,070
  • 18
  • 81
  • 118
  • The reason the (int) cast is there is for the "14.00001" case, wanting 1400 not 1400.001. I suppose I could round() instead? – Mala Feb 12 '14 at 17:58
  • You might not want to round the whole think, maybe truncate or if there is a way to round with higher precision? – Sterling Archer Feb 12 '14 at 18:02
  • It's "fixed" only because `$dollars * 100` is resulting in a floating-point number like `6498.99999999`, and the number is rounded for display. The `int` cast, on the other hand, truncates the fractional part. Floating-point numbers **should not** be used in financial calculations. **Ever.** – cdhowie Feb 12 '14 at 18:18
  • @cdhowie: All numerical arithmetic is subject to errors: Floating-point, integer, fixed point, extended precision, rational, binary based, decimal based. It is not correct to rule out floating point, or any one of them, from any use in financial calculations. Floating-point may be used appropriately, e.g., in sophisticated securities analysis. The correct rule is that a programmer must understand the software they write. I.e., do not using any arithmetic system if you do not understand its details. – Eric Postpischil Feb 12 '14 at 21:49
  • @EricPostpischil That is true, but limited-precision floating-point in particular is extremely difficult to reason about correctly even when it comes to basic arithmetic, especially in the context of financial applications -- specifically when computing amounts of money. Unlimited-precision floating point is much easier to reason about, as is limited-precision fixed point. It is extremely rare that I see someone use an imprecise floating point type correctly in financial applications. – cdhowie Feb 12 '14 at 23:54
  • @EricPostpischil In other words, while your advice may be true in the large picture of "all financial software," in the context of "manipulation of balances," (and specifically this question) using imprecise floating-point numbers an extremely common mistake and causes all sorts of problems. If you would like me to amend my statement, I will restate as "floating-point numbers should not be used to calculate balances, credits, or debits, unless you have a very specific reason that this is appropriate in your case, and usually it won't be." – cdhowie Feb 12 '14 at 23:57
  • @cdhowie to be fair, it can be unavoidable: case in point, this instance. I am not going to require the user type in a number of cents, that's just silly. Thus by definition, somewhere along the line, a floating point is going to be used (if only as the user input until it can be converted to integer cents, as I was trying to do here) unless you propose to do it via string manipulation – Mala Mar 03 '14 at 02:46
  • @Mala *"a floating point is going to be used"* If you are referring to IEEE floating-point (which is of limited precision), no, it does not have to be used. `bcmul()` and friends use (theoretically) infinite precision floating point. The fact that it's floating-point is not as important as the fact that it's imprecise. – cdhowie Mar 05 '14 at 03:39
3

Do not directly convert float to integer.
Convert float to string, then convert string to integer.

Solution:

<?php
$input = '$64.99';
$dollars = str_replace('$', '', $input);
$cents = (int) ( (string) ( $dollars * 100 ) );
echo $cents;
?>

Explained:

<?php
$input = '$64.99';  // value is given via form submission
$dollars = str_replace('$', '', $input);  // get rid of the dollar sign
$cents_as_float = $dollars * 100;  // multiply by 100 (it becomes float)
$cents_as_string = (string) $cents_as_float;  // convert float to string
$cents = (int) $cents_as_string;  // convert string to integer
echo $cents;
?>
mahfuz
  • 2,728
  • 20
  • 18
2
$test[] = 123;
$test[] = 123.45;
$test[] = 123.00;
$test[] = 123.3210123;
$test[] = '123.3210123';
$test[] = '123,3210123';
$test[] = 0.3210;
$test[] = '00.023';
$test[] = 0.01;
$test[] = 1;


foreach($test as $value){
    $amount = intval(
                strval(floatval(
                    preg_replace("/[^0-9.]/", "", str_replace(',','.',$value))
                ) * 100));
    echo $amount;
}

Results:

12300
12345
12300
12332
12332
12332
32
2
1
100
btavares
  • 111
  • 1
  • 9
1

Remove the dollar sign and then use bcmul() to multiply.

mirosval
  • 6,671
  • 3
  • 32
  • 46
1

The issue arises because casting to an int performs truncation instead of rounding. Simple fix: round the number before casting.

<?php
$input = '$64.99';
$dollars = str_replace('$', '', $input);
$cents = (int) round($dollars * 100);
echo $cents;
?>

Output: 6499

Longer explanation:

When PHP sees the string "64.99" and converts it to a (double-precision) floating point, the actual value of the floating point is:

64.9899999999999948840923025272786617279052734375

This is because the number 64.99 is not exactly representable as a floating point, and the above number is the closest possible floating point to 64.99. Then, you multiply it by 100 (which is exactly representable), and the result becomes:

6498.9999999999990905052982270717620849609375

If you cast it to an int, it would truncate this number, and therefore you get the integer 6498 which is not what you want. But if instead you round the floating point first, you exactly get 6499 as a floating point, and then casting this to an int gives you the expected integer.

Boris Dalstein
  • 7,015
  • 4
  • 30
  • 59