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

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');
}
}
}