Completed
Branch FET-10619-money-entity (f4b3f8)
by
unknown
139:32 queued 127:27
created

Money::parseAmount()   B

Complexity

Conditions 5
Paths 7

Size

Total Lines 39
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 25
nc 7
nop 1
dl 0
loc 39
rs 8.439
c 0
b 0
f 0
1
<?php
2
3
namespace EventEspresso\core\domain\values\currency;
4
5
use EE_Error;
6
use EventEspresso\core\exceptions\InvalidDataTypeException;
7
use EventEspresso\core\exceptions\InvalidInterfaceException;
8
use EventEspresso\core\services\currency\Calculator;
9
use EventEspresso\core\services\currency\CreateCurrency;
10
use EventEspresso\core\services\currency\CreateMoney;
11
use EventEspresso\core\services\currency\MoneyFormatter;
12
use InvalidArgumentException;
13
14
defined('EVENT_ESPRESSO_VERSION') || exit;
15
16
17
18
/**
19
 * Class Money
20
 * Immutable object representing an amount of money for a particular currency
21
 *
22
 * @package       Event Espresso
23
 * @author        Brent Christensen
24
 * @since         $VID:$
25
 */
26
class Money
27
{
28
29
    /**
30
     * number of decimal places to be added to currencies for internal computations,
31
     * but removed before any output or formatting is applied.
32
     * This allows us to avoid rounding errors during calculations.
33
     */
34
    const EXTRA_PRECISION = 3;
35
36
    /**
37
     * @var int $amount
38
     */
39
    private $amount;
40
41
    /**
42
     * @var Currency $currency
43
     */
44
    private $currency;
45
46
    /**
47
     * @var Calculator $calculator
48
     */
49
    protected $calculator;
50
51
    /**
52
     * @var MoneyFormatter[] $formatters
53
     */
54
    protected $formatters;
55
56
57
58
    /**
59
     * Money constructor.
60
     *
61
     * @param float|int|string $amount money amount IN THE STANDARD UNIT FOR THE CURRENCY ie: dollars, Euros, etc
62
     *                                 example: $12.5 USD would equate to a value amount of 12.50
63
     * @param Currency         $currency
64
     * @param Calculator       $calculator
65
     * @param MoneyFormatter[] $formatters
66
     * @throws InvalidDataTypeException
67
     */
68
    public function __construct($amount, Currency $currency, Calculator $calculator, array $formatters)
69
    {
70
        $this->currency   = $currency;
71
        $this->amount     = (string) $this->parseAmount($amount);
72
        $this->calculator = $calculator;
73
        $this->formatters = $formatters;
74
    }
75
76
77
78
    /**
79
     * @return Calculator
80
     */
81
    protected function calculator()
82
    {
83
        return $this->calculator;
84
    }
85
86
87
88
    /**
89
     * @return MoneyFormatter[]
90
     */
91
    protected function formatters()
92
    {
93
        return $this->formatters;
94
    }
95
96
97
98
    /**
99
     * @param float|int|string $amount money amount IN THE STANDARD UNIT FOR THE CURRENCY ie: dollars, Euros, etc
100
     *                                 example: $12.5 USD would equate to a value amount of 12.50
101
     * @return float|int|number|string
102
     * @throws InvalidDataTypeException
103
     */
104
    private function parseAmount($amount)
105
    {
106
        $type = gettype($amount);
107
        switch ($type) {
108
            case 'integer' :
109
            case 'double' :
110
            case 'string' :
111
                break;
112
            default  :
113
                throw new InvalidDataTypeException(
114
                    '$amount',
115
                    $amount,
116
                    'integer (or float or string)'
117
                );
118
        }
119
        if ($this->currency->decimalMark() !== '.') {
120
            // remove thousands separator and replace decimal place with standard decimal.
121
            $amount = str_replace(
122
                array(
123
                    $this->currency->thousands(),
124
                    $this->currency->decimalMark(),
125
                ),
126
                array(
127
                    '',
128
                    '.',
129
                ),
130
                $amount
131
            );
132
        }
133
        // remove any non numeric values but leave the decimal
134
        $amount = (float) preg_replace('/([^0-9\\.])/', '', $amount);
135
        // shift the decimal position by the number of decimal places used internally
136
        // ex: 12.5 for a currency using 2 decimal places, would become 1250
137
        // then if our extra internal precision was 3, it would become 1250000
138
        $amount *= pow(10, $this->precision());
139
        // then round up the remaining value if there is still a fractional amount left
140
        $amount = round($amount);
141
        return $amount;
142
    }
143
144
145
146
    /**
147
     * adds or subtracts additional decimal places based on the value of the Money::EXTRA_PRECISION constant
148
     *
149
     * @param bool $positive
150
     * @return int
151
     */
152
    private function precision($positive = true)
153
    {
154
        $sign = $positive ? 1 : -1;
155
        return ((int) $this->currency->decimalPlaces() + Money::EXTRA_PRECISION) * $sign;
156
    }
157
158
159
160
    /**
161
     * Returns the money amount as an unformatted string
162
     * IF YOU REQUIRE A FORMATTED STRING, THEN USE Money::format()
163
     *
164
     * @return string
165
     */
166
    public function amount()
167
    {
168
        // shift the decimal position BACK by the number of decimal places used internally
169
        // ex: 1250 for a currency using 2 decimal places, would become 12.50
170
        $amount = (string) $this->amount * pow(10, $this->precision(false));
171
        // then shave off our extra internal precision using the number of decimal places for the currency
172
        $amount = round($amount, $this->currency->decimalPlaces());
173
        return $amount;
174
    }
175
176
177
178
    /**
179
     * Returns the money SUBUNITS amount as an INTEGER
180
     *
181
     * @return integer
182
     */
183
    public function amountInSubunits()
184
    {
185
        // shift the decimal position BACK
186
        // by the number of decimal places used internally by the extra internal precision
187
        // ex: if our extra internal precision was 3,
188
        // then 1250000 would become 1250
189
        $amount = (string) $this->amount * pow(10, Money::EXTRA_PRECISION * -1);
190
        // then shave off anything after the decimal
191
        $amount = round($amount);
192
        return $amount;
193
    }
194
195
196
197
    /**
198
     * applies formatting based on the specified formatting level
199
     * corresponding to one of the constants on \EventEspresso\core\services\currency\MoneyFormatter
200
     *
201
     * @param int $formatting_level
202
     * @return string
203
     */
204
    public function format($formatting_level = MoneyFormatter::ADD_THOUSANDS)
205
    {
206
        $formatted_amount = $this->amount();
207
        $formatters       = $this->formatters();
208
        // if we are applying thousands formatting...
209
        if ($formatting_level >= MoneyFormatter::ADD_THOUSANDS) {
210
            // then let's remove decimal formatting since it's included in thousands formatting
211
            unset($formatters[ MoneyFormatter::DECIMAL_ONLY ]);
212
        }
213
        for ($x = 1; $x <= $formatting_level; $x++) {
214
            if (isset($formatters[ $x ]) && $formatters[ $x ] instanceof MoneyFormatter) {
215
                $formatted_amount = $formatters[ $x ]->format($formatted_amount, $this->currency);
216
            }
217
        }
218
        return $formatted_amount;
219
    }
220
221
222
223
    /**
224
     * Returns the Currency object for this money
225
     *
226
     * @return Currency
227
     */
228
    public function currency()
229
    {
230
        return $this->currency;
231
    }
232
233
234
235
    /**
236
     * adds the supplied Money amount to this Money amount
237
     * and returns a new Money object
238
     *
239
     * @param Money $other
240
     * @return Money
241
     * @throws InvalidArgumentException
242
     */
243 View Code Duplication
    public function add(Money $other)
244
    {
245
        $this->verifySameCurrency($other->currency());
246
        return new Money(
247
            $this->calculator()->add(
248
                $this->amount(),
249
                $other->amount()
250
            ),
251
            $this->currency(),
252
            $this->calculator(),
253
            $this->formatters()
254
        );
255
    }
256
257
258
259
    /**
260
     * subtracts the supplied Money amount from this Money amount
261
     * and returns a new Money object
262
     *
263
     * @param Money $other
264
     * @return Money
265
     * @throws InvalidArgumentException
266
     */
267 View Code Duplication
    public function subtract(Money $other)
268
    {
269
        $this->verifySameCurrency($other->currency());
270
        return new Money(
271
            $this->calculator()->subtract(
272
                $this->amount(),
273
                $other->amount()
274
            ),
275
            $this->currency(),
276
            $this->calculator(),
277
            $this->formatters()
278
        );
279
    }
280
281
282
283
    /**
284
     * multiplies this Money amount by the supplied $multiplier
285
     * and returns a new Money object
286
     *
287
     * @param float|int|string $multiplier
288
     * @param int              $rounding_mode
289
     * @return Money
290
     * @throws InvalidDataTypeException
291
     */
292 View Code Duplication
    public function multiply($multiplier, $rounding_mode = Calculator::ROUND_HALF_UP)
293
    {
294
        return new Money(
295
            $this->calculator()->multiply(
296
                $this->amount(),
297
                $multiplier,
298
                $this->precision(),
299
                $rounding_mode
300
            ),
301
            $this->currency(),
302
            $this->calculator(),
303
            $this->formatters()
304
        );
305
    }
306
307
308
309
    /**
310
     * divides this Money amount by the supplied $divisor
311
     * and returns a new Money object
312
     *
313
     * @param float|int|string $divisor
314
     * @param int              $rounding_mode
315
     * @return Money
316
     * @throws InvalidDataTypeException
317
     */
318 View Code Duplication
    public function divide($divisor, $rounding_mode = Calculator::ROUND_HALF_UP)
319
    {
320
        return new Money(
321
            $this->calculator()->divide(
322
                $this->amount(),
323
                $divisor,
324
                $this->precision(),
325
                $rounding_mode
326
            ),
327
            $this->currency(),
328
            $this->calculator(),
329
            $this->formatters()
330
        );
331
    }
332
333
334
335
    /**
336
     * @param Currency $other_currency
337
     * @throws InvalidArgumentException
338
     */
339
    public function verifySameCurrency(Currency $other_currency)
340
    {
341
        if ($this->currency()->equals($other_currency) !== true) {
342
            throw new InvalidArgumentException(
343
                esc_html__(
344
                    'Currencies must be the same in order to add or subtract their values.',
345
                    'event_espresso'
346
                )
347
            );
348
        }
349
    }
350
351
352
353
    /**
354
     * @return string
355
     */
356
    public function __toString()
357
    {
358
        return $this->format(MoneyFormatter::DECIMAL_ONLY);
359
    }
360
361
362
363
    /**
364
     * factory method that returns a Money object for the currency specified as if it were a class method
365
     * example: Money::USD(12.5);
366
     * money amount IN THE STANDARD UNIT FOR THE CURRENCY ie: dollars, Euros, etc
367
     * example: $12.5 USD would equate to a value amount of 12.50
368
     *
369
     * @param string $currency_code
370
     * @param array  $arguments
371
     * @return Money
372
     * @throws InvalidArgumentException
373
     * @throws InvalidDataTypeException
374
     * @throws EE_Error
375
     * @throws InvalidInterfaceException
376
     */
377
    public static function __callStatic($currency_code, $arguments)
378
    {
379
        return new Money(
380
            $arguments[0],
381
            CreateCurrency::fromCode($currency_code),
382
            CreateMoney::calculator(),
383
            CreateMoney::formatters()
384
        );
385
    }
386
387
388
}
389
// End of file Money.php
390
// Location: core/entities/money/Money.php
391