Commit 9b0668bc authored by Mathias Verraes's avatar Mathias Verraes
Browse files

initial import

parents
.project
.buildpath
.settings
build/
\ No newline at end of file
Copyright (c) 2011 Mathias Verraes
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.
\ No newline at end of file
Verraes\Money
=============
This is a PHP implementation of the Money pattern, as described in [Fowler2002].
The problem
-----------
From [Fowler2002]:
> A large proportion of the computers in this world manipulate money, so it's always puzzled me
> that money isn't actually a first class data type in any mainstream programming language. The
> lack of a type causes problems, the most obvious surrounding currencies. If all your calculations
> are done in a single currency, this isn't a huge problem, but once you involve multiple currencies
> you want to avoid adding your dollars to your yen without taking the currency differences into
> account. The more subtle problem is with rounding. Monetary calculations are often rounded to the
> smallest currency unit. When you do this it's easy to lose pennies (or your local equivalent)
> because of rounding errors.
The goal
--------
Implement a reusable Money class in PHP, using all the best practices and taking care of all the
subtle intricacies of handling money.
Usage
=====
<?php
use Verraes\Money\Money,
Verraes\Money\Usd,
Verraes\Money\Euro;
// One EURO, expressed in cents
$eur1 = new Money(100, new Euro);
// Shortcut
$eur2 = Money::euro(200);
Money::euro(300)->equals(
$eur1->add($eur2)
);
Inspiration
===========
* https://github.com/RubyMoney/money
* http://css.dzone.com/books/practical-php-patterns/basic/practical-php-patterns-value
* http://www.codeproject.com/KB/recipes/MoneyTypeForCLR.aspx
* http://www.michaelbrumm.com/money.html
* http://stackoverflow.com/questions/1679292/proof-that-fowlers-money-allocation-algorithm-is-correct
* http://timeandmoney.sourceforge.net/
* https://github.com/lucamarrocco/timeandmoney/blob/master/lib/money.rb
Bibliography
============
[Fowler2002]
Fowler, M., D. Rice, M. Foemmel, E. Hieatt, R. Mee, and R. Stafford, Patterns of Enterprise Application Architecture, Addison-Wesley, 2002.
http://martinfowler.com/books.html#eaa
http://en.wikipedia.org/wiki/ISO_4217
Todo
====
* https://github.com/RubyMoney/eu_central_bank
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated by PHP Project Wizard (PPW) 1.0.4 on Tue Mar 22 10:10:56 CET 2011 -->
<project name="fowler-money" default="build" basedir=".">
<property name="source" value="lib"/>
<target name="clean" description="Clean up and create artifact directories">
<delete dir="${basedir}/build/api"/>
<delete dir="${basedir}/build/code-browser"/>
<delete dir="${basedir}/build/coverage"/>
<delete dir="${basedir}/build/logs"/>
<delete dir="${basedir}/build/pdepend"/>
<mkdir dir="${basedir}/build/api"/>
<mkdir dir="${basedir}/build/code-browser"/>
<mkdir dir="${basedir}/build/coverage"/>
<mkdir dir="${basedir}/build/logs"/>
<mkdir dir="${basedir}/build/pdepend"/>
</target>
<target name="phpunit" description="Run unit tests using PHPUnit and generates junit.xml and clover.xml">
<exec executable="phpunit" failonerror="true"/>
</target>
<target name="parallelTasks" description="Run the pdepend, phpmd, phpcpd, phpcs, phpdoc and phploc tasks in parallel using a maximum of 2 threads.">
<parallel threadCount="2">
<sequential>
<antcall target="pdepend"/>
<antcall target="phpmd"/>
</sequential>
<antcall target="phpcpd"/>
<antcall target="phpcs"/>
<antcall target="phpdoc"/>
<antcall target="phploc"/>
</parallel>
</target>
<target name="pdepend" description="Generate jdepend.xml and software metrics charts using PHP_Depend">
<exec executable="pdepend">
<arg line="--jdepend-xml=${basedir}/build/logs/jdepend.xml
--jdepend-chart=${basedir}/build/pdepend/dependencies.svg
--overview-pyramid=${basedir}/build/pdepend/overview-pyramid.svg
${source}" />
</exec>
</target>
<target name="phpmd" description="Generate pmd.xml using PHPMD">
<exec executable="phpmd">
<arg line="${source}
xml
codesize,design,naming,unusedcode
--reportfile ${basedir}/build/logs/pmd.xml" />
</exec>
</target>
<target name="phpcpd" description="Generate pmd-cpd.xml using PHPCPD">
<exec executable="phpcpd">
<arg line="--log-pmd ${basedir}/build/logs/pmd-cpd.xml ${source}" />
</exec>
</target>
<target name="phploc" description="Generate phploc.csv">
<exec executable="phploc">
<arg line="--log-csv ${basedir}/build/logs/phploc.csv ${source}" />
</exec>
</target>
<target name="phpcs" description="Generate checkstyle.xml using PHP_CodeSniffer">
<exec executable="phpcs" output="/dev/null">
<arg line="--report=checkstyle
--report-file=${basedir}/build/logs/checkstyle.xml
--standard=PEAR
${source}" />
</exec>
</target>
<target name="phpdoc" description="Generate API documentation using PHPDocumentor">
<exec executable="phpdoc">
<arg line="-d ${source} -t ${basedir}/build/api" />
</exec>
</target>
<target name="phpcb" description="Aggregate tool output with PHP_CodeBrowser">
<exec executable="phpcb">
<arg line="--log ${basedir}/build/logs
--source ${source}
--output ${basedir}/build/code-browser" />
</exec>
</target>
<target name="build" depends="clean,parallelTasks,phpunit,phpcb"/>
</project>
<?php
/**
* This file is part of the Verraes\Money library
*
* Copyright (c) 2011 Mathias Verraes
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Verraes\Money;
interface Currency
{
/**
* @return string
*/
public function getName();
/**
* @return bool
*/
public function equals(Currency $currency);
}
\ No newline at end of file
<?php
/**
* This file is part of the Verraes\Money library
*
* Copyright (c) 2011 Mathias Verraes
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Verraes\Money;
final class Euro implements Currency
{
/**
* @return string
*/
public function getName()
{
return 'EUR';
}
/**
* @return bool
*/
public function equals(Currency $currency)
{
return $this->getName() == $currency->getName();
}
}
\ No newline at end of file
<?php
/**
* This file is part of the Verraes\Money library
*
* Copyright (c) 2011 Mathias Verraes
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Verraes\Money;
/**
* @see http://www.phpkode.com/tips/item/exception-best-practices-in-php-5-3/
*/
interface Exception
{
}
<?php
/**
* This file is part of the Verraes\Money library
*
* Copyright (c) 2011 Mathias Verraes
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Verraes\Money;
class InvalidArgumentException extends \InvalidArgumentException implements Exception
{
}
<?php
/**
* This file is part of the Verraes\Money library
*
* Copyright (c) 2011 Mathias Verraes
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Verraes\Money;
use Verraes\Money\InvalidArgumentException;
final class Money
{
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;
/**
* @var int
*/
private $units;
/** @var Verraes\Money\Currency */
private $currency;
/**
* Create a Money instance
* @param integer $units Amount, expressed in the smallest units of $currency (eg cents)
* @param Verraes\Money\Currency $currency
* @throws Verraes\Money\InvalidArgumentException
*/
public function __construct($units, Currency $currency)
{
if(!is_int($units)) {
throw new InvalidArgumentException("The first parameter of Money must be an integer");
}
$this->units = $units;
$this->currency = $currency;
}
/**
* Convenience factory method for an amount in EURO
* @return Money
*/
public static function euro($units)
{
return new Money($units, new Euro);
}
/**
* Convenience factory method for an amount in USD
* @return Money
*/
public static function usd($units)
{
return new Money($units, new Usd);
}
private function isSameCurrency(Money $other)
{
return $this->currency->equals($other->currency);
}
/**
* @throws Verraes\Money\InvalidArgumentException
*/
private function assertSameCurrency(Money $other)
{
if(!$this->isSameCurrency($other)) {
throw new InvalidArgumentException('Different currencies');
}
}
public function equals(Money $other)
{
return
$this->isSameCurrency($other)
&& $this->units == $other->units;
}
public function compare(Money $other)
{
$this->assertSameCurrency($other);
if($this->units < $other->units) {
return -1;
} elseif($this->units == $other->units) {
return 0;
} else {
return 1;
}
}
public function greaterThan(Money $other)
{
return 1 == $this->compare($other);
}
public function lessThan(Money $other)
{
return -1 == $this->compare($other);
}
/**
* @return int
*/
public function getUnits()
{
return $this->units;
}
/**
* @return Verraes\Money\Currency
*/
public function getCurrency()
{
return $this->currency;
}
public function add(Money $other)
{
$this->assertSameCurrency($other);
return new self($this->units + $other->units, $this->currency);
}
public function subtract(Money $other)
{
$this->assertSameCurrency($other);
return new self($this->units - $other->units, $this->currency);
}
/**
* @throws Verraes\Money\InvalidArgumentException
*/
private function assertOperand($operand)
{
if(!is_int($operand) && !is_float($operand)) {
throw new InvalidArgumentException('Operand should be an integer or a float');
}
}
/**
* @throws Verraes\Money\InvalidArgumentException
*/
private function assertRoundingMode($rounding_mode)
{
if(!in_array($rounding_mode, array(self::ROUND_HALF_DOWN, self::ROUND_HALF_EVEN, self::ROUND_HALF_ODD, self::ROUND_HALF_UP))) {
throw new InvalidArgumentException('Operand should be an integer or a float');
}
}
public function multiply($operand, $rounding_mode = self::ROUND_HALF_UP)
{
$this->assertOperand($operand);
$this->assertRoundingMode($rounding_mode);
$result = (int) round($this->units * $operand, 0, $rounding_mode);
return new Money($result, $this->currency);
}
public function divide($operand, $rounding_mode = self::ROUND_HALF_UP)
{
$this->assertOperand($operand);
$this->assertRoundingMode($rounding_mode);
$result = (int) round($this->units / $operand, 0, $rounding_mode);
return new Money($result, $this->currency);
}
/**
* Allocate the money according to a list of ratio's
* @param array $ratios List of ratio's
*/
public function allocate(array $ratios)
{
$remainder = $this->units;
$results = array();
$total = array_sum($ratios);
foreach($ratios as $ratio)
{
$share = (int) floor($this->units * $ratio / $total);
$results[] = new Money($share, $this->currency);
$remainder -= $share;
}
for($i = 0; $remainder > 0; $i++)
{
$results[$i]->units++;
$remainder--;
}
return $results;
}
}
\ No newline at end of file
<?php
/**
* This file is part of the Verraes\Money library
*
* Copyright (c) 2011 Mathias Verraes
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Verraes\Money;
final class Usd implements Currency
{
/**
* @return string
*/
public function getName()
{
return 'USD';
}
/**
* @return bool
*/
public function equals(Currency $currency)
{
return $this->getName() == $currency->getName();
}
}
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated by PHP Project Wizard (PPW) 1.0.4 on Tue Mar 22 10:10:56 CET 2011 -->
<phpunit bootstrap="tests/bootstrap.php"
backupGlobals="false"
backupStaticAttributes="false"
strict="true"
verbose="true">
<testsuites>
<testsuite name="fowler-money">
<directory suffix="Test.php">tests</directory>
</testsuite>
</testsuites>
<logging>
<log type="coverage-html" target="build/coverage" title="fowler-money"
charset="UTF-8" yui="true" highlight="true"
lowUpperBound="35" highLowerBound="70"/>
<log type="coverage-clover" target="build/logs/clover.xml"/>
<log type="junit" target="build/logs/junit.xml" logIncompleteSkipped="false"/>
</logging>
<filter>
<whitelist addUncoveredFilesFromWhitelist="true">
<directory suffix=".php">lib</directory>
</whitelist>
</filter>
</phpunit>
<?php
/**
* This file is part of the Verraes\Money library
*
* Copyright (c) 2011 Mathias Verraes
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
require_once 'bootstrap.php';
use Verraes\Money\Currency;
use Verraes\Money\Euro;
use Verraes\Money\Usd;
class CurrencyTest extends PHPUnit_Framework_TestCase
{
public function setUp()
{
$this->euro1 = new Euro;
$this->euro2 = new Euro;
$this->usd1 = new Usd;
$this->usd2 = new Usd;
}
public function testDifferentInstancesAreEqual()
{
$this->assertTrue(
$this->euro1->equals($this->euro2)
);
$this->assertTrue(
$this->usd1->equals($this->usd2)
);
}
public function testDifferentCurrenciesAreNotEqual()
{
$this->assertFalse(
$this->euro1->equals($this->usd1)
);
}
}
\ No newline at end of file
<?php
/**
* This file is part of the Verraes\Money library
*
* Copyright (c) 2011 Mathias Verraes
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
require_once 'bootstrap.php';
use Verraes\Money\Money;
use Verraes\Money\Currency;
use Verraes\Money\Usd;
use Verraes\Money\Euro;
class MoneyTest extends PHPUnit_Framework_TestCase
{
private function assertMoneyEquals(Money $expected, Money $actual, $message = null)
{
$str = sprintf(
"Failed asserting that <Money:%s %s> matches expected <Money:%s %s>",
$actual->getCurrency()->getName(), $actual->getUnits(),
$expected->getCurrency()->getName(), $expected->getUnits()
);
return $this->assertTrue(
$actual->equals($expected),
($message ? $message.PHP_EOL: '') . $str
);
}
public function testFactoryMethods()
{
$this->assertMoneyEquals(
Money::euro(25),
Money::euro(10)->add(Money::euro(15))
);
$this->assertMoneyEquals(
Money::usd(25),
Money::usd(10)->add(Money::usd(15))
);
}
public function testGetters()
{
$m = new Money(100, $euro = new Euro);
$this->assertEquals(100, $m->getUnits());
$this->assertEquals($euro, $m->getCurrency());