├── .gitignore ├── .travis.yml ├── tests ├── bootstrap.php └── unit │ └── MoneyMath │ ├── CurrencyTest.php │ └── Decimal2Test.php ├── phpunit.xml.dist ├── composer.json ├── src └── MoneyMath │ ├── Currency.php │ └── Decimal2.php └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | composer.phar 2 | composer.lock 3 | vendor 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.4 5 | - 5.5 6 | - 5.6 7 | - 7.0 8 | - 7.1 9 | - 7.2 10 | 11 | before_script: 12 | - composer install 13 | 14 | script: phpunit --coverage-text 15 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14 | 15 | 16 | ./tests/unit/ 17 | 18 | 19 | 20 | 21 | 22 | ./src/ 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ikr/money-math", 3 | "type": "library", 4 | "description": "gmp-based arbitrary precision operations on currency amounts \"XXX.YY\"; because floats are BAD for representing money", 5 | 6 | "keywords": [ 7 | "money", 8 | "gmp", 9 | "currency", 10 | "bigint", 11 | "bignum", 12 | "arithmetic", 13 | "arbitrary", 14 | "precision", 15 | "format" 16 | ], 17 | 18 | "homepage": "https://github.com/ikr/money-math-php", 19 | "license": "MIT", 20 | 21 | "authors": [ 22 | { 23 | "name": "Ivan Krechetov", 24 | "email": "ivan.krechetov@gmail.com" 25 | } 26 | ], 27 | 28 | "autoload": { 29 | "psr-4": { 30 | "MoneyMath\\": "src/MoneyMath" 31 | } 32 | }, 33 | 34 | "autoload-dev": { 35 | "psr-4": { 36 | "MoneyMath\\": "tests/unit/MoneyMath" 37 | } 38 | }, 39 | 40 | "minimum-stability": "dev", 41 | 42 | "require": { 43 | "php": ">=5.4.0", 44 | "ext-gmp": "*" 45 | }, 46 | 47 | "require-dev": { 48 | "ext-mbstring": "*", 49 | "phpunit/phpunit": "^4.8|^6.5|^7.0" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests/unit/MoneyMath/CurrencyTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('560.00', $chf->format(new Decimal2('560.00'))); 11 | $this->assertEquals('-1,560.00', $chf->format(new Decimal2('-1560.00'))); 12 | } 13 | 14 | public function test_formats_amount_2() { 15 | $jpy = new Currency('JPY'); 16 | $this->assertEquals('560', $jpy->format(new Decimal2('560.00'))); 17 | $this->assertEquals('236,800', $jpy->format(new Decimal2('236800.00'))); 18 | $this->assertEquals('-1,000,000,000', $jpy->format(new Decimal2('-1000000000.00'))); 19 | $this->assertEquals('-100,000,000,000', $jpy->format(new Decimal2('-100000000000.00'))); 20 | } 21 | 22 | public function test_formats_amount_3() { 23 | $eur = new Currency('EUR'); 24 | $this->assertEquals('560,00', $eur->format(new Decimal2('560.00'))); 25 | $this->assertEquals('-1.560,00', $eur->format(new Decimal2('-1560.00'))); 26 | $this->assertEquals('-100.000.000.000,00', $eur->format(new Decimal2('-100000000000.00'))); 27 | } 28 | 29 | public function test_formats_amount_4() { 30 | $eur = new Currency('USD'); 31 | $this->assertEquals('560.00', $eur->format(new Decimal2('560.00'))); 32 | $this->assertEquals('-1,560.00', $eur->format(new Decimal2('-1560.00'))); 33 | $this->assertEquals('-100,000,000,000.00', $eur->format(new Decimal2('-100000000000.00'))); 34 | } 35 | 36 | public function test_formats_amount_5() { 37 | $eur = new Currency('TWD'); 38 | $this->assertEquals('560.00', $eur->format(new Decimal2('560.00'))); 39 | $this->assertEquals('-1560.00', $eur->format(new Decimal2('-1560.00'))); 40 | $this->assertEquals('-100000000000.00', $eur->format(new Decimal2('-100000000000.00'))); 41 | } 42 | } 43 | 44 | -------------------------------------------------------------------------------- /src/MoneyMath/Currency.php: -------------------------------------------------------------------------------- 1 | code = $code; 15 | } 16 | 17 | /** 18 | * @param Decimal2 $amount 19 | * @return string 20 | */ 21 | public function format(Decimal2 $amount) { 22 | switch ($this->code) { 23 | case 'JPY': 24 | return self::separateThousands(strval($amount->integerValue()), ','); 25 | 26 | case 'EUR': 27 | case 'GBP': 28 | return self::separateThousands(strval($amount->integerValue()), '.') . ',' 29 | . substr(strval($amount), -2); 30 | 31 | case 'CHF': 32 | case 'USD': 33 | return self::separateThousands(strval($amount->integerValue()), ',') . '.' 34 | . substr(strval($amount), -2); 35 | 36 | default: 37 | return strval($amount); 38 | } 39 | } 40 | 41 | //-------------------------------------------------------------------------------------------------- 42 | 43 | private static function separateThousands($in_str, $with_str) { 44 | $sign = ''; 45 | $src = $in_str; 46 | 47 | if ('-' == $in_str[0]) { 48 | $sign = '-'; 49 | $src = substr($src, 1); 50 | } 51 | 52 | 53 | $ret = ''; 54 | 55 | while (strlen($src) > 0) { 56 | if (strlen($ret) > 0) { 57 | $ret = $with_str . $ret; 58 | } 59 | 60 | if (strlen($src) <= 3) { 61 | $ret = $src . $ret; 62 | break; 63 | } 64 | 65 | $appendix = substr($src, strlen($src) - 3, 3); 66 | $ret = $appendix . $ret; 67 | $src = substr($src, 0, strlen($src) - 3); 68 | } 69 | 70 | return $sign . $ret; 71 | } 72 | } 73 | 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://secure.travis-ci.org/ikr/money-math-php.png)](http://travis-ci.org/ikr/money-math-php) 2 | 3 | # What does it do? 4 | 5 | Arithmetic operations on currency amounts. Amounts on input and output are arbitrary large and 6 | precise: 7 | 8 | 99999999999999999999999999999999999999999999999999999999999999999999999999999999.99 9 | + 10 | 0.01 11 | = 12 | 100000000000000000000000000000000000000000000000000000000000000000000000000000000.00 13 | 14 | However, in cases when the division is involved — like for percentage calculation — the result is 15 | rounded to the whole cent: 33% of $0.50 is $0.17 instead of $0.165 16 | 17 | As a bonus feature, there's a simple formatting function for amounts in CHF, EUR, USD, GBP, and JPY. 18 | 19 | # Why does it exist? 20 | 21 | Because storing currency amounts in floats [is a really bad idea](http://stackoverflow.com/questions/3730019/why-not-use-double-or-float-to-represent-currency) 22 | 23 | # How to use it? 24 | 25 | ## Installation 26 | 27 | Install via [Composer package manager](http://packagist.org): 28 | 29 | 30 | Just create a `composer.json` file for your project: 31 | 32 | { 33 | "require": { 34 | "ikr/money-math": "0.1.*" 35 | } 36 | } 37 | 38 | And run these two commands to install the Composer dependencies: 39 | 40 | $ curl -s http://getcomposer.org/installer | php 41 | $ php composer.phar install 42 | 43 | Now you can add the Composer's autoloader, and you will have access to the `MoneyMath\*` classes: 44 | 45 | integerValue() // 3 57 | $a->fractionValue() // 50 58 | $a->centsValue() // 350 59 | 60 | strval(Decimal2::plus($a, $b)) // "8.00" 61 | strval(Decimal2::sum([$a, $a, $b])) // "11.50" 62 | strval(Decimal2::avg([$a, $b])) // "4.00" 63 | strval(Decimal2::minus($a, $b)) // "-1.00" 64 | strval(Decimal2::multiply($a, 2)) // "7.00" 65 | strval(Decimal2::mul($a, $b)) // "15.75" 66 | 67 | strval(Decimal2::div($a, $b)) // "0.78" 68 | strval(Decimal2::getPercentsOf($a, $b)) // "0.16" b% of a 69 | strval(Decimal2::cmp($a, $b)) // -1 70 | 71 | And last, but not least :) 72 | 73 | $c = new Decimal2("42.02"); 74 | strval(Decimal2::roundUpTo5Cents($c)) // "42.05" 75 | 76 | Which we use for bills in CHF that are required by law to be 0 (mod 5). 77 | 78 | For formatting please use the `Currency` class 79 | 80 | (new Currency('EUR'))->format(new Decimal2('-100000000000.00')) // -100.000.000.000,00 81 | 82 | # License (MIT) 83 | 84 | Copyright (c) 2014 Ivan Krechetov 85 | 86 | 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: 87 | 88 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 89 | 90 | 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. 91 | -------------------------------------------------------------------------------- /src/MoneyMath/Decimal2.php: -------------------------------------------------------------------------------- 1 | cents = gmp_strval( 24 | gmp_mul( 25 | gmp_init($stringRepresentation, 10), 26 | 100 27 | ) 28 | ); 29 | 30 | return; 31 | } 32 | 33 | $parts = explode(self::SEPARATOR, $stringRepresentation); 34 | 35 | while (strlen(trim($parts[1])) < 2) { 36 | $parts[1] .= '0'; 37 | } 38 | 39 | $is_negative = ('-' === trim($stringRepresentation[0])); 40 | 41 | $this->cents = gmp_strval( 42 | gmp_add( 43 | gmp_mul( 44 | gmp_abs(gmp_init($parts[0], 10)), 45 | 100 46 | ), 47 | self::roundToHundred(trim($parts[1])) 48 | ) 49 | ); 50 | 51 | if ($is_negative) { 52 | $this->cents = gmp_strval( 53 | gmp_neg( 54 | gmp_init($this->cents, 10) 55 | ) 56 | ); 57 | } 58 | } 59 | 60 | //-------------------------------------------------------------------------------------------------- 61 | 62 | /** 63 | * @return string 64 | */ 65 | public function __toString() { 66 | return $this->integerValue() . self::SEPARATOR 67 | . (abs($this->fractionValue()) < 10? '0' : '') . abs($this->fractionValue()); 68 | } 69 | 70 | /** 71 | * The integer part of this amount (without cents part). 72 | * 73 | * @return integer Or string, if the number is too big. 74 | */ 75 | public function integerValue() { 76 | $sign = ( 77 | gmp_cmp( 78 | gmp_init($this->cents, 10), 0 79 | ) < 0 80 | )? '-' : ''; 81 | 82 | return $sign . gmp_strval( 83 | gmp_div_q( 84 | gmp_abs( 85 | gmp_init($this->cents, 10) 86 | ), 87 | 100 88 | ), 89 | 10 90 | ); 91 | } 92 | 93 | /** 94 | * Returns the amount of hundredth's in this decimal fraction part. That would be cents for, 95 | * say, an USD amount. 96 | * 97 | * @return integer 98 | */ 99 | public function fractionValue() { 100 | $ret = gmp_div_r( 101 | gmp_abs( 102 | gmp_init($this->cents, 10) 103 | ), 104 | 100 105 | ); 106 | 107 | if (gmp_cmp(gmp_init($this->cents, 10), 0) < 0) { 108 | $ret = gmp_neg($ret); 109 | } 110 | 111 | return gmp_intval($ret); 112 | } 113 | 114 | /** 115 | * @return integer Or string, if the number is too big. 116 | */ 117 | public function centsValue() { 118 | if (gmp_cmp(gmp_abs(gmp_init($this->cents, 10)), PHP_INT_MAX) > 0) { 119 | return $this->cents; 120 | } 121 | 122 | return gmp_intval(gmp_init($this->cents, 10)); 123 | } 124 | 125 | //-------------------------------------------------------------------------------------------------- 126 | 127 | /** 128 | * Creates a new decimal which is a sum of the passed $a and $b. 129 | * 130 | * @param Decimal2 $a 131 | * @param Decimal2 $b 132 | * @return Decimal2 133 | */ 134 | public static function plus(Decimal2 $a, Decimal2 $b) { 135 | $ret = new Decimal2('0'); 136 | 137 | $ret->cents = gmp_strval( 138 | gmp_add( 139 | gmp_init($a->cents, 10), 140 | gmp_init($b->cents, 10) 141 | ) 142 | ); 143 | 144 | return $ret; 145 | } 146 | 147 | /** 148 | * Returns the sum of all the decimal numbers in the array. 149 | * 150 | * @param array $decimals The array of Decimal2 objects. 151 | * 152 | * @return Decimal2 or boolean false for an empty $decimals array. 153 | */ 154 | public static function sum(array $decimals) { 155 | $ret = new Decimal2('0'); 156 | 157 | foreach ($decimals as $d) { 158 | $ret = Decimal2::plus($ret, $d); 159 | } 160 | 161 | return $ret; 162 | } 163 | 164 | /** 165 | * Returns the average of all the decimal numbers in the array. 166 | * 167 | * @param array $decimals The array of Decimal2 objects. 168 | * 169 | * @return Decimal2 or boolean false for an empty $decimals array. 170 | */ 171 | public static function avg(array $decimals) { 172 | if (!count($decimals)) return false; 173 | 174 | return Decimal2::div( 175 | Decimal2::sum($decimals), 176 | new Decimal2(count($decimals)) 177 | ); 178 | } 179 | 180 | /** 181 | * Creates a new decimal which is a difference of the passed $a and $b. 182 | * 183 | * @param Decimal2 $a 184 | * @param Decimal2 $b 185 | * @return Decimal2 186 | */ 187 | public static function minus(Decimal2 $a, Decimal2 $b) { 188 | $ret = new Decimal2('0'); 189 | 190 | $ret->cents = gmp_strval( 191 | gmp_add( 192 | gmp_init($a->cents, 10), 193 | gmp_neg(gmp_init($b->cents, 10)) 194 | ) 195 | ); 196 | 197 | return $ret; 198 | } 199 | 200 | /** 201 | * Creates a new decimal which is a result of a multiplication of the passed decimal by the 202 | * passed integer factor. 203 | * 204 | * @param Decimal2 $decimal 205 | * @param integer $byIntFactor 206 | * @return Decimal2 207 | */ 208 | public static function multiply(Decimal2 $decimal, $byIntFactor) { 209 | $ret = new Decimal2('0'); 210 | 211 | $ret->cents = gmp_strval( 212 | gmp_mul( 213 | gmp_init($decimal->cents, 10), 214 | gmp_init($byIntFactor) 215 | ) 216 | ); 217 | 218 | return $ret; 219 | } 220 | 221 | /** 222 | * Creates a new decimal which is a result of a multiplication of the passed decimals. 223 | * 224 | * @param Decimal2 $a 225 | * @param Decimal2 $b 226 | * @return Decimal2 227 | */ 228 | public static function mul(Decimal2 $a, Decimal2 $b) { 229 | $ret = new Decimal2('0'); 230 | 231 | $ret->cents = gmp_strval( 232 | gmp_div_q( 233 | gmp_mul( 234 | gmp_init($a->cents, 10), 235 | gmp_init($b->cents, 10) 236 | ), 237 | 100 238 | ) 239 | ); 240 | 241 | return $ret; 242 | } 243 | 244 | /** 245 | * Creates a new decimal which is a result of a division of $a by $b. 246 | * 247 | * @param Decimal2 $a 248 | * @param Decimal2 $b 249 | * @return Decimal2 250 | */ 251 | public static function div(Decimal2 $a, Decimal2 $b) { 252 | $strA = strval($a); 253 | $strB = strval($b); 254 | 255 | $sign_a = ('-' === $strA[0]) ? -1 : 1; 256 | $sign_b = ('-' === $strB[0])? -1 : 1; 257 | 258 | $ret = new Decimal2('0'); 259 | 260 | $aAbsCentsMul100 = gmp_mul( 261 | gmp_abs(gmp_init($a->cents, 10)), 262 | 100 263 | ); 264 | 265 | $bAbsCents = gmp_abs(gmp_init($b->cents, 10)); 266 | 267 | $retCents = gmp_div_q( 268 | $aAbsCentsMul100, 269 | $bAbsCents, 270 | GMP_ROUND_ZERO 271 | ); 272 | 273 | $retCentsMod = gmp_mod($aAbsCentsMul100, $bAbsCents); 274 | 275 | if (gmp_cmp($retCentsMod, gmp_sub($bAbsCents, $retCentsMod)) >=0 ) { 276 | $retCents = gmp_add($retCents, 1); 277 | } 278 | 279 | $ret->cents = gmp_strval($retCents); 280 | 281 | if (($sign_a * $sign_b) < 0) { 282 | $ret->cents = gmp_strval( 283 | gmp_neg( 284 | gmp_init($ret->cents, 10) 285 | ) 286 | ); 287 | } 288 | 289 | return $ret; 290 | } 291 | 292 | /** 293 | * Returns the specified amount of percents of the passed $decimal value. 294 | * 295 | * @param Decimal2 $decimal 296 | * 297 | * @param Decimal2 $percents 298 | * 299 | * @return Decimal2 300 | */ 301 | public static function getPercentsOf(Decimal2 $decimal, Decimal2 $percents) { 302 | $ret = new Decimal2(strval($decimal)); 303 | 304 | $ret->cents = gmp_strval( 305 | gmp_mul( 306 | gmp_init($ret->cents, 10), 307 | gmp_init($percents->cents, 10) 308 | ) 309 | ); 310 | 311 | $ret->cents = gmp_strval( 312 | gmp_div_q( 313 | gmp_init($ret->cents, 10), 314 | 10000, 315 | GMP_ROUND_PLUSINF 316 | ) 317 | ); 318 | 319 | return $ret; 320 | } 321 | 322 | /** 323 | * Comparison operator. 324 | * 325 | * @param Decimal2 $a 326 | * 327 | * @param Decimal2 $b 328 | * 329 | * @return integer Returns a positive value if a > b, zero if a = b and a negative value 330 | * if a < b 331 | */ 332 | 333 | public static function cmp(Decimal2 $a, Decimal2 $b) { 334 | return gmp_cmp( 335 | gmp_init($a->cents, 10), 336 | gmp_init($b->cents, 10) 337 | ); 338 | } 339 | 340 | /** 341 | * @param Decimal2 $d 342 | * @return Decimal2 343 | */ 344 | public static function roundUpTo5Cents(Decimal2 $d) { 345 | $lastDigit = intval(substr(strval($d), -1)); 346 | $additon = 0; 347 | 348 | if (($lastDigit % 5) != 0) { 349 | $additon = '0.0' . strval(5 - ($lastDigit % 5)); 350 | } 351 | 352 | return self::plus($d, new Decimal2($additon)); 353 | } 354 | 355 | //-------------------------------------------------------------------------------------------------- 356 | 357 | private static function roundToHundred($num) { 358 | $digitsCount = strlen($num); 359 | if ($digitsCount < 3) return gmp_init($num, 10); 360 | 361 | $divider = gmp_pow(10, $digitsCount - 2); 362 | $addition = gmp_mul(5, gmp_pow(10, $digitsCount - 3)); 363 | 364 | return gmp_div_q( 365 | gmp_add(gmp_init($num, 10), $addition), 366 | $divider, 367 | GMP_ROUND_ZERO); 368 | } 369 | } 370 | -------------------------------------------------------------------------------- /tests/unit/MoneyMath/Decimal2Test.php: -------------------------------------------------------------------------------- 1 | assertD(new Decimal2('3456'), 3456, 0, '3456.00'); 10 | } 11 | 12 | public function testParsesNegativeInteger() { 13 | $this->assertD(new Decimal2('-13'), -13, 0, '-13.00'); 14 | } 15 | 16 | public function testParsesInteger2() { 17 | $this->assertD(new Decimal2('003456'), 3456, 0, '3456.00'); 18 | } 19 | 20 | public function testParsesIntegerZero1() { 21 | $this->assertD(new Decimal2('0'), 0, 0, '0.00'); 22 | } 23 | 24 | public function testParsesIntegerZero2() { 25 | $this->assertD(new Decimal2('000000'), 0, 0, '0.00'); 26 | } 27 | 28 | public function testParsesFractionZero() { 29 | $this->assertD(new Decimal2('0.0'), 0, 0, '0.00'); 30 | } 31 | 32 | public function testParsesFraction1() { 33 | $this->assertD(new Decimal2('399.9'), 399, 90, '399.90'); 34 | } 35 | 36 | public function testParsesFraction2() { 37 | $this->assertD(new Decimal2('1.05'), 1, 5, '1.05'); 38 | } 39 | 40 | public function testParsesFraction3() { 41 | $this->assertD(new Decimal2('210.00'), 210, 0, '210.00'); 42 | } 43 | 44 | public function testParsesFraction4() { 45 | $this->assertD(new Decimal2('0.99'), 0, 99, '0.99'); 46 | } 47 | 48 | public function testParsesNegativeFraction1() { 49 | $this->assertD(new Decimal2('-1.1'), -1, -10, '-1.10'); 50 | } 51 | 52 | public function testParsesNegativeFraction2() { 53 | $this->assertD(new Decimal2('-14.05'), -14, -5, '-14.05'); 54 | } 55 | 56 | //-------------------------------------------------------------------------------------------------- 57 | 58 | public function testPositiveDecimalsCanBeSummed1() { 59 | $a = new Decimal2('16.11'); 60 | $b = new Decimal2('17.07'); 61 | $this->assertD(Decimal2::plus($a, $b), 33, 18, '33.18'); 62 | } 63 | 64 | public function testPositiveDecimalsCanBeSummed2() { 65 | $a = new Decimal2('65535.79'); 66 | $b = new Decimal2('1024.85'); 67 | $this->assertD(Decimal2::plus($a, $b), 66560, 64, '66560.64'); 68 | } 69 | 70 | public function testPositiveDecimalsCanBeSummed3() { 71 | $a = new Decimal2('1.99'); 72 | $b = new Decimal2('0.02'); 73 | $this->assertD(Decimal2::plus($a, $b), 2, 1, '2.01'); 74 | } 75 | 76 | public function testNegativeDecimalsCanBeSummed() { 77 | $a = new Decimal2('-1.99'); 78 | $b = new Decimal2('-0.02'); 79 | $this->assertD(Decimal2::plus($a, $b), -2, -1, '-2.01'); 80 | } 81 | 82 | public function testMixedDecimalsCanBeSummed1() { 83 | $a = new Decimal2('-1.99'); 84 | $b = new Decimal2('1.99'); 85 | $this->assertD(Decimal2::plus($a, $b), 0, 0, '0.00'); 86 | } 87 | 88 | public function testMixedDecimalsCanBeSummed2() { 89 | $a = new Decimal2('-1.99'); 90 | $b = new Decimal2('0.98'); 91 | $this->assertD(Decimal2::plus($a, $b), -1, -1, '-1.01'); 92 | } 93 | 94 | public function testMixedDecimalsCanBeSummedInChain() { 95 | $a = new Decimal2('194.00'); 96 | $b = new Decimal2('23.30'); 97 | $c = new Decimal2('210.00'); 98 | $d = new Decimal2('355.00'); 99 | 100 | $this->assertD( 101 | Decimal2::plus($a, Decimal2::plus($b, Decimal2::plus($c, $d))), 782, 30, '782.30'); 102 | } 103 | 104 | public function testDecimalsAdditionIsCommutative() { 105 | $a = new Decimal2('194.00'); 106 | $b = new Decimal2('23.30'); 107 | 108 | $this->assertEquals(strval(Decimal2::plus($a, $b)), strval(Decimal2::plus($b, $a))); 109 | } 110 | 111 | //-------------------------------------------------------------------------------------------------- 112 | 113 | public function testDecimalCanBeMultiplied1() { 114 | $d = new Decimal2('-1.99'); 115 | $this->assertD(Decimal2::multiply($d, 100), -199, 0, '-199.00'); 116 | } 117 | 118 | public function testDecimalCanBeMultiplied2() { 119 | $d = new Decimal2('150'); 120 | $this->assertD(Decimal2::multiply($d, 2), 300, 0, '300.00'); 121 | } 122 | 123 | public function testDecimalCanBeMultiplied3() { 124 | $d = new Decimal2('1.55'); 125 | $this->assertD(Decimal2::multiply($d, -10), -15, -50, '-15.50'); 126 | } 127 | 128 | public function testDecimalCanBeMultiplied4() { 129 | $d = new Decimal2('210.0'); 130 | $this->assertD(Decimal2::multiply($d, 5), 1050, 0, '1050.00'); 131 | } 132 | 133 | //-------------------------------------------------------------------------------------------------- 134 | 135 | public function testSurvivesBigNumbers1() { 136 | $a = new Decimal2('9000000000.00'); 137 | $b = new Decimal2('9000000000.00'); 138 | $this->assertD(Decimal2::plus($a, $b), '18000000000', 0, '18000000000.00'); 139 | } 140 | 141 | public function testSurvivesBigNumbers2() { 142 | $a = new Decimal2('9000000000.20'); 143 | $this->assertD(Decimal2::multiply($a, 1), '9000000000', 20, '9000000000.20'); 144 | } 145 | 146 | public function testSurvivesBigNumbers3() { 147 | $a = new Decimal2('9000000000.20'); 148 | $this->assertD(Decimal2::multiply($a, 5), '45000000001', 0, '45000000001.00'); 149 | } 150 | 151 | public function testSurvivesBigNumbers4() { 152 | $a = new Decimal2('9000000000.20'); 153 | $this->assertD(Decimal2::multiply($a, -5), '-45000000001', 0, '-45000000001.00'); 154 | } 155 | 156 | public function testSurvivesBigNumbers5() { 157 | $a = new Decimal2('99999999999999999999999999999999999999999999999999999999999999999999999999999999.99'); 158 | 159 | $this->assertEquals( 160 | '100000000000000000000000000000000000000000000000000000000000000000000000000000000.00', 161 | strval(Decimal2::plus($a, new Decimal2('0.01'))) 162 | ); 163 | } 164 | 165 | //-------------------------------------------------------------------------------------------------- 166 | 167 | public function testReturnsCents1() { 168 | $a = new Decimal2('-9000000000.20'); 169 | $this->assertEquals('-900000000020', $a->centsValue()); 170 | } 171 | 172 | public function testReturnsCents2() { 173 | $a = new Decimal2('1.99'); 174 | $this->assertEquals(199, $a->centsValue()); 175 | } 176 | 177 | public function testReturnsCents3() { 178 | $a = new Decimal2('9223372036854775807.20'); 179 | $this->assertEquals(922337203685477580720, $a->centsValue()); 180 | } 181 | 182 | //-------------------------------------------------------------------------------------------------- 183 | 184 | public function testHandlesGracefullyTooPreciseValues1() { 185 | $a = new Decimal2('0.001'); 186 | $this->assertEquals('0.00', strval($a)); 187 | 188 | $b = new Decimal2('0.009'); 189 | $this->assertEquals('0.01', strval($b)); 190 | 191 | $this->assertEquals('0.01', strval(Decimal2::plus($a, $b))); 192 | } 193 | 194 | public function testHandlesGracefullyTooPreciseValues2() { 195 | $a = new Decimal2('1597.847056'); 196 | $this->assertEquals('1597.85', strval($a)); 197 | } 198 | 199 | public function testHandlesGracefullyTooPreciseValues3() { 200 | $a = new Decimal2('-7.455'); 201 | $this->assertEquals('-7.46', strval($a)); 202 | } 203 | 204 | public function testHandlesGracefullyTooPreciseValues4() { 205 | $a = new Decimal2('7.455'); 206 | $this->assertEquals('7.46', strval($a)); 207 | } 208 | 209 | public function testHandlesGracefullyTooPreciseValues5() { 210 | $a = new Decimal2('7.450'); 211 | $this->assertEquals('7.45', strval($a)); 212 | } 213 | 214 | public function testHandlesGracefullyTooPreciseValues6() { 215 | $a = new Decimal2('7.451'); 216 | $this->assertEquals('7.45', strval($a)); 217 | } 218 | 219 | public function testHandlesGracefullyTooPreciseValues7() { 220 | $a = new Decimal2('7.454'); 221 | $this->assertEquals('7.45', strval($a)); 222 | } 223 | 224 | //-------------------------------------------------------------------------------------------------- 225 | 226 | public function testCalculatesPercents1() { 227 | $d = new Decimal2('100'); 228 | $p = new Decimal2('7.45'); 229 | $this->assertEquals('7.45', strval(Decimal2::getPercentsOf($d, $p))); 230 | } 231 | 232 | public function testCalculatesPercents2() { 233 | $d = new Decimal2('100'); 234 | $p = new Decimal2('7.60'); 235 | $this->assertEquals('7.60', strval(Decimal2::getPercentsOf($d, $p))); 236 | } 237 | 238 | public function testCalculatesPercents3() { 239 | $d = new Decimal2('100'); 240 | $p = new Decimal2('0.01'); 241 | $this->assertEquals('0.01', strval(Decimal2::getPercentsOf($d, $p))); 242 | } 243 | 244 | public function testCalculatesPercents4() { 245 | $d = new Decimal2('100'); 246 | $p = new Decimal2('110'); 247 | $this->assertEquals('110.00', strval(Decimal2::getPercentsOf($d, $p))); 248 | } 249 | 250 | public function testCalculatesPercents5() { 251 | $d = new Decimal2('-200'); 252 | $p = new Decimal2('3.25'); 253 | $this->assertEquals('-6.50', strval(Decimal2::getPercentsOf($d, $p))); 254 | } 255 | 256 | public function testCalculatesPercents6() { 257 | $d = new Decimal2('0.50'); 258 | $p = new Decimal2('33.00'); 259 | $this->assertEquals('0.17', strval(Decimal2::getPercentsOf($d, $p))); 260 | } 261 | 262 | //-------------------------------------------------------------------------------------------------- 263 | 264 | public function testMultipliesTwoDecimals1() { 265 | $a = new Decimal2('-2'); 266 | $b = new Decimal2('2'); 267 | $this->assertEquals('-4.00', strval(Decimal2::mul($a, $b))); 268 | } 269 | 270 | public function testMultipliesTwoDecimals2() { 271 | $a = new Decimal2('24.0'); 272 | $b = new Decimal2('0.25'); 273 | $this->assertEquals('6.00', strval(Decimal2::mul($a, $b))); 274 | } 275 | 276 | //-------------------------------------------------------------------------------------------------- 277 | 278 | public function testDividesTwoDecimals1() { 279 | $a = new Decimal2('2'); 280 | $b = new Decimal2('2'); 281 | $this->assertEquals('1.00', strval(Decimal2::div($a, $b))); 282 | } 283 | 284 | public function testDividesTwoDecimals2() { 285 | $a = new Decimal2('-1'); 286 | $b = new Decimal2('4'); 287 | $this->assertEquals('-0.25', strval(Decimal2::div($a, $b))); 288 | } 289 | 290 | public function testDividesTwoDecimals3() { 291 | $a = new Decimal2('-399'); 292 | $b = new Decimal2('-1.2'); 293 | $this->assertEquals('332.50', strval(Decimal2::div($a, $b))); 294 | } 295 | 296 | public function testDividesTwoDecimals4() { 297 | $a = new Decimal2('140.10'); 298 | $b = new Decimal2('1.55'); 299 | $this->assertEquals('90.39', strval(Decimal2::div($a, $b))); 300 | } 301 | 302 | public function testDividesTwoDecimals5() { 303 | $a = new Decimal2('210'); 304 | $b = new Decimal2('1.55'); 305 | $this->assertEquals('135.48', strval(Decimal2::div($a, $b))); 306 | } 307 | 308 | public function testDividesTwoDecimals6() { 309 | $a = new Decimal2('45.99'); 310 | $b = new Decimal2('-1'); 311 | $this->assertEquals('-45.99', strval(Decimal2::div($a, $b))); 312 | } 313 | 314 | public function testDividesTwoDecimals7() { 315 | $a = new Decimal2('0'); 316 | $b = new Decimal2('-1'); 317 | $this->assertEquals('0.00', strval(Decimal2::div($a, $b))); 318 | } 319 | 320 | public function testDividesTwoDecimals8() { 321 | $a = new Decimal2('2'); 322 | $b = new Decimal2('3'); 323 | $this->assertEquals('0.67', strval(Decimal2::div($a, $b))); 324 | } 325 | 326 | public function testDividesTwoDecimals9() { 327 | $a = new Decimal2('0.02'); 328 | $b = new Decimal2('0.03'); 329 | $this->assertEquals('0.67', strval(Decimal2::div($a, $b))); 330 | } 331 | 332 | //-------------------------------------------------------------------------------------------------- 333 | 334 | public function testCalculatesDifference1() { 335 | $a = new Decimal2('700000000000000000000'); 336 | $b = new Decimal2('700000000000000000000'); 337 | $this->assertEquals('0.00', strval(Decimal2::minus($a, $b))); 338 | } 339 | 340 | public function testCalculatesDifference2() { 341 | $a = new Decimal2('-10'); 342 | $b = new Decimal2('5'); 343 | $this->assertEquals('-15.00', strval(Decimal2::minus($a, $b))); 344 | } 345 | 346 | //-------------------------------------------------------------------------------------------------- 347 | 348 | public function testAddsPercentsToAValue1() { 349 | $a = new Decimal2('377.80'); 350 | $p = Decimal2::getPercentsOf($a, new Decimal2('1.00')); 351 | 352 | $this->assertEquals('3.78', strval($p)); 353 | } 354 | 355 | //-------------------------------------------------------------------------------------------------- 356 | 357 | public function testSumsDecimalsInEmptyArray() { 358 | $this->assertEquals( 359 | '0.00', 360 | strval( 361 | Decimal2::sum([]) 362 | ) 363 | ); 364 | } 365 | 366 | public function testSumsDecimalsInArray1() { 367 | $this->assertEquals( 368 | '3.00', 369 | strval( 370 | Decimal2::sum([ 371 | new Decimal2('1'), 372 | new Decimal2('1'), 373 | new Decimal2('2'), 374 | new Decimal2('-1') 375 | ]) 376 | ) 377 | ); 378 | } 379 | 380 | //-------------------------------------------------------------------------------------------------- 381 | 382 | public function testAveragesDecimalsInEmptyArray() { 383 | $this->assertFalse(Decimal2::avg([])); 384 | } 385 | 386 | public function testAveragesDecimalsInOneElementArray() { 387 | $this->assertEquals( 388 | '100000000000.99', 389 | strval( 390 | Decimal2::avg([ 391 | new Decimal2('100000000000.99') 392 | ]) 393 | ) 394 | ); 395 | } 396 | 397 | public function testAveragesDecimalsInArray1() { 398 | $this->assertEquals( 399 | '1.00', 400 | strval( 401 | Decimal2::avg([ 402 | new Decimal2('1'), 403 | new Decimal2('1'), 404 | new Decimal2('1') 405 | ]) 406 | ) 407 | ); 408 | } 409 | 410 | public function testAveragesDecimalsInArray2() { 411 | $this->assertEquals( 412 | '1.50', 413 | strval( 414 | Decimal2::avg([ 415 | new Decimal2('1'), 416 | new Decimal2('2'), 417 | new Decimal2('1'), 418 | new Decimal2('2') 419 | ]) 420 | ) 421 | ); 422 | } 423 | 424 | public function testAveragesDecimalsInArray3() { 425 | $this->assertEquals( 426 | '0.33', 427 | strval( 428 | Decimal2::avg([ 429 | new Decimal2('-1'), 430 | new Decimal2('2'), 431 | new Decimal2('0') 432 | ]) 433 | ) 434 | ); 435 | } 436 | 437 | //-------------------------------------------------------------------------------------------------- 438 | 439 | public function testDecimalsCanBeCompared() { 440 | $this->assertEquals( 441 | 0, 442 | Decimal2::cmp(new Decimal2('-1'), new Decimal2('-1')) 443 | ); 444 | 445 | $this->assertEquals( 446 | 0, 447 | Decimal2::cmp(new Decimal2('111119898989898.23'), new Decimal2('111119898989898.23')) 448 | ); 449 | 450 | $this->assertGreaterThan( 451 | 0, 452 | Decimal2::cmp(new Decimal2('1.01'), new Decimal2('1')) 453 | ); 454 | 455 | $this->assertLessThan( 456 | 0, 457 | Decimal2::cmp(new Decimal2('-0'), new Decimal2('100')) 458 | ); 459 | } 460 | 461 | //-------------------------------------------------------------------------------------------------- 462 | 463 | public function testRoundsUpZeroTo5Cents() { 464 | $this->assertEquals('0.00', strval(Decimal2::roundUpTo5Cents(new Decimal2('0')))); 465 | } 466 | 467 | public function testRoundsUpTo5CentsDoesNothingToA5CentsRoundAmount1() { 468 | $this->assertEquals( 469 | '1.05', strval(Decimal2::roundUpTo5Cents(new Decimal2('1.05')))); 470 | } 471 | 472 | public function testRoundsUpTo5CentsDoesNothingToA5CentsRoundAmount2() { 473 | $this->assertEquals( 474 | '1.10', strval(Decimal2::roundUpTo5Cents(new Decimal2('1.10')))); 475 | } 476 | 477 | public function testRoundsUpTo5Cents1() { 478 | $this->assertEquals( 479 | '0.05', strval(Decimal2::roundUpTo5Cents(new Decimal2('0.02')))); 480 | } 481 | 482 | public function testRoundsUpTo5Cents2() { 483 | $this->assertEquals( 484 | '0.10', strval(Decimal2::roundUpTo5Cents(new Decimal2('0.06')))); 485 | } 486 | 487 | //-------------------------------------------------------------------------------------------------- 488 | 489 | /** 490 | * @param Decimal2 $d 491 | * @param integer $int 492 | * @param integer $fraction 493 | * @param string $strRep 494 | */ 495 | private function assertD($d, $int, $fraction, $strRep) { 496 | $this->assertEquals($int, $d->integerValue()); 497 | $this->assertEquals($fraction, $d->fractionValue()); 498 | $this->assertEquals($strRep, strval($d)); 499 | } 500 | } 501 | --------------------------------------------------------------------------------