Commit a9d0cdcf authored by Thorsten Buss's avatar Thorsten Buss

* add some math and compare methods to Money and Currency +tests

* FIX: wrong path on loading currencies.php
* add __toString, toArray, toJson methods
* add static Calling of Currency
* FIX: switch decimal/thousand for EUR
parent a16653af
......@@ -23,8 +23,8 @@ return [
'code' => 'EUR',
'decimal_place' => 2,
'value' => 0.74970001,
'decimal_mark' => '.',
'thousands_separator' => ',',
'decimal_mark' => ',',
'thousands_separator' => '.',
'unit_factor' => 100
],
[
......
......@@ -11,6 +11,12 @@ namespace Bnet\Money;
use Bnet\Money\Repositories\CurrencyRepositoryInterface;
use Bnet\Money\Repositories\Exception\CurrencyRepositoryException;
/**
* Class Currency
* @package Bnet\Money
* @method static Currency EUR()
* @method static Currency USD()
*/
class Currency {
public static $default_currency = 'EUR';
......@@ -103,9 +109,9 @@ class Currency {
*/
protected function assertRepository() {
if (!self::$repository instanceof CurrencyRepositoryInterface)
if (self::$use_default_currency_repository && file_exists(__DIR__.'../../currencies.php')) {
self::registerCurrencyRepository(new \Bnet\Money\Repositories\ArrayRepository(
include(__DIR__ . '../../currencies.php')
if (self::$use_default_currency_repository && file_exists(__DIR__.'/../../currencies.php')) {
self::registerCurrencyRepository(new Repositories\ArrayRepository(
include(__DIR__ . '/../../currencies.php')
));
} else {
throw new CurrencyRepositoryException;
......@@ -114,11 +120,89 @@ class Currency {
/**
* use a default repository with the main currencies if no repository is defined
* @param boolean $use_default_currency_repository
* @param bool $use
* @return $this
* @internal param bool $use_default_currency_repository
*/
public static function useDefaultCurrencyRepository($use=true) {
self::$use_default_currency_repository = $use;
}
/**
* equals.
*
* @param Currency $currency
*
* @return bool
*/
public function equals(self $currency) {
return $this->code == $currency->code;
}
/**
* __callStatic.
*
* @param string $method
* @param array $arguments
*
* @return Currency
*/
public static function __callStatic($method, array $arguments) {
return new static($method);
}
/**
* Get the instance as an array.
*
* @return array
*/
public function toArray() {
return [$this->code => [
'iso' => $this->iso,
'symbol_left' => $this->symbol_left,
'symbol_right' => $this->symbol_right,
'decimal_place' => $this->decimal_place,
'decimal_mark' => $this->decimal_mark,
'thousands_separator' => $this->thousands_separator,
'unit_factor' => $this->unit_factor
]
];
}
/**
* Convert the object to its JSON representation.
*
* @param int $options
* @return string
*/
public function toJson($options = 0) {
return json_encode($this->toArray(), $options);
}
/**
* jsonSerialize.
*
* @return array
*/
public function jsonSerialize() {
return $this->toArray();
}
/**
* Get the evaluated contents of the object.
*
* @return string
*/
public function render() {
return $this->code . ' (' . $this->name . ')';
}
/**
* __toString.
*
* @return string
*/
public function __toString() {
return $this->render();
}
}
\ No newline at end of file
......@@ -10,6 +10,13 @@ namespace Bnet\Money;
use Illuminate\Support\Str;
/**
* Class Money
* @package Bnet\Money
*
* @method static Money EUR(int $amount)
* @method static Money USD(int $amount)
*/
class Money {
/**
......@@ -29,7 +36,7 @@ class Money {
* @throws MoneyException
*/
public function __construct($amount, $currency=null) {
if (intval($amount) != $amount) {
if (intval($amount) !== $amount) {
throw new MoneyException('Amount must be an integer');
}
......@@ -161,6 +168,299 @@ class Money {
if ($units === '' || $units === '-') {
$units = '0';
}
return new static($units, $currency);
return new static((int)$units, $currency);
}
/**
* @return Currency
*/
public function currency() {
return $this->currency;
}
/**
* @param Money $money
* @return bool
*/
public function isSameCurrency(Money $money) {
return $this->currency()->equals($money->currency());
}
/**
* assertSameCurrency.
*
* @param Money $other
*
* @throws \InvalidArgumentException
*/
protected function assertSameCurrency(self $other) {
if (!$this->isSameCurrency($other)) {
throw new \InvalidArgumentException('Different currencies "' . $this->currency . '" and "' . $other->currency() . '"');
}
}
/**
* compare.
*
* @param Money $other
*
* @return int
*
* @throws \InvalidArgumentException
*/
public function compare(self $other) {
$this->assertSameCurrency($other);
if ($this->amount < $other->amount) {
return -1;
}
if ($this->amount > $other->amount) {
return 1;
}
return 0;
}
/**
* equals.
*
* @param Money $other
*
* @return bool
*/
public function equals(self $other) {
return $this->compare($other) == 0;
}
/**
* greaterThan.
*
* @param Money $other
*
* @return bool
*/
public function greaterThan(self $other) {
return $this->compare($other) == 1;
}
/**
* greaterThanOrEqual.
*
* @param Money $other
*
* @return bool
*/
public function greaterThanOrEqual(self $other) {
return $this->compare($other) >= 0;
}
/**
* lessThan.
*
* @param Money $other
*
* @return bool
*/
public function lessThan(self $other) {
return $this->compare($other) == -1;
}
/**
* lessThanOrEqual.
*
* @param Money $other
*
* @return bool
*/
public function lessThanOrEqual(self $other) {
return $this->compare($other) <= 0;
}
/**
* add.
*
* @param Money $addend
*
* @return Money
*
* @throws \InvalidArgumentException
*/
public function add(self $addend) {
$this->assertSameCurrency($addend);
return new self($this->amount + $addend->amount, $this->currency);
}
/**
* subtract.
*
* @param Money $subtrahend
*
* @return Money
*
* @throws \InvalidArgumentException
*/
public function subtract(self $subtrahend) {
$this->assertSameCurrency($subtrahend);
return new self($this->amount - $subtrahend->amount, $this->currency);
}
/**
* multiply.
*
* @param int|float $multiplier
* @param int $roundingMode
*
* @return Money
*
* @throws \InvalidArgumentException
* @throws \OutOfBoundsException
*/
public function multiply($multiplier, $roundingMode = PHP_ROUND_HALF_UP) {
return new self((int)round($this->amount * $multiplier, 0, $roundingMode), $this->currency);
}
/**
* assertOperand.
*
* @param int|float $operand
*
* @throws \InvalidArgumentException
*/
protected function assertOperand($operand) {
if (!is_int($operand) && !is_float($operand)) {
throw new \InvalidArgumentException('Operand "' . $operand . '" should be an integer or a float');
}
}
/**
* divide.
*
* @param int|float $divisor
* @param int $roundingMode
*
* @return Money
*
* @throws \InvalidArgumentException
* @throws \OutOfBoundsException
*/
public function divide($divisor, $roundingMode = PHP_ROUND_HALF_UP) {
$this->assertOperand($divisor);
if ($divisor == 0) {
throw new \InvalidArgumentException('Division by zero');
}
return new self((int)round($this->amount / $divisor, 0, $roundingMode), $this->currency);
}
/**
* allocate.
*
* @param array $ratios
*
* @return array
*/
public function allocate(array $ratios) {
$remainder = $this->amount;
$results = [];
$total = array_sum($ratios);
foreach ($ratios as $ratio) {
$share = (int)floor($this->amount * $ratio / $total);
$results[] = new self($share, $this->currency);
$remainder -= $share;
}
for ($i = 0; $remainder > 0; $i++) {
$results[$i]->amount++;
$remainder--;
}
return $results;
}
/**
* isZero.
*
* @return bool
*/
public function isZero() {
return $this->amount == 0;
}
/**
* isPositive.
*
* @return bool
*/
public function isPositive() {
return $this->amount > 0;
}
/**
* isNegative.
*
* @return bool
*/
public function isNegative() {
return $this->amount < 0;
}
/**
* Get the instance as an array.
*
* @return array
*/
public function toArray() {
return [
'amount' => $this->amount,
'number' => $this->normalize(),
'format' => $this->format(),
'currency' => $this->currency,
];
}
/**
* Convert the object to its JSON representation.
*
* @param int $options
* @return string
*/
public function toJson($options = 0) {
return json_encode($this->toArray(), $options);
}
/**
* jsonSerialize.
*
* @return array
*/
public function jsonSerialize() {
return $this->toArray();
}
/**
* Get the evaluated contents of the object.
*
* @return string
*/
public function render() {
return $this->format();
}
/**
* __toString.
*
* @return string
*/
public function __toString() {
return $this->render();
}
/**
* __callStatic.
*
* @param string $method
* @param array $arguments
*
* @return Money
*/
public static function __callStatic($method, array $arguments) {
$convert = (isset($arguments[1]) && is_bool($arguments[1])) ? (bool)$arguments[1] : false;
return new static($arguments[0], new Currency($method), $convert);
}
}
\ No newline at end of file
......@@ -8,13 +8,15 @@
namespace Tests\Bnet\Money;
use Bnet\Money\Currency;
class CurrencyTest extends \PHPUnit_Framework_TestCase {
public function testManualAttributes() {
$a = [
'code' => 'EUR',
'iso_numeric' => 123,
'iso' => 123,
'name' => 'Euro',
'symbol_left' => '',
'symbol_right' => '€',
......@@ -23,7 +25,7 @@ class CurrencyTest extends \PHPUnit_Framework_TestCase {
'thousands_separator' => '.',
'unit_factor' => 100
];
$c = new \Bnet\Money\Currency('EUR', $a);
$c = new Currency('EUR', $a);
$this->assertEquals($a['code'], $c->code, 'code');
$this->assertEquals($a['iso'], $c->iso, 'iso');
......@@ -37,11 +39,43 @@ class CurrencyTest extends \PHPUnit_Framework_TestCase {
}
/**
* @expectedException \Bnet\Money\Repositories\Exception\CurrencyRepositoryException
*
*/
public function testNotFoundRepository() {
new \Bnet\Money\Currency('EUR');
public function testFactoryMethods() {
$this->assertEquals(Currency::EUR(), new Currency('EUR'));
$this->assertEquals(Currency::USD(), new Currency('USD'));
}
/**
* @expectedException \Bnet\Money\Repositories\Exception\CurrencyNotFoundException
*/
public function testCantInstantiateUnknownCurrency() {
new Currency('unknown');
}
public function testComparison() {
$c1 = new Currency('EUR');
$c2 = new Currency('USD');
$this->assertTrue($c1->equals(new Currency('EUR')));
$this->assertTrue($c2->equals(new Currency('USD')));
$this->assertFalse($c1->equals($c2));
$this->assertFalse($c2->equals($c1));
}
public function testGetters() {
$c = new Currency('EUR');
$this->assertEquals('EUR', $c->code, 'code');
$this->assertEquals('Euro', $c->name, 'name');
$this->assertEquals(2, $c->decimal_place, 'decimal_place');
$this->assertEquals(100, $c->unit_factor, 'unit_factor');
$this->assertEquals('€', $c->symbol_left, 'symbol_left');
$this->assertEquals('', $c->symbol_right, 'symbol_right');
$this->assertEquals(',', $c->decimal_mark, 'decimal_mark');
$this->assertEquals('.', $c->thousands_separator, 'thousands_separator');
}
public function testToString() {
$this->assertEquals('EUR (Euro)', (string)new Currency('EUR'));
$this->assertEquals('USD (U.S. Dollar)', (string)new Currency('USD'));
}
}
......@@ -8,6 +8,7 @@
namespace Tests\Bnet\Money;
use Bnet\Money\Currency;
use Bnet\Money\Money;
use Bnet\Money\MoneyException;
use Bnet\Money\Repositories\ArrayRepository;
......@@ -39,7 +40,7 @@ class MoneyTest extends \PHPUnit_Framework_TestCase {
'thousands_separator' => ',',
]
]);
\Bnet\Money\Currency::registerCurrencyRepository($r);
Currency::registerCurrencyRepository($r);
}
public function testBasicFunctions() {
......@@ -52,7 +53,7 @@ class MoneyTest extends \PHPUnit_Framework_TestCase {
}
protected function currency() {
$c = new \Bnet\Money\Currency('EUR', [
$c = new Currency('EUR', [
'code' => 'EUR',
'iso' => 978,
'name' => 'Euro',
......@@ -166,4 +167,146 @@ class MoneyTest extends \PHPUnit_Framework_TestCase {
}
}
public function testFactoryMethods() {
$this->assertEquals(Money::EUR(25), Money::EUR(10)->add(Money::EUR(15)));
$this->assertEquals(Money::USD(25), Money::USD(10)->add(Money::USD(15)));
}
/**
* @expectedException \Bnet\Money\MoneyException
*/
public function testStringThrowsException() {
new Money('foo', new Currency('EUR'));
}
public function testGetters() {
$m = new Money(100, new Currency('EUR'));
$this->assertEquals(100, $m->amount());
$this->assertEquals(1, $m->normalize());
$this->assertEquals(new Currency('EUR'), $m->currency());
}
public function testSameCurrency() {
$m = new Money(100, new Currency('EUR'));
$this->assertTrue($m->isSameCurrency(new Money(100, new Currency('EUR'))));
$this->assertFalse($m->isSameCurrency(new Money(100, new Currency('USD'))));
}
public function testComparison() {
$m1 = new Money(50, new Currency('EUR'));
$m2 = new Money(100, new Currency('EUR'));
$m3 = new Money(200, new Currency('EUR'));
$this->assertEquals(-1, $m2->compare($m3));
$this->assertEquals(1, $m2->compare($m1));
$this->assertEquals(0, $m2->compare($m2));
$this->assertTrue($m2->equals($m2));
$this->assertFalse($m3->equals($m2));
$this->assertTrue($m3->greaterThan($m2));
$this->assertFalse($m2->greaterThan($m3));
$this->assertTrue($m2->greaterThanOrEqual($m2));
$this->assertFalse($m2->greaterThanOrEqual($m3));
$this->assertTrue($m2->lessThan($m3));
$this->assertFalse($m3->lessThan($m2));
$this->assertTrue($m2->lessThanOrEqual($m2));
$this->assertFalse($m3->lessThanOrEqual($m2));
}
/**
* @expectedException \InvalidArgumentException
*/
public function testDifferentCurrenciesCannotBeCompared() {
$m1 = new Money(100, new Currency('EUR'));
$m2 = new Money(100, new Currency('USD'));
$m1->compare($m2);
}
public function testAddition() {
$m1 = new Money(1100101, new Currency('EUR'));
$m2 = new Money(1100021, new Currency('EUR'));
$sum = $m1->add($m2);
$this->assertEquals(new Money(2200122, new Currency('EUR')), $sum);
$this->assertNotEquals($sum, $m1);
$this->assertNotEquals($sum, $m2);
}
/**
* @expectedException \InvalidArgumentException
*/
public function testDifferentCurrenciesCannotBeAdded() {
$m1 = new Money(100, new Currency('EUR'));
$m2 = new Money(100, new Currency('USD'));
$m1->add($m2);
}
public function testSubtraction() {
$m1 = new Money(10010, new Currency('EUR'));
$m2 = new Money(10002, new Currency('EUR'));
$diff = $m1->subtract($m2);
$this->assertEquals(new Money(8, new Currency('EUR')), $diff);
$this->assertNotSame($diff, $m1);
$this->assertNotSame($diff, $m2);
}
/**
* @expectedException \InvalidArgumentException
*/
public function testDifferentCurrenciesCannotBeSubtracted() {
$m1 = new Money(100, new Currency('EUR'));
$m2 = new Money(100, new Currency('USD'));
$m1->subtract($m2);
}
public function testMultiplication() {
$m1 = new Money(15, new Currency('EUR'));
$m2 = new Money(1, new Currency('EUR'));
$this->assertEquals($m1, $m2->multiply(15));
$this->assertNotEquals($m1, $m2->multiply(10));
}
public function testDivision() {
$m1 = new Money(3, new Currency('EUR'));
$m2 = new Money(10, new Currency('EUR'));
$this->assertEquals($m1, $m2->divide(3));
$this->assertNotEquals($m1, $m2->divide(2));
}
public function testAllocation() {
$m1 = new Money(100, new Currency('EUR'));
list($part1, $part2, $part3) = $m1->allocate([1, 1, 1]);
$this->assertEquals(new Money(34, new Currency('EUR')), $part1);
$this->assertEquals(new Money(33, new Currency('EUR')), $part2);
$this->assertEquals(new Money(33, new Currency('EUR')), $part3);
$m2 = new Money(101, new Currency('EUR'));
list($part1, $part2, $part3) = $m2->allocate([1, 1, 1]);
$this->assertEquals(new Money(34, new Currency('EUR')), $part1);
$this->assertEquals(new Money(34, new Currency('EUR')), $part2);
$this->assertEquals(new Money(33, new Currency('EUR')), $part3);
}
public function testAllocationOrderIsImportant() {
$m = new Money(5, new Currency('EUR'));
list($part1, $part2) = $m->allocate([3, 7]);
$this->assertEquals(new Money(2, new Currency('EUR')), $part1);
$this->assertEquals(new Money(3, new Currency('EUR')), $part2);
list($part1, $part2) = $m->allocate([7, 3]);
$this->assertEquals(new Money(4, new Currency('EUR')), $part1);
$this->assertEquals(new Money(1, new Currency('EUR')), $part2);
}
public function testComparators() {
$m1 = new Money(0, new Currency('EUR'));
$m2 = new Money(-1, new Currency('EUR'));
$m3 = new Money(1, new Currency('EUR'));
$m4 = new Money(1, new Currency('EUR'));
$m5 = new Money(1, new Currency('EUR'));
$m6 = new Money(-1, new Currency('EUR'));
$this->assertTrue($m1->isZero());
$this->assertTrue($m2->isNegative());
$this->assertTrue($m3->isPositive());
$this->assertFalse($m4->isZero());
$this->assertFalse($m5->isNegative());
$this->assertFalse($m6->isPositive());
}
}
......@@ -8,27 +8,31 @@
namespace Tests\Bnet\Money\Repositories;
use Bnet\Money\Currency;
use Bnet\Money\Repositories\CallbackRepository;
use Bnet\Money\Repositories\Exception\CurrencyRepositoryException;
class CallbackRepositoryTest extends \PHPUnit_Framework_TestCase {
public function testClosureWork() {
$r = new \Bnet\Money\Repositories\CallbackRepository(function($code) {
$r = new CallbackRepository(function($code) {
if ($code !== 'EUR')
throw new \Bnet\Money\Repositories\Exception\CurrencyRepositoryException;
throw new CurrencyRepositoryException;
return ['name' => 'TestCurrency'];
});
$this->assertEquals('TestCurrency', $r->get('EUR')['name'], 'Loading successfull');
}
public function testWithCurrency() {
$r = new \Bnet\Money\Repositories\CallbackRepository(function($code) {
$r = new CallbackRepository(function($code) {
if ($code !== 'EUR')
throw new \Bnet\Money\Repositories\Exception\CurrencyRepositoryException;
throw new CurrencyRepositoryException;
return ['name' => 'TestCurrency'];
});
\Bnet\Money\Currency::registerCurrencyRepository($r);
$c = new \Bnet\Money\Currency('EUR');
Currency::registerCurrencyRepository($r);
$c = new Currency('EUR');
$this->assertEquals('TestCurrency', $c->name, 'Name matches');
}
......