Calculator
4 years ago
Currencies
4 years ago
Exception
4 years ago
Exchange
4 years ago
Formatter
4 years ago
PHPUnit
4 years ago
Parser
4 years ago
Calculator.php
4 years ago
Converter.php
4 years ago
Currencies.php
4 years ago
Currency.php
4 years ago
CurrencyPair.php
4 years ago
Exception.php
4 years ago
Exchange.php
4 years ago
Money.php
4 years ago
MoneyFactory.php
4 years ago
MoneyFormatter.php
4 years ago
MoneyParser.php
4 years ago
Number.php
4 years ago
Money.php
640 lines
| 1 | <?php |
| 2 | |
| 3 | namespace Money; |
| 4 | |
| 5 | use Money\Calculator\BcMathCalculator; |
| 6 | use Money\Calculator\GmpCalculator; |
| 7 | use Money\Calculator\PhpCalculator; |
| 8 | |
| 9 | /** |
| 10 | * Money Value Object. |
| 11 | * |
| 12 | * @author Mathias Verraes |
| 13 | * |
| 14 | * @psalm-immutable |
| 15 | */ |
| 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 | |
| 36 | /** |
| 37 | * Internal value. |
| 38 | * |
| 39 | * @var string |
| 40 | */ |
| 41 | private $amount; |
| 42 | |
| 43 | /** |
| 44 | * @var Currency |
| 45 | */ |
| 46 | private $currency; |
| 47 | |
| 48 | /** |
| 49 | * @var Calculator |
| 50 | */ |
| 51 | private static $calculator; |
| 52 | |
| 53 | /** |
| 54 | * @var array |
| 55 | */ |
| 56 | private static $calculators = [ |
| 57 | BcMathCalculator::class, |
| 58 | GmpCalculator::class, |
| 59 | PhpCalculator::class, |
| 60 | ]; |
| 61 | |
| 62 | /** |
| 63 | * @param int|string $amount Amount, expressed in the smallest units of $currency (eg cents) |
| 64 | * @param Currency $currency |
| 65 | * |
| 66 | * @throws \InvalidArgumentException If amount is not integer |
| 67 | */ |
| 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 | |
| 83 | /** |
| 84 | * Returns a new Money instance based on the current one using the Currency. |
| 85 | * |
| 86 | * @param int|string $amount |
| 87 | * |
| 88 | * @return Money |
| 89 | * |
| 90 | * @throws \InvalidArgumentException If amount is not integer |
| 91 | */ |
| 92 | private function newInstance($amount) |
| 93 | { |
| 94 | return new self($amount, $this->currency); |
| 95 | } |
| 96 | |
| 97 | /** |
| 98 | * Checks whether a Money has the same Currency as this. |
| 99 | * |
| 100 | * @param Money $other |
| 101 | * |
| 102 | * @return bool |
| 103 | */ |
| 104 | public function isSameCurrency(Money $other) |
| 105 | { |
| 106 | return $this->currency->equals($other->currency); |
| 107 | } |
| 108 | |
| 109 | /** |
| 110 | * Asserts that a Money has the same currency as this. |
| 111 | * |
| 112 | * @param Money $other |
| 113 | * |
| 114 | * @throws \InvalidArgumentException If $other has a different currency |
| 115 | */ |
| 116 | private function assertSameCurrency(Money $other) |
| 117 | { |
| 118 | if (!$this->isSameCurrency($other)) { |
| 119 | throw new \InvalidArgumentException('Currencies must be identical'); |
| 120 | } |
| 121 | } |
| 122 | |
| 123 | /** |
| 124 | * Checks whether the value represented by this object equals to the other. |
| 125 | * |
| 126 | * @param Money $other |
| 127 | * |
| 128 | * @return bool |
| 129 | */ |
| 130 | public function equals(Money $other) |
| 131 | { |
| 132 | return $this->isSameCurrency($other) && $this->amount === $other->amount; |
| 133 | } |
| 134 | |
| 135 | /** |
| 136 | * Returns an integer less than, equal to, or greater than zero |
| 137 | * if the value of this object is considered to be respectively |
| 138 | * less than, equal to, or greater than the other. |
| 139 | * |
| 140 | * @param Money $other |
| 141 | * |
| 142 | * @return int |
| 143 | */ |
| 144 | public function compare(Money $other) |
| 145 | { |
| 146 | $this->assertSameCurrency($other); |
| 147 | |
| 148 | return $this->getCalculator()->compare($this->amount, $other->amount); |
| 149 | } |
| 150 | |
| 151 | /** |
| 152 | * Checks whether the value represented by this object is greater than the other. |
| 153 | * |
| 154 | * @param Money $other |
| 155 | * |
| 156 | * @return bool |
| 157 | */ |
| 158 | public function greaterThan(Money $other) |
| 159 | { |
| 160 | return $this->compare($other) > 0; |
| 161 | } |
| 162 | |
| 163 | /** |
| 164 | * @param \Money\Money $other |
| 165 | * |
| 166 | * @return bool |
| 167 | */ |
| 168 | public function greaterThanOrEqual(Money $other) |
| 169 | { |
| 170 | return $this->compare($other) >= 0; |
| 171 | } |
| 172 | |
| 173 | /** |
| 174 | * Checks whether the value represented by this object is less than the other. |
| 175 | * |
| 176 | * @param Money $other |
| 177 | * |
| 178 | * @return bool |
| 179 | */ |
| 180 | public function lessThan(Money $other) |
| 181 | { |
| 182 | return $this->compare($other) < 0; |
| 183 | } |
| 184 | |
| 185 | /** |
| 186 | * @param \Money\Money $other |
| 187 | * |
| 188 | * @return bool |
| 189 | */ |
| 190 | public function lessThanOrEqual(Money $other) |
| 191 | { |
| 192 | return $this->compare($other) <= 0; |
| 193 | } |
| 194 | |
| 195 | /** |
| 196 | * Returns the value represented by this object. |
| 197 | * |
| 198 | * @return string |
| 199 | */ |
| 200 | public function getAmount() |
| 201 | { |
| 202 | return $this->amount; |
| 203 | } |
| 204 | |
| 205 | /** |
| 206 | * Returns the currency of this object. |
| 207 | * |
| 208 | * @return Currency |
| 209 | */ |
| 210 | public function getCurrency() |
| 211 | { |
| 212 | return $this->currency; |
| 213 | } |
| 214 | |
| 215 | /** |
| 216 | * Returns a new Money object that represents |
| 217 | * the sum of this and an other Money object. |
| 218 | * |
| 219 | * @param Money[] $addends |
| 220 | * |
| 221 | * @return Money |
| 222 | */ |
| 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 | |
| 237 | /** |
| 238 | * Returns a new Money object that represents |
| 239 | * the difference of this and an other Money object. |
| 240 | * |
| 241 | * @param Money[] $subtrahends |
| 242 | * |
| 243 | * @return Money |
| 244 | * |
| 245 | * @psalm-pure |
| 246 | */ |
| 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 | |
| 261 | /** |
| 262 | * Asserts that the operand is integer or float. |
| 263 | * |
| 264 | * @param float|int|string $operand |
| 265 | * |
| 266 | * @throws \InvalidArgumentException If $operand is neither integer nor float |
| 267 | */ |
| 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 | |
| 278 | /** |
| 279 | * Asserts that rounding mode is a valid integer value. |
| 280 | * |
| 281 | * @param int $roundingMode |
| 282 | * |
| 283 | * @throws \InvalidArgumentException If $roundingMode is not valid |
| 284 | */ |
| 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 | |
| 303 | /** |
| 304 | * Returns a new Money object that represents |
| 305 | * the multiplied value by the given factor. |
| 306 | * |
| 307 | * @param float|int|string $multiplier |
| 308 | * @param int $roundingMode |
| 309 | * |
| 310 | * @return Money |
| 311 | */ |
| 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 | |
| 322 | /** |
| 323 | * Returns a new Money object that represents |
| 324 | * the divided value by the given factor. |
| 325 | * |
| 326 | * @param float|int|string $divisor |
| 327 | * @param int $roundingMode |
| 328 | * |
| 329 | * @return Money |
| 330 | */ |
| 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 | |
| 347 | /** |
| 348 | * Returns a new Money object that represents |
| 349 | * the remainder after dividing the value by |
| 350 | * the given factor. |
| 351 | * |
| 352 | * @param Money $divisor |
| 353 | * |
| 354 | * @return Money |
| 355 | */ |
| 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 | |
| 363 | /** |
| 364 | * Allocate the money according to a list of ratios. |
| 365 | * |
| 366 | * @param array $ratios |
| 367 | * |
| 368 | * @return Money[] |
| 369 | */ |
| 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 | |
| 413 | /** |
| 414 | * Allocate the money among N targets. |
| 415 | * |
| 416 | * @param int $n |
| 417 | * |
| 418 | * @return Money[] |
| 419 | * |
| 420 | * @throws \InvalidArgumentException If number of targets is not an integer |
| 421 | */ |
| 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 | |
| 435 | /** |
| 436 | * @param Money $money |
| 437 | * |
| 438 | * @return string |
| 439 | */ |
| 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 | |
| 449 | /** |
| 450 | * @param string $amount |
| 451 | * @param int $rounding_mode |
| 452 | * |
| 453 | * @return string |
| 454 | */ |
| 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 | |
| 470 | /** |
| 471 | * @return Money |
| 472 | */ |
| 473 | public function absolute() |
| 474 | { |
| 475 | return $this->newInstance($this->getCalculator()->absolute($this->amount)); |
| 476 | } |
| 477 | |
| 478 | /** |
| 479 | * @return Money |
| 480 | */ |
| 481 | public function negative() |
| 482 | { |
| 483 | return $this->newInstance(0)->subtract($this); |
| 484 | } |
| 485 | |
| 486 | /** |
| 487 | * Checks if the value represented by this object is zero. |
| 488 | * |
| 489 | * @return bool |
| 490 | */ |
| 491 | public function isZero() |
| 492 | { |
| 493 | return $this->getCalculator()->compare($this->amount, 0) === 0; |
| 494 | } |
| 495 | |
| 496 | /** |
| 497 | * Checks if the value represented by this object is positive. |
| 498 | * |
| 499 | * @return bool |
| 500 | */ |
| 501 | public function isPositive() |
| 502 | { |
| 503 | return $this->getCalculator()->compare($this->amount, 0) > 0; |
| 504 | } |
| 505 | |
| 506 | /** |
| 507 | * Checks if the value represented by this object is negative. |
| 508 | * |
| 509 | * @return bool |
| 510 | */ |
| 511 | public function isNegative() |
| 512 | { |
| 513 | return $this->getCalculator()->compare($this->amount, 0) < 0; |
| 514 | } |
| 515 | |
| 516 | /** |
| 517 | * {@inheritdoc} |
| 518 | * |
| 519 | * @return array |
| 520 | */ |
| 521 | public function jsonSerialize() |
| 522 | { |
| 523 | return [ |
| 524 | 'amount' => $this->amount, |
| 525 | 'currency' => $this->currency->jsonSerialize(), |
| 526 | ]; |
| 527 | } |
| 528 | |
| 529 | /** |
| 530 | * @param Money $first |
| 531 | * @param Money ...$collection |
| 532 | * |
| 533 | * @return Money |
| 534 | * |
| 535 | * @psalm-pure |
| 536 | */ |
| 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 | |
| 550 | /** |
| 551 | * @param Money $first |
| 552 | * @param Money ...$collection |
| 553 | * |
| 554 | * @return Money |
| 555 | * |
| 556 | * @psalm-pure |
| 557 | */ |
| 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 | |
| 571 | /** |
| 572 | * @param Money $first |
| 573 | * @param Money ...$collection |
| 574 | * |
| 575 | * @return Money |
| 576 | * |
| 577 | * @psalm-pure |
| 578 | */ |
| 579 | public static function sum(self $first, self ...$collection) |
| 580 | { |
| 581 | return $first->add(...$collection); |
| 582 | } |
| 583 | |
| 584 | /** |
| 585 | * @param Money $first |
| 586 | * @param Money ...$collection |
| 587 | * |
| 588 | * @return Money |
| 589 | * |
| 590 | * @psalm-pure |
| 591 | */ |
| 592 | public static function avg(self $first, self ...$collection) |
| 593 | { |
| 594 | return $first->add(...$collection)->divide(func_num_args()); |
| 595 | } |
| 596 | |
| 597 | /** |
| 598 | * @param string $calculator |
| 599 | */ |
| 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 | |
| 609 | /** |
| 610 | * @return Calculator |
| 611 | * |
| 612 | * @throws \RuntimeException If cannot find calculator for money calculations |
| 613 | */ |
| 614 | private static function initializeCalculator() |
| 615 | { |
| 616 | $calculators = self::$calculators; |
| 617 | |
| 618 | foreach ($calculators as $calculator) { |
| 619 | /** @var Calculator $calculator */ |
| 620 | if ($calculator::supported()) { |
| 621 | return new $calculator(); |
| 622 | } |
| 623 | } |
| 624 | |
| 625 | throw new \RuntimeException('Cannot find calculator for money calculations'); |
| 626 | } |
| 627 | |
| 628 | /** |
| 629 | * @return Calculator |
| 630 | */ |
| 631 | private function getCalculator() |
| 632 | { |
| 633 | if (null === self::$calculator) { |
| 634 | self::$calculator = self::initializeCalculator(); |
| 635 | } |
| 636 | |
| 637 | return self::$calculator; |
| 638 | } |
| 639 | } |
| 640 |