Skip to content

Commit

Permalink
add intl localized decimal parser/formatter (moneyphp#443)
Browse files Browse the repository at this point in the history
* add intl localized decimal parser and formatter
  • Loading branch information
frederikbosch authored Jan 5, 2018
1 parent eb82ddf commit c7e55b8
Show file tree
Hide file tree
Showing 8 changed files with 434 additions and 5 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- modulus method to Money
- ratioOf method to Money
- Comparator for easier testing Money object with PHPUnit
- IntlLocalizedDecimalParser
- fromLocale and fromCurrentLocale added to IntlMoneyParserto easier instantiate parser

### Changed

Expand Down
31 changes: 29 additions & 2 deletions doc/features/formatting.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ This is where formatters help you. You can turn a money object into a human read
Money comes with the following implementations out of the box:


Intl Formatter
--------------
Intl Money Formatter
--------------------

As its name says, this formatter requires the `intl` extension and uses ``NumberFormatter``. In order to provide the
correct subunit for the specific currency, you should also provide the specific currency repository.
Expand All @@ -36,6 +36,33 @@ correct subunit for the specific currency, you should also provide the specific
echo $moneyFormatter->format($money); // outputs $1.00
Intl Decimal Formatter
----------------------

As its name says, this formatter requires the `intl` extension and uses ``NumberFormatter``. In order to provide the
correct subunit for the specific currency, you should also provide the specific currency repository.


.. warning::
Please be aware that using the `intl` extension can give different results in different environments.


.. code-block:: php
use Money\Currencies\ISOCurrencies;
use Money\Currency;
use Money\Formatter\IntlMoneyFormatter;
use Money\Money;
$money = new Money(100000, new Currency('EUR'));
$currencies = new ISOCurrencies();
$numberFormatter = new \NumberFormatter('nl_NL', \NumberFormatter::DECIMAL);
$moneyFormatter = new IntlMoneyFormatter($numberFormatter, $currencies);
echo $moneyFormatter->format($money); // outputs 1.000,00
Decimal Formatter
-----------------

Expand Down
30 changes: 28 additions & 2 deletions doc/features/parsing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ and moving the ``stringToUnits`` to ``StringToUnitsParser`` (later replaced by `
Money comes with the following implementations out of the box:


Intl Parser
-----------
Intl Money Parser
-----------------

As its name says, this parser requires the `intl` extension and uses ``NumberFormatter``. In order to provide the
correct subunit for the specific currency, you should also provide the specific currency repository.
Expand All @@ -37,6 +37,32 @@ correct subunit for the specific currency, you should also provide the specific
echo $money->getAmount(); // outputs 100
Intl Localized Decimal Parser
-----------------------------

As its name says, this parser requires the `intl` extension and uses ``NumberFormatter``. In order to provide the
correct subunit for the specific currency, you should also provide the specific currency repository.


.. warning::
Please be aware that using the `intl` extension can give different results in different environments.


.. code-block:: php
use Money\Currencies\ISOCurrencies;
use Money\Parser\IntlMoneyParser;
$currencies = new ISOCurrencies();
$numberFormatter = new \NumberFormatter('nl_NL', \NumberFormatter::DECIMAL);
$moneyParser = new IntlMoneyParser($numberFormatter, $currencies);
$money = $moneyParser->parse('1.000,00');
echo $money->getAmount(); // outputs 100000
Decimal Parser
--------------

Expand Down
69 changes: 69 additions & 0 deletions src/Formatter/IntlLocalizedDecimalFormatter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?php

namespace Money\Formatter;

use Money\Currencies;
use Money\Money;
use Money\MoneyFormatter;

/**
* Formats a Money object using intl extension.
*
* @author Frederik Bosch <[email protected]>
*/
final class IntlLocalizedDecimalFormatter implements MoneyFormatter
{
/**
* @var \NumberFormatter
*/
private $formatter;

/**
* @var Currencies
*/
private $currencies;

/**
* @param \NumberFormatter $formatter
* @param Currencies $currencies
*/
public function __construct(\NumberFormatter $formatter, Currencies $currencies)
{
$this->formatter = $formatter;
$this->currencies = $currencies;
}

/**
* {@inheritdoc}
*/
public function format(Money $money)
{
$valueBase = $money->getAmount();
$negative = false;

if ($valueBase[0] === '-') {
$negative = true;
$valueBase = substr($valueBase, 1);
}

$subunit = $this->currencies->subunitFor($money->getCurrency());
$valueLength = strlen($valueBase);

if ($valueLength > $subunit) {
$formatted = substr($valueBase, 0, $valueLength - $subunit);
$decimalDigits = substr($valueBase, $valueLength - $subunit);

if (strlen($decimalDigits) > 0) {
$formatted .= '.'.$decimalDigits;
}
} else {
$formatted = '0.'.str_pad('', $subunit - $valueLength, '0').$valueBase;
}

if ($negative === true) {
$formatted = '-'.$formatted;
}

return $this->formatter->format($formatted);
}
}
102 changes: 102 additions & 0 deletions src/Parser/IntlLocalizedDecimalParser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
<?php

namespace Money\Parser;

use Money\Currencies;
use Money\Currency;
use Money\Exception\ParserException;
use Money\Money;
use Money\MoneyParser;
use Money\Number;

/**
* Parses a string into a Money object using intl extension.
*
* @author Frederik Bosch <[email protected]>
*/
final class IntlLocalizedDecimalParser implements MoneyParser
{
/**
* @var \NumberFormatter
*/
private $formatter;

/**
* @var Currencies
*/
private $currencies;

/**
* @param \NumberFormatter $formatter
* @param Currencies $currencies
*/
public function __construct(\NumberFormatter $formatter, Currencies $currencies)
{
$this->formatter = $formatter;
$this->currencies = $currencies;
}

/**
* {@inheritdoc}
*/
public function parse($money, $forceCurrency = null)
{
if (!is_string($money)) {
throw new ParserException('Formatted raw money should be string, e.g. $1.00');
}

if (null === $forceCurrency) {
throw new ParserException(
'IntlLocalizedDecimalParser cannot parse currency symbols. Use forceCurrency argument'
);
}

$decimal = $this->formatter->parse($money);

if (false === $decimal) {
throw new ParserException(
'Cannot parse '.$money.' to Money. '.$this->formatter->getErrorMessage()
);
}

/*
* This conversion is only required whilst currency can be either a string or a
* Currency object.
*/
if (!$forceCurrency instanceof Currency) {
@trigger_error('Passing a currency as string is deprecated since 3.1 and will be removed in 4.0. Please pass a '.Currency::class.' instance instead.', E_USER_DEPRECATED);
$forceCurrency = new Currency($forceCurrency);
}

$decimal = (string) $decimal;
$subunit = $this->currencies->subunitFor($forceCurrency);
$decimalPosition = strpos($decimal, '.');

if (false !== $decimalPosition) {
$decimalLength = strlen($decimal);
$fractionDigits = $decimalLength - $decimalPosition - 1;
$decimal = str_replace('.', '', $decimal);
$decimal = Number::roundMoneyValue($decimal, $subunit, $fractionDigits);

if ($fractionDigits > $subunit) {
$decimal = substr($decimal, 0, $decimalPosition + $subunit);
} elseif ($fractionDigits < $subunit) {
$decimal .= str_pad('', $subunit - $fractionDigits, '0');
}
} else {
$decimal .= str_pad('', $subunit, '0');
}

if ('-' === $decimal[0]) {
$decimal = '-'.ltrim(substr($decimal, 1), '0');
} else {
$decimal = ltrim($decimal, '0');
}

if ('' === $decimal) {
$decimal = '0';
}

return new Money($decimal, $forceCurrency);
}
}
61 changes: 61 additions & 0 deletions tests/Formatter/IntlLocalizedDecimalFormatterTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php

namespace Tests\Money\Formatter;

use Money\Currencies;
use Money\Currency;
use Money\Formatter\IntlLocalizedDecimalFormatter;
use Money\Money;
use Prophecy\Argument;

final class IntlLocalizedDecimalFormatterTest extends \PHPUnit_Framework_TestCase
{
/**
* @dataProvider moneyExamples
* @test
*/
public function it_formats_money($amount, $currency, $subunit, $result, $mode, $fractionDigits)
{
$money = new Money($amount, new Currency($currency));

$numberFormatter = new \NumberFormatter('en_US', $mode);

$numberFormatter->setAttribute(\NumberFormatter::FRACTION_DIGITS, $fractionDigits);

$currencies = $this->prophesize(Currencies::class);

$currencies->subunitFor(Argument::allOf(
Argument::type(Currency::class),
Argument::which('getCode', $currency)
))->willReturn($subunit);

$moneyFormatter = new IntlLocalizedDecimalFormatter($numberFormatter, $currencies->reveal());
$this->assertSame($result, $moneyFormatter->format($money));
}

public static function moneyExamples()
{
return [
[5005, 'USD', 2, '50', \NumberFormatter::DECIMAL, 0],
[100, 'USD', 2, '1.00', \NumberFormatter::DECIMAL, 2],
[41, 'USD', 2, '0.41', \NumberFormatter::DECIMAL, 2],
[5, 'USD', 2, '0.05', \NumberFormatter::DECIMAL, 2],
[5, 'USD', 2, '0.050', \NumberFormatter::DECIMAL, 3],
[35, 'USD', 2, '0.350', \NumberFormatter::DECIMAL, 3],
[135, 'USD', 2, '1.350', \NumberFormatter::DECIMAL, 3],
[6135, 'USD', 2, '61.350', \NumberFormatter::DECIMAL, 3],
[-6135, 'USD', 2, '-61.350', \NumberFormatter::DECIMAL, 3],
[-6152, 'USD', 2, '-61.5', \NumberFormatter::DECIMAL, 1],
[5, 'EUR', 2, '0.05', \NumberFormatter::DECIMAL, 2],
[50, 'EUR', 2, '0.50', \NumberFormatter::DECIMAL, 2],
[500, 'EUR', 2, '5.00', \NumberFormatter::DECIMAL, 2],
[5, 'EUR', 2, '0.05', \NumberFormatter::DECIMAL, 2],
[50, 'EUR', 2, '0.50', \NumberFormatter::DECIMAL, 2],
[500, 'EUR', 2, '5.00', \NumberFormatter::DECIMAL, 2],
[5, 'EUR', 2, '0', \NumberFormatter::DECIMAL, 0],
[50, 'EUR', 2, '0', \NumberFormatter::DECIMAL, 0],
[500, 'EUR', 2, '5', \NumberFormatter::DECIMAL, 0],
[5055, 'USD', 2, '51', \NumberFormatter::DECIMAL, 0],
];
}
}
3 changes: 2 additions & 1 deletion tests/MoneyTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ public function it_multiplies_the_amount($multiplier, $roundingMode, $result)
*/
public function it_multiplies_the_amount_with_locale_that_uses_comma_separator()
{
$current = setlocale(LC_ALL, '0');
setlocale(LC_ALL, 'es_ES.utf8');

$money = new Money(100, new Currency(self::CURRENCY));
Expand All @@ -88,7 +89,7 @@ public function it_multiplies_the_amount_with_locale_that_uses_comma_separator()
$this->assertInstanceOf(Money::class, $money);
$this->assertEquals(10, $money->getAmount());

setlocale(LC_ALL, null);
setlocale(LC_ALL, $current);
}

/**
Expand Down
Loading

0 comments on commit c7e55b8

Please sign in to comment.