Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

How to count money using PHP and not lose money

58 views

Published on

Introduction to the MoneyPHP library and description of different issues caused by using FLOAT type to handle monetary amounts.

Published in: Software
  • Be the first to comment

  • Be the first to like this

How to count money using PHP and not lose money

  1. 1. How to count money and not lose it Piotr Horzycki www.peterdev.pl | twitter.com/peterdevpl
  2. 2. var_dump((int) ('4.20' * 100)); → int(420) var_dump((int) ('4.10' * 100)); → int(409) “I can set a product price to 4.20 PLN, but not 4.10 – it’s being changed to 4.09. Why?” Bugs...
  3. 3. if (parseInt(amount) > 0) { /* proceed with payment */ } else { /* disable payment button */ } Why does 0.5 not work? Bugs...
  4. 4. <p>Price: <?php echo strtr($price, '.', ','); ?> PLN</p> <p>Price: <?php echo str_replace($price, '.', ','); ?> PLN</p> <p>Price: <?php echo number_format($price, 2, ',', ' '); ?> PLN</p> Formatting price strings – the wrong way...
  5. 5. “A large proportion of the computers in this world manipulate money, so it's always puzzled me that money isn't actually a first class data type in any mainstream programming language. The lack of a type causes problems, the most obvious surrounding currencies. (...) The more subtle problem is with rounding. Monetary calculations are often rounded to the smallest currency unit. When you do this it's easy to lose pennies (or your local equivalent) because of rounding errors. The good thing about object-oriented programming is that you can fix these problems by creating a Money class that handles them. Of course, it's still surprising that none of the mainstream base class libraries actually do this.” https://martinfowler.com/eaaCatalog/money.html
  6. 6. use MoneyCurrency; use MoneyMoney; // 5,00 USD $fiver = new Money(500, new Currency('USD')); // or shorter: $fiver = Money::USD('500'); MoneyPHP: PHP implementation of the Money pattern
  7. 7. final class Money implements JsonSerializable { /** @var string */ private $amount; /** @var Currency */ private $currency; /** @var Calculator */ private static $calculator; private static $calculators = [ BcMathCalculator::class, GmpCalculator::class, PhpCalculator::class, ]; /* … */ }
  8. 8. $value1 = Money::EUR(800); $value2 = Money::EUR(500); $value3 = Money::EUR(100); $value1->add($value2); $value1->subtract($value2, $value3); $value1->multiply(2); $value1->divide(2); $value1->mod($value2); // 3.00 EUR $value1->ratioOf($value2); // '1.6' Basic money arithmetics
  9. 9. $value1 = Money::USD(800); // $8.00 $value2 = Money::USD(100); // $1.00 $result = $value1->isSameCurrency($value2); // true $result = $value1->equals($value2); // false $result = $value1->greaterThan($value2); // true Comparing money objects
  10. 10. Immutability $jimPrice = $hannahPrice = Money::EUR(2500); $coupon = Money::EUR(500); // wrong $jimPrice->subtract($coupon); $jimPrice->equals($hannahPrice); // true
  11. 11. Immutability $jimPrice = $hannahPrice = Money::EUR(2500); $coupon = Money::EUR(500); // wrong $jimPrice->subtract($coupon); $jimPrice->equals($hannahPrice); // true // correct $jimPrice = $jimPrice->subtract($coupon); $jimPrice->lessThan($hannahPrice); // true $jimPrice->equals(Money::EUR(2000)); // true $jimPrice and $hannahPrice are immutable value objects
  12. 12. use MoneyMoney; $profit = Money::EUR(5); // 5 euro cents list($my_cut, $investors_cut) = $profit->allocate([70, 30]); // $my_cut is 4 cents, $investors_cut is 1 cent Allocating profits
  13. 13. use MoneyMoney; $profit = Money::EUR(5); // 5 euro cents list($my_cut, $investors_cut) = $profit->allocate([70, 30]); // $my_cut is 4 cents, $investors_cut is 1 cent // The order is important: list($investors_cut, $my_cut) = $profit->allocate([30, 70]); // $my_cut is 3 cents, $investors_cut is 2 cents Allocating profits
  14. 14. Database INT, BIGINT… VARCHAR... DECIMAL (MySQL) NUMERIC (Oracle) FLOAT, DOUBLE...
  15. 15. Database INT, BIGINT… VARCHAR... DECIMAL (MySQL) NUMERIC (Oracle) FLOAT, DOUBLE... DECIMAL(10, 2) total decimal digits places max 99,999,999.99
  16. 16. Exchange rates... https://www.bankofengland.co.uk/boeapps/database/Rates.asp
  17. 17. https://en.wikipedia.org/wiki/Redenomination
  18. 18. https://www.theguardian.com/world/2018/aug/20/venezuela-bolivars-hyperinflation-banknotes
  19. 19. https://www.amusingplanet.com/2018/08/hungarys-hyperinflation-worst-case-of.html Hungary’s hyperinflation in 1946
  20. 20. Converting currencies with MoneyPHP use MoneyConverter; use MoneyCurrency; use MoneyExchangeFixedExchange; $exchange = new FixedExchange([ 'EUR' => [ 'USD' => 1.25 ] ]); $converter = new Converter(new ISOCurrencies(), $exchange); $eur100 = Money::EUR(100); $usd125 = $converter->convert($eur100, new Currency('USD'));
  21. 21. Converting currencies with MoneyPHP and Swap use MoneyMoney; use MoneyConverter; use MoneyExchangeSwapExchange; use SwapBuilder; $swap = (new Builder()) ->add('fixer', ['access_key' => 'your-access-key']) ->build(); $exchange = new SwapExchange($swap); $converter = new Converter(new ISOCurrencies(), $exchange); $eur100 = Money::EUR(100); $usd125 = $converter->convert($eur100, new Currency('USD'));
  22. 22. Save the conversion rate!
  23. 23. <p>Price: <?php echo strtr($price, '.', ','); ?> PLN</p> <p>Price: <?php echo str_replace($price, '.', ','); ?> PLN</p> <p>Price: <?php echo number_format($price, 2, ',', ' '); ?> PLN</p> Price formatting again...
  24. 24. $100 €100 £100 ¥100 ... Price formatting again...
  25. 25. $100 €100 £100 ¥100 ... 100 zł Price formatting again...
  26. 26. $money = new Money(100, new Currency('USD')); $currencies = new ISOCurrencies(); $numberFormatter = new NumberFormatter( 'en_US', NumberFormatter::CURRENCY ); $moneyFormatter = new IntlMoneyFormatter( $numberFormatter, $currencies ); echo $moneyFormatter->format($money); // outputs $1.00
  27. 27. $money = new Money('12345678', new Currency('PLN')); $currencies = new ISOCurrencies(); $numberFormatter = new NumberFormatter( 'pl_PL', NumberFormatter::CURRENCY ); $moneyFormatter = new IntlMoneyFormatter( $numberFormatter, $currencies ); echo $moneyFormatter->format($money); // outputs 123 456,78 zł
  28. 28. https://en.wikipedia.org/wiki/Dollar_sign
  29. 29. $currencies = new ISOCurrencies(); $americanNumberFormatter = new NumberFormatter('en_US', NumberFormatter::CURRENCY); $mexicanNumberFormatter = new NumberFormatter('es_MX', NumberFormatter::CURRENCY); $dollarsFormatter = new IntlMoneyFormatter($americanNumberFormatter, $currencies); $pesosFormatter = new IntlMoneyFormatter($mexicanNumberFormatter, $currencies); $dollars = new Money(12345, new Currency('USD')); $pesos = new Money(12345, new Currency('MXN')); echo $dollarsFormatter->format($dollars) . PHP_EOL; // $123.45 echo $pesosFormatter->format($pesos); // $123.45 Be careful with parsing money strings!
  30. 30. Rounding ● https://en.wikipedia.org/wiki/Cash_rounding (aka Swedish rounding) ● Polish income tax: round to the nearest whole zloty
  31. 31. Rounding $money = new Money('12345', new Currency('USD')); // $123.45 is the initial amount $multiplied = $money->multiply('1.23', Money::ROUND_UP); // $151.8435 before rounding echo $dollarsFormatter->format($multiplied); // output: $151.85
  32. 32. Architecture final class Invoice { private $seller; private $buyer; private $issueDate; private $dueDate; /* … */ public addItem(Money $unitPrice, int $quantity, Tax $tax) {} public getTotalAmount(): Money {} public getTaxAmount(): Money {} }
  33. 33. Architecture interface InvoiceBuilder { fromSeller(Contractor $seller): self; toBuyer(Contractor $buyer): self; addItem(Money $unitPrice, int $quantity, Tax $tax): self; /* … */ build(): Invoice; }
  34. 34. Further reading ● https://www.h-schmidt.net/FloatConverter/IEEE754.html ● https://martinfowler.com/eaaCatalog/money.html ● http://moneyphp.org/en/stable/ ● https://romaricdrigon.github.io/2019/07/05/value-objects-doctrine-and-symfony-forms ● https://github.com/Sylius/SyliusMoneyBundle
  35. 35. Thank you! Piotr Horzycki www.peterdev.pl | twitter.com/peterdevpl

×