Money::castValueToString()   A
last analyzed

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 2
Bugs 0 Features 1
Metric Value
c 2
b 0
f 1
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
cc 1
eloc 2
nc 1
nop 1
crap 1
1
<?php namespace Keios\MoneyRight;
2
3
/**
4
 * This file is part of the arbitrary precision arithmetic-based
5
 * money value object Keios\MoneyRight package. Keios\MoneyRight
6
 * is heavily inspired by Mathias Verroes' Money library and was
7
 * designed to be a drop-in replacement (only some use statement
8
 * tweaking required) for mentioned library. Public APIs are
9
 * identical, functionality is extended with additional methods
10
 * and parameters.
11
 *
12
 *
13
 * Copyright (c) 2015 Łukasz Biały
14
 *
15
 * For the full copyright and license information, please view the
16
 * LICENSE file that was distributed with this source code.
17
 */
18
use JsonSerializable;
19
use Keios\MoneyRight\Exceptions\InvalidArgumentException;
20
use Serializable;
21
22
/**
23
 * Class Money
24
 * Money Value Object
25
 *
26
 * @package Keios\MoneyRight
27
 */
28
class Money implements Serializable, JsonSerializable
29
{
30
31
    /**
32
     * @const
33
     */
34
    const GAAP_PRECISION = 4;
35
36
    /**
37
     * @const
38
     */
39
    const BASIC_PRECISION = 2;
40
41
    /**
42
     * @const
43
     */
44
    const ROUND_HALF_UP = PHP_ROUND_HALF_UP;
45
46
    /**
47
     * @const
48
     */
49
    const ROUND_HALF_DOWN = PHP_ROUND_HALF_DOWN;
50
51
    /**
52
     * @const
53
     */
54
    const ROUND_HALF_EVEN = PHP_ROUND_HALF_EVEN;
55
56
    /**
57
     * @const
58
     */
59
    const ROUND_HALF_ODD = PHP_ROUND_HALF_ODD;
60
61
    /**
62
     * @var string
63
     */
64
    private $amount;
65
66
    /**
67
     * @var \Keios\MoneyRight\Currency
68
     */
69
    private $currency;
70
71
    /**
72
     * Create a new Money Instance
73
     *
74
     * @param string                     $amount
75
     * @param \Keios\MoneyRight\Currency $currency
76
     */
77 183
    public function __construct($amount, Currency $currency)
78
    {
79 183
        if (is_int($amount)) {
80 60
            $amount = $amount / 100;
81 60
        }
82
83 183
        $stringAmount = $this->castValueToString($amount);
84
85 183
        $this->currency = $currency;
86 183
        $this->bootWith($stringAmount);
87 180
    }
88
89
    /**
90
     * Convenience factory method for an Keios\MoneyRight\Money object
91
     *
92
     * @example $fiveDollar = Money::USD(500);
93
     *
94
     * @param string $method
95
     * @param array  $arguments
96
     *
97
     * @return \Keios\MoneyRight\Money
98
     */
99 33
    public static function __callStatic($method, $arguments)
100
    {
101 33
        return new Money($arguments[0], new Currency($method));
102
    }
103
104
    // SERIALIZATION
105
106
    /**
107
     * Serialization of Money value object
108
     *
109
     * @return string
110
     */
111 3
    public function serialize()
112
    {
113 3
        return serialize(
114
            [
115 3
                'amount' => $this->amount,
116 3
                'currency' => serialize($this->currency),
117
            ]
118 3
        );
119
    }
120
121
    /**
122
     * Unserializing Money value object
123
     *
124
     * @param string $serialized
125
     */
126 3
    public function unserialize($serialized)
127
    {
128 3
        $unserialized = unserialize($serialized);
129
130 3
        $this->amount = $unserialized['amount'];
131 3
        $this->currency = unserialize($unserialized['currency']);
132 3
    }
133
134
    /**
135
     * @return string
136
     */
137
    public function jsonserialize()
138
    {
139
        return [
140
            'amount' => $this->amount,
141
            'currency' => $this->currency->jsonserialize(),
142
        ];
143
    }
144
145
    // GETTERS
146
147
    /**
148
     * Return full GAAP precision amount string
149
     *
150
     * @return string
151
     */
152 78
    public function getAmountString()
153
    {
154 78
        return $this->amount;
155
    }
156
157
    /**
158
     * Compatibility with Verraes' Money
159
     *
160
     * @return integer
161
     */
162 21
    public function getAmount()
163
    {
164 21
        return (integer)bcmul('100', Math::bcround($this->amount, self::BASIC_PRECISION));
165
    }
166
167
    /**
168
     * Useful for payment systems that don't use high precision
169
     *
170
     * @param int $roundingMode
171
     *
172
     * @return string
173
     */
174
    public function getAmountBasic($roundingMode = self::ROUND_HALF_UP)
175
    {
176
        return Math::bcround($this->amount, self::BASIC_PRECISION, $roundingMode);
177
    }
178
179
    /**
180
     * @return \Keios\MoneyRight\Currency
181
     */
182 93
    public function getCurrency()
183
    {
184 93
        return $this->currency;
185
    }
186
187
    // LOGIC
188
189
    /**
190
     * @param \Keios\MoneyRight\Money
191
     *
192
     * @return bool
193
     */
194 6
    public function greaterThan(Money $other)
195
    {
196 6
        return 1 == $this->compare($other);
197
    }
198
199
    /**
200
     * @param \Keios\MoneyRight\Money $other
201
     *
202
     * @return bool
203
     */
204
    public function greaterThanOrEqual(Money $other)
205
    {
206
        return 0 <= $this->compare($other);
207
    }
208
209
    /**
210
     * @param \Keios\MoneyRight\Money $other
211
     *
212
     * @return bool
213
     */
214 6
    public function lessThan(Money $other)
215
    {
216 6
        return -1 == $this->compare($other);
217
    }
218
219
    /**
220
     * @param \Keios\MoneyRight\Money $other
221
     *
222
     * @return bool
223
     */
224
    public function lessThanOrEqual(Money $other)
225
    {
226
        return 0 >= $this->compare($other);
227
    }
228
229
    /**
230
     * Compatibility with Verraes' Money
231
     *
232
     * @deprecated Use getAmount() instead
233
     * @return int
234
     */
235
    public function getUnits()
236
    {
237
        return $this->amount;
238
    }
239
240
    /**
241
     * @param \Keios\MoneyRight\Money $other
242
     *
243
     * @return bool
244
     */
245 78
    public function isSameCurrency(Money $other)
246
    {
247 78
        return $this->currency->equals($other->getCurrency());
248
    }
249
250
    /**
251
     * @param \Keios\MoneyRight\Money $other
252
     *
253
     * @return bool
254
     */
255 33
    public function equals(Money $other)
256
    {
257 33
        return $this->isSameCurrency($other) && $this->isSameAmount($other);
258
    }
259
260
    /**
261
     * @param \Keios\MoneyRight\Money $other
262
     *
263
     * @return int
264
     * @throws \Keios\MoneyRight\Exceptions\InvalidArgumentException
265
     */
266 15
    public function compare(Money $other)
267
    {
268 15
        $this->assertSameCurrency($other);
269
270 9
        return bccomp($this->amount, $other->getAmountString(), self::GAAP_PRECISION);
271
    }
272
273
    /**
274
     * @param \Keios\MoneyRight\Money $addend
275
     *
276
     * @return \Keios\MoneyRight\Money
277
     * @throws \Keios\MoneyRight\Exceptions\InvalidArgumentException
278
     */
279 24
    public function add(Money $addend)
280
    {
281 24
        $this->assertSameCurrency($addend);
282
283 18
        return new Money(bcadd($this->amount, $addend->getAmountString(), self::GAAP_PRECISION), $this->currency);
284
    }
285
286
    /**
287
     * @param \Keios\MoneyRight\Money $subtrahend
288
     *
289
     * @return \Keios\MoneyRight\Money
290
     * @throws \Keios\MoneyRight\Exceptions\InvalidArgumentException
291
     */
292 12
    public function subtract(Money $subtrahend)
293
    {
294 12
        $this->assertSameCurrency($subtrahend);
295
296 6
        return new Money(bcsub($this->amount, $subtrahend->getAmountString(), self::GAAP_PRECISION), $this->currency);
297
    }
298
299
    /**
300
     * Multiplying compatible with Verraes' Money
301
     * To use GAAP precision rounding, pass true as third argument
302
     *
303
     * @param      $operand
304
     * @param int  $roundingMode
305
     * @param bool $useGaapPrecision
306
     *
307
     * @return \Keios\MoneyRight\Money
308
     * @throws \Keios\MoneyRight\Exceptions\InvalidArgumentException
309
     */
310 6
    public function multiply($operand, $roundingMode = self::ROUND_HALF_UP, $useGaapPrecision = false)
311
    {
312 6
        $this->assertOperand($operand);
313
314 6
        $validatedOperand = $this->normalizeOperand($operand);
315
316 6
        if ($useGaapPrecision) {
317 3
            $amount = Math::bcround(
318 3
                bcmul($this->amount, $validatedOperand, self::GAAP_PRECISION + 1),
319 3
                self::GAAP_PRECISION,
320
                $roundingMode
321 3
            );
322 3
        } else {
323 3
            $amount = Math::bcround(
324 3
                bcmul($this->amount, $validatedOperand, self::GAAP_PRECISION + 1),
325 3
                self::BASIC_PRECISION,
326
                $roundingMode
327 3
            );
328
        }
329
330 6
        return new Money($amount, $this->currency);
331
    }
332
333
    /**
334
     * Division compatible with Verraes' Money
335
     * To use GAAP precision rounding, pass true as third argument
336
     *
337
     * @param      $operand
338
     * @param int  $roundingMode
339
     * @param bool $useGaapPrecision
340
     *
341
     * @return \Keios\MoneyRight\Money
342
     * @throws \Keios\MoneyRight\Exceptions\InvalidArgumentException
343
     */
344 9
    public function divide($operand, $roundingMode = self::ROUND_HALF_UP, $useGaapPrecision = false)
345
    {
346 9
        $this->assertOperand($operand, true);
347
348 6
        $validatedOperand = $this->normalizeOperand($operand);
349
350 6
        if ($useGaapPrecision) {
351 3
            $amount = Math::bcround(
352 3
                bcdiv($this->amount, $validatedOperand, self::GAAP_PRECISION + 1),
353 3
                self::GAAP_PRECISION,
354
                $roundingMode
355 3
            );
356 3
        } else {
357 3
            $amount = Math::bcround(
358 3
                bcdiv($this->amount, $validatedOperand, self::GAAP_PRECISION + 1),
359 3
                self::BASIC_PRECISION,
360
                $roundingMode
361 3
            );
362
        }
363
364 6
        return new Money($amount, $this->currency);
365
    }
366
367
    /**
368
     * Allocate the money according to a list of ratio's
369
     * Allocation is compatible with Verraes' Money
370
     * To use GAAP precision rounding, pass true as second argument
371
     *
372
     * @param array $ratios List of ratio's
373
     * @param bool  $useGaapPrecision
374
     *
375
     * @return array
376
     */
377 9
    public function allocate(array $ratios, $useGaapPrecision = false)
378
    {
379 9
        $useGaapPrecision ? $precision = self::GAAP_PRECISION : $precision = self::BASIC_PRECISION;
380
381 9
        $remainder = $this->amount;
382 9
        $results = [];
383 9
        $total = array_sum($ratios);
384
385 9
        foreach ($ratios as $ratio) {
386 9
            $share = bcdiv(
387 9
                bcmul(
388 9
                    $this->amount,
389 9
                    (string)$ratio,
390
                    $precision
391 9
                ),
392 9
                (string)$total,
393
                $precision
394 9
            );
395 9
            $results[] = new Money($share, $this->currency);
396 9
            $remainder = bcsub($remainder, $share, $precision);
397 9
        }
398
399 9
        $count = count($results) - 1;
400 9
        $index = 0;
401 9
        $minValue = '0.'.str_repeat('0', $precision - 1).'1';
402
403 9
        while (bccomp($remainder, '0'.str_repeat('0', $precision), $precision) !== 0) {
404 9
            $remainder = bcsub($remainder, $minValue, $precision);
405 9
            $results[$index] = $results[$index]->add(new Money($minValue, $this->currency));
406 9
            if ($index !== $count) {
407 9
                $index++;
408 9
            } else {
409
                $index = 0;
410
            }
411 9
        }
412
413 9
        return $results;
414
    }
415
416
    /**
417
     * @return \Keios\MoneyRight\Money
418
     */
419 3
    public function abs()
420
    {
421 3
        $amount = $this->isNegative() ? substr($this->amount, 1): $this->amount;
422
        
423 3
        return new Money($amount, $this->currency);
424
    }
425
426
    /**
427
     * @return bool
428
     */
429 6
    public function isZero()
430
    {
431 6
        return bccomp('0', $this->amount, self::GAAP_PRECISION) === 0;
432
    }
433
434
    /**
435
     * @return bool
436
     */
437 6
    public function isPositive()
438
    {
439 6
        return bccomp($this->amount, '0', self::GAAP_PRECISION) === 1;
440
    }
441
442
    /**
443
     * @return bool
444
     */
445 9
    public function isNegative()
446
    {
447 9
        return bccomp($this->amount, '0', self::GAAP_PRECISION) === -1;
448
    }
449
450
    // INTERNALS
451
452
    /**
453
     * @param mixed $value
454
     *
455
     * @return string
456
     */
457 183
    private function castValueToString($value)
458
    {
459 183
        return (string)$value;
460
    }
461
462
    /**
463
     * @param $stringAmount
464
     *
465
     * @throws \Keios\MoneyRight\Exceptions\InvalidArgumentException
466
     */
467 183
    private function bootWith($stringAmount)
468
    {
469 183
        $this->assertValidAmountString($stringAmount);
470 180
        $this->amount = $this->normalizeAmountFromString($stringAmount);
471 180
    }
472
473
    /**
474
     * @param $string
475
     *
476
     * @return string
477
     */
478 183
    private function normalizeString($string)
479
    {
480 183
        return str_replace(',', '.', $string);
481
    }
482
483
    /**
484
     * @param $operand
485
     *
486
     * @return mixed
487
     */
488 15
    private function normalizeOperand($operand)
489
    {
490 15
        return $this->normalizeString($this->castValueToString($operand));
491
    }
492
493
    /**
494
     * @param $stringAmount
495
     *
496
     * @return string
497
     */
498 180
    private function normalizeAmountFromString($stringAmount)
499
    {
500 180
        $normalizedAmount = $this->normalizeString($stringAmount);
501
502 180
        return Math::bcround($normalizedAmount, self::GAAP_PRECISION);
503
    }
504
505
    /**
506
     * @param \Keios\MoneyRight\Money $other
507
     *
508
     * @return bool
509
     */
510 30
    private function isSameAmount(Money $other)
511
    {
512 30
        return bccomp($this->getAmountString(), $other->getAmountString(), self::GAAP_PRECISION) === 0;
513
    }
514
515
    // ASSERTIONS
516
517
    /**
518
     * @param $stringAmount
519
     *
520
     * @throws \Keios\MoneyRight\Exceptions\InvalidArgumentException
521
     */
522 183
    private function assertValidAmountString($stringAmount)
523
    {
524 183
        if (!is_numeric($this->normalizeString($stringAmount))) {
525 3
            throw new InvalidArgumentException(sprintf('Value %s is not a valid money amount.', $stringAmount));
526
        }
527 180
    }
528
529
    /**
530
     * @param $stringOperand
531
     *
532
     * @throws \Keios\MoneyRight\Exceptions\InvalidArgumentException
533
     */
534 15
    private function assertValidOperandString($stringOperand)
535
    {
536 15
        if (!is_numeric($this->normalizeString($stringOperand))) {
537
            throw new InvalidArgumentException(sprintf('Value %s is not a valid operand.', $stringOperand));
538
        }
539 15
    }
540
541
    /**
542
     * @param \Keios\MoneyRight\Money $other
543
     *
544
     * @throws \Keios\MoneyRight\Exceptions\InvalidArgumentException
545
     */
546 51
    private function assertSameCurrency(Money $other)
547
    {
548 51
        if (!$this->isSameCurrency($other)) {
549 18
            throw new InvalidArgumentException(
550 18
                sprintf(
551 18
                    'Cannot add Money with different currency. Have: %s, given: %s.',
552 18
                    $this->currency->getIsoCode(),
553 18
                    $other->getCurrency()->getIsoCode()
554 18
                )
555 18
            );
556
        }
557 33
    }
558
559
    /**
560
     * @param int|float $operand
561
     * @param bool      $isDivision
562
     *
563
     * @throws \Keios\MoneyRight\Exceptions\InvalidArgumentException
564
     */
565 15
    private function assertOperand($operand, $isDivision = false)
566
    {
567 15
        if (!is_int($operand) && !is_float($operand) && !is_string($operand)) {
568
            throw new InvalidArgumentException(
569
                sprintf(
570
                    'Operand should be a string, integer or a float, %s of value %s given.',
571
                    gettype($operand),
572
                    (string)$operand
573
                )
574
            );
575
        }
576
577 15
        $this->assertValidOperandString($this->castValueToString($operand));
578
579 15
        if ($isDivision) {
580 9
            $this->assertOperandNotZero($operand);
581 6
        }
582 12
    }
583
584
    /**
585
     * @param $operand
586
     *
587
     * @throws \Keios\MoneyRight\Exceptions\InvalidArgumentException
588
     */
589 9
    private function assertOperandNotZero($operand)
590
    {
591 9
        $normalizedOperand = $this->normalizeOperand($operand);
592 9
        if ($normalizedOperand === '0' || $normalizedOperand === '-0') {
593 3
            throw new InvalidArgumentException('Division by zero.');
594
        }
595 6
    }
596
597
    // STATIC METHODS
598
599
    /**
600
     * @param $string
601
     *
602
     * @throws \Keios\MoneyRight\Exceptions\InvalidArgumentException
603
     * @return int
604
     */
605 51
    public static function stringToUnits($string)
606
    {
607 51
        new Money($string, new Currency('USD')); // TODO optimize?
608
609 51
        return (int)bcmul($string, '100', self::GAAP_PRECISION);
610
    }
611
}
612