1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* User: alec |
4
|
|
|
* Date: 05.11.18 |
5
|
|
|
* Time: 23:51 |
6
|
|
|
*/ |
7
|
|
|
|
8
|
|
|
namespace AlecRabbit\Money; |
9
|
|
|
|
10
|
|
|
use AlecRabbit\Money\CalculatorFactory as Factory; |
11
|
|
|
use AlecRabbit\Money\Contracts\CalculatorInterface; |
12
|
|
|
use AlecRabbit\Money\Contracts\MoneyInterface; |
13
|
|
|
|
14
|
|
|
/** |
15
|
|
|
* Money Value Object. |
16
|
|
|
* |
17
|
|
|
* @author Mathias Verraes |
18
|
|
|
*/ |
19
|
|
|
class Money implements MoneyInterface, \JsonSerializable |
20
|
|
|
{ |
21
|
|
|
use MoneyFactory, |
22
|
|
|
MoneyFunctions; |
23
|
|
|
|
24
|
|
|
/** @var CalculatorInterface */ |
25
|
|
|
private $calculator; |
26
|
|
|
|
27
|
|
|
/** @var string */ |
28
|
|
|
private $amount; |
29
|
|
|
|
30
|
|
|
/** @var Currency */ |
31
|
|
|
private $currency; |
32
|
|
|
|
33
|
|
|
/** |
34
|
|
|
* @param null|int|float|string $amount |
35
|
|
|
* @param Currency $currency |
36
|
|
|
* |
37
|
|
|
* @throws \InvalidArgumentException If amount is not integer |
38
|
|
|
*/ |
39
|
137 |
|
public function __construct($amount, Currency $currency) |
40
|
|
|
{ |
41
|
137 |
|
if (null === $amount) { |
42
|
3 |
|
$amount = 0; |
43
|
|
|
} |
44
|
137 |
|
if (!\is_numeric($amount)) { |
45
|
2 |
|
throw new \InvalidArgumentException('Amount must be int|float|string'); |
46
|
|
|
} |
47
|
135 |
|
$this->calculator = Factory::getCalculator(); |
48
|
|
|
|
49
|
135 |
|
$this->setAmount((string)$amount); |
50
|
135 |
|
$this->setCurrency($currency); |
51
|
135 |
|
} |
52
|
|
|
|
53
|
|
|
/** |
54
|
|
|
* @param string $amount |
55
|
|
|
*/ |
56
|
135 |
|
private function setAmount(string $amount): void |
57
|
|
|
{ |
58
|
135 |
|
$this->amount = trim_zeros($amount); |
59
|
135 |
|
} |
60
|
|
|
|
61
|
|
|
/** |
62
|
|
|
* @param Currency $currency |
63
|
|
|
*/ |
64
|
135 |
|
private function setCurrency(Currency $currency): void |
65
|
|
|
{ |
66
|
135 |
|
$this->currency = $currency; |
67
|
135 |
|
} |
68
|
|
|
|
69
|
|
|
|
70
|
|
|
/** |
71
|
|
|
* Asserts that a Money has the same currency as this. |
72
|
|
|
* |
73
|
|
|
* @param Money $other |
74
|
|
|
* |
75
|
|
|
* @throws \InvalidArgumentException If $other has a different currency |
76
|
|
|
*/ |
77
|
42 |
|
private function assertSameCurrency(Money $other): void |
78
|
|
|
{ |
79
|
42 |
|
if (!$this->isSameCurrency($other)) { |
80
|
3 |
|
throw new \InvalidArgumentException('Currencies must be identical.'); |
81
|
|
|
} |
82
|
42 |
|
} |
83
|
|
|
|
84
|
|
|
/** |
85
|
|
|
* Returns an integer less than, equal to, or greater than zero |
86
|
|
|
* if the value of this object is considered to be respectively |
87
|
|
|
* less than, equal to, or greater than the other. |
88
|
|
|
* |
89
|
|
|
* @param Money $other |
90
|
|
|
* |
91
|
|
|
* @return int |
92
|
|
|
*/ |
93
|
7 |
|
public function compare(Money $other): int |
94
|
|
|
{ |
95
|
7 |
|
$this->assertSameCurrency($other); |
96
|
|
|
|
97
|
7 |
|
return $this->calculator->compare($this->amount, $other->amount); |
98
|
|
|
} |
99
|
|
|
|
100
|
|
|
/** |
101
|
|
|
* Checks whether a Money has the same Currency as this. |
102
|
|
|
* |
103
|
|
|
* @param Money $other |
104
|
|
|
* |
105
|
|
|
* @return bool |
106
|
|
|
*/ |
107
|
50 |
|
public function isSameCurrency(Money $other): bool |
108
|
|
|
{ |
109
|
50 |
|
return $this->currency->equals($other->currency); |
110
|
|
|
} |
111
|
|
|
|
112
|
|
|
/** |
113
|
|
|
* Returns a new Money object that represents |
114
|
|
|
* the sum of this and an other Money object. |
115
|
|
|
* |
116
|
|
|
* @param Money ...$addends |
117
|
|
|
* |
118
|
|
|
* @return Money |
119
|
|
|
*/ |
120
|
28 |
|
public function add(Money ...$addends): Money |
121
|
|
|
{ |
122
|
28 |
|
$amount = $this->amount; |
123
|
28 |
|
$calculator = $this->calculator; |
124
|
|
|
|
125
|
28 |
|
foreach ($addends as $addend) { |
126
|
25 |
|
$this->assertSameCurrency($addend); |
127
|
|
|
|
128
|
25 |
|
$amount = $calculator->add($amount, $addend->amount); |
129
|
|
|
} |
130
|
28 |
|
return new self($amount, $this->currency); |
131
|
|
|
} |
132
|
|
|
|
133
|
|
|
/** |
134
|
|
|
* @param Money $first |
135
|
|
|
* @param Money ...$collection |
136
|
|
|
* |
137
|
|
|
* @return Money |
138
|
|
|
*/ |
139
|
5 |
|
public static function avg(Money $first, Money ...$collection): Money |
140
|
|
|
{ |
141
|
5 |
|
return $first->add(...$collection)->divide(\func_num_args()); |
142
|
|
|
} |
143
|
|
|
|
144
|
|
|
/** |
145
|
|
|
* Returns a new Money object that represents |
146
|
|
|
* the divided value by the given factor. |
147
|
|
|
* |
148
|
|
|
* @param float|int|string $divisor |
149
|
|
|
* |
150
|
|
|
* @return Money |
151
|
|
|
*/ |
152
|
16 |
|
public function divide($divisor): Money |
153
|
|
|
{ |
154
|
16 |
|
$this->assertOperand($divisor); |
155
|
|
|
|
156
|
11 |
|
if ($this->calculator->compare((string)$divisor, '0') === 0) { |
157
|
1 |
|
throw new \InvalidArgumentException('Division by zero.'); |
158
|
|
|
} |
159
|
|
|
|
160
|
10 |
|
$quotient = $this->calculator->divide($this->amount, $divisor); |
161
|
|
|
return |
162
|
10 |
|
$this->newInstance($quotient); |
163
|
|
|
} |
164
|
|
|
|
165
|
|
|
/** |
166
|
|
|
* Asserts that the operand is integer or float. |
167
|
|
|
* |
168
|
|
|
* @param float|int|string|object $operand |
169
|
|
|
* |
170
|
|
|
* @throws \InvalidArgumentException If $operand is neither integer nor float |
171
|
|
|
*/ |
172
|
25 |
|
private function assertOperand($operand): void |
173
|
|
|
{ |
174
|
25 |
|
if (!\is_numeric($operand)) { |
175
|
10 |
|
throw new \InvalidArgumentException(sprintf( |
176
|
10 |
|
'Operand should be a numeric value, "%s" given.', |
177
|
10 |
|
\is_object($operand) ? \get_class($operand) : \gettype($operand) |
178
|
|
|
)); |
179
|
|
|
} |
180
|
15 |
|
} |
181
|
|
|
|
182
|
|
|
/** |
183
|
|
|
* Returns a new Money instance based on the current one using the Currency. |
184
|
|
|
* |
185
|
|
|
* @param int|string|float|null $amount |
186
|
|
|
* |
187
|
|
|
* @return Money |
188
|
|
|
* |
189
|
|
|
* @throws \InvalidArgumentException |
190
|
|
|
*/ |
191
|
64 |
|
private function newInstance($amount): Money |
192
|
|
|
{ |
193
|
64 |
|
return new self($amount, $this->currency); |
194
|
|
|
} |
195
|
|
|
|
196
|
|
|
/** |
197
|
|
|
* Checks whether the value represented by this object equals to the other. |
198
|
|
|
* |
199
|
|
|
* @param Money $other |
200
|
|
|
* |
201
|
|
|
* @return bool |
202
|
|
|
*/ |
203
|
8 |
|
public function equals(Money $other): bool |
204
|
|
|
{ |
205
|
8 |
|
return $this->isSameCurrency($other) && $this->amount === $other->amount; |
206
|
|
|
} |
207
|
|
|
|
208
|
|
|
/** |
209
|
|
|
* @param Money $other |
210
|
|
|
* |
211
|
|
|
* @return bool |
212
|
|
|
*/ |
213
|
3 |
|
public function greaterThanOrEqual(Money $other): bool |
214
|
|
|
{ |
215
|
3 |
|
return $this->compare($other) >= 0; |
216
|
|
|
} |
217
|
|
|
|
218
|
|
|
/** |
219
|
|
|
* @param Money $other |
220
|
|
|
* |
221
|
|
|
* @return bool |
222
|
|
|
*/ |
223
|
3 |
|
public function lessThanOrEqual(Money $other): bool |
224
|
|
|
{ |
225
|
3 |
|
return $this->compare($other) <= 0; |
226
|
|
|
} |
227
|
|
|
|
228
|
|
|
/** |
229
|
|
|
* Returns the value represented by this object. |
230
|
|
|
* |
231
|
|
|
* @return string |
232
|
|
|
*/ |
233
|
32 |
|
public function getAmount(): string |
234
|
|
|
{ |
235
|
32 |
|
return $this->amount; |
236
|
|
|
} |
237
|
|
|
|
238
|
|
|
/** |
239
|
|
|
* Returns the currency of this object. |
240
|
|
|
* |
241
|
|
|
* @return Currency |
242
|
|
|
*/ |
243
|
55 |
|
public function getCurrency(): Currency |
244
|
|
|
{ |
245
|
55 |
|
return $this->currency; |
246
|
|
|
} |
247
|
|
|
|
248
|
|
|
/** |
249
|
|
|
* Returns a new Money object that represents |
250
|
|
|
* the multiplied value by the given factor. |
251
|
|
|
* |
252
|
|
|
* @param float|int|string $multiplier |
253
|
|
|
* |
254
|
|
|
* @return Money |
255
|
|
|
*/ |
256
|
9 |
|
public function multiply($multiplier): Money |
257
|
|
|
{ |
258
|
9 |
|
$this->assertOperand($multiplier); |
259
|
|
|
|
260
|
4 |
|
$product = $this->calculator->multiply($this->amount, $multiplier); |
261
|
|
|
|
262
|
|
|
return |
263
|
4 |
|
$this->newInstance($product); |
264
|
|
|
} |
265
|
|
|
|
266
|
|
|
/** |
267
|
|
|
* Returns a new Money object that represents |
268
|
|
|
* the remainder after dividing the value by |
269
|
|
|
* the given factor. |
270
|
|
|
* |
271
|
|
|
* @param Money $divisor |
272
|
|
|
* |
273
|
|
|
* @return Money |
274
|
|
|
*/ |
275
|
4 |
|
public function mod(Money $divisor): Money |
276
|
|
|
{ |
277
|
4 |
|
$this->assertSameCurrency($divisor); |
278
|
|
|
|
279
|
4 |
|
return new self($this->calculator->mod($this->amount, $divisor->amount), $this->currency); |
280
|
|
|
} |
281
|
|
|
|
282
|
|
|
/** |
283
|
|
|
* Allocate the money among N targets. |
284
|
|
|
* |
285
|
|
|
* @param int $n |
286
|
|
|
* |
287
|
|
|
* @param int|null $precision |
288
|
|
|
* @return Money[] |
289
|
|
|
* |
290
|
|
|
*/ |
291
|
7 |
|
public function allocateTo(int $n, ?int $precision = null): array |
292
|
|
|
{ |
293
|
7 |
|
if ($n <= 0) { |
294
|
2 |
|
throw new \InvalidArgumentException('Number to allocateTo must be greater than zero.'); |
295
|
|
|
} |
296
|
|
|
|
297
|
5 |
|
return $this->allocate(array_fill(0, $n, 1), $precision); |
298
|
|
|
} |
299
|
|
|
|
300
|
|
|
/** |
301
|
|
|
* Allocate the money according to a list of ratios. |
302
|
|
|
* |
303
|
|
|
* @param array $ratios |
304
|
|
|
* |
305
|
|
|
* @param int|null $precision |
306
|
|
|
* @return Money[] |
307
|
|
|
*/ |
308
|
37 |
|
public function allocate(array $ratios, ?int $precision = null): array |
309
|
|
|
{ |
310
|
37 |
|
$precision = $precision ?? 2; |
311
|
37 |
|
if (0 === $allocations = \count($ratios)) { |
312
|
1 |
|
throw new \InvalidArgumentException('Cannot allocate to none, ratios cannot be an empty array.'); |
313
|
|
|
} |
314
|
|
|
|
315
|
36 |
|
$remainder = $this->amount; |
316
|
36 |
|
$results = []; |
317
|
36 |
|
$total = array_sum($ratios); |
318
|
|
|
|
319
|
36 |
|
if ($total <= 0) { |
320
|
1 |
|
throw new \InvalidArgumentException('Sum of ratios must be greater than zero.'); |
321
|
|
|
} |
322
|
|
|
|
323
|
35 |
|
foreach ($ratios as $ratio) { |
324
|
35 |
|
if ($ratio < 0) { |
325
|
1 |
|
throw new \InvalidArgumentException('Ratio must be zero or positive.'); |
326
|
|
|
} |
327
|
|
|
|
328
|
35 |
|
$share = $this->calculator->share($this->amount, $ratio, $total, $precision); |
329
|
35 |
|
$results[] = $this->newInstance($share); |
330
|
35 |
|
$remainder = $this->calculator->subtract($remainder, $share); |
331
|
|
|
} |
332
|
34 |
|
switch ($this->calculator->compare($remainder, '0')) { |
333
|
|
|
case -1: |
334
|
13 |
|
for ($i = $allocations - 1; $i >= 0; $i--) { |
335
|
13 |
|
if (!$ratios[$i]) { |
336
|
1 |
|
continue; |
337
|
|
|
} |
338
|
13 |
|
$results[$i]->setAmount($this->calculator->add($results[$i]->amount, $remainder)); |
339
|
13 |
|
break; |
340
|
|
|
} |
341
|
13 |
|
break; |
342
|
21 |
|
case 1: |
343
|
7 |
|
for ($i = 0; $i < $allocations; $i++) { |
344
|
7 |
|
if (!$ratios[$i]) { |
345
|
1 |
|
continue; |
346
|
|
|
} |
347
|
7 |
|
$results[$i]->setAmount($this->calculator->add($results[$i]->amount, $remainder)); |
348
|
7 |
|
break; |
349
|
|
|
} |
350
|
7 |
|
break; |
351
|
|
|
default: |
352
|
14 |
|
break; |
353
|
|
|
} |
354
|
34 |
|
return $results; |
355
|
|
|
} |
356
|
|
|
|
357
|
|
|
/** |
358
|
|
|
* @param Money $money |
359
|
|
|
* |
360
|
|
|
* @return string |
361
|
|
|
*/ |
362
|
2 |
|
public function ratioOf(Money $money): string |
363
|
|
|
{ |
364
|
2 |
|
if ($money->isZero()) { |
365
|
1 |
|
throw new \InvalidArgumentException('Cannot calculate a ratio of zero.'); |
366
|
|
|
} |
367
|
|
|
|
368
|
1 |
|
return $this->calculator->divide($this->amount, $money->amount); |
369
|
|
|
} |
370
|
|
|
|
371
|
|
|
/** |
372
|
|
|
* Checks if the value represented by this object is zero. |
373
|
|
|
* |
374
|
|
|
* @return bool |
375
|
|
|
*/ |
376
|
30 |
|
public function isZero(): bool |
377
|
|
|
{ |
378
|
30 |
|
return $this->calculator->compare($this->amount, '0') === 0; |
379
|
|
|
} |
380
|
|
|
|
381
|
|
|
/** |
382
|
|
|
* @return Money |
383
|
|
|
*/ |
384
|
7 |
|
public function absolute(): Money |
385
|
|
|
{ |
386
|
7 |
|
return $this->newInstance($this->calculator->absolute($this->amount)); |
387
|
|
|
} |
388
|
|
|
|
389
|
|
|
/** |
390
|
|
|
* @return Money |
391
|
|
|
*/ |
392
|
8 |
|
public function negative(): Money |
393
|
|
|
{ |
394
|
8 |
|
return $this->newInstance(0)->subtract($this); |
395
|
|
|
} |
396
|
|
|
|
397
|
|
|
/** |
398
|
|
|
* Returns a new Money object that represents |
399
|
|
|
* the difference of this and an other Money object. |
400
|
|
|
* |
401
|
|
|
* @param Money ...$subtrahends |
402
|
|
|
* |
403
|
|
|
* @return Money |
404
|
|
|
*/ |
405
|
20 |
|
public function subtract(Money ...$subtrahends): Money |
406
|
|
|
{ |
407
|
20 |
|
$amount = $this->amount; |
408
|
20 |
|
$calculator = $this->calculator; |
409
|
|
|
|
410
|
20 |
|
foreach ($subtrahends as $subtrahend) { |
411
|
20 |
|
$this->assertSameCurrency($subtrahend); |
412
|
|
|
|
413
|
20 |
|
$amount = $calculator->subtract($amount, $subtrahend->amount); |
414
|
|
|
} |
415
|
|
|
|
416
|
20 |
|
return new self($amount, $this->currency); |
417
|
|
|
} |
418
|
|
|
|
419
|
|
|
/** |
420
|
|
|
* Checks if the value represented by this object is not negative. |
421
|
|
|
* |
422
|
|
|
* @return bool |
423
|
|
|
*/ |
424
|
6 |
|
public function isNotNegative(): bool |
425
|
|
|
{ |
426
|
|
|
return |
427
|
6 |
|
!$this->isNegative(); |
428
|
|
|
} |
429
|
|
|
|
430
|
|
|
/** |
431
|
|
|
* Checks if the value represented by this object is negative. |
432
|
|
|
* |
433
|
|
|
* @return bool |
434
|
|
|
*/ |
435
|
13 |
|
public function isNegative(): bool |
436
|
|
|
{ |
437
|
13 |
|
return $this->calculator->compare($this->amount, '0') === -1; |
438
|
|
|
} |
439
|
|
|
|
440
|
|
|
/** |
441
|
|
|
* Checks if the value represented by this object is not zero. |
442
|
|
|
* |
443
|
|
|
* @return bool |
444
|
|
|
*/ |
445
|
6 |
|
public function isNotZero(): bool |
446
|
|
|
{ |
447
|
|
|
return |
448
|
6 |
|
!$this->isZero(); |
449
|
|
|
} |
450
|
|
|
|
451
|
|
|
/** |
452
|
|
|
* {@inheritdoc} |
453
|
|
|
* |
454
|
|
|
* @return array |
455
|
|
|
*/ |
456
|
1 |
|
public function jsonSerialize(): array |
457
|
|
|
{ |
458
|
|
|
return [ |
459
|
1 |
|
'amount' => $this->amount, |
460
|
1 |
|
'currency' => $this->currency, |
461
|
|
|
]; |
462
|
|
|
} |
463
|
|
|
|
464
|
|
|
/** |
465
|
|
|
* Checks if the value represented by this object is not positive. |
466
|
|
|
* |
467
|
|
|
* @return bool |
468
|
|
|
*/ |
469
|
17 |
|
public function isNotPositive(): bool |
470
|
|
|
{ |
471
|
|
|
return |
472
|
17 |
|
!$this->isPositive(); |
473
|
|
|
} |
474
|
|
|
|
475
|
|
|
/** |
476
|
|
|
* Checks if the value represented by this object is positive. |
477
|
|
|
* |
478
|
|
|
* @return bool |
479
|
|
|
*/ |
480
|
23 |
|
public function isPositive(): bool |
481
|
|
|
{ |
482
|
23 |
|
return $this->calculator->compare($this->amount, '0') === 1; |
483
|
|
|
} |
484
|
|
|
} |
485
|
|
|
|