Skip to content

Commit

Permalink
Modulus support (#400)
Browse files Browse the repository at this point in the history
* Add specs and tests for calculator mod()

* Add specs and tests for money mod()

* Add mod() to calculators

* Fix money mod tests

* Add mod() to money

* Document mod()

* Add negative integer test cases for mod() calculators

* Fix GmpCalculator::mod() for negative amounts
  • Loading branch information
jaikdean authored and frederikbosch committed Nov 14, 2017
1 parent 1507938 commit 2671c74
Show file tree
Hide file tree
Showing 10 changed files with 147 additions and 0 deletions.
14 changes: 14 additions & 0 deletions doc/features/operation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,20 @@ Divisions can be performed using ``divide()``.
$result = $value->divide(2); // €4.00
.. _modulus:

Modulus
-------

Modulus operations can be performed using ``mod()``.

.. code-block:: php
$value = Money::EUR(830); // €8.30
$divisor = Money::EUR(300); // €3.00
$result = $value->mod($divisor); // €2.30
.. _rounding_modes:

Rounding Modes
Expand Down
5 changes: 5 additions & 0 deletions spec/Calculator/CalculatorBehavior.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@ function it_shares_a_value()
$this->share(10, 2, 4)->shouldBeString();
}

function it_calculates_the_modulus()
{
$this->mod(11, 5)->shouldBeString();
}

public function getMatchers()
{
return [
Expand Down
17 changes: 17 additions & 0 deletions spec/MoneySpec.php
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,23 @@ function it_calculates_the_absolute_amount(Calculator $calculator)
$money->getAmount()->shouldBeLike(1);
}

function it_calculates_a_modulus_with_an_other_money(Calculator $calculator)
{
$result = self::AMOUNT % self::OTHER_AMOUNT;
$calculator->mod((string) self::AMOUNT, (string) self::OTHER_AMOUNT)->willReturn((string) $result);
$money = $this->mod(new Money(self::OTHER_AMOUNT, new Currency(self::CURRENCY)));

$money->shouldHaveType(Money::class);
$money->getAmount()->shouldBe((string) $result);
}

function it_throws_an_exception_when_currency_is_different_during_modulus(Calculator $calculator)
{
$calculator->mod((string) self::AMOUNT, (string) self::AMOUNT)->shouldNotBeCalled();

$this->shouldThrow(\InvalidArgumentException::class)->duringMod(new Money(self::AMOUNT, new Currency(self::OTHER_CURRENCY)));
}

public function getMatchers()
{
return [
Expand Down
10 changes: 10 additions & 0 deletions src/Calculator.php
Original file line number Diff line number Diff line change
Expand Up @@ -114,4 +114,14 @@ public function round($number, $roundingMode);
* @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);
}
8 changes: 8 additions & 0 deletions src/Calculator/BcMathCalculator.php
Original file line number Diff line number Diff line change
Expand Up @@ -228,4 +228,12 @@ 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);
}
}
18 changes: 18 additions & 0 deletions src/Calculator/GmpCalculator.php
Original file line number Diff line number Diff line change
Expand Up @@ -286,4 +286,22 @@ 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::fromString((string) $amount);

if (true === $amount->isNegative()) {
$remainder = gmp_neg($remainder);
}

return gmp_strval($remainder);
}
}
12 changes: 12 additions & 0 deletions src/Calculator/PhpCalculator.php
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,18 @@ 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).
Expand Down
16 changes: 16 additions & 0 deletions src/Money.php
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,22 @@ public function divide($divisor, $roundingMode = self::ROUND_HALF_UP)
return $this->newInstance($quotient);
}

/**
* Returns a new Money object that represents
* the remainder after dividing the value by
* the given factor.
*
* @param Money $divisor
*
* @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.
*
Expand Down
22 changes: 22 additions & 0 deletions tests/Calculator/CalculatorTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,15 @@ public function it_compares_values($left, $right, $expected)
$this->assertEquals($expected, $this->getCalculator()->compare($left, $right));
}

/**
* @dataProvider modExamples
* @test
*/
public function it_calculates_the_modulus_of_a_value($left, $right, $expected)
{
$this->assertEquals($expected, $this->getCalculator()->mod($left, $right));
}

public function additionExamples()
{
return [
Expand Down Expand Up @@ -198,4 +207,17 @@ public function compareExamples()
['1', '0.000000000000000000000000005', 1],
];
}

public function modExamples()
{
return [
[11, 5, '1'],
[9, 3, '0'],
[1006, 10, '6'],
[1007, 10, '7'],
[-13, -5, '-3'],
[-13, 5, '-3'],
[13, -5, '3'],
];
}
}
25 changes: 25 additions & 0 deletions tests/MoneyTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,21 @@ public function it_calculates_the_negative_amount($amount, $result)
$this->assertEquals($result, $money->getAmount());
}

/**
* @dataProvider modExamples
* @test
*/
public function it_calculates_the_modulus_of_an_amount($left, $right, $expected)
{
$money = new Money($left, new Currency(self::CURRENCY));
$rightMoney = new Money($right, new Currency(self::CURRENCY));

$money = $money->mod($rightMoney);

$this->assertInstanceOf(Money::class, $money);
$this->assertEquals($expected, $money->getAmount());
}

public function test_it_converts_to_json()
{
$this->assertEquals(
Expand Down Expand Up @@ -296,4 +311,14 @@ public function negativeExamples()
['-1', 1],
];
}

public function modExamples()
{
return [
[11, 5, '1'],
[9, 3, '0'],
[1006, 10, '6'],
[1007, 10, '7'],
];
}
}

0 comments on commit 2671c74

Please sign in to comment.