%PDF- %PDF-
Direktori : /home/forge/api-takeaseat.eco-n-tech.co.uk/vendor/brick/money/src/ |
Current File : //home/forge/api-takeaseat.eco-n-tech.co.uk/vendor/brick/money/src/Money.php |
<?php declare(strict_types=1); namespace Brick\Money; use Brick\Money\Context\DefaultContext; use Brick\Money\Exception\MoneyMismatchException; use Brick\Money\Exception\UnknownCurrencyException; use Brick\Math\BigDecimal; use Brick\Math\BigInteger; use Brick\Math\BigNumber; use Brick\Math\BigRational; use Brick\Math\RoundingMode; use Brick\Math\Exception\MathException; use Brick\Math\Exception\NumberFormatException; use Brick\Math\Exception\RoundingNecessaryException; /** * A monetary value in a given currency. This class is immutable. * * A Money has an amount, a currency, and a context. The context defines the scale of the amount, and an optional cash * rounding step, for monies that do not have coins or notes for their smallest units. * * All operations on a Money return another Money with the same context. The available contexts are: * * - DefaultContext handles monies with the default scale for the currency. * - CashContext is similar to DefaultContext, but supports a cash rounding step. * - CustomContext handles monies with a custom scale, and optionally step. * - AutoContext automatically adjusts the scale of the money to the minimum required. */ final class Money extends AbstractMoney { /** * The amount. * * @var \Brick\Math\BigDecimal */ private $amount; /** * The currency. * * @var \Brick\Money\Currency */ private $currency; /** * The context that defines the capability of this Money. * * @var Context */ private $context; /** * @param BigDecimal $amount * @param Currency $currency * @param Context $context */ private function __construct(BigDecimal $amount, Currency $currency, Context $context) { $this->amount = $amount; $this->currency = $currency; $this->context = $context; } /** * Returns the minimum of the given monies. * * If several monies are equal to the minimum value, the first one is returned. * * @param Money $money The first money. * @param Money ...$monies The subsequent monies. * * @return Money * * @throws MoneyMismatchException If all the monies are not in the same currency. */ public static function min(Money $money, Money ...$monies) : Money { $min = $money; foreach ($monies as $money) { if ($money->isLessThan($min)) { $min = $money; } } return $min; } /** * Returns the maximum of the given monies. * * If several monies are equal to the maximum value, the first one is returned. * * @param Money $money The first money. * @param Money ...$monies The subsequent monies. * * @return Money * * @throws MoneyMismatchException If all the monies are not in the same currency. */ public static function max(Money $money, Money ...$monies) : Money { $max = $money; foreach ($monies as $money) { if ($money->isGreaterThan($max)) { $max = $money; } } return $max; } /** * Returns the total of the given monies. * * The monies must share the same currency and context. * * @param Money $money The first money. * @param Money ...$monies The subsequent monies. * * @return Money * * @throws MoneyMismatchException If all the monies are not in the same currency and context. */ public static function total(Money $money, Money ...$monies) : Money { $total = $money; foreach ($monies as $money) { $total = $total->plus($money); } return $total; } /** * Creates a Money from a rational amount, a currency, and a context. * * @psalm-param RoundingMode::* $roundingMode * * @param BigNumber $amount The amount. * @param Currency $currency The currency. * @param Context $context The context. * @param int $roundingMode An optional rounding mode if the amount does not fit the context. * * @return Money * * @throws RoundingNecessaryException If RoundingMode::UNNECESSARY is used but rounding is necessary. */ public static function create(BigNumber $amount, Currency $currency, Context $context, int $roundingMode = RoundingMode::UNNECESSARY) : Money { $amount = $context->applyTo($amount, $currency, $roundingMode); return new Money($amount, $currency, $context); } /** * Returns a Money of the given amount and currency. * * By default, the money is created with a DefaultContext. This means that the amount is scaled to match the * currency's default fraction digits. For example, `Money::of('2.5', 'USD')` will yield `USD 2.50`. * If the amount cannot be safely converted to this scale, an exception is thrown. * * To override this behaviour, a Context instance can be provided. * Operations on this Money return a Money with the same context. * * @psalm-param RoundingMode::* $roundingMode * * @param BigNumber|int|float|string $amount The monetary amount. * @param Currency|string|int $currency The Currency instance, ISO currency code or ISO numeric currency code. * @param Context|null $context An optional Context. * @param int $roundingMode An optional RoundingMode, if the amount does not fit the context. * * @return Money * * @throws NumberFormatException If the amount is a string in a non-supported format. * @throws UnknownCurrencyException If the currency is an unknown currency code. * @throws RoundingNecessaryException If the rounding mode is RoundingMode::UNNECESSARY, and rounding is necessary * to represent the amount at the requested scale. */ public static function of($amount, $currency, ?Context $context = null, int $roundingMode = RoundingMode::UNNECESSARY) : Money { if (! $currency instanceof Currency) { $currency = Currency::of($currency); } if ($context === null) { $context = new DefaultContext(); } $amount = BigNumber::of($amount); return self::create($amount, $currency, $context, $roundingMode); } /** * Returns a Money from a number of minor units. * * By default, the money is created with a DefaultContext. This means that the amount is scaled to match the * currency's default fraction digits. For example, `Money::ofMinor(1234, 'USD')` will yield `USD 12.34`. * If the amount cannot be safely converted to this scale, an exception is thrown. * * @psalm-param RoundingMode::* $roundingMode * * @param BigNumber|int|float|string $minorAmount The amount, in minor currency units. * @param Currency|string|int $currency The Currency instance, ISO currency code or ISO numeric currency code. * @param Context|null $context An optional Context. * @param int $roundingMode An optional RoundingMode, if the amount does not fit the context. * * @return Money * * @throws NumberFormatException If the amount is a string in a non-supported format. * @throws UnknownCurrencyException If the currency is an unknown currency code. * @throws RoundingNecessaryException If the rounding mode is RoundingMode::UNNECESSARY, and rounding is necessary * to represent the amount at the requested scale. */ public static function ofMinor($minorAmount, $currency, ?Context $context = null, int $roundingMode = RoundingMode::UNNECESSARY) : Money { if (! $currency instanceof Currency) { $currency = Currency::of($currency); } if ($context === null) { $context = new DefaultContext(); } $amount = BigRational::of($minorAmount)->dividedBy(10 ** $currency->getDefaultFractionDigits()); return self::create($amount, $currency, $context, $roundingMode); } /** * Returns a Money with zero value, in the given currency. * * By default, the money is created with a DefaultContext: it has the default scale for the currency. * A Context instance can be provided to override the default. * * @param Currency|string|int $currency The Currency instance, ISO currency code or ISO numeric currency code. * @param Context|null $context An optional context. * * @return Money */ public static function zero($currency, ?Context $context = null) : Money { if (! $currency instanceof Currency) { $currency = Currency::of($currency); } if ($context === null) { $context = new DefaultContext(); } $amount = BigDecimal::zero(); return self::create($amount, $currency, $context); } /** * Returns the amount of this Money, as a BigDecimal. * * @return BigDecimal */ public function getAmount() : BigDecimal { return $this->amount; } /** * Returns the amount of this Money in minor units (cents) for the currency. * * The value is returned as a BigDecimal. If this Money has a scale greater than that of the currency, the result * will have a non-zero scale. * * For example, `USD 1.23` will return a BigDecimal of `123`, while `USD 1.2345` will return `123.45`. * * @return BigDecimal */ public function getMinorAmount() : BigDecimal { return $this->amount->withPointMovedRight($this->currency->getDefaultFractionDigits()); } /** * Returns a BigInteger containing the unscaled value (all digits) of this money. * * For example, `123.4567 USD` will return a BigInteger of `1234567`. * * @return BigInteger */ public function getUnscaledAmount() : BigInteger { return $this->amount->getUnscaledValue(); } /** * Returns the Currency of this Money. * * @return Currency */ public function getCurrency() : Currency { return $this->currency; } /** * Returns the Context of this Money. * * @return Context */ public function getContext() : Context { return $this->context; } /** * Returns the sum of this Money and the given amount. * * If the operand is a Money, it must have the same context as this Money, or an exception is thrown. * This is by design, to ensure that contexts are not mixed accidentally. * If you do need to add a Money in a different context, you can use `plus($money->toRational())`. * * The resulting Money has the same context as this Money. If the result needs rounding to fit this context, a * rounding mode can be provided. If a rounding mode is not provided and rounding is necessary, an exception is * thrown. * * @psalm-param RoundingMode::* $roundingMode * * @param AbstractMoney|BigNumber|int|float|string $that The money or amount to add. * @param int $roundingMode An optional RoundingMode constant. * * @return Money * * @throws MathException If the argument is an invalid number or rounding is necessary. * @throws MoneyMismatchException If the argument is a money in a different currency or in a different context. */ public function plus($that, int $roundingMode = RoundingMode::UNNECESSARY) : Money { $amount = $this->getAmountOf($that); if ($that instanceof Money) { $this->checkContext($that->getContext(), __FUNCTION__); if ($this->context->isFixedScale()) { return new Money($this->amount->plus($that->amount), $this->currency, $this->context); } } $amount = $this->amount->toBigRational()->plus($amount); return self::create($amount, $this->currency, $this->context, $roundingMode); } /** * Returns the difference of this Money and the given amount. * * If the operand is a Money, it must have the same context as this Money, or an exception is thrown. * This is by design, to ensure that contexts are not mixed accidentally. * If you do need to subtract a Money in a different context, you can use `minus($money->toRational())`. * * The resulting Money has the same context as this Money. If the result needs rounding to fit this context, a * rounding mode can be provided. If a rounding mode is not provided and rounding is necessary, an exception is * thrown. * * @psalm-param RoundingMode::* $roundingMode * * @param AbstractMoney|BigNumber|int|float|string $that The money or amount to subtract. * @param int $roundingMode An optional RoundingMode constant. * * @return Money * * @throws MathException If the argument is an invalid number or rounding is necessary. * @throws MoneyMismatchException If the argument is a money in a different currency or in a different context. */ public function minus($that, int $roundingMode = RoundingMode::UNNECESSARY) : Money { $amount = $this->getAmountOf($that); if ($that instanceof Money) { $this->checkContext($that->getContext(), __FUNCTION__); if ($this->context->isFixedScale()) { return new Money($this->amount->minus($that->amount), $this->currency, $this->context); } } $amount = $this->amount->toBigRational()->minus($amount); return self::create($amount, $this->currency, $this->context, $roundingMode); } /** * Returns the product of this Money and the given number. * * The resulting Money has the same context as this Money. If the result needs rounding to fit this context, a * rounding mode can be provided. If a rounding mode is not provided and rounding is necessary, an exception is * thrown. * * @psalm-param RoundingMode::* $roundingMode * * @param BigNumber|int|float|string $that The multiplier. * @param int $roundingMode An optional RoundingMode constant. * * @return Money * * @throws MathException If the argument is an invalid number or rounding is necessary. */ public function multipliedBy($that, int $roundingMode = RoundingMode::UNNECESSARY) : Money { $amount = $this->amount->toBigRational()->multipliedBy($that); return self::create($amount, $this->currency, $this->context, $roundingMode); } /** * Returns the result of the division of this Money by the given number. * * The resulting Money has the same context as this Money. If the result needs rounding to fit this context, a * rounding mode can be provided. If a rounding mode is not provided and rounding is necessary, an exception is * thrown. * * @psalm-param RoundingMode::* $roundingMode * * @param BigNumber|int|float|string $that The divisor. * @param int $roundingMode An optional RoundingMode constant. * * @return Money * * @throws MathException If the argument is an invalid number or is zero, or rounding is necessary. */ public function dividedBy($that, int $roundingMode = RoundingMode::UNNECESSARY) : Money { $amount = $this->amount->toBigRational()->dividedBy($that); return self::create($amount, $this->currency, $this->context, $roundingMode); } /** * Returns the quotient of the division of this Money by the given number. * * The given number must be a integer value. The resulting Money has the same context as this Money. * This method can serve as a basis for a money allocation algorithm. * * @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigInteger. * * @return Money * * @throws MathException If the divisor cannot be converted to a BigInteger. */ public function quotient($that) : Money { $that = BigInteger::of($that); $step = $this->context->getStep(); $scale = $this->amount->getScale(); $amount = $this->amount->withPointMovedRight($scale)->dividedBy($step); $q = $amount->quotient($that); $q = $q->multipliedBy($step)->withPointMovedLeft($scale); return new Money($q, $this->currency, $this->context); } /** * Returns the quotient and the remainder of the division of this Money by the given number. * * The given number must be an integer value. The resulting monies have the same context as this Money. * This method can serve as a basis for a money allocation algorithm. * * @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigInteger. * * @return Money[] The quotient and the remainder. * * @throws MathException If the divisor cannot be converted to a BigInteger. */ public function quotientAndRemainder($that) : array { $that = BigInteger::of($that); $step = $this->context->getStep(); $scale = $this->amount->getScale(); $amount = $this->amount->withPointMovedRight($scale)->dividedBy($step); [$q, $r] = $amount->quotientAndRemainder($that); $q = $q->multipliedBy($step)->withPointMovedLeft($scale); $r = $r->multipliedBy($step)->withPointMovedLeft($scale); $quotient = new Money($q, $this->currency, $this->context); $remainder = new Money($r, $this->currency, $this->context); return [$quotient, $remainder]; } /** * Allocates this Money according to a list of ratios. * * If the allocation yields a remainder, its amount is split over the first monies in the list, * so that the total of the resulting monies is always equal to this Money. * * For example, given a `USD 49.99` money in the default context, * `allocate(1, 2, 3, 4)` returns [`USD 5.00`, `USD 10.00`, `USD 15.00`, `USD 19.99`] * * The resulting monies have the same context as this Money. * * @param int[] $ratios The ratios. * * @return Money[] * * @throws \InvalidArgumentException If called with invalid parameters. */ public function allocate(int ...$ratios) : array { if (! $ratios) { throw new \InvalidArgumentException('Cannot allocate() an empty list of ratios.'); } foreach ($ratios as $ratio) { if ($ratio < 0) { throw new \InvalidArgumentException('Cannot allocate() negative ratios.'); } } $total = array_sum($ratios); if ($total === 0) { throw new \InvalidArgumentException('Cannot allocate() to zero ratios only.'); } $step = $this->context->getStep(); $monies = []; $unit = BigDecimal::ofUnscaledValue($step, $this->amount->getScale()); $unit = new Money($unit, $this->currency, $this->context); if ($this->isNegative()) { $unit = $unit->negated(); } $remainder = $this; foreach ($ratios as $ratio) { $money = $this->multipliedBy($ratio)->quotient($total); $remainder = $remainder->minus($money); $monies[] = $money; } foreach ($monies as $key => $money) { if ($remainder->isZero()) { break; } $monies[$key] = $money->plus($unit); $remainder = $remainder->minus($unit); } return $monies; } /** * Allocates this Money according to a list of ratios. * * The remainder is also present, appended at the end of the list. * * For example, given a `USD 49.99` money in the default context, * `allocateWithRemainder(1, 2, 3, 4)` returns [`USD 4.99`, `USD 9.99`, `USD 14.99`, `USD 19.99`, `USD 0.03`] * * The resulting monies have the same context as this Money. * * @param int[] $ratios The ratios. * * @return Money[] * * @throws \InvalidArgumentException If called with invalid parameters. */ public function allocateWithRemainder(int ...$ratios) : array { if (! $ratios) { throw new \InvalidArgumentException('Cannot allocateWithRemainder() an empty list of ratios.'); } foreach ($ratios as $ratio) { if ($ratio < 0) { throw new \InvalidArgumentException('Cannot allocateWithRemainder() negative ratios.'); } } $total = array_sum($ratios); if ($total === 0) { throw new \InvalidArgumentException('Cannot allocateWithRemainder() to zero ratios only.'); } $monies = []; $remainder = $this; foreach ($ratios as $ratio) { $money = $this->multipliedBy($ratio)->quotient($total); $remainder = $remainder->minus($money); $monies[] = $money; } $monies[] = $remainder; return $monies; } /** * Splits this Money into a number of parts. * * If the division of this Money by the number of parts yields a remainder, its amount is split over the first * monies in the list, so that the total of the resulting monies is always equal to this Money. * * For example, given a `USD 100.00` money in the default context, * `split(3)` returns [`USD 33.34`, `USD 33.33`, `USD 33.33`] * * The resulting monies have the same context as this Money. * * @param int $parts The number of parts. * * @return Money[] * * @throws \InvalidArgumentException If called with invalid parameters. */ public function split(int $parts) : array { if ($parts < 1) { throw new \InvalidArgumentException('Cannot split() into less than 1 part.'); } return $this->allocate(...array_fill(0, $parts, 1)); } /** * Splits this Money into a number of parts and a remainder. * * For example, given a `USD 100.00` money in the default context, * `splitWithRemainder(3)` returns [`USD 33.33`, `USD 33.33`, `USD 33.33`, `USD 0.01`] * * The resulting monies have the same context as this Money. * * @param int $parts The number of parts * * @return Money[] * * @throws \InvalidArgumentException If called with invalid parameters. */ public function splitWithRemainder(int $parts) : array { if ($parts < 1) { throw new \InvalidArgumentException('Cannot splitWithRemainder() into less than 1 part.'); } return $this->allocateWithRemainder(...array_fill(0, $parts, 1)); } /** * Returns a Money whose value is the absolute value of this Money. * * The resulting Money has the same context as this Money. * * @return Money */ public function abs() : Money { return new Money($this->amount->abs(), $this->currency, $this->context); } /** * Returns a Money whose value is the negated value of this Money. * * The resulting Money has the same context as this Money. * * @return Money */ public function negated() : Money { return new Money($this->amount->negated(), $this->currency, $this->context); } /** * Converts this Money to another currency, using an exchange rate. * * By default, the resulting Money has the same context as this Money. * This can be overridden by providing a Context. * * For example, converting a default money of `USD 1.23` to `EUR` with an exchange rate of `0.91` and * RoundingMode::UP will yield `EUR 1.12`. * * @psalm-param RoundingMode::* $roundingMode * * @param Currency|string|int $currency The Currency instance, ISO currency code or ISO numeric currency code. * @param BigNumber|int|float|string $exchangeRate The exchange rate to multiply by. * @param Context|null $context An optional context. * @param int $roundingMode An optional rounding mode. * * @return Money * * @throws UnknownCurrencyException If an unknown currency code is given. * @throws MathException If the exchange rate or rounding mode is invalid, or rounding is necessary. */ public function convertedTo($currency, $exchangeRate, ?Context $context = null, int $roundingMode = RoundingMode::UNNECESSARY) : Money { if (! $currency instanceof Currency) { $currency = Currency::of($currency); } if ($context === null) { $context = $this->context; } $amount = $this->amount->toBigRational()->multipliedBy($exchangeRate); return self::create($amount, $currency, $context, $roundingMode); } /** * Formats this Money with the given NumberFormatter. * * Note that NumberFormatter internally represents values using floating point arithmetic, * so discrepancies can appear when formatting very large monetary values. * * @param \NumberFormatter $formatter The formatter to format with. * * @return string */ public function formatWith(\NumberFormatter $formatter) : string { return $formatter->formatCurrency( $this->amount->toFloat(), $this->currency->getCurrencyCode() ); } /** * Formats this Money to the given locale. * * Note that this method uses NumberFormatter, which internally represents values using floating point arithmetic, * so discrepancies can appear when formatting very large monetary values. * * @param string $locale The locale to format to. * @param bool $allowWholeNumber Whether to allow formatting as a whole number if the amount has no fraction. * * @return string */ public function formatTo(string $locale, bool $allowWholeNumber = false) : string { /** @var \NumberFormatter|null $lastFormatter */ static $lastFormatter = null; static $lastFormatterLocale; static $lastFormatterScale; if ($allowWholeNumber && ! $this->amount->hasNonZeroFractionalPart()) { $scale = 0; } else { $scale = $this->amount->getScale(); } if ($lastFormatter !== null && $lastFormatterLocale === $locale) { $formatter = $lastFormatter; if ($lastFormatterScale !== $scale) { $formatter->setAttribute(\NumberFormatter::MIN_FRACTION_DIGITS, $scale); $formatter->setAttribute(\NumberFormatter::MAX_FRACTION_DIGITS, $scale); $lastFormatterScale = $scale; } } else { $formatter = new \NumberFormatter($locale, \NumberFormatter::CURRENCY); $formatter->setAttribute(\NumberFormatter::MIN_FRACTION_DIGITS, $scale); $formatter->setAttribute(\NumberFormatter::MAX_FRACTION_DIGITS, $scale); $lastFormatter = $formatter; $lastFormatterLocale = $locale; $lastFormatterScale = $scale; } return $this->formatWith($formatter); } /** * @return RationalMoney */ public function toRational() : RationalMoney { return new RationalMoney($this->amount->toBigRational(), $this->currency); } /** * Returns a non-localized string representation of this Money, e.g. "EUR 23.00". * * @return string */ public function __toString() : string { return $this->currency . ' ' . $this->amount; } /** * @param Context $context The Context to check against this Money. * @param string $method The invoked method name. * * @return void * * @throws MoneyMismatchException If monies don't match. */ protected function checkContext(Context $context, string $method) : void { if ($this->context != $context) { // non-strict equality on purpose throw MoneyMismatchException::contextMismatch($method); } } }