Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Parse exponential float as Money #520

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 119 additions & 0 deletions src/Parser/ExponentialMoneyParser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
<?php

namespace Money\Parser;

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

/**
* Parses an exponential string into a Money object.
*
* @example `2.8865798640254e+15`
*
* @author George Mponos <[email protected]>
*/
final class ExponentialMoneyParser implements MoneyParser
{
const EXPO_DECIMAL_PATTERN = '/^(?P<sign>-)?(?P<digits>0|[1-9]\d*)?\.?(?P<fraction>\d+)?[eE][-+]\d+$/';

const DECIMAL_PATTERN = '/^(?P<sign>-)?(?P<digits>0|[1-9]\d*)?\.?(?P<fraction>\d+)?$/';

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

/**
* @param Currencies $currencies
*/
public function __construct(Currencies $currencies)
{
$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(
'ExponentialMoneyParser cannot parse currency symbols. Use forceCurrency argument'
);
}

/*
* This conversion is only required whilst currency can be either a string or a
* Currency object.
*/
$currency = $forceCurrency;
if (!$currency 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);
$currency = new Currency($currency);
}

$expo = trim($money);
if ($expo === '') {
return new Money(0, $currency);
}

$subunit = $this->currencies->subunitFor($currency);

if (!preg_match(self::EXPO_DECIMAL_PATTERN, $expo, $matches) || !isset($matches['digits'])) {
throw new ParserException(sprintf(
'Cannot parse "%s" to Money.',
$expo
));
}

$number = number_format($expo, $subunit, '.', '');
if (!preg_match(self::DECIMAL_PATTERN, $number, $matches) || !isset($matches['digits'])) {
throw new ParserException(sprintf(
'Cannot parse "%s" to Money.',
$expo
));
}

$negative = isset($matches['sign']) && $matches['sign'] === '-';

$decimal = $matches['digits'];

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

if (isset($matches['fraction'])) {
$fractionDigits = strlen($matches['fraction']);
$decimal .= $matches['fraction'];
$decimal = Number::roundMoneyValue($decimal, $subunit, $fractionDigits);

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

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

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

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

namespace Tests\Money\Parser;

use Money\Currencies;
use Money\Currency;
use Money\Exception\ParserException;
use Money\Parser\ExponentialMoneyParser;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;

final class ExponentialMoneyParserTest extends TestCase
{
/**
* @dataProvider formattedMoneyExamples
* @test
*/
public function it_parses_money($decimal, $currency, $subunit, $result)
{
$currencies = $this->prophesize(Currencies::class);

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

$parser = new ExponentialMoneyParser($currencies->reveal());

$this->assertEquals($result, $parser->parse($decimal, new Currency($currency))->getAmount());
}

/**
* @dataProvider invalidMoneyExamples
* @test
*/
public function it_throws_an_exception_upon_invalid_inputs($input)
{
$this->expectException(ParserException::class);

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

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

$parser = new ExponentialMoneyParser($currencies->reveal());

$parser->parse($input, new Currency('USD'))->getAmount();
}

/**
* @group legacy
* @expectedDeprecation Passing a currency as string is deprecated since 3.1 and will be removed in 4.0. Please pass a Money\Currency instance instead.
* @test
*/
public function it_accepts_only_a_currency_object()
{
$currencies = $this->prophesize(Currencies::class);

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

$parser = new ExponentialMoneyParser($currencies->reveal());

$parser->parse('2.8865798640254e+15', 'USD')->getAmount();
}

public function formattedMoneyExamples()
{
return [
['2.8865798640254e+15', 'USD', 2, 288657986402540000],
['2.8865798640254e-15', 'USD', 2, 0],
['0.8865798640254e+15', 'USD', 2, 88657986402540000],
['2.8865798640254e+15', 'JPY', 0, 2886579864025400],
['2.8865798640254e-15', 'JPY', 0, 0],
['0.8865798640254e+15', 'JPY', 0, 886579864025400],
['-2.8865798640254e+15', 'USD', 2, -288657986402540000],
['-2.8865798640254e-15', 'USD', 2, 0],
['-0.8865798640254e+15', 'USD', 2, -88657986402540000],
];
}

public static function invalidMoneyExamples()
{
return [
['INVALID'],
['2.00'],
['2'],
['0.02'],
['.'],
];
}
}