ok
Direktori : /proc/thread-self/root/home2/selectio/www/fms-worksuite/vendor/moneyphp/money/src/ |
Current File : //proc/thread-self/root/home2/selectio/www/fms-worksuite/vendor/moneyphp/money/src/Money.php |
<?php declare(strict_types=1); namespace Money; use InvalidArgumentException; use JsonSerializable; use Money\Calculator\BcMathCalculator; use function array_fill; use function array_keys; use function array_map; use function array_sum; use function count; use function filter_var; use function floor; use function is_int; use function max; use function str_pad; use function strlen; use function substr; use const FILTER_VALIDATE_INT; use const PHP_ROUND_HALF_DOWN; use const PHP_ROUND_HALF_EVEN; use const PHP_ROUND_HALF_ODD; use const PHP_ROUND_HALF_UP; /** * Money Value Object. * * @psalm-immutable */ final class Money implements JsonSerializable { use MoneyFactory; public const ROUND_HALF_UP = PHP_ROUND_HALF_UP; public const ROUND_HALF_DOWN = PHP_ROUND_HALF_DOWN; public const ROUND_HALF_EVEN = PHP_ROUND_HALF_EVEN; public const ROUND_HALF_ODD = PHP_ROUND_HALF_ODD; public const ROUND_UP = 5; public const ROUND_DOWN = 6; public const ROUND_HALF_POSITIVE_INFINITY = 7; public const ROUND_HALF_NEGATIVE_INFINITY = 8; /** * Internal value. * * @psalm-var numeric-string */ private string $amount; private Currency $currency; /** * @var Calculator * @psalm-var class-string<Calculator> */ private static string $calculator = BcMathCalculator::class; /** * @param int|string $amount Amount, expressed in the smallest units of $currency (eg cents) * @psalm-param int|numeric-string $amount * * @throws InvalidArgumentException If amount is not integer(ish). */ public function __construct(int|string $amount, Currency $currency) { $this->currency = $currency; if (filter_var($amount, FILTER_VALIDATE_INT) === false) { $numberFromString = Number::fromString((string) $amount); if (! $numberFromString->isInteger()) { throw new InvalidArgumentException('Amount must be an integer(ish) value'); } $this->amount = $numberFromString->getIntegerPart(); return; } $this->amount = (string) $amount; } /** * Checks whether a Money has the same Currency as this. */ public function isSameCurrency(Money ...$others): bool { foreach ($others as $other) { // Note: non-strict equality is intentional here, since `Currency` is `final` and reliable. if ($this->currency != $other->currency) { return false; } } return true; } /** * Checks whether the value represented by this object equals to the other. */ public function equals(Money $other): bool { // Note: non-strict equality is intentional here, since `Currency` is `final` and reliable. if ($this->currency != $other->currency) { return false; } if ($this->amount === $other->amount) { return true; } // @TODO do we want Money instance to be byte-equivalent when trailing zeroes exist? Very expensive! // Assumption: Money#equals() is called **less** than other number-based comparisons, and probably // only within test suites. Therefore, using complex normalization here is acceptable waste of performance. return $this->compare($other) === 0; } /** * Returns an integer less than, equal to, or greater than zero * if the value of this object is considered to be respectively * less than, equal to, or greater than the other. */ public function compare(Money $other): int { // Note: non-strict equality is intentional here, since `Currency` is `final` and reliable. if ($this->currency != $other->currency) { throw new InvalidArgumentException('Currencies must be identical'); } return self::$calculator::compare($this->amount, $other->amount); } /** * Checks whether the value represented by this object is greater than the other. */ public function greaterThan(Money $other): bool { return $this->compare($other) > 0; } public function greaterThanOrEqual(Money $other): bool { return $this->compare($other) >= 0; } /** * Checks whether the value represented by this object is less than the other. */ public function lessThan(Money $other): bool { return $this->compare($other) < 0; } public function lessThanOrEqual(Money $other): bool { return $this->compare($other) <= 0; } /** * Returns the value represented by this object. * * @psalm-return numeric-string */ public function getAmount(): string { return $this->amount; } /** * Returns the currency of this object. */ public function getCurrency(): Currency { return $this->currency; } /** * Returns a new Money object that represents * the sum of this and an other Money object. * * @param Money[] $addends */ public function add(Money ...$addends): Money { $amount = $this->amount; foreach ($addends as $addend) { // Note: non-strict equality is intentional here, since `Currency` is `final` and reliable. if ($this->currency != $addend->currency) { throw new InvalidArgumentException('Currencies must be identical'); } $amount = self::$calculator::add($amount, $addend->amount); } return new self($amount, $this->currency); } /** * Returns a new Money object that represents * the difference of this and an other Money object. * * @param Money[] $subtrahends * * @psalm-pure */ public function subtract(Money ...$subtrahends): Money { $amount = $this->amount; foreach ($subtrahends as $subtrahend) { // Note: non-strict equality is intentional here, since `Currency` is `final` and reliable. if ($this->currency != $subtrahend->currency) { throw new InvalidArgumentException('Currencies must be identical'); } $amount = self::$calculator::subtract($amount, $subtrahend->amount); } return new self($amount, $this->currency); } /** * Returns a new Money object that represents * the multiplied value by the given factor. * * @psalm-param int|numeric-string $multiplier * @psalm-param self::ROUND_* $roundingMode */ public function multiply(int|string $multiplier, int $roundingMode = self::ROUND_HALF_UP): Money { if (is_int($multiplier)) { $multiplier = (string) $multiplier; } $product = $this->round(self::$calculator::multiply($this->amount, $multiplier), $roundingMode); return new self($product, $this->currency); } /** * Returns a new Money object that represents * the divided value by the given factor. * * @psalm-param int|numeric-string $divisor * @psalm-param self::ROUND_* $roundingMode */ public function divide(int|string $divisor, int $roundingMode = self::ROUND_HALF_UP): Money { if (is_int($divisor)) { $divisor = (string) $divisor; } $quotient = $this->round(self::$calculator::divide($this->amount, $divisor), $roundingMode); return new self($quotient, $this->currency); } /** * Returns a new Money object that represents * the remainder after dividing the value by * the given factor. */ public function mod(Money $divisor): Money { // Note: non-strict equality is intentional here, since `Currency` is `final` and reliable. if ($this->currency != $divisor->currency) { throw new InvalidArgumentException('Currencies must be identical'); } return new self(self::$calculator::mod($this->amount, $divisor->amount), $this->currency); } /** * Allocate the money according to a list of ratios. * * @psalm-param TRatios $ratios * * @return Money[] * @psalm-return ( * TRatios is list * ? non-empty-list<Money> * : non-empty-array<Money> * ) * * @template TRatios as non-empty-array<float|int> */ public function allocate(array $ratios): array { $remainder = $this->amount; $results = []; $total = array_sum($ratios); if ($total <= 0) { throw new InvalidArgumentException('Cannot allocate to none, sum of ratios must be greater than zero'); } foreach ($ratios as $key => $ratio) { if ($ratio < 0) { throw new InvalidArgumentException('Cannot allocate to none, ratio must be zero or positive'); } $share = self::$calculator::share($this->amount, (string) $ratio, (string) $total); $results[$key] = new self($share, $this->currency); $remainder = self::$calculator::subtract($remainder, $share); } if (self::$calculator::compare($remainder, '0') === 0) { return $results; } $amount = $this->amount; $fractions = array_map(static function (float|int $ratio) use ($total, $amount) { $share = (float) $ratio / $total * (float) $amount; return $share - floor($share); }, $ratios); while (self::$calculator::compare($remainder, '0') > 0) { $index = $fractions !== [] ? array_keys($fractions, max($fractions))[0] : 0; $results[$index] = new self(self::$calculator::add($results[$index]->amount, '1'), $results[$index]->currency); $remainder = self::$calculator::subtract($remainder, '1'); unset($fractions[$index]); } return $results; } /** * Allocate the money among N targets. * * @psalm-param positive-int $n * * @return Money[] * @psalm-return non-empty-list<Money> * * @throws InvalidArgumentException If number of targets is not an integer. */ public function allocateTo(int $n): array { return $this->allocate(array_fill(0, $n, 1)); } /** * @psalm-return numeric-string * * @throws InvalidArgumentException if the given $money is zero. */ public function ratioOf(Money $money): string { if ($money->isZero()) { throw new InvalidArgumentException('Cannot calculate a ratio of zero'); } // Note: non-strict equality is intentional here, since `Currency` is `final` and reliable. if ($this->currency != $money->currency) { throw new InvalidArgumentException('Currencies must be identical'); } return self::$calculator::divide($this->amount, $money->amount); } /** * @psalm-param numeric-string $amount * @psalm-param self::ROUND_* $roundingMode * * @psalm-return numeric-string */ private function round(string $amount, int $roundingMode): string { if ($roundingMode === self::ROUND_UP) { return self::$calculator::ceil($amount); } if ($roundingMode === self::ROUND_DOWN) { return self::$calculator::floor($amount); } return self::$calculator::round($amount, $roundingMode); } /** * Round to a specific unit. * * @psalm-param positive-int|0 $unit * @psalm-param self::ROUND_* $roundingMode */ public function roundToUnit(int $unit, int $roundingMode = self::ROUND_HALF_UP): self { if ($unit === 0) { return $this; } $abs = self::$calculator::absolute($this->amount); if (strlen($abs) < $unit) { return new self('0', $this->currency); } /** @psalm-var numeric-string $toBeRounded */ $toBeRounded = substr($this->amount, 0, strlen($this->amount) - $unit) . '.' . substr($this->amount, $unit * -1); $result = $this->round($toBeRounded, $roundingMode); if ($result !== '0') { $result .= str_pad('', $unit, '0'); } /** @psalm-var numeric-string $result */ return new self($result, $this->currency); } public function absolute(): Money { return new self( self::$calculator::absolute($this->amount), $this->currency ); } public function negative(): Money { return (new self(0, $this->currency)) ->subtract($this); } /** * Checks if the value represented by this object is zero. */ public function isZero(): bool { return self::$calculator::compare($this->amount, '0') === 0; } /** * Checks if the value represented by this object is positive. */ public function isPositive(): bool { return self::$calculator::compare($this->amount, '0') > 0; } /** * Checks if the value represented by this object is negative. */ public function isNegative(): bool { return self::$calculator::compare($this->amount, '0') < 0; } /** * {@inheritdoc} * * @psalm-return array{amount: string, currency: string} */ public function jsonSerialize(): array { return [ 'amount' => $this->amount, 'currency' => $this->currency->jsonSerialize(), ]; } /** * @param Money $first * @param Money ...$collection * * @psalm-pure */ public static function min(self $first, self ...$collection): Money { $min = $first; foreach ($collection as $money) { if (! $money->lessThan($min)) { continue; } $min = $money; } return $min; } /** * @param Money $first * @param Money ...$collection * * @psalm-pure */ public static function max(self $first, self ...$collection): Money { $max = $first; foreach ($collection as $money) { if (! $money->greaterThan($max)) { continue; } $max = $money; } return $max; } /** @psalm-pure */ public static function sum(self $first, self ...$collection): Money { return $first->add(...$collection); } /** @psalm-pure */ public static function avg(self $first, self ...$collection): Money { return $first->add(...$collection)->divide((string) (count($collection) + 1)); } /** @psalm-param class-string<Calculator> $calculator */ public static function registerCalculator(string $calculator): void { self::$calculator = $calculator; } /** @psalm-return class-string<Calculator> */ public static function getCalculator(): string { return self::$calculator; } }