new appraoch

This commit is contained in:
Tim Bendt
2025-11-26 13:22:58 -05:00
parent de3d100844
commit c520b7df89
6760 changed files with 1009780 additions and 0 deletions

21
pancake/system/vendor/moneyphp/money/LICENSE vendored Executable file
View File

@@ -0,0 +1,21 @@
Copyright (c) 2011-2016 Mathias Verraes
Copyright (c) 2016 Márk Sági-Kazár <mark.sagikazar@gmail.com>
Copyright (c) 2016 Frederik Bosch <f.bosch@genkgo.nl>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,95 @@
{
"name": "moneyphp/money",
"description": "PHP implementation of Fowler's Money pattern",
"license": "MIT",
"keywords": [
"money",
"vo",
"value object"
],
"authors": [
{
"name": "Mathias Verraes",
"email": "mathias@verraes.net",
"homepage": "http://verraes.net"
},
{
"name": "Márk Sági-Kazár",
"email": "mark.sagikazar@gmail.com"
},
{
"name": "Frederik Bosch",
"email": "f.bosch@genkgo.nl"
}
],
"homepage": "http://moneyphp.org",
"require": {
"php": ">=5.6",
"ext-json": "*"
},
"require-dev": {
"ext-bcmath": "*",
"ext-gmp": "*",
"ext-intl": "*",
"cache/taggable-cache": "^0.4.0",
"doctrine/instantiator": "^1.0.5",
"florianv/exchanger": "^1.0",
"florianv/swap": "^3.0",
"friends-of-phpspec/phpspec-code-coverage": "^3.1.1 || ^4.3",
"moneyphp/iso-currencies": "^3.2.1",
"php-http/message": "^1.4",
"php-http/mock-client": "^1.0.0",
"phpspec/phpspec": "^3.4.3",
"phpunit/phpunit": "^5.7.27 || ^6.5.14 || ^7.5.18 || ^8.5",
"psr/cache": "^1.0",
"symfony/phpunit-bridge": "^4"
},
"suggest": {
"ext-bcmath": "Calculate without integer limits",
"ext-gmp": "Calculate without integer limits",
"ext-intl": "Format Money objects with intl",
"florianv/exchanger": "Exchange rates library for PHP",
"florianv/swap": "Exchange rates library for PHP",
"psr/cache-implementation": "Used for Currency caching"
},
"minimum-stability": "dev",
"prefer-stable": true,
"autoload": {
"psr-4": {
"Money\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Tests\\Money\\": "tests/",
"spec\\Money\\": "spec/"
}
},
"config": {
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true,
"ergebnis/composer-normalize": true
},
"sort-packages": true
},
"extra": {
"branch-alias": {
"dev-master": "3.x-dev"
}
},
"scripts": {
"clean": "rm -rf build/ vendor/",
"test": [
"vendor/bin/phpspec run",
"vendor/bin/phpunit -v"
],
"test-coverage": [
"vendor/bin/phpspec run -c phpspec.ci.yml",
"vendor/bin/phpunit -v --coverage-text --coverage-clover=build/unit_coverage.xml"
],
"update-currencies": [
"cp vendor/moneyphp/iso-currencies/resources/current.php resources/currency.php",
"php resources/generate-money-factory.php"
]
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,61 @@
<?php
require __DIR__.'/../vendor/autoload.php';
use Money\Currencies;
$buffer = <<<PHP
<?php
namespace Money;
/**
* This is a generated file. Do not edit it manually!
*
PHPDOC
*/
trait MoneyFactory
{
/**
* Convenience factory method for a Money object.
*
* <code>
* \$fiveDollar = Money::USD(500);
* </code>
*
* @param string \$method
* @param array \$arguments
*
* @return Money
*
* @throws \InvalidArgumentException If amount is not integer(ish)
*/
public static function __callStatic(\$method, \$arguments)
{
return new Money(\$arguments[0], new Currency(\$method));
}
}
PHP;
$methodBuffer = '';
$currencies = new Currencies\AggregateCurrencies([
new Currencies\ISOCurrencies(),
new Currencies\BitcoinCurrencies(),
]);
$currencies = iterator_to_array($currencies);
usort($currencies, function (\Money\Currency $a, \Money\Currency $b) {
return strcmp($a->getCode(), $b->getCode());
});
/** @var \Money\Currency[] $currencies */
foreach ($currencies as $currency) {
$methodBuffer .= sprintf(" * @method static Money %s(string|int \$amount)\n", $currency->getCode());
}
$buffer = str_replace('PHPDOC', rtrim($methodBuffer), $buffer);
file_put_contents(__DIR__.'/../src/MoneyFactory.php', $buffer);

View File

@@ -0,0 +1,127 @@
<?php
namespace Money;
/**
* Money calculations abstracted away from the Money value object.
*
* @author Frederik Bosch <f.bosch@genkgo.nl>
*/
interface Calculator
{
/**
* Returns whether the calculator is supported in
* the current server environment.
*
* @return bool
*/
public static function supported();
/**
* Compare a to b.
*
* @param string $a
* @param string $b
*
* @return int
*/
public function compare($a, $b);
/**
* Add added to amount.
*
* @param string $amount
* @param string $addend
*
* @return string
*/
public function add($amount, $addend);
/**
* Subtract subtrahend from amount.
*
* @param string $amount
* @param string $subtrahend
*
* @return string
*/
public function subtract($amount, $subtrahend);
/**
* Multiply amount with multiplier.
*
* @param string $amount
* @param int|float|string $multiplier
*
* @return string
*/
public function multiply($amount, $multiplier);
/**
* Divide amount with divisor.
*
* @param string $amount
* @param int|float|string $divisor
*
* @return string
*/
public function divide($amount, $divisor);
/**
* Round number to following integer.
*
* @param string $number
*
* @return string
*/
public function ceil($number);
/**
* Round number to preceding integer.
*
* @param string $number
*
* @return string
*/
public function floor($number);
/**
* Returns the absolute value of the number.
*
* @param string $number
*
* @return string
*/
public function absolute($number);
/**
* Round number, use rounding mode for tie-breaker.
*
* @param int|float|string $number
* @param int $roundingMode
*
* @return string
*/
public function round($number, $roundingMode);
/**
* Share amount among ratio / total portions.
*
* @param string $amount
* @param int|float|string $ratio
* @param int|float|string $total
*
* @return string
*/
public function share($amount, $ratio, $total);
/**
* Get the modulus of an amount.
*
* @param string $amount
* @param int|float|string $divisor
*
* @return string
*/
public function mod($amount, $divisor);
}

View File

@@ -0,0 +1,241 @@
<?php
namespace Money\Calculator;
use Money\Calculator;
use Money\Money;
use Money\Number;
/**
* @author Frederik Bosch <f.bosch@genkgo.nl>
*/
final class BcMathCalculator implements Calculator
{
/**
* @var string
*/
private $scale;
/**
* @param int $scale
*/
public function __construct($scale = 14)
{
$this->scale = $scale;
}
/**
* {@inheritdoc}
*/
public static function supported()
{
return extension_loaded('bcmath');
}
/**
* {@inheritdoc}
*/
public function compare($a, $b)
{
return bccomp($a, $b, $this->scale);
}
/**
* {@inheritdoc}
*/
public function add($amount, $addend)
{
return (string) Number::fromString(bcadd($amount, $addend, $this->scale));
}
/**
* {@inheritdoc}
*
* @param $amount
* @param $subtrahend
*
* @return string
*/
public function subtract($amount, $subtrahend)
{
return (string) Number::fromString(bcsub($amount, $subtrahend, $this->scale));
}
/**
* {@inheritdoc}
*/
public function multiply($amount, $multiplier)
{
$multiplier = Number::fromNumber($multiplier);
return bcmul($amount, (string) $multiplier, $this->scale);
}
/**
* {@inheritdoc}
*/
public function divide($amount, $divisor)
{
$divisor = Number::fromNumber($divisor);
return bcdiv($amount, (string) $divisor, $this->scale);
}
/**
* {@inheritdoc}
*/
public function ceil($number)
{
$number = Number::fromNumber($number);
if ($number->isInteger()) {
return (string) $number;
}
if ($number->isNegative()) {
return bcadd((string) $number, '0', 0);
}
return bcadd((string) $number, '1', 0);
}
/**
* {@inheritdoc}
*/
public function floor($number)
{
$number = Number::fromNumber($number);
if ($number->isInteger()) {
return (string) $number;
}
if ($number->isNegative()) {
return bcadd((string) $number, '-1', 0);
}
return bcadd($number, '0', 0);
}
/**
* {@inheritdoc}
*/
public function absolute($number)
{
return ltrim($number, '-');
}
/**
* {@inheritdoc}
*/
public function round($number, $roundingMode)
{
$number = Number::fromNumber($number);
if ($number->isInteger()) {
return (string) $number;
}
if ($number->isHalf() === false) {
return $this->roundDigit($number);
}
if (Money::ROUND_HALF_UP === $roundingMode) {
return bcadd(
(string) $number,
$number->getIntegerRoundingMultiplier(),
0
);
}
if (Money::ROUND_HALF_DOWN === $roundingMode) {
return bcadd((string) $number, '0', 0);
}
if (Money::ROUND_HALF_EVEN === $roundingMode) {
if ($number->isCurrentEven()) {
return bcadd((string) $number, '0', 0);
}
return bcadd(
(string) $number,
$number->getIntegerRoundingMultiplier(),
0
);
}
if (Money::ROUND_HALF_ODD === $roundingMode) {
if ($number->isCurrentEven()) {
return bcadd(
(string) $number,
$number->getIntegerRoundingMultiplier(),
0
);
}
return bcadd((string) $number, '0', 0);
}
if (Money::ROUND_HALF_POSITIVE_INFINITY === $roundingMode) {
if ($number->isNegative()) {
return bcadd((string) $number, '0', 0);
}
return bcadd(
(string) $number,
$number->getIntegerRoundingMultiplier(),
0
);
}
if (Money::ROUND_HALF_NEGATIVE_INFINITY === $roundingMode) {
if ($number->isNegative()) {
return bcadd(
(string) $number,
$number->getIntegerRoundingMultiplier(),
0
);
}
return bcadd(
(string) $number,
'0',
0
);
}
throw new \InvalidArgumentException('Unknown rounding mode');
}
/**
* @return string
*/
private function roundDigit(Number $number)
{
if ($number->isCloserToNext()) {
return bcadd(
(string) $number,
$number->getIntegerRoundingMultiplier(),
0
);
}
return bcadd((string) $number, '0', 0);
}
/**
* {@inheritdoc}
*/
public function share($amount, $ratio, $total)
{
return $this->floor(bcdiv(bcmul($amount, $ratio, $this->scale), $total, $this->scale));
}
/**
* {@inheritdoc}
*/
public function mod($amount, $divisor)
{
return bcmod($amount, $divisor);
}
}

View File

@@ -0,0 +1,322 @@
<?php
namespace Money\Calculator;
use Money\Calculator;
use Money\Money;
use Money\Number;
/**
* @author Frederik Bosch <f.bosch@genkgo.nl>
*/
final class GmpCalculator implements Calculator
{
/**
* @var string
*/
private $scale;
/**
* @param int $scale
*/
public function __construct($scale = 14)
{
$this->scale = $scale;
}
/**
* {@inheritdoc}
*/
public static function supported()
{
return extension_loaded('gmp');
}
/**
* {@inheritdoc}
*/
public function compare($a, $b)
{
$aNum = Number::fromNumber($a);
$bNum = Number::fromNumber($b);
if ($aNum->isDecimal() || $bNum->isDecimal()) {
$integersCompared = gmp_cmp($aNum->getIntegerPart(), $bNum->getIntegerPart());
if ($integersCompared !== 0) {
return $integersCompared;
}
$aNumFractional = $aNum->getFractionalPart() === '' ? '0' : $aNum->getFractionalPart();
$bNumFractional = $bNum->getFractionalPart() === '' ? '0' : $bNum->getFractionalPart();
return gmp_cmp($aNumFractional, $bNumFractional);
}
return gmp_cmp($a, $b);
}
/**
* {@inheritdoc}
*/
public function add($amount, $addend)
{
return gmp_strval(gmp_add($amount, $addend));
}
/**
* {@inheritdoc}
*/
public function subtract($amount, $subtrahend)
{
return gmp_strval(gmp_sub($amount, $subtrahend));
}
/**
* {@inheritdoc}
*/
public function multiply($amount, $multiplier)
{
$multiplier = Number::fromNumber($multiplier);
if ($multiplier->isDecimal()) {
$decimalPlaces = strlen($multiplier->getFractionalPart());
$multiplierBase = $multiplier->getIntegerPart();
if ($multiplierBase) {
$multiplierBase .= $multiplier->getFractionalPart();
} else {
$multiplierBase = ltrim($multiplier->getFractionalPart(), '0');
}
$resultBase = gmp_strval(gmp_mul(gmp_init($amount), gmp_init($multiplierBase)));
if ('0' === $resultBase) {
return '0';
}
$result = substr($resultBase, $decimalPlaces * -1);
$resultLength = strlen($result);
if ($decimalPlaces > $resultLength) {
return '0.'.str_pad('', $decimalPlaces - $resultLength, '0').$result;
}
return substr($resultBase, 0, $decimalPlaces * -1).'.'.$result;
}
return gmp_strval(gmp_mul(gmp_init($amount), gmp_init((string) $multiplier)));
}
/**
* {@inheritdoc}
*/
public function divide($amount, $divisor)
{
$divisor = Number::fromNumber($divisor);
if ($divisor->isDecimal()) {
$decimalPlaces = strlen($divisor->getFractionalPart());
if ($divisor->getIntegerPart()) {
$divisor = new Number($divisor->getIntegerPart().$divisor->getFractionalPart());
} else {
$divisor = new Number(ltrim($divisor->getFractionalPart(), '0'));
}
$amount = gmp_strval(gmp_mul(gmp_init($amount), gmp_init('1'.str_pad('', $decimalPlaces, '0'))));
}
list($integer, $remainder) = gmp_div_qr(gmp_init($amount), gmp_init((string) $divisor));
if (gmp_cmp($remainder, '0') === 0) {
return gmp_strval($integer);
}
$divisionOfRemainder = gmp_strval(
gmp_div_q(
gmp_mul($remainder, gmp_init('1'.str_pad('', $this->scale, '0'))),
gmp_init((string) $divisor),
GMP_ROUND_MINUSINF
)
);
if ($divisionOfRemainder[0] === '-') {
$divisionOfRemainder = substr($divisionOfRemainder, 1);
}
return gmp_strval($integer).'.'.str_pad($divisionOfRemainder, $this->scale, '0', STR_PAD_LEFT);
}
/**
* {@inheritdoc}
*/
public function ceil($number)
{
$number = Number::fromNumber($number);
if ($number->isInteger()) {
return (string) $number;
}
if ($number->isNegative()) {
return $this->add($number->getIntegerPart(), '0');
}
return $this->add($number->getIntegerPart(), '1');
}
/**
* {@inheritdoc}
*/
public function floor($number)
{
$number = Number::fromNumber($number);
if ($number->isInteger()) {
return (string) $number;
}
if ($number->isNegative()) {
return $this->add($number->getIntegerPart(), '-1');
}
return $this->add($number->getIntegerPart(), '0');
}
/**
* {@inheritdoc}
*/
public function absolute($number)
{
return ltrim($number, '-');
}
/**
* {@inheritdoc}
*/
public function round($number, $roundingMode)
{
$number = Number::fromNumber($number);
if ($number->isInteger()) {
return (string) $number;
}
if ($number->isHalf() === false) {
return $this->roundDigit($number);
}
if (Money::ROUND_HALF_UP === $roundingMode) {
return $this->add(
$number->getIntegerPart(),
$number->getIntegerRoundingMultiplier()
);
}
if (Money::ROUND_HALF_DOWN === $roundingMode) {
return $this->add($number->getIntegerPart(), '0');
}
if (Money::ROUND_HALF_EVEN === $roundingMode) {
if ($number->isCurrentEven()) {
return $this->add($number->getIntegerPart(), '0');
}
return $this->add(
$number->getIntegerPart(),
$number->getIntegerRoundingMultiplier()
);
}
if (Money::ROUND_HALF_ODD === $roundingMode) {
if ($number->isCurrentEven()) {
return $this->add(
$number->getIntegerPart(),
$number->getIntegerRoundingMultiplier()
);
}
return $this->add($number->getIntegerPart(), '0');
}
if (Money::ROUND_HALF_POSITIVE_INFINITY === $roundingMode) {
if ($number->isNegative()) {
return $this->add(
$number->getIntegerPart(),
'0'
);
}
return $this->add(
$number->getIntegerPart(),
$number->getIntegerRoundingMultiplier()
);
}
if (Money::ROUND_HALF_NEGATIVE_INFINITY === $roundingMode) {
if ($number->isNegative()) {
return $this->add(
$number->getIntegerPart(),
$number->getIntegerRoundingMultiplier()
);
}
return $this->add(
$number->getIntegerPart(),
'0'
);
}
throw new \InvalidArgumentException('Unknown rounding mode');
}
/**
* @param $number
*
* @return string
*/
private function roundDigit(Number $number)
{
if ($number->isCloserToNext()) {
return $this->add(
$number->getIntegerPart(),
$number->getIntegerRoundingMultiplier()
);
}
return $this->add($number->getIntegerPart(), '0');
}
/**
* {@inheritdoc}
*/
public function share($amount, $ratio, $total)
{
return $this->floor($this->divide($this->multiply($amount, $ratio), $total));
}
/**
* {@inheritdoc}
*/
public function mod($amount, $divisor)
{
// gmp_mod() only calculates non-negative integers, so we use absolutes
$remainder = gmp_mod($this->absolute($amount), $this->absolute($divisor));
// If the amount was negative, we negate the result of the modulus operation
$amount = Number::fromNumber($amount);
if ($amount->isNegative()) {
$remainder = gmp_neg($remainder);
}
return gmp_strval($remainder);
}
/**
* @test
*/
public function it_divides_bug538()
{
$this->assertSame('-4.54545454545455', $this->getCalculator()->divide('-500', 110));
}
}

View File

@@ -0,0 +1,197 @@
<?php
namespace Money\Calculator;
use Money\Calculator;
use Money\Money;
use Money\Number;
/**
* @author Frederik Bosch <f.bosch@genkgo.nl>
*/
final class PhpCalculator implements Calculator
{
/**
* {@inheritdoc}
*/
public static function supported()
{
return true;
}
/**
* {@inheritdoc}
*/
public function compare($a, $b)
{
return ($a < $b) ? -1 : (($a > $b) ? 1 : 0);
}
/**
* {@inheritdoc}
*/
public function add($amount, $addend)
{
$result = $amount + $addend;
$this->assertInteger($result);
return (string) $result;
}
/**
* {@inheritdoc}
*/
public function subtract($amount, $subtrahend)
{
$result = $amount - $subtrahend;
$this->assertInteger($result);
return (string) $result;
}
/**
* {@inheritdoc}
*/
public function multiply($amount, $multiplier)
{
$result = $amount * $multiplier;
$this->assertIntegerBounds($result);
return (string) Number::fromNumber($result);
}
/**
* {@inheritdoc}
*/
public function divide($amount, $divisor)
{
$result = $amount / $divisor;
$this->assertIntegerBounds($result);
return (string) Number::fromNumber($result);
}
/**
* {@inheritdoc}
*/
public function ceil($number)
{
return $this->castInteger(ceil($number));
}
/**
* {@inheritdoc}
*/
public function floor($number)
{
return $this->castInteger(floor($number));
}
/**
* {@inheritdoc}
*/
public function absolute($number)
{
$result = ltrim($number, '-');
$this->assertIntegerBounds($result);
return (string) $result;
}
/**
* {@inheritdoc}
*/
public function round($number, $roundingMode)
{
if (Money::ROUND_HALF_POSITIVE_INFINITY === $roundingMode) {
$number = Number::fromNumber($number);
if ($number->isHalf()) {
return $this->castInteger(ceil((string) $number));
}
return $this->castInteger(round((string) $number, 0, Money::ROUND_HALF_UP));
}
if (Money::ROUND_HALF_NEGATIVE_INFINITY === $roundingMode) {
$number = Number::fromNumber($number);
if ($number->isHalf()) {
return $this->castInteger(floor((string) $number));
}
return $this->castInteger(round((string) $number, 0, Money::ROUND_HALF_DOWN));
}
return $this->castInteger(round($number, 0, $roundingMode));
}
/**
* {@inheritdoc}
*/
public function share($amount, $ratio, $total)
{
return $this->castInteger(floor($amount * $ratio / $total));
}
/**
* {@inheritdoc}
*/
public function mod($amount, $divisor)
{
$result = $amount % $divisor;
$this->assertIntegerBounds($result);
return (string) $result;
}
/**
* Asserts that an integer value didn't become something else
* (after some arithmetic operation).
*
* @param int $amount
*
* @throws \OverflowException If integer overflow occured
* @throws \UnderflowException If integer underflow occured
*/
private function assertIntegerBounds($amount)
{
if ($amount > PHP_INT_MAX) {
throw new \OverflowException('You overflowed the maximum allowed integer (PHP_INT_MAX)');
} elseif ($amount < ~PHP_INT_MAX) {
throw new \UnderflowException('You underflowed the minimum allowed integer (PHP_INT_MAX)');
}
}
/**
* Casts an amount to integer ensuring that an overflow/underflow did not occur.
*
* @param int $amount
*
* @return string
*/
private function castInteger($amount)
{
$this->assertIntegerBounds($amount);
return (string) intval($amount);
}
/**
* Asserts that integer remains integer after arithmetic operations.
*
* @param int $amount
*/
private function assertInteger($amount)
{
if (filter_var($amount, FILTER_VALIDATE_INT) === false) {
throw new \UnexpectedValueException('The result of arithmetic operation is not an integer');
}
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace Money;
/**
* Provides a way to convert Money to Money in another Currency using an exchange rate.
*
* @author Frederik Bosch <f.bosch@genkgo.nl>
*/
final class Converter
{
/**
* @var Currencies
*/
private $currencies;
/**
* @var Exchange
*/
private $exchange;
public function __construct(Currencies $currencies, Exchange $exchange)
{
$this->currencies = $currencies;
$this->exchange = $exchange;
}
/**
* @param int $roundingMode
*
* @return Money
*/
public function convert(Money $money, Currency $counterCurrency, $roundingMode = Money::ROUND_HALF_UP)
{
$baseCurrency = $money->getCurrency();
$ratio = $this->exchange->quote($baseCurrency, $counterCurrency)->getConversionRatio();
$baseCurrencySubunit = $this->currencies->subunitFor($baseCurrency);
$counterCurrencySubunit = $this->currencies->subunitFor($counterCurrency);
$subunitDifference = $baseCurrencySubunit - $counterCurrencySubunit;
$ratio = (string) Number::fromFloat($ratio)->base10($subunitDifference);
$counterValue = $money->multiply($ratio, $roundingMode);
return new Money($counterValue->getAmount(), $counterCurrency);
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Money;
use Money\Exception\UnknownCurrencyException;
/**
* Implement this to provide a list of currencies.
*
* @author Mathias Verraes
*/
interface Currencies extends \IteratorAggregate
{
/**
* Checks whether a currency is available in the current context.
*
* @return bool
*/
public function contains(Currency $currency);
/**
* Returns the subunit for a currency.
*
* @return int
*
* @throws UnknownCurrencyException If currency is not available in the current context
*/
public function subunitFor(Currency $currency);
}

View File

@@ -0,0 +1,77 @@
<?php
namespace Money\Currencies;
use Money\Currencies;
use Money\Currency;
use Money\Exception\UnknownCurrencyException;
/**
* Aggregates several currency repositories.
*
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*/
final class AggregateCurrencies implements Currencies
{
/**
* @var Currencies[]
*/
private $currencies;
/**
* @param Currencies[] $currencies
*/
public function __construct(array $currencies)
{
foreach ($currencies as $c) {
if (false === $c instanceof Currencies) {
throw new \InvalidArgumentException('All currency repositories must implement '.Currencies::class);
}
}
$this->currencies = $currencies;
}
/**
* {@inheritdoc}
*/
public function contains(Currency $currency)
{
foreach ($this->currencies as $currencies) {
if ($currencies->contains($currency)) {
return true;
}
}
return false;
}
/**
* {@inheritdoc}
*/
public function subunitFor(Currency $currency)
{
foreach ($this->currencies as $currencies) {
if ($currencies->contains($currency)) {
return $currencies->subunitFor($currency);
}
}
throw new UnknownCurrencyException('Cannot find currency '.$currency->getCode());
}
/**
* {@inheritdoc}
*/
#[\ReturnTypeWillChange]
public function getIterator()
{
$iterator = new \AppendIterator();
foreach ($this->currencies as $currencies) {
$iterator->append($currencies->getIterator());
}
return $iterator;
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace Money\Currencies;
use Money\Currencies;
use Money\Currency;
use Money\Exception\UnknownCurrencyException;
/**
* @author Frederik Bosch <f.bosch@genkgo.nl>
*/
final class BitcoinCurrencies implements Currencies
{
const CODE = 'XBT';
const SYMBOL = "\xC9\x83";
/**
* {@inheritdoc}
*/
public function contains(Currency $currency)
{
return self::CODE === $currency->getCode();
}
/**
* {@inheritdoc}
*/
public function subunitFor(Currency $currency)
{
if ($currency->getCode() !== self::CODE) {
throw new UnknownCurrencyException($currency->getCode().' is not bitcoin and is not supported by this currency repository');
}
return 8;
}
/**
* {@inheritdoc}
*/
#[\ReturnTypeWillChange]
public function getIterator()
{
return new \ArrayIterator([new Currency(self::CODE)]);
}
}

View File

@@ -0,0 +1,95 @@
<?php
namespace Money\Currencies;
use Cache\Taggable\TaggableItemInterface;
use Money\Currencies;
use Money\Currency;
use Psr\Cache\CacheItemPoolInterface;
/**
* Cache the result of currency checking.
*
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*/
final class CachedCurrencies implements Currencies
{
/**
* @var Currencies
*/
private $currencies;
/**
* @var CacheItemPoolInterface
*/
private $pool;
public function __construct(Currencies $currencies, CacheItemPoolInterface $pool)
{
$this->currencies = $currencies;
$this->pool = $pool;
}
/**
* {@inheritdoc}
*/
public function contains(Currency $currency)
{
$item = $this->pool->getItem('currency|availability|'.$currency->getCode());
if (false === $item->isHit()) {
$item->set($this->currencies->contains($currency));
if ($item instanceof TaggableItemInterface) {
$item->addTag('currency.availability');
}
$this->pool->save($item);
}
return $item->get();
}
/**
* {@inheritdoc}
*/
public function subunitFor(Currency $currency)
{
$item = $this->pool->getItem('currency|subunit|'.$currency->getCode());
if (false === $item->isHit()) {
$item->set($this->currencies->subunitFor($currency));
if ($item instanceof TaggableItemInterface) {
$item->addTag('currency.subunit');
}
$this->pool->save($item);
}
return $item->get();
}
/**
* {@inheritdoc}
*/
#[\ReturnTypeWillChange]
public function getIterator()
{
return new \CallbackFilterIterator(
$this->currencies->getIterator(),
function (Currency $currency) {
$item = $this->pool->getItem('currency|availability|'.$currency->getCode());
$item->set(true);
if ($item instanceof TaggableItemInterface) {
$item->addTag('currency.availability');
}
$this->pool->save($item);
return true;
}
);
}
}

View File

@@ -0,0 +1,73 @@
<?php
namespace Money\Currencies;
use Money\Currencies;
use Money\Currency;
use Money\Exception\UnknownCurrencyException;
/**
* A list of custom currencies.
*
* @author George Mponos <gmponos@gmail.com>
*/
final class CurrencyList implements Currencies
{
/**
* Map of currencies indexed by code.
*
* @var array
*/
private $currencies;
public function __construct(array $currencies)
{
foreach ($currencies as $currencyCode => $subunit) {
if (empty($currencyCode) || !is_string($currencyCode)) {
throw new \InvalidArgumentException(sprintf('Currency code must be a string and not empty. "%s" given', $currencyCode));
}
if (!is_int($subunit) || $subunit < 0) {
throw new \InvalidArgumentException(sprintf('Currency %s does not have a valid minor unit. Must be a positive integer.', $currencyCode));
}
}
$this->currencies = $currencies;
}
/**
* {@inheritdoc}
*/
public function contains(Currency $currency)
{
return isset($this->currencies[$currency->getCode()]);
}
/**
* {@inheritdoc}
*/
public function subunitFor(Currency $currency)
{
if (!$this->contains($currency)) {
throw new UnknownCurrencyException('Cannot find currency '.$currency->getCode());
}
return $this->currencies[$currency->getCode()];
}
/**
* {@inheritdoc}
*/
#[\ReturnTypeWillChange]
public function getIterator()
{
return new \ArrayIterator(
array_map(
function ($code) {
return new Currency($code);
},
array_keys($this->currencies)
)
);
}
}

View File

@@ -0,0 +1,102 @@
<?php
namespace Money\Currencies;
use Money\Currencies;
use Money\Currency;
use Money\Exception\UnknownCurrencyException;
/**
* List of supported ISO 4217 currency codes and names.
*
* @author Mathias Verraes
*/
final class ISOCurrencies implements Currencies
{
/**
* Map of known currencies indexed by code.
*
* @var array
*/
private static $currencies;
/**
* {@inheritdoc}
*/
public function contains(Currency $currency)
{
return isset($this->getCurrencies()[$currency->getCode()]);
}
/**
* {@inheritdoc}
*/
public function subunitFor(Currency $currency)
{
if (!$this->contains($currency)) {
throw new UnknownCurrencyException('Cannot find ISO currency '.$currency->getCode());
}
return $this->getCurrencies()[$currency->getCode()]['minorUnit'];
}
/**
* Returns the numeric code for a currency.
*
* @return int
*
* @throws UnknownCurrencyException If currency is not available in the current context
*/
public function numericCodeFor(Currency $currency)
{
if (!$this->contains($currency)) {
throw new UnknownCurrencyException('Cannot find ISO currency '.$currency->getCode());
}
return $this->getCurrencies()[$currency->getCode()]['numericCode'];
}
/**
* @return \Traversable
*/
#[\ReturnTypeWillChange]
public function getIterator()
{
return new \ArrayIterator(
array_map(
function ($code) {
return new Currency($code);
},
array_keys($this->getCurrencies())
)
);
}
/**
* Returns a map of known currencies indexed by code.
*
* @return array
*/
private function getCurrencies()
{
if (null === self::$currencies) {
self::$currencies = $this->loadCurrencies();
}
return self::$currencies;
}
/**
* @return array
*/
private function loadCurrencies()
{
$file = __DIR__.'/../../resources/currency.php';
if (file_exists($file)) {
return require $file;
}
throw new \RuntimeException('Failed to load currency ISO codes.');
}
}

View File

@@ -0,0 +1,87 @@
<?php
namespace Money;
/**
* Currency Value Object.
*
* Holds Currency specific data.
*
* @author Mathias Verraes
*
* @psalm-immutable
*/
final class Currency implements \JsonSerializable
{
/**
* Currency code.
*
* @var string
*/
private $code;
/**
* @param string $code
*/
public function __construct($code)
{
if (!is_string($code)) {
throw new \InvalidArgumentException('Currency code should be string');
}
if ($code === '') {
throw new \InvalidArgumentException('Currency code should not be empty string');
}
$this->code = $code;
}
/**
* Returns the currency code.
*
* @return string
*/
public function getCode()
{
return $this->code;
}
/**
* Checks whether this currency is the same as an other.
*
* @return bool
*/
public function equals(Currency $other)
{
return $this->code === $other->code;
}
/**
* Checks whether this currency is available in the passed context.
*
* @return bool
*/
public function isAvailableWithin(Currencies $currencies)
{
return $currencies->contains($this);
}
/**
* @return string
*/
public function __toString()
{
return $this->code;
}
/**
* {@inheritdoc}
*
* @return string
*/
#[\ReturnTypeWillChange]
public function jsonSerialize()
{
return $this->code;
}
}

View File

@@ -0,0 +1,130 @@
<?php
namespace Money;
/**
* Currency Pair holding a base, a counter currency and a conversion ratio.
*
* @author Mathias Verraes
*
* @see http://en.wikipedia.org/wiki/Currency_pair
*/
final class CurrencyPair implements \JsonSerializable
{
/**
* Currency to convert from.
*
* @var Currency
*/
private $baseCurrency;
/**
* Currency to convert to.
*
* @var Currency
*/
private $counterCurrency;
/**
* @var float
*/
private $conversionRatio;
/**
* @param float $conversionRatio
*
* @throws \InvalidArgumentException If conversion ratio is not numeric
*/
public function __construct(Currency $baseCurrency, Currency $counterCurrency, $conversionRatio)
{
if (!is_numeric($conversionRatio)) {
throw new \InvalidArgumentException('Conversion ratio must be numeric');
}
$this->counterCurrency = $counterCurrency;
$this->baseCurrency = $baseCurrency;
$this->conversionRatio = (float) $conversionRatio;
}
/**
* Creates a new Currency Pair based on "EUR/USD 1.2500" form representation.
*
* @param string $iso String representation of the form "EUR/USD 1.2500"
*
* @return CurrencyPair
*
* @throws \InvalidArgumentException Format of $iso is invalid
*/
public static function createFromIso($iso)
{
$currency = '([A-Z]{2,3})';
$ratio = "([0-9]*\.?[0-9]+)"; // @see http://www.regular-expressions.info/floatingpoint.html
$pattern = '#'.$currency.'/'.$currency.' '.$ratio.'#';
$matches = [];
if (!preg_match($pattern, $iso, $matches)) {
throw new \InvalidArgumentException(sprintf('Cannot create currency pair from ISO string "%s", format of string is invalid', $iso));
}
return new self(new Currency($matches[1]), new Currency($matches[2]), $matches[3]);
}
/**
* Returns the counter currency.
*
* @return Currency
*/
public function getCounterCurrency()
{
return $this->counterCurrency;
}
/**
* Returns the base currency.
*
* @return Currency
*/
public function getBaseCurrency()
{
return $this->baseCurrency;
}
/**
* Returns the conversion ratio.
*
* @return float
*/
public function getConversionRatio()
{
return $this->conversionRatio;
}
/**
* Checks if an other CurrencyPair has the same parameters as this.
*
* @return bool
*/
public function equals(CurrencyPair $other)
{
return
$this->baseCurrency->equals($other->baseCurrency)
&& $this->counterCurrency->equals($other->counterCurrency)
&& $this->conversionRatio === $other->conversionRatio
;
}
/**
* {@inheritdoc}
*
* @return array
*/
public function jsonSerialize()
{
return [
'baseCurrency' => $this->baseCurrency,
'counterCurrency' => $this->counterCurrency,
'ratio' => $this->conversionRatio,
];
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace Money;
/**
* Common interface for all exceptions thrown by this library.
*
* @author Frederik Bosch <f.bosch@genkgo.nl>
*/
interface Exception
{
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Money\Exception;
use Money\Exception;
/**
* Thrown when a Money object cannot be formatted into a string.
*
* @author Frederik Bosch <f.bosch@genkgo.nl>
*/
final class FormatterException extends \RuntimeException implements Exception
{
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Money\Exception;
use Money\Exception;
/**
* Thrown when a string cannot be parsed to a Money object.
*
* @author Frederik Bosch <f.bosch@genkgo.nl>
*/
final class ParserException extends \RuntimeException implements Exception
{
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Money\Exception;
use Money\Exception;
/**
* Thrown when trying to get ISO currency that does not exists.
*
* @author Frederik Bosch <f.bosch@genkgo.nl>
*/
final class UnknownCurrencyException extends \DomainException implements Exception
{
}

View File

@@ -0,0 +1,30 @@
<?php
namespace Money\Exception;
use Money\Currency;
use Money\Exception;
/**
* Thrown when there is no currency pair (rate) available for the given currencies.
*
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*/
final class UnresolvableCurrencyPairException extends \InvalidArgumentException implements Exception
{
/**
* Creates an exception from Currency objects.
*
* @return UnresolvableCurrencyPairException
*/
public static function createFromCurrencies(Currency $baseCurrency, Currency $counterCurrency)
{
$message = sprintf(
'Cannot resolve a currency pair for currencies: %s/%s',
$baseCurrency->getCode(),
$counterCurrency->getCode()
);
return new self($message);
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace Money;
use Money\Exception\UnresolvableCurrencyPairException;
/**
* Provides a way to get exchange rate from a third-party source and return a currency pair.
*
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*/
interface Exchange
{
/**
* Returns a currency pair for the passed currencies with the rate coming from a third-party source.
*
* @return CurrencyPair
*
* @throws UnresolvableCurrencyPairException When there is no currency pair (rate) available for the given currencies
*/
public function quote(Currency $baseCurrency, Currency $counterCurrency);
}

View File

@@ -0,0 +1,47 @@
<?php
namespace Money\Exchange;
use Exchanger\Contract\ExchangeRateProvider;
use Exchanger\CurrencyPair as ExchangerCurrencyPair;
use Exchanger\Exception\Exception as ExchangerException;
use Exchanger\ExchangeRateQuery;
use Money\Currency;
use Money\CurrencyPair;
use Money\Exception\UnresolvableCurrencyPairException;
use Money\Exchange;
/**
* Provides a way to get exchange rate from a third-party source and return a currency pair.
*
* @author Maksim (Ellrion) Platonov <ellrion11@gmail.com>
*/
final class ExchangerExchange implements Exchange
{
/**
* @var ExchangeRateProvider
*/
private $exchanger;
public function __construct(ExchangeRateProvider $exchanger)
{
$this->exchanger = $exchanger;
}
/**
* {@inheritdoc}
*/
public function quote(Currency $baseCurrency, Currency $counterCurrency)
{
try {
$query = new ExchangeRateQuery(
new ExchangerCurrencyPair($baseCurrency->getCode(), $counterCurrency->getCode())
);
$rate = $this->exchanger->getExchangeRate($query);
} catch (ExchangerException $e) {
throw UnresolvableCurrencyPairException::createFromCurrencies($baseCurrency, $counterCurrency);
}
return new CurrencyPair($baseCurrency, $counterCurrency, $rate->getValue());
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace Money\Exchange;
use Money\Currency;
use Money\CurrencyPair;
use Money\Exception\UnresolvableCurrencyPairException;
use Money\Exchange;
/**
* Provides a way to get exchange rate from a static list (array).
*
* @author Frederik Bosch <f.bosch@genkgo.nl>
*/
final class FixedExchange implements Exchange
{
/**
* @var array
*/
private $list;
public function __construct(array $list)
{
$this->list = $list;
}
/**
* {@inheritdoc}
*/
public function quote(Currency $baseCurrency, Currency $counterCurrency)
{
if (isset($this->list[$baseCurrency->getCode()][$counterCurrency->getCode()])) {
return new CurrencyPair(
$baseCurrency,
$counterCurrency,
$this->list[$baseCurrency->getCode()][$counterCurrency->getCode()]
);
}
throw UnresolvableCurrencyPairException::createFromCurrencies($baseCurrency, $counterCurrency);
}
}

View File

@@ -0,0 +1,195 @@
<?php
namespace Money\Exchange;
use Money\Calculator;
use Money\Calculator\BcMathCalculator;
use Money\Calculator\GmpCalculator;
use Money\Calculator\PhpCalculator;
use Money\Currencies;
use Money\Currency;
use Money\CurrencyPair;
use Money\Exception\UnresolvableCurrencyPairException;
use Money\Exchange;
/**
* Provides a way to get an exchange rate through a minimal set of intermediate conversions.
*
* @author Michael Cordingley <Michael.Cordingley@gmail.com>
*/
final class IndirectExchange implements Exchange
{
/**
* @var Calculator
*/
private static $calculator;
/**
* @var array
*/
private static $calculators = [
BcMathCalculator::class,
GmpCalculator::class,
PhpCalculator::class,
];
/**
* @var Currencies
*/
private $currencies;
/**
* @var Exchange
*/
private $exchange;
public function __construct(Exchange $exchange, Currencies $currencies)
{
$this->exchange = $exchange;
$this->currencies = $currencies;
}
/**
* @param string $calculator
*/
public static function registerCalculator($calculator)
{
if (is_a($calculator, Calculator::class, true) === false) {
throw new \InvalidArgumentException('Calculator must implement '.Calculator::class);
}
array_unshift(self::$calculators, $calculator);
}
/**
* {@inheritdoc}
*/
public function quote(Currency $baseCurrency, Currency $counterCurrency)
{
try {
return $this->exchange->quote($baseCurrency, $counterCurrency);
} catch (UnresolvableCurrencyPairException $exception) {
$rate = array_reduce($this->getConversions($baseCurrency, $counterCurrency), function ($carry, CurrencyPair $pair) {
return static::getCalculator()->multiply($carry, $pair->getConversionRatio());
}, '1.0');
return new CurrencyPair($baseCurrency, $counterCurrency, $rate);
}
}
/**
* @return CurrencyPair[]
*
* @throws UnresolvableCurrencyPairException
*/
private function getConversions(Currency $baseCurrency, Currency $counterCurrency)
{
$startNode = $this->initializeNode($baseCurrency);
$startNode->discovered = true;
$nodes = [$baseCurrency->getCode() => $startNode];
$frontier = new \SplQueue();
$frontier->enqueue($startNode);
while ($frontier->count()) {
/** @var \stdClass $currentNode */
$currentNode = $frontier->dequeue();
/** @var Currency $currentCurrency */
$currentCurrency = $currentNode->currency;
if ($currentCurrency->equals($counterCurrency)) {
return $this->reconstructConversionChain($nodes, $currentNode);
}
/** @var Currency $candidateCurrency */
foreach ($this->currencies as $candidateCurrency) {
if (!isset($nodes[$candidateCurrency->getCode()])) {
$nodes[$candidateCurrency->getCode()] = $this->initializeNode($candidateCurrency);
}
/** @var \stdClass $node */
$node = $nodes[$candidateCurrency->getCode()];
if (!$node->discovered) {
try {
// Check if the candidate is a neighbor. This will throw an exception if it isn't.
$this->exchange->quote($currentCurrency, $candidateCurrency);
$node->discovered = true;
$node->parent = $currentNode;
$frontier->enqueue($node);
} catch (UnresolvableCurrencyPairException $exception) {
// Not a neighbor. Move on.
}
}
}
}
throw UnresolvableCurrencyPairException::createFromCurrencies($baseCurrency, $counterCurrency);
}
/**
* @return \stdClass
*/
private function initializeNode(Currency $currency)
{
$node = new \stdClass();
$node->currency = $currency;
$node->discovered = false;
$node->parent = null;
return $node;
}
/**
* @return CurrencyPair[]
*/
private function reconstructConversionChain(array $currencies, \stdClass $goalNode)
{
$current = $goalNode;
$conversions = [];
while ($current->parent) {
$previous = $currencies[$current->parent->currency->getCode()];
$conversions[] = $this->exchange->quote($previous->currency, $current->currency);
$current = $previous;
}
return array_reverse($conversions);
}
/**
* @return Calculator
*/
private function getCalculator()
{
if (null === self::$calculator) {
self::$calculator = self::initializeCalculator();
}
return self::$calculator;
}
/**
* @return Calculator
*
* @throws \RuntimeException If cannot find calculator for money calculations
*/
private static function initializeCalculator()
{
$calculators = self::$calculators;
foreach ($calculators as $calculator) {
/** @var Calculator $calculator */
if ($calculator::supported()) {
return new $calculator();
}
}
throw new \RuntimeException('Cannot find calculator for money calculations');
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace Money\Exchange;
use Money\Currency;
use Money\CurrencyPair;
use Money\Exception\UnresolvableCurrencyPairException;
use Money\Exchange;
/**
* Tries the reverse of the currency pair if one is not available.
*
* Note: adding nested ReversedCurrenciesExchange could cause a huge performance hit.
*
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*/
final class ReversedCurrenciesExchange implements Exchange
{
/**
* @var Exchange
*/
private $exchange;
public function __construct(Exchange $exchange)
{
$this->exchange = $exchange;
}
/**
* {@inheritdoc}
*/
public function quote(Currency $baseCurrency, Currency $counterCurrency)
{
try {
return $this->exchange->quote($baseCurrency, $counterCurrency);
} catch (UnresolvableCurrencyPairException $exception) {
try {
$currencyPair = $this->exchange->quote($counterCurrency, $baseCurrency);
return new CurrencyPair($baseCurrency, $counterCurrency, 1 / $currencyPair->getConversionRatio());
} catch (UnresolvableCurrencyPairException $inversedException) {
throw $exception;
}
}
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace Money\Exchange;
use Exchanger\Exception\Exception as ExchangerException;
use Money\Currency;
use Money\CurrencyPair;
use Money\Exception\UnresolvableCurrencyPairException;
use Money\Exchange;
use Swap\Swap;
/**
* Provides a way to get exchange rate from a third-party source and return a currency pair.
*
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*/
final class SwapExchange implements Exchange
{
/**
* @var Swap
*/
private $swap;
public function __construct(Swap $swap)
{
$this->swap = $swap;
}
/**
* {@inheritdoc}
*/
public function quote(Currency $baseCurrency, Currency $counterCurrency)
{
try {
$rate = $this->swap->latest($baseCurrency->getCode().'/'.$counterCurrency->getCode());
} catch (ExchangerException $e) {
throw UnresolvableCurrencyPairException::createFromCurrencies($baseCurrency, $counterCurrency);
}
return new CurrencyPair($baseCurrency, $counterCurrency, $rate->getValue());
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace Money\Formatter;
use Money\Exception\FormatterException;
use Money\Money;
use Money\MoneyFormatter;
/**
* Formats a Money object using other Money formatters.
*
* @author Frederik Bosch <f.bosch@genkgo.nl>
*/
final class AggregateMoneyFormatter implements MoneyFormatter
{
/**
* @var MoneyFormatter[]
*/
private $formatters = [];
/**
* @param MoneyFormatter[] $formatters
*/
public function __construct(array $formatters)
{
if (empty($formatters)) {
throw new \InvalidArgumentException(sprintf('Initialize an empty %s is not possible', self::class));
}
foreach ($formatters as $currencyCode => $formatter) {
if (false === $formatter instanceof MoneyFormatter) {
throw new \InvalidArgumentException('All formatters must implement '.MoneyFormatter::class);
}
$this->formatters[$currencyCode] = $formatter;
}
}
/**
* {@inheritdoc}
*/
public function format(Money $money)
{
$currencyCode = $money->getCurrency()->getCode();
if (isset($this->formatters[$currencyCode])) {
return $this->formatters[$currencyCode]->format($money);
}
if (isset($this->formatters['*'])) {
return $this->formatters['*']->format($money);
}
throw new FormatterException('No formatter found for currency '.$currencyCode);
}
}

View File

@@ -0,0 +1,87 @@
<?php
namespace Money\Formatter;
use Money\Currencies;
use Money\Currencies\BitcoinCurrencies;
use Money\Exception\FormatterException;
use Money\Money;
use Money\MoneyFormatter;
use Money\Number;
/**
* Formats Money to Bitcoin currency.
*
* @author Frederik Bosch <f.bosch@genkgo.nl>
*/
final class BitcoinMoneyFormatter implements MoneyFormatter
{
/**
* @var int
*/
private $fractionDigits;
/**
* @var Currencies
*/
private $currencies;
/**
* @param int $fractionDigits
*/
public function __construct($fractionDigits, Currencies $currencies)
{
$this->fractionDigits = $fractionDigits;
$this->currencies = $currencies;
}
/**
* {@inheritdoc}
*/
public function format(Money $money)
{
if (BitcoinCurrencies::CODE !== $money->getCurrency()->getCode()) {
throw new FormatterException('Bitcoin Formatter can only format Bitcoin currency');
}
$valueBase = $money->getAmount();
$negative = false;
if ('-' === $valueBase[0]) {
$negative = true;
$valueBase = substr($valueBase, 1);
}
$subunit = $this->currencies->subunitFor($money->getCurrency());
$valueBase = Number::roundMoneyValue($valueBase, $this->fractionDigits, $subunit);
$valueLength = strlen($valueBase);
if ($valueLength > $subunit) {
$formatted = substr($valueBase, 0, $valueLength - $subunit);
if ($subunit) {
$formatted .= '.';
$formatted .= substr($valueBase, $valueLength - $subunit);
}
} else {
$formatted = '0.'.str_pad('', $subunit - $valueLength, '0').$valueBase;
}
if ($this->fractionDigits === 0) {
$formatted = substr($formatted, 0, strpos($formatted, '.'));
} elseif ($this->fractionDigits > $subunit) {
$formatted .= str_pad('', $this->fractionDigits - $subunit, '0');
} elseif ($this->fractionDigits < $subunit) {
$lastDigit = strpos($formatted, '.') + $this->fractionDigits + 1;
$formatted = substr($formatted, 0, $lastDigit);
}
$formatted = BitcoinCurrencies::SYMBOL.$formatted;
if (true === $negative) {
$formatted = '-'.$formatted;
}
return $formatted;
}
}

View File

@@ -0,0 +1,59 @@
<?php
namespace Money\Formatter;
use Money\Currencies;
use Money\Money;
use Money\MoneyFormatter;
/**
* Formats a Money object as a decimal string.
*
* @author Teoh Han Hui <teohhanhui@gmail.com>
*/
final class DecimalMoneyFormatter implements MoneyFormatter
{
/**
* @var Currencies
*/
private $currencies;
public function __construct(Currencies $currencies)
{
$this->currencies = $currencies;
}
/**
* {@inheritdoc}
*/
public function format(Money $money)
{
$valueBase = $money->getAmount();
$negative = false;
if ($valueBase[0] === '-') {
$negative = true;
$valueBase = substr($valueBase, 1);
}
$subunit = $this->currencies->subunitFor($money->getCurrency());
$valueLength = strlen($valueBase);
if ($valueLength > $subunit) {
$formatted = substr($valueBase, 0, $valueLength - $subunit);
$decimalDigits = substr($valueBase, $valueLength - $subunit);
if (strlen($decimalDigits) > 0) {
$formatted .= '.'.$decimalDigits;
}
} else {
$formatted = '0.'.str_pad('', $subunit - $valueLength, '0').$valueBase;
}
if ($negative === true) {
$formatted = '-'.$formatted;
}
return $formatted;
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace Money\Formatter;
use Money\Currencies;
use Money\Money;
use Money\MoneyFormatter;
/**
* Formats a Money object using intl extension.
*
* @author Frederik Bosch <f.bosch@genkgo.nl>
*/
final class IntlLocalizedDecimalFormatter implements MoneyFormatter
{
/**
* @var \NumberFormatter
*/
private $formatter;
/**
* @var Currencies
*/
private $currencies;
public function __construct(\NumberFormatter $formatter, Currencies $currencies)
{
$this->formatter = $formatter;
$this->currencies = $currencies;
}
/**
* {@inheritdoc}
*/
public function format(Money $money)
{
$valueBase = $money->getAmount();
$negative = false;
if ($valueBase[0] === '-') {
$negative = true;
$valueBase = substr($valueBase, 1);
}
$subunit = $this->currencies->subunitFor($money->getCurrency());
$valueLength = strlen($valueBase);
if ($valueLength > $subunit) {
$formatted = substr($valueBase, 0, $valueLength - $subunit);
$decimalDigits = substr($valueBase, $valueLength - $subunit);
if (strlen($decimalDigits) > 0) {
$formatted .= '.'.$decimalDigits;
}
} else {
$formatted = '0.'.str_pad('', $subunit - $valueLength, '0').$valueBase;
}
if ($negative === true) {
$formatted = '-'.$formatted;
}
return $this->formatter->format($formatted);
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace Money\Formatter;
use Money\Currencies;
use Money\Money;
use Money\MoneyFormatter;
/**
* Formats a Money object using intl extension.
*
* @author Frederik Bosch <f.bosch@genkgo.nl>
*/
final class IntlMoneyFormatter implements MoneyFormatter
{
/**
* @var \NumberFormatter
*/
private $formatter;
/**
* @var Currencies
*/
private $currencies;
public function __construct(\NumberFormatter $formatter, Currencies $currencies)
{
$this->formatter = $formatter;
$this->currencies = $currencies;
}
/**
* {@inheritdoc}
*/
public function format(Money $money)
{
$valueBase = $money->getAmount();
$negative = false;
if ($valueBase[0] === '-') {
$negative = true;
$valueBase = substr($valueBase, 1);
}
$subunit = $this->currencies->subunitFor($money->getCurrency());
$valueLength = strlen($valueBase);
if ($valueLength > $subunit) {
$formatted = substr($valueBase, 0, $valueLength - $subunit);
$decimalDigits = substr($valueBase, $valueLength - $subunit);
if (strlen($decimalDigits) > 0) {
$formatted .= '.'.$decimalDigits;
}
} else {
$formatted = '0.'.str_pad('', $subunit - $valueLength, '0').$valueBase;
}
if ($negative === true) {
$formatted = '-'.$formatted;
}
return $this->formatter->formatCurrency($formatted, $money->getCurrency()->getCode());
}
}

View File

@@ -0,0 +1,613 @@
<?php
namespace Money;
use Money\Calculator\BcMathCalculator;
use Money\Calculator\GmpCalculator;
use Money\Calculator\PhpCalculator;
/**
* Money Value Object.
*
* @author Mathias Verraes
*
* @psalm-immutable
*/
final class Money implements \JsonSerializable
{
use MoneyFactory;
const ROUND_HALF_UP = PHP_ROUND_HALF_UP;
const ROUND_HALF_DOWN = PHP_ROUND_HALF_DOWN;
const ROUND_HALF_EVEN = PHP_ROUND_HALF_EVEN;
const ROUND_HALF_ODD = PHP_ROUND_HALF_ODD;
const ROUND_UP = 5;
const ROUND_DOWN = 6;
const ROUND_HALF_POSITIVE_INFINITY = 7;
const ROUND_HALF_NEGATIVE_INFINITY = 8;
/**
* Internal value.
*
* @var string
*/
private $amount;
/**
* @var Currency
*/
private $currency;
/**
* @var Calculator
*/
private static $calculator;
/**
* @var array
*/
private static $calculators = [
BcMathCalculator::class,
GmpCalculator::class,
PhpCalculator::class,
];
/**
* @param int|string $amount Amount, expressed in the smallest units of $currency (eg cents)
*
* @throws \InvalidArgumentException If amount is not integer
*/
public function __construct($amount, Currency $currency)
{
if (filter_var($amount, FILTER_VALIDATE_INT) === false) {
$numberFromString = Number::fromString($amount);
if (!$numberFromString->isInteger()) {
throw new \InvalidArgumentException('Amount must be an integer(ish) value');
}
$amount = $numberFromString->getIntegerPart();
}
$this->amount = (string) $amount;
$this->currency = $currency;
}
/**
* Returns a new Money instance based on the current one using the Currency.
*
* @param int|string $amount
*
* @return Money
*
* @throws \InvalidArgumentException If amount is not integer
*/
private function newInstance($amount)
{
return new self($amount, $this->currency);
}
/**
* Checks whether a Money has the same Currency as this.
*
* @return bool
*/
public function isSameCurrency(Money $other)
{
return $this->currency->equals($other->currency);
}
/**
* Asserts that a Money has the same currency as this.
*
* @throws \InvalidArgumentException If $other has a different currency
*/
private function assertSameCurrency(Money $other)
{
if (!$this->isSameCurrency($other)) {
throw new \InvalidArgumentException('Currencies must be identical');
}
}
/**
* Checks whether the value represented by this object equals to the other.
*
* @return bool
*/
public function equals(Money $other)
{
return $this->isSameCurrency($other) && $this->amount === $other->amount;
}
/**
* 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.
*
* @return int
*/
public function compare(Money $other)
{
$this->assertSameCurrency($other);
return $this->getCalculator()->compare($this->amount, $other->amount);
}
/**
* Checks whether the value represented by this object is greater than the other.
*
* @return bool
*/
public function greaterThan(Money $other)
{
return $this->compare($other) > 0;
}
/**
* @param \Money\Money $other
*
* @return bool
*/
public function greaterThanOrEqual(Money $other)
{
return $this->compare($other) >= 0;
}
/**
* Checks whether the value represented by this object is less than the other.
*
* @return bool
*/
public function lessThan(Money $other)
{
return $this->compare($other) < 0;
}
/**
* @param \Money\Money $other
*
* @return bool
*/
public function lessThanOrEqual(Money $other)
{
return $this->compare($other) <= 0;
}
/**
* Returns the value represented by this object.
*
* @return string
*/
public function getAmount()
{
return $this->amount;
}
/**
* Returns the currency of this object.
*
* @return Currency
*/
public function getCurrency()
{
return $this->currency;
}
/**
* Returns a new Money object that represents
* the sum of this and an other Money object.
*
* @param Money[] $addends
*
* @return Money
*/
public function add(Money ...$addends)
{
$amount = $this->amount;
$calculator = $this->getCalculator();
foreach ($addends as $addend) {
$this->assertSameCurrency($addend);
$amount = $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
*
* @return Money
*
* @psalm-pure
*/
public function subtract(Money ...$subtrahends)
{
$amount = $this->amount;
$calculator = $this->getCalculator();
foreach ($subtrahends as $subtrahend) {
$this->assertSameCurrency($subtrahend);
$amount = $calculator->subtract($amount, $subtrahend->amount);
}
return new self($amount, $this->currency);
}
/**
* Asserts that the operand is integer or float.
*
* @param float|int|string $operand
*
* @throws \InvalidArgumentException If $operand is neither integer nor float
*/
private function assertOperand($operand)
{
if (!is_numeric($operand)) {
throw new \InvalidArgumentException(sprintf('Operand should be a numeric value, "%s" given.', is_object($operand) ? get_class($operand) : gettype($operand)));
}
}
/**
* Asserts that rounding mode is a valid integer value.
*
* @param int $roundingMode
*
* @throws \InvalidArgumentException If $roundingMode is not valid
*/
private function assertRoundingMode($roundingMode)
{
if (!in_array(
$roundingMode, [
self::ROUND_HALF_DOWN, self::ROUND_HALF_EVEN, self::ROUND_HALF_ODD,
self::ROUND_HALF_UP, self::ROUND_UP, self::ROUND_DOWN,
self::ROUND_HALF_POSITIVE_INFINITY, self::ROUND_HALF_NEGATIVE_INFINITY,
], true
)) {
throw new \InvalidArgumentException('Rounding mode should be Money::ROUND_HALF_DOWN | '.'Money::ROUND_HALF_EVEN | Money::ROUND_HALF_ODD | '.'Money::ROUND_HALF_UP | Money::ROUND_UP | Money::ROUND_DOWN'.'Money::ROUND_HALF_POSITIVE_INFINITY | Money::ROUND_HALF_NEGATIVE_INFINITY');
}
}
/**
* Returns a new Money object that represents
* the multiplied value by the given factor.
*
* @param float|int|string $multiplier
* @param int $roundingMode
*
* @return Money
*/
public function multiply($multiplier, $roundingMode = self::ROUND_HALF_UP)
{
$this->assertOperand($multiplier);
$this->assertRoundingMode($roundingMode);
$product = $this->round($this->getCalculator()->multiply($this->amount, $multiplier), $roundingMode);
return $this->newInstance($product);
}
/**
* Returns a new Money object that represents
* the divided value by the given factor.
*
* @param float|int|string $divisor
* @param int $roundingMode
*
* @return Money
*/
public function divide($divisor, $roundingMode = self::ROUND_HALF_UP)
{
$this->assertOperand($divisor);
$this->assertRoundingMode($roundingMode);
$divisor = (string) Number::fromNumber($divisor);
if ($this->getCalculator()->compare($divisor, '0') === 0) {
throw new \InvalidArgumentException('Division by zero');
}
$quotient = $this->round($this->getCalculator()->divide($this->amount, $divisor), $roundingMode);
return $this->newInstance($quotient);
}
/**
* Returns a new Money object that represents
* the remainder after dividing the value by
* the given factor.
*
* @return Money
*/
public function mod(Money $divisor)
{
$this->assertSameCurrency($divisor);
return new self($this->getCalculator()->mod($this->amount, $divisor->amount), $this->currency);
}
/**
* Allocate the money according to a list of ratios.
*
* @return Money[]
*/
public function allocate(array $ratios)
{
if (count($ratios) === 0) {
throw new \InvalidArgumentException('Cannot allocate to none, ratios cannot be an empty 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 = $this->getCalculator()->share($this->amount, $ratio, $total);
$results[$key] = $this->newInstance($share);
$remainder = $this->getCalculator()->subtract($remainder, $share);
}
if ($this->getCalculator()->compare($remainder, '0') === 0) {
return $results;
}
$fractions = array_map(function ($ratio) use ($total) {
$share = ($ratio / $total) * $this->amount;
return $share - floor($share);
}, $ratios);
while ($this->getCalculator()->compare($remainder, '0') > 0) {
$index = !empty($fractions) ? array_keys($fractions, max($fractions))[0] : 0;
$results[$index]->amount = $this->getCalculator()->add($results[$index]->amount, '1');
$remainder = $this->getCalculator()->subtract($remainder, '1');
unset($fractions[$index]);
}
return $results;
}
/**
* Allocate the money among N targets.
*
* @param int $n
*
* @return Money[]
*
* @throws \InvalidArgumentException If number of targets is not an integer
*/
public function allocateTo($n)
{
if (!is_int($n)) {
throw new \InvalidArgumentException('Number of targets must be an integer');
}
if ($n <= 0) {
throw new \InvalidArgumentException('Cannot allocate to none, target must be greater than zero');
}
return $this->allocate(array_fill(0, $n, 1));
}
/**
* @return string
*/
public function ratioOf(Money $money)
{
if ($money->isZero()) {
throw new \InvalidArgumentException('Cannot calculate a ratio of zero');
}
return $this->getCalculator()->divide($this->amount, $money->amount);
}
/**
* @param string $amount
* @param int $rounding_mode
*
* @return string
*/
private function round($amount, $rounding_mode)
{
$this->assertRoundingMode($rounding_mode);
if ($rounding_mode === self::ROUND_UP) {
return $this->getCalculator()->ceil($amount);
}
if ($rounding_mode === self::ROUND_DOWN) {
return $this->getCalculator()->floor($amount);
}
return $this->getCalculator()->round($amount, $rounding_mode);
}
/**
* @return Money
*/
public function absolute()
{
return $this->newInstance($this->getCalculator()->absolute($this->amount));
}
/**
* @return Money
*/
public function negative()
{
return $this->newInstance(0)->subtract($this);
}
/**
* Checks if the value represented by this object is zero.
*
* @return bool
*/
public function isZero()
{
return $this->getCalculator()->compare($this->amount, 0) === 0;
}
/**
* Checks if the value represented by this object is positive.
*
* @return bool
*/
public function isPositive()
{
return $this->getCalculator()->compare($this->amount, 0) > 0;
}
/**
* Checks if the value represented by this object is negative.
*
* @return bool
*/
public function isNegative()
{
return $this->getCalculator()->compare($this->amount, 0) < 0;
}
/**
* {@inheritdoc}
*
* @return array
*/
#[\ReturnTypeWillChange]
public function jsonSerialize()
{
return [
'amount' => $this->amount,
'currency' => $this->currency->jsonSerialize(),
];
}
/**
* @param Money $first
* @param Money ...$collection
*
* @return Money
*
* @psalm-pure
*/
public static function min(self $first, self ...$collection)
{
$min = $first;
foreach ($collection as $money) {
if ($money->lessThan($min)) {
$min = $money;
}
}
return $min;
}
/**
* @param Money $first
* @param Money ...$collection
*
* @return Money
*
* @psalm-pure
*/
public static function max(self $first, self ...$collection)
{
$max = $first;
foreach ($collection as $money) {
if ($money->greaterThan($max)) {
$max = $money;
}
}
return $max;
}
/**
* @param Money $first
* @param Money ...$collection
*
* @return Money
*
* @psalm-pure
*/
public static function sum(self $first, self ...$collection)
{
return $first->add(...$collection);
}
/**
* @param Money $first
* @param Money ...$collection
*
* @return Money
*
* @psalm-pure
*/
public static function avg(self $first, self ...$collection)
{
return $first->add(...$collection)->divide(func_num_args());
}
/**
* @param string $calculator
*/
public static function registerCalculator($calculator)
{
if (is_a($calculator, Calculator::class, true) === false) {
throw new \InvalidArgumentException('Calculator must implement '.Calculator::class);
}
array_unshift(self::$calculators, $calculator);
}
/**
* @return Calculator
*
* @throws \RuntimeException If cannot find calculator for money calculations
*/
private static function initializeCalculator()
{
$calculators = self::$calculators;
foreach ($calculators as $calculator) {
/** @var Calculator $calculator */
if ($calculator::supported()) {
return new $calculator();
}
}
throw new \RuntimeException('Cannot find calculator for money calculations');
}
/**
* @return Calculator
*/
private function getCalculator()
{
if (null === self::$calculator) {
self::$calculator = self::initializeCalculator();
}
return self::$calculator;
}
}

View File

@@ -0,0 +1,208 @@
<?php
namespace Money;
/**
* This is a generated file. Do not edit it manually!
*
* @method static Money AED(string|int $amount)
* @method static Money ALL(string|int $amount)
* @method static Money AMD(string|int $amount)
* @method static Money ANG(string|int $amount)
* @method static Money AOA(string|int $amount)
* @method static Money ARS(string|int $amount)
* @method static Money AUD(string|int $amount)
* @method static Money AWG(string|int $amount)
* @method static Money AZN(string|int $amount)
* @method static Money BAM(string|int $amount)
* @method static Money BBD(string|int $amount)
* @method static Money BDT(string|int $amount)
* @method static Money BGN(string|int $amount)
* @method static Money BHD(string|int $amount)
* @method static Money BIF(string|int $amount)
* @method static Money BMD(string|int $amount)
* @method static Money BND(string|int $amount)
* @method static Money BOB(string|int $amount)
* @method static Money BOV(string|int $amount)
* @method static Money BRL(string|int $amount)
* @method static Money BSD(string|int $amount)
* @method static Money BTN(string|int $amount)
* @method static Money BWP(string|int $amount)
* @method static Money BYN(string|int $amount)
* @method static Money BZD(string|int $amount)
* @method static Money CAD(string|int $amount)
* @method static Money CDF(string|int $amount)
* @method static Money CHE(string|int $amount)
* @method static Money CHF(string|int $amount)
* @method static Money CHW(string|int $amount)
* @method static Money CLF(string|int $amount)
* @method static Money CLP(string|int $amount)
* @method static Money CNY(string|int $amount)
* @method static Money COP(string|int $amount)
* @method static Money COU(string|int $amount)
* @method static Money CRC(string|int $amount)
* @method static Money CUC(string|int $amount)
* @method static Money CUP(string|int $amount)
* @method static Money CVE(string|int $amount)
* @method static Money CZK(string|int $amount)
* @method static Money DJF(string|int $amount)
* @method static Money DKK(string|int $amount)
* @method static Money DOP(string|int $amount)
* @method static Money DZD(string|int $amount)
* @method static Money EGP(string|int $amount)
* @method static Money ERN(string|int $amount)
* @method static Money ETB(string|int $amount)
* @method static Money EUR(string|int $amount)
* @method static Money FJD(string|int $amount)
* @method static Money FKP(string|int $amount)
* @method static Money GBP(string|int $amount)
* @method static Money GEL(string|int $amount)
* @method static Money GHS(string|int $amount)
* @method static Money GIP(string|int $amount)
* @method static Money GMD(string|int $amount)
* @method static Money GNF(string|int $amount)
* @method static Money GTQ(string|int $amount)
* @method static Money GYD(string|int $amount)
* @method static Money HKD(string|int $amount)
* @method static Money HNL(string|int $amount)
* @method static Money HRK(string|int $amount)
* @method static Money HTG(string|int $amount)
* @method static Money HUF(string|int $amount)
* @method static Money IDR(string|int $amount)
* @method static Money ILS(string|int $amount)
* @method static Money INR(string|int $amount)
* @method static Money IQD(string|int $amount)
* @method static Money IRR(string|int $amount)
* @method static Money ISK(string|int $amount)
* @method static Money JMD(string|int $amount)
* @method static Money JOD(string|int $amount)
* @method static Money JPY(string|int $amount)
* @method static Money KES(string|int $amount)
* @method static Money KGS(string|int $amount)
* @method static Money KHR(string|int $amount)
* @method static Money KMF(string|int $amount)
* @method static Money KPW(string|int $amount)
* @method static Money KRW(string|int $amount)
* @method static Money KWD(string|int $amount)
* @method static Money KYD(string|int $amount)
* @method static Money KZT(string|int $amount)
* @method static Money LAK(string|int $amount)
* @method static Money LBP(string|int $amount)
* @method static Money LKR(string|int $amount)
* @method static Money LRD(string|int $amount)
* @method static Money LSL(string|int $amount)
* @method static Money LYD(string|int $amount)
* @method static Money MAD(string|int $amount)
* @method static Money MDL(string|int $amount)
* @method static Money MGA(string|int $amount)
* @method static Money MKD(string|int $amount)
* @method static Money MMK(string|int $amount)
* @method static Money MNT(string|int $amount)
* @method static Money MOP(string|int $amount)
* @method static Money MRU(string|int $amount)
* @method static Money MUR(string|int $amount)
* @method static Money MVR(string|int $amount)
* @method static Money MWK(string|int $amount)
* @method static Money MXN(string|int $amount)
* @method static Money MXV(string|int $amount)
* @method static Money MYR(string|int $amount)
* @method static Money MZN(string|int $amount)
* @method static Money NAD(string|int $amount)
* @method static Money NGN(string|int $amount)
* @method static Money NIO(string|int $amount)
* @method static Money NOK(string|int $amount)
* @method static Money NPR(string|int $amount)
* @method static Money NZD(string|int $amount)
* @method static Money OMR(string|int $amount)
* @method static Money PAB(string|int $amount)
* @method static Money PEN(string|int $amount)
* @method static Money PGK(string|int $amount)
* @method static Money PHP(string|int $amount)
* @method static Money PKR(string|int $amount)
* @method static Money PLN(string|int $amount)
* @method static Money PYG(string|int $amount)
* @method static Money QAR(string|int $amount)
* @method static Money RON(string|int $amount)
* @method static Money RSD(string|int $amount)
* @method static Money RUB(string|int $amount)
* @method static Money RWF(string|int $amount)
* @method static Money SAR(string|int $amount)
* @method static Money SBD(string|int $amount)
* @method static Money SCR(string|int $amount)
* @method static Money SDG(string|int $amount)
* @method static Money SEK(string|int $amount)
* @method static Money SGD(string|int $amount)
* @method static Money SHP(string|int $amount)
* @method static Money SLL(string|int $amount)
* @method static Money SOS(string|int $amount)
* @method static Money SRD(string|int $amount)
* @method static Money SSP(string|int $amount)
* @method static Money STN(string|int $amount)
* @method static Money SVC(string|int $amount)
* @method static Money SYP(string|int $amount)
* @method static Money SZL(string|int $amount)
* @method static Money THB(string|int $amount)
* @method static Money TJS(string|int $amount)
* @method static Money TMT(string|int $amount)
* @method static Money TND(string|int $amount)
* @method static Money TOP(string|int $amount)
* @method static Money TRY(string|int $amount)
* @method static Money TTD(string|int $amount)
* @method static Money TWD(string|int $amount)
* @method static Money TZS(string|int $amount)
* @method static Money UAH(string|int $amount)
* @method static Money UGX(string|int $amount)
* @method static Money USD(string|int $amount)
* @method static Money USN(string|int $amount)
* @method static Money UYI(string|int $amount)
* @method static Money UYU(string|int $amount)
* @method static Money UYW(string|int $amount)
* @method static Money UZS(string|int $amount)
* @method static Money VES(string|int $amount)
* @method static Money VND(string|int $amount)
* @method static Money VUV(string|int $amount)
* @method static Money WST(string|int $amount)
* @method static Money XAF(string|int $amount)
* @method static Money XAG(string|int $amount)
* @method static Money XAU(string|int $amount)
* @method static Money XBA(string|int $amount)
* @method static Money XBB(string|int $amount)
* @method static Money XBC(string|int $amount)
* @method static Money XBD(string|int $amount)
* @method static Money XBT(string|int $amount)
* @method static Money XCD(string|int $amount)
* @method static Money XDR(string|int $amount)
* @method static Money XOF(string|int $amount)
* @method static Money XPD(string|int $amount)
* @method static Money XPF(string|int $amount)
* @method static Money XPT(string|int $amount)
* @method static Money XSU(string|int $amount)
* @method static Money XTS(string|int $amount)
* @method static Money XUA(string|int $amount)
* @method static Money XXX(string|int $amount)
* @method static Money YER(string|int $amount)
* @method static Money ZAR(string|int $amount)
* @method static Money ZMW(string|int $amount)
* @method static Money ZWL(string|int $amount)
*/
trait MoneyFactory
{
/**
* Convenience factory method for a Money object.
*
* <code>
* $fiveDollar = Money::USD(500);
* </code>
*
* @param string $method
* @param array $arguments
*
* @return Money
*
* @throws \InvalidArgumentException If amount is not integer(ish)
*/
public static function __callStatic($method, $arguments)
{
return new Money($arguments[0], new Currency($method));
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace Money;
/**
* Formats Money objects.
*
* @author Frederik Bosch <f.bosch@genkgo.nl>
*/
interface MoneyFormatter
{
/**
* Formats a Money object as string.
*
* @return string
*
* Exception\FormatterException
*/
public function format(Money $money);
}

View File

@@ -0,0 +1,23 @@
<?php
namespace Money;
/**
* Parses a string into a Money object.
*
* @author Frederik Bosch <f.bosch@genkgo.nl>
*/
interface MoneyParser
{
/**
* Parses a string into a Money object (including currency).
*
* @param string $money
* @param Currency|string|null $forceCurrency
*
* @return Money
*
* @throws Exception\ParserException
*/
public function parse($money, $forceCurrency = null);
}

View File

@@ -0,0 +1,331 @@
<?php
namespace Money;
/**
* Represents a numeric value.
*
* @author Frederik Bosch <f.bosch@genkgo.nl>
*/
final class Number
{
/**
* @var string
*/
private $integerPart;
/**
* @var string
*/
private $fractionalPart;
/**
* @var array
*/
private static $numbers = [0 => 1, 1 => 1, 2 => 1, 3 => 1, 4 => 1, 5 => 1, 6 => 1, 7 => 1, 8 => 1, 9 => 1];
/**
* @param string $integerPart
* @param string $fractionalPart
*/
public function __construct($integerPart, $fractionalPart = '')
{
if ('' === $integerPart && '' === $fractionalPart) {
throw new \InvalidArgumentException('Empty number is invalid');
}
$this->integerPart = $this->parseIntegerPart((string) $integerPart);
$this->fractionalPart = $this->parseFractionalPart((string) $fractionalPart);
}
/**
* @param $number
*
* @return self
*/
public static function fromString($number)
{
$decimalSeparatorPosition = strpos($number, '.');
if ($decimalSeparatorPosition === false) {
return new self($number, '');
}
return new self(
substr($number, 0, $decimalSeparatorPosition),
rtrim(substr($number, $decimalSeparatorPosition + 1), '0')
);
}
/**
* @param float $number
*
* @return self
*/
public static function fromFloat($number)
{
if (is_float($number) === false) {
throw new \InvalidArgumentException('Floating point value expected');
}
return self::fromString(sprintf('%.14F', $number));
}
/**
* @param float|int|string $number
*
* @return self
*/
public static function fromNumber($number)
{
if (is_float($number)) {
return self::fromString(sprintf('%.14F', $number));
}
if (is_int($number)) {
return new self($number);
}
if (is_string($number)) {
return self::fromString($number);
}
throw new \InvalidArgumentException('Valid numeric value expected');
}
/**
* @return bool
*/
public function isDecimal()
{
return $this->fractionalPart !== '';
}
/**
* @return bool
*/
public function isInteger()
{
return $this->fractionalPart === '';
}
/**
* @return bool
*/
public function isHalf()
{
return $this->fractionalPart === '5';
}
/**
* @return bool
*/
public function isCurrentEven()
{
$lastIntegerPartNumber = $this->integerPart[strlen($this->integerPart) - 1];
return $lastIntegerPartNumber % 2 === 0;
}
/**
* @return bool
*/
public function isCloserToNext()
{
if ($this->fractionalPart === '') {
return false;
}
return $this->fractionalPart[0] >= 5;
}
/**
* @return string
*/
public function __toString()
{
if ($this->fractionalPart === '') {
return $this->integerPart;
}
return $this->integerPart.'.'.$this->fractionalPart;
}
/**
* @return bool
*/
public function isNegative()
{
return $this->integerPart[0] === '-';
}
/**
* @return string
*/
public function getIntegerPart()
{
return $this->integerPart;
}
/**
* @return string
*/
public function getFractionalPart()
{
return $this->fractionalPart;
}
/**
* @return string
*/
public function getIntegerRoundingMultiplier()
{
if ($this->integerPart[0] === '-') {
return '-1';
}
return '1';
}
/**
* @param int $number
*
* @return self
*/
public function base10($number)
{
if (!is_int($number)) {
throw new \InvalidArgumentException('Expecting integer');
}
if ($this->integerPart === '0' && !$this->fractionalPart) {
return $this;
}
$sign = '';
$integerPart = $this->integerPart;
if ($integerPart[0] === '-') {
$sign = '-';
$integerPart = substr($integerPart, 1);
}
if ($number >= 0) {
$integerPart = ltrim($integerPart, '0');
$lengthIntegerPart = strlen($integerPart);
$integers = $lengthIntegerPart - min($number, $lengthIntegerPart);
$zeroPad = $number - min($number, $lengthIntegerPart);
return new self(
$sign.substr($integerPart, 0, $integers),
rtrim(str_pad('', $zeroPad, '0').substr($integerPart, $integers).$this->fractionalPart, '0')
);
}
$number = abs($number);
$lengthFractionalPart = strlen($this->fractionalPart);
$fractions = $lengthFractionalPart - min($number, $lengthFractionalPart);
$zeroPad = $number - min($number, $lengthFractionalPart);
return new self(
$sign.ltrim($integerPart.substr($this->fractionalPart, 0, $lengthFractionalPart - $fractions).str_pad('', $zeroPad, '0'), '0'),
substr($this->fractionalPart, $lengthFractionalPart - $fractions)
);
}
/**
* @param string $number
*
* @return string
*/
private static function parseIntegerPart($number)
{
if ('' === $number || '0' === $number) {
return '0';
}
if ('-' === $number) {
return '-0';
}
$nonZero = false;
for ($position = 0, $characters = strlen($number); $position < $characters; ++$position) {
$digit = $number[$position];
if (!isset(static::$numbers[$digit]) && !(0 === $position && '-' === $digit)) {
throw new \InvalidArgumentException(sprintf('Invalid integer part %1$s. Invalid digit %2$s found', $number, $digit));
}
if (false === $nonZero && '0' === $digit) {
throw new \InvalidArgumentException('Leading zeros are not allowed');
}
$nonZero = true;
}
return $number;
}
/**
* @param string $number
*
* @return string
*/
private static function parseFractionalPart($number)
{
if ('' === $number) {
return $number;
}
for ($position = 0, $characters = strlen($number); $position < $characters; ++$position) {
$digit = $number[$position];
if (!isset(static::$numbers[$digit])) {
throw new \InvalidArgumentException(sprintf('Invalid fractional part %1$s. Invalid digit %2$s found', $number, $digit));
}
}
return $number;
}
/**
* @param string $moneyValue
* @param int $targetDigits
* @param int $havingDigits
*
* @return string
*/
public static function roundMoneyValue($moneyValue, $targetDigits, $havingDigits)
{
$valueLength = strlen($moneyValue);
$shouldRound = $targetDigits < $havingDigits && $valueLength - $havingDigits + $targetDigits > 0;
if ($shouldRound && $moneyValue[$valueLength - $havingDigits + $targetDigits] >= 5) {
$position = $valueLength - $havingDigits + $targetDigits;
$addend = 1;
while ($position > 0) {
$newValue = (string) ((int) $moneyValue[$position - 1] + $addend);
if ($newValue >= 10) {
$moneyValue[$position - 1] = $newValue[1];
$addend = $newValue[0];
--$position;
if ($position === 0) {
$moneyValue = $addend.$moneyValue;
}
} else {
if ($moneyValue[$position - 1] === '-') {
$moneyValue[$position - 1] = $newValue[0];
$moneyValue = '-'.$moneyValue;
} else {
$moneyValue[$position - 1] = $newValue[0];
}
break;
}
}
}
return $moneyValue;
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace Money\PHPUnit;
use Money\Currencies\AggregateCurrencies;
use Money\Currencies\BitcoinCurrencies;
use Money\Currencies\ISOCurrencies;
use Money\Formatter\IntlMoneyFormatter;
use Money\Money;
use SebastianBergmann\Comparator\ComparisonFailure;
/**
* The comparator is for comparing Money objects in PHPUnit tests.
*
* Add this to your bootstrap file:
*
* \SebastianBergmann\Comparator\Factory::getInstance()->register(new \Money\PHPUnit\Comparator());
*/
final class Comparator extends \SebastianBergmann\Comparator\Comparator
{
/**
* @var IntlMoneyFormatter
*/
private $formatter;
public function __construct()
{
parent::__construct();
$currencies = new AggregateCurrencies([
new ISOCurrencies(),
new BitcoinCurrencies(),
]);
$numberFormatter = new \NumberFormatter('en_US', \NumberFormatter::CURRENCY);
$this->formatter = new IntlMoneyFormatter($numberFormatter, $currencies);
}
public function accepts($expected, $actual)
{
return $expected instanceof Money && $actual instanceof Money;
}
/**
* @param Money $expected
* @param Money $actual
* @param float $delta
* @param bool $canonicalize
* @param bool $ignoreCase
*/
public function assertEquals(
$expected,
$actual,
$delta = 0.0,
$canonicalize = false,
$ignoreCase = false,
array &$processed = []
) {
if (!$expected->equals($actual)) {
throw new ComparisonFailure($expected, $actual, $this->formatter->format($expected), $this->formatter->format($actual), false, 'Failed asserting that two Money objects are equal.');
}
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace Money\Parser;
use Money\Currency;
use Money\Exception;
use Money\MoneyParser;
/**
* Parses a string into a Money object using other parsers.
*
* @author Frederik Bosch <f.bosch@genkgo.nl>
*/
final class AggregateMoneyParser implements MoneyParser
{
/**
* @var MoneyParser[]
*/
private $parsers = [];
/**
* @param MoneyParser[] $parsers
*/
public function __construct(array $parsers)
{
if (empty($parsers)) {
throw new \InvalidArgumentException(sprintf('Initialize an empty %s is not possible', self::class));
}
foreach ($parsers as $parser) {
if (false === $parser instanceof MoneyParser) {
throw new \InvalidArgumentException('All parsers must implement '.MoneyParser::class);
}
$this->parsers[] = $parser;
}
}
/**
* {@inheritdoc}
*/
public function parse($money, $forceCurrency = null)
{
if ($forceCurrency !== null && !$forceCurrency instanceof Currency) {
@trigger_error('Passing a currency as string is deprecated since 3.1 and will be removed in 4.0. Please pass a '.Currency::class.' instance instead.', E_USER_DEPRECATED);
$forceCurrency = new Currency($forceCurrency);
}
foreach ($this->parsers as $parser) {
try {
return $parser->parse($money, $forceCurrency);
} catch (Exception\ParserException $e) {
}
}
throw new Exception\ParserException(sprintf('Unable to parse %s', $money));
}
}

View File

@@ -0,0 +1,82 @@
<?php
namespace Money\Parser;
use Money\Currencies\BitcoinCurrencies;
use Money\Currency;
use Money\Exception\ParserException;
use Money\Money;
use Money\MoneyParser;
/**
* Parses Bitcoin currency to Money.
*
* @author Frederik Bosch <f.bosch@genkgo.nl>
*/
final class BitcoinMoneyParser implements MoneyParser
{
/**
* @var int
*/
private $fractionDigits;
/**
* @param int $fractionDigits
*/
public function __construct($fractionDigits)
{
$this->fractionDigits = $fractionDigits;
}
/**
* {@inheritdoc}
*/
public function parse($money, $forceCurrency = null)
{
if (is_string($money) === false) {
throw new ParserException('Formatted raw money should be string, e.g. $1.00');
}
if (strpos($money, BitcoinCurrencies::SYMBOL) === false) {
throw new ParserException('Value cannot be parsed as Bitcoin');
}
if ($forceCurrency === null) {
$forceCurrency = new Currency(BitcoinCurrencies::CODE);
}
/*
* This conversion is only required whilst currency can be either a string or a
* Currency object.
*/
$currency = $forceCurrency;
if (!$currency instanceof Currency) {
@trigger_error('Passing a currency as string is deprecated since 3.1 and will be removed in 4.0. Please pass a '.Currency::class.' instance instead.', E_USER_DEPRECATED);
$currency = new Currency($currency);
}
$decimal = str_replace(BitcoinCurrencies::SYMBOL, '', $money);
$decimalSeparator = strpos($decimal, '.');
if (false !== $decimalSeparator) {
$decimal = rtrim($decimal, '0');
$lengthDecimal = strlen($decimal);
$decimal = str_replace('.', '', $decimal);
$decimal .= str_pad('', ($lengthDecimal - $decimalSeparator - $this->fractionDigits - 1) * -1, '0');
} else {
$decimal .= str_pad('', $this->fractionDigits, '0');
}
if (substr($decimal, 0, 1) === '-') {
$decimal = '-'.ltrim(substr($decimal, 1), '0');
} else {
$decimal = ltrim($decimal, '0');
}
if ('' === $decimal) {
$decimal = '0';
}
return new Money($decimal, $currency);
}
}

View File

@@ -0,0 +1,100 @@
<?php
namespace Money\Parser;
use Money\Currencies;
use Money\Currency;
use Money\Exception\ParserException;
use Money\Money;
use Money\MoneyParser;
use Money\Number;
/**
* Parses a decimal string into a Money object.
*
* @author Teoh Han Hui <teohhanhui@gmail.com>
*/
final class DecimalMoneyParser implements MoneyParser
{
const DECIMAL_PATTERN = '/^(?P<sign>-)?(?P<digits>0|[1-9]\d*)?\.?(?P<fraction>\d+)?$/';
/**
* @var Currencies
*/
private $currencies;
public function __construct(Currencies $currencies)
{
$this->currencies = $currencies;
}
/**
* {@inheritdoc}
*/
public function parse($money, $forceCurrency = null)
{
if (!is_string($money)) {
throw new ParserException('Formatted raw money should be string, e.g. 1.00');
}
if (null === $forceCurrency) {
throw new ParserException('DecimalMoneyParser cannot parse currency symbols. Use forceCurrency argument');
}
/*
* This conversion is only required whilst currency can be either a string or a
* Currency object.
*/
$currency = $forceCurrency;
if (!$currency instanceof Currency) {
@trigger_error('Passing a currency as string is deprecated since 3.1 and will be removed in 4.0. Please pass a '.Currency::class.' instance instead.', E_USER_DEPRECATED);
$currency = new Currency($currency);
}
$decimal = trim($money);
if ($decimal === '') {
return new Money(0, $currency);
}
$subunit = $this->currencies->subunitFor($currency);
if (!preg_match(self::DECIMAL_PATTERN, $decimal, $matches) || !isset($matches['digits'])) {
throw new ParserException(sprintf('Cannot parse "%s" to Money.', $decimal));
}
$negative = isset($matches['sign']) && $matches['sign'] === '-';
$decimal = $matches['digits'];
if ($negative) {
$decimal = '-'.$decimal;
}
if (isset($matches['fraction'])) {
$fractionDigits = strlen($matches['fraction']);
$decimal .= $matches['fraction'];
$decimal = Number::roundMoneyValue($decimal, $subunit, $fractionDigits);
if ($fractionDigits > $subunit) {
$decimal = substr($decimal, 0, $subunit - $fractionDigits);
} elseif ($fractionDigits < $subunit) {
$decimal .= str_pad('', $subunit - $fractionDigits, '0');
}
} else {
$decimal .= str_pad('', $subunit, '0');
}
if ($negative) {
$decimal = '-'.ltrim(substr($decimal, 1), '0');
} else {
$decimal = ltrim($decimal, '0');
}
if ($decimal === '' || $decimal === '-') {
$decimal = '0';
}
return new Money($decimal, $currency);
}
}

View File

@@ -0,0 +1,94 @@
<?php
namespace Money\Parser;
use Money\Currencies;
use Money\Currency;
use Money\Exception\ParserException;
use Money\Money;
use Money\MoneyParser;
use Money\Number;
/**
* Parses a string into a Money object using intl extension.
*
* @author Frederik Bosch <f.bosch@genkgo.nl>
*/
final class IntlLocalizedDecimalParser implements MoneyParser
{
/**
* @var \NumberFormatter
*/
private $formatter;
/**
* @var Currencies
*/
private $currencies;
public function __construct(\NumberFormatter $formatter, Currencies $currencies)
{
$this->formatter = $formatter;
$this->currencies = $currencies;
}
/**
* {@inheritdoc}
*/
public function parse($money, $forceCurrency = null)
{
if (!is_string($money)) {
throw new ParserException('Formatted raw money should be string, e.g. $1.00');
}
if (null === $forceCurrency) {
throw new ParserException('IntlLocalizedDecimalParser cannot parse currency symbols. Use forceCurrency argument');
}
$decimal = $this->formatter->parse($money);
if (false === $decimal) {
throw new ParserException('Cannot parse '.$money.' to Money. '.$this->formatter->getErrorMessage());
}
/*
* This conversion is only required whilst currency can be either a string or a
* Currency object.
*/
if (!$forceCurrency instanceof Currency) {
@trigger_error('Passing a currency as string is deprecated since 3.1 and will be removed in 4.0. Please pass a '.Currency::class.' instance instead.', E_USER_DEPRECATED);
$forceCurrency = new Currency($forceCurrency);
}
$decimal = (string) $decimal;
$subunit = $this->currencies->subunitFor($forceCurrency);
$decimalPosition = strpos($decimal, '.');
if (false !== $decimalPosition) {
$decimalLength = strlen($decimal);
$fractionDigits = $decimalLength - $decimalPosition - 1;
$decimal = str_replace('.', '', $decimal);
$decimal = Number::roundMoneyValue($decimal, $subunit, $fractionDigits);
if ($fractionDigits > $subunit) {
$decimal = substr($decimal, 0, $decimalPosition + $subunit);
} elseif ($fractionDigits < $subunit) {
$decimal .= str_pad('', $subunit - $fractionDigits, '0');
}
} else {
$decimal .= str_pad('', $subunit, '0');
}
if ('-' === $decimal[0]) {
$decimal = '-'.ltrim(substr($decimal, 1), '0');
} else {
$decimal = ltrim($decimal, '0');
}
if ('' === $decimal) {
$decimal = '0';
}
return new Money($decimal, $forceCurrency);
}
}

View File

@@ -0,0 +1,97 @@
<?php
namespace Money\Parser;
use Money\Currencies;
use Money\Currency;
use Money\Exception\ParserException;
use Money\Money;
use Money\MoneyParser;
use Money\Number;
/**
* Parses a string into a Money object using intl extension.
*
* @author Frederik Bosch <f.bosch@genkgo.nl>
*/
final class IntlMoneyParser implements MoneyParser
{
/**
* @var \NumberFormatter
*/
private $formatter;
/**
* @var Currencies
*/
private $currencies;
public function __construct(\NumberFormatter $formatter, Currencies $currencies)
{
$this->formatter = $formatter;
$this->currencies = $currencies;
}
/**
* {@inheritdoc}
*/
public function parse($money, $forceCurrency = null)
{
if (!is_string($money)) {
throw new ParserException('Formatted raw money should be string, e.g. $1.00');
}
$currency = null;
$decimal = $this->formatter->parseCurrency($money, $currency);
if (false === $decimal) {
throw new ParserException('Cannot parse '.$money.' to Money. '.$this->formatter->getErrorMessage());
}
if (null !== $forceCurrency) {
$currency = $forceCurrency;
} else {
$currency = new Currency($currency);
}
/*
* This conversion is only required whilst currency can be either a string or a
* Currency object.
*/
if (!$currency instanceof Currency) {
@trigger_error('Passing a currency as string is deprecated since 3.1 and will be removed in 4.0. Please pass a '.Currency::class.' instance instead.', E_USER_DEPRECATED);
$currency = new Currency($currency);
}
$decimal = (string) $decimal;
$subunit = $this->currencies->subunitFor($currency);
$decimalPosition = strpos($decimal, '.');
if (false !== $decimalPosition) {
$decimalLength = strlen($decimal);
$fractionDigits = $decimalLength - $decimalPosition - 1;
$decimal = str_replace('.', '', $decimal);
$decimal = Number::roundMoneyValue($decimal, $subunit, $fractionDigits);
if ($fractionDigits > $subunit) {
$decimal = substr($decimal, 0, $decimalPosition + $subunit);
} elseif ($fractionDigits < $subunit) {
$decimal .= str_pad('', $subunit - $fractionDigits, '0');
}
} else {
$decimal .= str_pad('', $subunit, '0');
}
if ('-' === $decimal[0]) {
$decimal = '-'.ltrim(substr($decimal, 1), '0');
} else {
$decimal = ltrim($decimal, '0');
}
if ('' === $decimal) {
$decimal = '0';
}
return new Money($decimal, $currency);
}
}