Open Monograph Press  3.3.0
Money.php
1 <?php
2 
3 namespace Money;
4 
8 
16 final class Money implements \JsonSerializable
17 {
18  use MoneyFactory;
19 
20  const ROUND_HALF_UP = PHP_ROUND_HALF_UP;
21 
22  const ROUND_HALF_DOWN = PHP_ROUND_HALF_DOWN;
23 
24  const ROUND_HALF_EVEN = PHP_ROUND_HALF_EVEN;
25 
26  const ROUND_HALF_ODD = PHP_ROUND_HALF_ODD;
27 
28  const ROUND_UP = 5;
29 
30  const ROUND_DOWN = 6;
31 
32  const ROUND_HALF_POSITIVE_INFINITY = 7;
33 
34  const ROUND_HALF_NEGATIVE_INFINITY = 8;
35 
41  private $amount;
42 
46  private $currency;
47 
51  private static $calculator;
52 
56  private static $calculators = [
57  BcMathCalculator::class,
58  GmpCalculator::class,
59  PhpCalculator::class,
60  ];
61 
68  public function __construct($amount, Currency $currency)
69  {
70  if (filter_var($amount, FILTER_VALIDATE_INT) === false) {
71  $numberFromString = Number::fromString($amount);
72  if (!$numberFromString->isInteger()) {
73  throw new \InvalidArgumentException('Amount must be an integer(ish) value');
74  }
75 
76  $amount = $numberFromString->getIntegerPart();
77  }
78 
79  $this->amount = (string) $amount;
80  $this->currency = $currency;
81  }
82 
92  private function newInstance($amount)
93  {
94  return new self($amount, $this->currency);
95  }
96 
104  public function isSameCurrency(Money $other)
105  {
106  return $this->currency->equals($other->currency);
107  }
108 
116  private function assertSameCurrency(Money $other)
117  {
118  if (!$this->isSameCurrency($other)) {
119  throw new \InvalidArgumentException('Currencies must be identical');
120  }
121  }
122 
130  public function equals(Money $other)
131  {
132  return $this->isSameCurrency($other) && $this->amount === $other->amount;
133  }
134 
144  public function compare(Money $other)
145  {
146  $this->assertSameCurrency($other);
147 
148  return $this->getCalculator()->compare($this->amount, $other->amount);
149  }
150 
158  public function greaterThan(Money $other)
159  {
160  return $this->compare($other) > 0;
161  }
162 
168  public function greaterThanOrEqual(Money $other)
169  {
170  return $this->compare($other) >= 0;
171  }
172 
180  public function lessThan(Money $other)
181  {
182  return $this->compare($other) < 0;
183  }
184 
190  public function lessThanOrEqual(Money $other)
191  {
192  return $this->compare($other) <= 0;
193  }
194 
200  public function getAmount()
201  {
202  return $this->amount;
203  }
204 
210  public function getCurrency()
211  {
212  return $this->currency;
213  }
214 
223  public function add(Money ...$addends)
224  {
225  $amount = $this->amount;
226  $calculator = $this->getCalculator();
227 
228  foreach ($addends as $addend) {
229  $this->assertSameCurrency($addend);
230 
231  $amount = $calculator->add($amount, $addend->amount);
232  }
233 
234  return new self($amount, $this->currency);
235  }
236 
247  public function subtract(Money ...$subtrahends)
248  {
249  $amount = $this->amount;
250  $calculator = $this->getCalculator();
251 
252  foreach ($subtrahends as $subtrahend) {
253  $this->assertSameCurrency($subtrahend);
254 
255  $amount = $calculator->subtract($amount, $subtrahend->amount);
256  }
257 
258  return new self($amount, $this->currency);
259  }
260 
268  private function assertOperand($operand)
269  {
270  if (!is_numeric($operand)) {
271  throw new \InvalidArgumentException(sprintf(
272  'Operand should be a numeric value, "%s" given.',
273  is_object($operand) ? get_class($operand) : gettype($operand)
274  ));
275  }
276  }
277 
285  private function assertRoundingMode($roundingMode)
286  {
287  if (!in_array(
288  $roundingMode, [
289  self::ROUND_HALF_DOWN, self::ROUND_HALF_EVEN, self::ROUND_HALF_ODD,
290  self::ROUND_HALF_UP, self::ROUND_UP, self::ROUND_DOWN,
291  self::ROUND_HALF_POSITIVE_INFINITY, self::ROUND_HALF_NEGATIVE_INFINITY,
292  ], true
293  )) {
294  throw new \InvalidArgumentException(
295  'Rounding mode should be Money::ROUND_HALF_DOWN | '.
296  'Money::ROUND_HALF_EVEN | Money::ROUND_HALF_ODD | '.
297  'Money::ROUND_HALF_UP | Money::ROUND_UP | Money::ROUND_DOWN'.
298  'Money::ROUND_HALF_POSITIVE_INFINITY | Money::ROUND_HALF_NEGATIVE_INFINITY'
299  );
300  }
301  }
302 
312  public function multiply($multiplier, $roundingMode = self::ROUND_HALF_UP)
313  {
314  $this->assertOperand($multiplier);
315  $this->assertRoundingMode($roundingMode);
316 
317  $product = $this->round($this->getCalculator()->multiply($this->amount, $multiplier), $roundingMode);
318 
319  return $this->newInstance($product);
320  }
321 
331  public function divide($divisor, $roundingMode = self::ROUND_HALF_UP)
332  {
333  $this->assertOperand($divisor);
334  $this->assertRoundingMode($roundingMode);
335 
336  $divisor = (string) Number::fromNumber($divisor);
337 
338  if ($this->getCalculator()->compare($divisor, '0') === 0) {
339  throw new \InvalidArgumentException('Division by zero');
340  }
341 
342  $quotient = $this->round($this->getCalculator()->divide($this->amount, $divisor), $roundingMode);
343 
344  return $this->newInstance($quotient);
345  }
346 
356  public function mod(Money $divisor)
357  {
358  $this->assertSameCurrency($divisor);
359 
360  return new self($this->getCalculator()->mod($this->amount, $divisor->amount), $this->currency);
361  }
362 
370  public function allocate(array $ratios)
371  {
372  if (count($ratios) === 0) {
373  throw new \InvalidArgumentException('Cannot allocate to none, ratios cannot be an empty array');
374  }
375 
376  $remainder = $this->amount;
377  $results = [];
378  $total = array_sum($ratios);
379 
380  if ($total <= 0) {
381  throw new \InvalidArgumentException('Cannot allocate to none, sum of ratios must be greater than zero');
382  }
383 
384  foreach ($ratios as $key => $ratio) {
385  if ($ratio < 0) {
386  throw new \InvalidArgumentException('Cannot allocate to none, ratio must be zero or positive');
387  }
388  $share = $this->getCalculator()->share($this->amount, $ratio, $total);
389  $results[$key] = $this->newInstance($share);
390  $remainder = $this->getCalculator()->subtract($remainder, $share);
391  }
392 
393  if ($this->getCalculator()->compare($remainder, '0') === 0) {
394  return $results;
395  }
396 
397  $fractions = array_map(function ($ratio) use ($total) {
398  $share = ($ratio / $total) * $this->amount;
399 
400  return $share - floor($share);
401  }, $ratios);
402 
403  while ($this->getCalculator()->compare($remainder, '0') > 0) {
404  $index = !empty($fractions) ? array_keys($fractions, max($fractions))[0] : 0;
405  $results[$index]->amount = $this->getCalculator()->add($results[$index]->amount, '1');
406  $remainder = $this->getCalculator()->subtract($remainder, '1');
407  unset($fractions[$index]);
408  }
409 
410  return $results;
411  }
412 
422  public function allocateTo($n)
423  {
424  if (!is_int($n)) {
425  throw new \InvalidArgumentException('Number of targets must be an integer');
426  }
427 
428  if ($n <= 0) {
429  throw new \InvalidArgumentException('Cannot allocate to none, target must be greater than zero');
430  }
431 
432  return $this->allocate(array_fill(0, $n, 1));
433  }
434 
440  public function ratioOf(Money $money)
441  {
442  if ($money->isZero()) {
443  throw new \InvalidArgumentException('Cannot calculate a ratio of zero');
444  }
445 
446  return $this->getCalculator()->divide($this->amount, $money->amount);
447  }
448 
455  private function round($amount, $rounding_mode)
456  {
457  $this->assertRoundingMode($rounding_mode);
458 
459  if ($rounding_mode === self::ROUND_UP) {
460  return $this->getCalculator()->ceil($amount);
461  }
462 
463  if ($rounding_mode === self::ROUND_DOWN) {
464  return $this->getCalculator()->floor($amount);
465  }
466 
467  return $this->getCalculator()->round($amount, $rounding_mode);
468  }
469 
473  public function absolute()
474  {
475  return $this->newInstance($this->getCalculator()->absolute($this->amount));
476  }
477 
481  public function negative()
482  {
483  return $this->newInstance(0)->subtract($this);
484  }
485 
491  public function isZero()
492  {
493  return $this->getCalculator()->compare($this->amount, 0) === 0;
494  }
495 
501  public function isPositive()
502  {
503  return $this->getCalculator()->compare($this->amount, 0) > 0;
504  }
505 
511  public function isNegative()
512  {
513  return $this->getCalculator()->compare($this->amount, 0) < 0;
514  }
515 
521  public function jsonSerialize()
522  {
523  return [
524  'amount' => $this->amount,
525  'currency' => $this->currency->jsonSerialize(),
526  ];
527  }
528 
537  public static function min(self $first, self ...$collection)
538  {
539  $min = $first;
540 
541  foreach ($collection as $money) {
542  if ($money->lessThan($min)) {
543  $min = $money;
544  }
545  }
546 
547  return $min;
548  }
549 
558  public static function max(self $first, self ...$collection)
559  {
560  $max = $first;
561 
562  foreach ($collection as $money) {
563  if ($money->greaterThan($max)) {
564  $max = $money;
565  }
566  }
567 
568  return $max;
569  }
570 
579  public static function sum(self $first, self ...$collection)
580  {
581  return $first->add(...$collection);
582  }
583 
592  public static function avg(self $first, self ...$collection)
593  {
594  return $first->add(...$collection)->divide(func_num_args());
595  }
596 
600  public static function registerCalculator($calculator)
601  {
602  if (is_a($calculator, Calculator::class, true) === false) {
603  throw new \InvalidArgumentException('Calculator must implement '.Calculator::class);
604  }
605 
606  array_unshift(self::$calculators, $calculator);
607  }
608 
614  private static function initializeCalculator()
615  {
616  $calculators = self::$calculators;
617 
618  foreach ($calculators as $calculator) {
620  if ($calculator::supported()) {
621  return new $calculator();
622  }
623  }
624 
625  throw new \RuntimeException('Cannot find calculator for money calculations');
626  }
627 
631  private function getCalculator()
632  {
633  if (null === self::$calculator) {
634  self::$calculator = self::initializeCalculator();
635  }
636 
637  return self::$calculator;
638  }
639 }
Money\Money\registerCalculator
static registerCalculator($calculator)
Definition: Money.php:606
Money\Money\negative
negative()
Definition: Money.php:487
Money\Money\isNegative
isNegative()
Definition: Money.php:517
Money\Money\compare
compare(Money $other)
Definition: Money.php:150
Money\Money\subtract
subtract(Money ... $subtrahends)
Definition: Money.php:253
Money\Money\max
static max(self $first, self ... $collection)
Definition: Money.php:564
Money
Money\Money\avg
static avg(self $first, self ... $collection)
Definition: Money.php:598
Money\Calculator\GmpCalculator
Definition: GmpCalculator.php:12
Money\Money\lessThan
lessThan(Money $other)
Definition: Money.php:186
Money\Calculator\BcMathCalculator
Definition: BcMathCalculator.php:12
Money\Money\allocate
allocate(array $ratios)
Definition: Money.php:376
Money\Number\fromNumber
static fromNumber($number)
Definition: Number.php:84
Money\Money\isSameCurrency
isSameCurrency(Money $other)
Definition: Money.php:110
Money\Money\jsonSerialize
jsonSerialize()
Definition: Money.php:527
Money\Money\divide
divide($divisor, $roundingMode=self::ROUND_HALF_UP)
Definition: Money.php:337
Money\Money\isZero
isZero()
Definition: Money.php:497
Money\Currency\equals
equals(Currency $other)
Definition: vendor/moneyphp/money/src/Currency.php:59
Money\Money\equals
equals(Money $other)
Definition: Money.php:136
Money\Money\isPositive
isPositive()
Definition: Money.php:507
Money\Money\lessThanOrEqual
lessThanOrEqual(Money $other)
Definition: Money.php:196
Money\Currency
Definition: vendor/moneyphp/money/src/Currency.php:14
Money\Money\greaterThan
greaterThan(Money $other)
Definition: Money.php:164
Money\Number\fromString
static fromString($number)
Definition: Number.php:52
Money\Money\add
add(Money ... $addends)
Definition: Money.php:229
Money\Money\absolute
absolute()
Definition: Money.php:479
Money\Money\multiply
multiply($multiplier, $roundingMode=self::ROUND_HALF_UP)
Definition: Money.php:318
Money\Calculator\PhpCalculator
Definition: PhpCalculator.php:12
Money\Money\ratioOf
ratioOf(Money $money)
Definition: Money.php:446
Money\Money\__construct
__construct($amount, Currency $currency)
Definition: Money.php:74
Money\Money\mod
mod(Money $divisor)
Definition: Money.php:362
Money\Money\allocateTo
allocateTo($n)
Definition: Money.php:428
Money\Money\greaterThanOrEqual
greaterThanOrEqual(Money $other)
Definition: Money.php:174
Money\Money\getCurrency
getCurrency()
Definition: Money.php:216
Money\Money\getAmount
getAmount()
Definition: Money.php:206
Money\Money\min
static min(self $first, self ... $collection)
Definition: Money.php:543
Money\Money\sum
static sum(self $first, self ... $collection)
Definition: Money.php:585