Completed
Branch BUG/11288/fix-datepicker (d15367)
by
unknown
108:07 queued 94:31
created

Money::parseAmount()   B

Complexity

Conditions 4
Paths 5

Size

Total Lines 37
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

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