Skip to content

Commit

Permalink
Merge pull request #55 from sagikazarmark/feature/money-refactor
Browse files Browse the repository at this point in the history
Feature/money refactor
  • Loading branch information
mathiasverraes committed Sep 2, 2014
2 parents d0337df + 42c67ce commit 50b0074
Show file tree
Hide file tree
Showing 2 changed files with 156 additions and 13 deletions.
85 changes: 73 additions & 12 deletions src/Money.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
namespace Money;

use InvalidArgumentException;
use OverflowException;
use UnderflowException;
use UnexpectedValueException;

/**
* Money Value Object
Expand Down Expand Up @@ -46,8 +49,9 @@ class Money
public function __construct($amount, Currency $currency)
{
if (!is_int($amount)) {
throw new InvalidArgumentException("The first parameter of Money must be an integer. It's the amount, expressed in the smallest units of currency (eg cents)");
throw new InvalidArgumentException('Amount must be an integer');
}

$this->amount = $amount;
$this->currency = $currency;
}
Expand Down Expand Up @@ -101,7 +105,7 @@ public function isSameCurrency(Money $other)
private function assertSameCurrency(Money $other)
{
if (!$this->isSameCurrency($other)) {
throw new InvalidArgumentException('Different currencies');
throw new InvalidArgumentException('Currencies must be identical');
}
}

Expand All @@ -114,9 +118,7 @@ private function assertSameCurrency(Money $other)
*/
public function equals(Money $other)
{
return
$this->isSameCurrency($other)
&& $this->amount == $other->amount;
return $this->isSameCurrency($other) && $this->amount == $other->amount;
}

/**
Expand All @@ -131,6 +133,7 @@ public function equals(Money $other)
public function compare(Money $other)
{
$this->assertSameCurrency($other);

if ($this->amount < $other->amount) {
return -1;
} elseif ($this->amount == $other->amount) {
Expand Down Expand Up @@ -196,6 +199,18 @@ public function getCurrency()
return $this->currency;
}

/**
* Asserts that integer remains integer after arithmetic operations
*
* @param numeric $amount
*/
private function assertInteger($amount)
{
if (!is_int($amount)) {
throw new UnexpectedValueException('The result of arithmetic operation is not an integer');
}
}

/**
* Returns a new Money object that represents
* the sum of this and an other Money object
Expand All @@ -208,7 +223,11 @@ public function add(Money $addend)
{
$this->assertSameCurrency($addend);

return $this->newInstance($this->amount + $addend->amount);
$amount = $this->amount + $addend->amount;

$this->assertInteger($amount);

return $this->newInstance($amount);
}

/**
Expand All @@ -223,7 +242,11 @@ public function subtract(Money $subtrahend)
{
$this->assertSameCurrency($subtrahend);

return $this->newInstance($this->amount - $subtrahend->amount);
$amount = $this->amount - $subtrahend->amount;

$this->assertInteger($amount);

return $this->newInstance($amount);
}

/**
Expand All @@ -238,6 +261,38 @@ private function assertOperand($operand)
}
}

/**
* Asserts that an integer value didn't become something else
* (after some arithmetic operation)
*
* @param numeric $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;
} elseif ($amount < ~PHP_INT_MAX) {
throw new UnderflowException;
}
}

/**
* Casts an amount to integer ensuring that an overflow/underflow did not occur
*
* @param numeric $amount
*
* @return integer
*/
private function castInteger($amount)
{
$this->assertIntegerBounds($amount);

return intval($amount);
}

/**
* Returns a new Money object that represents
* the multiplied value by the given factor
Expand All @@ -255,7 +310,9 @@ public function multiply($multiplier, $rounding_mode = self::ROUND_HALF_UP)
$rounding_mode = new RoundingMode($rounding_mode);
}

$product = (int) round($this->amount * $multiplier, 0, $rounding_mode->getRoundingMode());
$product = round($this->amount * $multiplier, 0, $rounding_mode->getRoundingMode());

$product = $this->castInteger($product);

return $this->newInstance($product);
}
Expand All @@ -277,7 +334,9 @@ public function divide($divisor, $rounding_mode = self::ROUND_HALF_UP)
$rounding_mode = new RoundingMode($rounding_mode);
}

$quotient = (int) round($this->amount / $divisor, 0, $rounding_mode->getRoundingMode());
$quotient = round($this->amount / $divisor, 0, $rounding_mode->getRoundingMode());

$quotient = $this->castInteger($quotient);

return $this->newInstance($quotient);
}
Expand All @@ -287,7 +346,7 @@ public function divide($divisor, $rounding_mode = self::ROUND_HALF_UP)
*
* @param array $ratios
*
* @return Money
* @return Money[]
*/
public function allocate(array $ratios)
{
Expand All @@ -296,10 +355,11 @@ public function allocate(array $ratios)
$total = array_sum($ratios);

foreach ($ratios as $ratio) {
$share = (int) floor($this->amount * $ratio / $total);
$share = $this->castInteger($this->amount * $ratio / $total);
$results[] = $this->newInstance($share);
$remainder -= $share;
}

for ($i = 0; $remainder > 0; $i++) {
$results[$i]->amount++;
$remainder--;
Expand Down Expand Up @@ -351,8 +411,9 @@ public static function stringToUnits($string)
{
//@todo extend the regular expression with grouping characters and eventually currencies
if (!preg_match("/(-)?(\d+)([.,])?(\d)?(\d)?/", $string, $matches)) {
throw new InvalidArgumentException("The value could not be parsed as money");
throw new InvalidArgumentException('The value could not be parsed as money');
}

$units = $matches[1] == "-" ? "-" : "";
$units .= $matches[2];
$units .= isset($matches[4]) ? $matches[4] : "0";
Expand Down
84 changes: 83 additions & 1 deletion tests/MoneyTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,18 @@

namespace Money;

/**
* @coversDefaultClass Money\Money
* @uses Money\Currency
* @uses Money\Money
* @uses Money\RoundingMode
* @uses Money\CurrencyPair
*/
class MoneyTest extends \PHPUnit_Framework_TestCase
{
/**
* @covers ::__callStatic
*/
public function testFactoryMethods()
{
$this->assertEquals(
Expand All @@ -25,6 +35,10 @@ public function testFactoryMethods()
);
}

/**
* @covers ::getAmount
* @covers ::getCurrency
*/
public function testGetters()
{
$m = new Money(100, $euro = new Currency('EUR'));
Expand All @@ -33,6 +47,7 @@ public function testGetters()
}

/**
* @covers ::__construct
* @expectedException InvalidArgumentException
*/
public function testDecimalsThrowException()
Expand All @@ -41,13 +56,17 @@ public function testDecimalsThrowException()
}

/**
* @covers ::__construct
* @expectedException InvalidArgumentException
*/
public function testStringThrowsException()
{
$money = new Money('100', new Currency('EUR'));
}

/**
* @covers ::equals
*/
public function testEquality()
{
$m1 = new Money(100, new Currency('EUR'));
Expand Down Expand Up @@ -84,6 +103,19 @@ public function testDifferentCurrenciesCannotBeAdded()
$m1->add($m2);
}

/**
* @expectedException UnexpectedValueException
*/
public function testResultNotAnInteger()
{
$m1 = new Money(PHP_INT_MAX, new Currency('EUR'));
$m2 = new Money(1, new Currency('EUR'));
$m1->add($m2);
}

/**
* @covers ::subtract
*/
public function testSubtraction()
{
$m1 = new Money(100, new Currency('EUR'));
Expand Down Expand Up @@ -123,6 +155,36 @@ public function testMultiplication()
$this->assertNotSame($m, $m->multiply(2));
}

/**
* @expectedException InvalidArgumentException
*/
public function testInvalidMultiplicationOperand()
{
$m = new Money(1, new Currency('EUR'));
$m->multiply('operand');
}

/**
* @expectedException OverflowException
*/
public function testMultiplicationOverflow()
{
$m = new Money(PHP_INT_MAX, new Currency('EUR'));
$m->multiply(2);
}

/**
* @expectedException UnderflowException
*/
public function testMultiplicationUnderflow()
{
$m = new Money(~PHP_INT_MAX, new Currency('EUR'));
$m->multiply(2);
}

/**
* @covers ::divide
*/
public function testDivision()
{
$m = new Money(10, new Currency('EUR'));
Expand Down Expand Up @@ -166,6 +228,9 @@ public function testDifferentCurrenciesCannotBeCompared()
Money::EUR(1)->compare(Money::USD(1));
}

/**
* @covers ::allocate
*/
public function testAllocation()
{
$m = new Money(100, new Currency('EUR'));
Expand All @@ -181,9 +246,11 @@ public function testAllocation()
$this->assertEquals(new Money(33, new Currency('EUR')), $part3);
}

/**
* @covers ::allocate
*/
public function testAllocationOrderIsImportant()
{

$m = new Money(5, new Currency('EUR'));
list($part1, $part2) = $m->allocate(array(3, 7));
$this->assertEquals(new Money(2, new Currency('EUR')), $part1);
Expand All @@ -195,6 +262,11 @@ public function testAllocationOrderIsImportant()
$this->assertEquals(new Money(1, new Currency('EUR')), $part2);
}

/**
* @covers ::isZero
* @covers ::isNegative
* @covers ::isPositive
*/
public function testComparators()
{
$this->assertTrue(Money::EUR(0)->isZero());
Expand Down Expand Up @@ -227,10 +299,20 @@ public static function provideStrings()
}

/**
* @covers ::stringToUnits
* @dataProvider provideStrings
*/
public function testStringToUnits($string, $units)
{
$this->assertEquals($units, Money::stringToUnits($string));
}

/**
* @covers ::stringToUnits
* @expectedException InvalidArgumentException
*/
public function testCannotConvertStringToUnits()
{
Money::stringToUnits('THIS_IS_NOT_CONVERTABLE_TO_UNIT');
}
}

0 comments on commit 50b0074

Please sign in to comment.