Money::fromDecimal()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 2
dl 0
loc 7
ccs 4
cts 4
cp 1
crap 1
rs 10
c 0
b 0
f 0
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
        $this->amount = $amount;
34 56
        $this->currency = $currency;
35 56
    }
36
37 11
    public static function fromFloat(float $amount, CurrencyContract $currency): Money
38
    {
39
        try {
40 11
            return new self(
41 11
                Decimal::fromFloat($amount, self::INNER_FRACTIONAL_DIGITS),
42 8
                $currency
43
            );
44 3
        } catch (InfiniteInputError $e) {
45 2
            throw new InvalidArgumentException('Currency amounts must be finite', 0, $e);
46 1
        } catch (NaNInputError $e) {
47 1
            throw new InvalidArgumentException('Currency amounts must be numbers', 0, $e);
48
        }
49
    }
50
51 8
    public static function fromFractionalUnits(int $amount, CurrencyContract $currency): Money
52
    {
53 8
        $decimalAmount = Decimal::fromInteger($amount)
54 8
            ->div(
55 8
                Decimal::fromInteger(10 ** $currency->getNumFractionalDigits()),
56 8
                self::INNER_FRACTIONAL_DIGITS
57
            );
58
59 8
        return new self($decimalAmount, $currency);
60
    }
61
62 64
    public static function fromString(string $amount, CurrencyContract $currency): Money
63
    {
64 64
        return new self(
65 64
            self::extractNumericAmount($amount, $currency),
66 32
            $currency
67
        );
68
    }
69
70 64
    private static function extractNumericAmount(string $amount, CurrencyContract $currency): Decimal
71
    {
72
        if (
73 64
            1 === \preg_match(self::SIMPLE_CURRENCY_PATTERN, $amount, $matches) ||
74 56
            1 === \preg_match(self::getAmountPlusIsoCodePattern($currency), $amount, $matches) ||
75 64
            1 === \preg_match(self::getAmountPlusSymbolPattern($currency), $amount, $matches)
76
        ) {
77 32
            return Decimal::fromString($matches['amount'], self::INNER_FRACTIONAL_DIGITS);
78
        }
79
80 32
        throw new InvalidArgumentException('Invalid currency value');
81
    }
82
83
    /**
84
     * @param CurrencyContract $currency
85
     *
86
     * @return string
87
     */
88 56
    private static function getAmountPlusIsoCodePattern(CurrencyContract $currency): string
89
    {
90 56
        $amountPlusIsoCodePattern = '/^'.self::DECIMAL_NUMBER_REGEXP.'\s*'.$currency->getISOCode().'$/x';
91
92 56
        return $amountPlusIsoCodePattern;
93
    }
94
95 48
    private static function getAmountPlusSymbolPattern(CurrencyContract $currency): string
96
    {
97 48
        $escapedSymbol = \preg_quote($currency->getSymbol());
98
99 48
        return (CurrencyContract::BEFORE_PLACEMENT === $currency->getSymbolPlacement())
100 24
            ? '/^'.$escapedSymbol.'\s*'.self::DECIMAL_NUMBER_REGEXP.'$/x'
101 48
            : '/^'.self::DECIMAL_NUMBER_REGEXP.'\s*'.$escapedSymbol.'$/x';
102
    }
103
104 8
    public static function fromDecimal(Decimal $amount, CurrencyContract $currency): Money
105
    {
106 8
        return new self(
107 8
            Decimal::fromDecimal($amount, self::INNER_FRACTIONAL_DIGITS),
108 8
            $currency
109
        );
110
    }
111
112 9
    public function getCurrency(): CurrencyContract
113
    {
114 9
        return $this->currency;
115
    }
116
117
    /**
118
     * {@inheritdoc}
119
     */
120 13
    public function getAmountAsDecimal(): Decimal
121
    {
122 13
        return $this->amount;
123
    }
124
125
    /**
126
     * {@inheritdoc}
127
     */
128 24
    public function getAmountAsFractionalUnits(): int
129
    {
130 24
        return $this->amount
131 24
            ->mul(
132 24
                Decimal::fromInteger(10 ** $this->currency->getNumFractionalDigits()),
133 24
                self::INNER_FRACTIONAL_DIGITS
134
            )
135 24
            ->asInteger();
136
    }
137
138
    /**
139
     * {@inheritdoc}
140
     */
141 126
    public function format(MoneyFormatInterface $currencyFormat = null): string
142
    {
143 126
        if (is_null($currencyFormat)) {
144 24
            $currencyFormat = MoneyFormat::default();
145
        }
146
147 126
        $nDecimals = $currencyFormat->getPrecision();
148 126
        if (is_null($nDecimals)) {
149 110
            $nDecimals = $this->currency->getNumFractionalDigits() + $currencyFormat->getExtraPrecision();
150
        }
151
152 126
        $amount = Decimal::fromDecimal($this->amount, $nDecimals);
153
154 126
        $number = ('' === $currencyFormat->getThousandsSeparator())
155 110
            ? \str_replace('.', $currencyFormat->getDecimalsSeparator(), $amount->__toString())  // This is safer!
156 16
            : \number_format(
157 16
                $amount->asFloat(),
158 16
                $nDecimals,
159 16
                $currencyFormat->getDecimalsSeparator(),
160 126
                $currencyFormat->getThousandsSeparator()
161
            );
162
163 126
        return $this->decorate($number, $currencyFormat);
164
    }
165
166
    /**
167
     * @param string               $number
168
     * @param MoneyFormatInterface $currencyFormat
169
     *
170
     * @return string
171
     */
172 126
    private function decorate(string $number, MoneyFormatInterface $currencyFormat): string
173
    {
174 126
        $separator = (MoneyFormat::DECORATION_WITH_SPACE === $currencyFormat->getDecorationSpace())
175 3
            ? ' '
176 126
            : '';
177
178 126
        switch ($currencyFormat->getDecorationType()) {
179 126
            case MoneyFormat::DECORATION_NO_DECORATION:
180 24
                return $number;
181 102
            case MoneyFormat::DECORATION_ISO_CODE:
182 26
                return $number.$separator.$this->currency->getISOCode();
183
            default:
184 76
                return (CurrencyContract::BEFORE_PLACEMENT === $this->currency->getSymbolPlacement())
185 18
                    ? $this->currency->getSymbol().$separator.$number
186 76
                    : $number.$separator.$this->currency->getSymbol();
187
        }
188
    }
189
190
    /**
191
     * {@inheritdoc}
192
     */
193 12
    public function equals(MoneyContract $currency): bool
194
    {
195 12
        return $currency === $this || (
196 12
                $this->amount->equals($currency->getAmountAsDecimal()) &&
197 12
                $this->currency->equals($currency->getCurrency())
198
            );
199
    }
200
}
201