Completed
Push — master ( f58852...1d5ae8 )
by Andreu
01:52
created

Currency::getAmountPlusIsoCodePattern()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 6
ccs 3
cts 3
cp 1
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 3
nc 1
nop 1
crap 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Adsmurai\Currency;
6
7
use Adsmurai\Currency\Contracts\Currency as CurrencyInterface;
8
use Adsmurai\Currency\Contracts\CurrencyFormat as CurrencyFormatInterface;
9
use Adsmurai\Currency\Contracts\CurrencyType;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Adsmurai\Currency\CurrencyType.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
10
use InvalidArgumentException;
11
use Litipk\BigNumbers\Decimal;
12
use Litipk\BigNumbers\Errors\InfiniteInputError;
13
use Litipk\BigNumbers\Errors\NaNInputError;
14
15
final class Currency implements CurrencyInterface
16
{
17
    const DECIMAL_NUMBER_REGEXP = '(?P<amount> 0*(([1-9][0-9]*|[0-9])(\.[0-9]+)?))';
18
    const SIMPLE_CURRENCY_PATTERN = '/^'.self::DECIMAL_NUMBER_REGEXP.'$/x';
19
    const INNER_FRACTIONAL_DIGITS = 8;
20
21
    /** @var Decimal */
22
    private $amount;
23
24
    /** @var CurrencyType */
25
    private $currencyType;
26
27
    /**
28
     * @param Decimal      $amount
29
     * @param CurrencyType $currencyType
30
     */
31 56
    private function __construct(Decimal $amount, CurrencyType $currencyType)
32
    {
33 56
        if ($amount->isNegative()) {
34 12
            throw new InvalidArgumentException('Currency amounts must be positive');
35
        }
36
37 44
        $this->amount = $amount;
38 44
        $this->currencyType = $currencyType;
39 44
    }
40
41 11
    public static function fromFloat(float $amount, CurrencyType $currencyType): Currency
42
    {
43
        try {
44 11
            return new self(
45 11
                Decimal::fromFloat($amount, self::INNER_FRACTIONAL_DIGITS),
46
                $currencyType
47
            );
48 7
        } catch (InfiniteInputError $e) {
49 2
            throw new InvalidArgumentException('Currency amounts must be finite', 0, $e);
50 5
        } catch (NaNInputError $e) {
51 1
            throw new InvalidArgumentException('Currency amounts must be numbers', 0, $e);
52
        }
53
    }
54
55 8
    public static function fromFractionalUnits(int $amount, CurrencyType $currencyType): Currency
56
    {
57 8
        $decimalAmount = Decimal::fromInteger($amount)
58 8
            ->div(
59 8
                Decimal::fromInteger(10 ** $currencyType->getNumFractionalDigits()),
60 8
                self::INNER_FRACTIONAL_DIGITS
61
            );
62
63 8
        return new self($decimalAmount, $currencyType);
64
    }
65
66 64
    public static function fromString(string $amount, CurrencyType $currencyType): Currency
67
    {
68 64
        return new self(
69 64
            self::extractNumericAmount($amount, $currencyType),
70
            $currencyType
71
        );
72
    }
73
74 64
    private static function extractNumericAmount(string $amount, CurrencyType $currencyType): Decimal
75
    {
76
        try {
77
            if (
78 64
                1 === \preg_match(self::SIMPLE_CURRENCY_PATTERN, $amount, $matches) ||
79 56
                1 === \preg_match(self::getAmountPlusIsoCodePattern($currencyType), $amount, $matches) ||
80 64
                1 === \preg_match(self::getAmountPlusSymbolPattern($currencyType), $amount, $matches)
81
            ) {
82 32
                return Decimal::fromString($matches['amount'], self::INNER_FRACTIONAL_DIGITS);
83
            } else {
84 32
                throw new InvalidArgumentException('Invalid currency value');
85
            }
86 32
        } catch (NaNInputError $e) {
87
            throw new InvalidArgumentException('Currency amounts must be numbers', 0, $e);
88
        }
89
    }
90
91
    /**
92
     * @param CurrencyType $currencyType
93
     *
94
     * @return string
95
     */
96 56
    private static function getAmountPlusIsoCodePattern(CurrencyType $currencyType): string
97
    {
98 56
        $amountPlusIsoCodePattern = '/^'.self::DECIMAL_NUMBER_REGEXP.'\s*'.$currencyType->getISOCode().'$/x';
99
100 56
        return $amountPlusIsoCodePattern;
101
    }
102
103 48
    private static function getAmountPlusSymbolPattern(CurrencyType $currencyType): string
104
    {
105 48
        $escapedSymbol = \preg_quote($currencyType->getSymbol());
106
107 48
        return (CurrencyType::BEFORE_PLACEMENT === $currencyType->getSymbolPlacement())
108 24
            ? '/^'.$escapedSymbol.'\s*'.self::DECIMAL_NUMBER_REGEXP.'$/x'
109 48
            : '/^'.self::DECIMAL_NUMBER_REGEXP.'\s*'.$escapedSymbol.'$/x';
110
    }
111
112 8
    public static function fromDecimal(Decimal $amount, CurrencyType $currencyType): Currency
113
    {
114 8
        return new self(
115 8
            Decimal::fromDecimal($amount, self::INNER_FRACTIONAL_DIGITS),
116
            $currencyType
117
        );
118
    }
119
120 9
    public function getCurrencyType(): CurrencyType
121
    {
122 9
        return $this->currencyType;
123
    }
124
125
    /**
126
     * {@inheritdoc}
127
     */
128 13
    public function getAmountAsDecimal(): Decimal
129
    {
130 13
        return $this->amount;
131
    }
132
133
    /**
134
     * {@inheritdoc}
135
     */
136 24
    public function getAmountAsFractionalUnits(): int
137
    {
138 24
        return $this->amount
139 24
            ->mul(
140 24
                Decimal::fromInteger(10 ** $this->currencyType->getNumFractionalDigits()),
141 24
                self::INNER_FRACTIONAL_DIGITS
142
            )
143 24
            ->asInteger();
144
    }
145
146
    /**
147
     * {@inheritdoc}
148
     */
149 126
    public function format(CurrencyFormatInterface $currencyFormat = null): string
150
    {
151 126
        if (is_null($currencyFormat)) {
152 24
            $currencyFormat = CurrencyFormat::default();
153
        }
154
155 126
        $nDecimals = $currencyFormat->getPrecision();
156 126
        if (is_null($nDecimals)) {
157 110
            $nDecimals = $this->currencyType->getNumFractionalDigits() + $currencyFormat->getExtraPrecision();
158
        }
159
160 126
        $amount = Decimal::fromDecimal($this->amount, $nDecimals);
161
162 126
        $number = ('' === $currencyFormat->getThousandsSeparator())
163 110
            ? \str_replace('.', $currencyFormat->getDecimalsSeparator(), $amount->__toString())  // This is safer!
164 16
            : \number_format(
165 16
                $amount->asFloat(),
166
                $nDecimals,
167 16
                $currencyFormat->getDecimalsSeparator(),
168 126
                $currencyFormat->getThousandsSeparator()
169
            );
170
171 126
        return $this->decorate($number, $currencyFormat);
172
    }
173
174
    /**
175
     * @param string                  $number
176
     * @param CurrencyFormatInterface $currencyFormat
177
     *
178
     * @return string
179
     */
180
    private function decorate(string $number, CurrencyFormatInterface $currencyFormat): string
181
    {
182
        $separator = (CurrencyFormat::DECORATION_WITH_SPACE === $currencyFormat->getDecorationSpace())
183
            ? ' '
184
            : '';
185
        switch ($currencyFormat->getDecorationType()) {
186
            case CurrencyFormat::DECORATION_NO_DECORATION:
187
                return $number;
188
                break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
189
            case CurrencyFormat::DECORATION_ISO_CODE:
190
                return $number.$separator.$this->currencyType->getISOCode();
191
                break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
192
            default:
193
                return (CurrencyType::BEFORE_PLACEMENT === $this->currencyType->getSymbolPlacement())
194
                    ? $this->currencyType->getSymbol().$separator.$number
195
                    : $number.$separator.$this->currencyType->getSymbol();
196
        }
197
    }
198
199
    /**
200
     * {@inheritdoc}
201
     */
202 12
    public function equals(CurrencyInterface $currency): bool
203
    {
204 12
        return $currency === $this || (
205 12
                $this->amount->equals($currency->getAmountAsDecimal()) &&
206 12
                $this->currencyType->equals($currency->getCurrencyType())
207
            );
208
    }
209
}
210