Completed
Push — master ( db6fea...5e07c3 )
by Andreu
02:48 queued 01:21
created

Money::getCurrency()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
crap 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Adsmurai\Currency;
6
7
use Adsmurai\Currency\Contracts\Money as MoneyContract;
8
use Adsmurai\Currency\Contracts\MoneyFormat as MoneyFormatInterface;
9
use Adsmurai\Currency\Contracts\Currency as CurrencyContract;
10
use InvalidArgumentException;
11
use Litipk\BigNumbers\Decimal;
12
use Litipk\BigNumbers\Errors\InfiniteInputError;
13
use Litipk\BigNumbers\Errors\NaNInputError;
14
15
final class Money implements MoneyContract
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 CurrencyContract */
25
    private $currency;
26
27
    /**
28
     * @param Decimal      $amount
29
     * @param CurrencyContract $currency
30
     */
31 56
    private function __construct(Decimal $amount, CurrencyContract $currency)
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->currency = $currency;
39 44
    }
40
41 11
    public static function fromFloat(float $amount, CurrencyContract $currency): Money
42
    {
43
        try {
44 11
            return new self(
45 11
                Decimal::fromFloat($amount, self::INNER_FRACTIONAL_DIGITS),
46
                $currency
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, CurrencyContract $currency): Money
56
    {
57 8
        $decimalAmount = Decimal::fromInteger($amount)
58 8
            ->div(
59 8
                Decimal::fromInteger(10 ** $currency->getNumFractionalDigits()),
60 8
                self::INNER_FRACTIONAL_DIGITS
61
            );
62
63 8
        return new self($decimalAmount, $currency);
64
    }
65
66 64
    public static function fromString(string $amount, CurrencyContract $currency): Money
67
    {
68 64
        return new self(
69 64
            self::extractNumericAmount($amount, $currency),
70
            $currency
71
        );
72
    }
73
74 64
    private static function extractNumericAmount(string $amount, CurrencyContract $currency): Decimal
75
    {
76
        try {
77
            if (
78 64
                1 === \preg_match(self::SIMPLE_CURRENCY_PATTERN, $amount, $matches) ||
79 56
                1 === \preg_match(self::getAmountPlusIsoCodePattern($currency), $amount, $matches) ||
80 64
                1 === \preg_match(self::getAmountPlusSymbolPattern($currency), $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('Money amounts must be numbers', 0, $e);
88
        }
89
    }
90
91
    /**
92
     * @param CurrencyContract $currency
93
     *
94
     * @return string
95
     */
96 56
    private static function getAmountPlusIsoCodePattern(CurrencyContract $currency): string
97
    {
98 56
        $amountPlusIsoCodePattern = '/^'.self::DECIMAL_NUMBER_REGEXP.'\s*'.$currency->getISOCode().'$/x';
99
100 56
        return $amountPlusIsoCodePattern;
101
    }
102
103 48
    private static function getAmountPlusSymbolPattern(CurrencyContract $currency): string
104
    {
105 48
        $escapedSymbol = \preg_quote($currency->getSymbol());
106
107 48
        return (CurrencyContract::BEFORE_PLACEMENT === $currency->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, CurrencyContract $currency): Money
113
    {
114 8
        return new self(
115 8
            Decimal::fromDecimal($amount, self::INNER_FRACTIONAL_DIGITS),
116
            $currency
117
        );
118
    }
119
120 9
    public function getCurrency(): CurrencyContract
121
    {
122 9
        return $this->currency;
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->currency->getNumFractionalDigits()),
141 24
                self::INNER_FRACTIONAL_DIGITS
142
            )
143 24
            ->asInteger();
144
    }
145
146
    /**
147
     * {@inheritdoc}
148
     */
149 126
    public function format(MoneyFormatInterface $currencyFormat = null): string
150
    {
151 126
        if (is_null($currencyFormat)) {
152 24
            $currencyFormat = MoneyFormat::default();
153
        }
154
155 126
        $nDecimals = $currencyFormat->getPrecision();
156 126
        if (is_null($nDecimals)) {
157 110
            $nDecimals = $this->currency->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 MoneyFormatInterface $currencyFormat
177
     *
178
     * @return string
179
     */
180 126
    private function decorate(string $number, MoneyFormatInterface $currencyFormat): string
181
    {
182 126
        $separator = (MoneyFormat::DECORATION_WITH_SPACE === $currencyFormat->getDecorationSpace())
183 3
            ? ' '
184 126
            : '';
185
186 126
        switch ($currencyFormat->getDecorationType()) {
187 126
            case MoneyFormat::DECORATION_NO_DECORATION:
188 24
                return $number;
189 102
            case MoneyFormat::DECORATION_ISO_CODE:
190 26
                return $number.$separator.$this->currency->getISOCode();
191
            default:
192 76
                return (CurrencyContract::BEFORE_PLACEMENT === $this->currency->getSymbolPlacement())
193 18
                    ? $this->currency->getSymbol().$separator.$number
194 76
                    : $number.$separator.$this->currency->getSymbol();
195
        }
196
    }
197
198
    /**
199
     * {@inheritdoc}
200
     */
201 12
    public function equals(MoneyContract $currency): bool
202
    {
203 12
        return $currency === $this || (
204 12
                $this->amount->equals($currency->getAmountAsDecimal()) &&
205 12
                $this->currency->equals($currency->getCurrency())
206
            );
207
    }
208
}
209