currency bcmath

This commit is contained in:
Yuri Kuznetsov
2022-04-10 13:13:23 +03:00
parent c1e4ca4850
commit 68fa506f32
5 changed files with 231 additions and 59 deletions
@@ -0,0 +1,122 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2022 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Currency;
use DivisionByZeroError;
class CalculatorUtil
{
private const SCALE = 14;
public static function add(string $arg1, string $arg2): string
{
if (!function_exists('bcadd')) {
return (string) (
(float) $arg1 + (float) $arg2
);
}
return bcadd(
$arg1,
$arg2,
self::SCALE
);
}
public static function subtract(string $arg1, string $arg2): string
{
if (!function_exists('bcsub')) {
return (string) (
(float) $arg1 - (float) $arg2
);
}
return bcsub(
$arg1,
$arg2,
self::SCALE
);
}
public static function multiply(string $arg1, string $arg2): string
{
if (!function_exists('bcmul')) {
return (string) (
(float) $arg1 * (float) $arg2
);
}
return bcmul(
$arg1,
$arg2,
self::SCALE
);
}
public static function divide(string $arg1, string $arg2): string
{
if (!function_exists('bcdiv')) {
return (string) (
(float) $arg1 / (float) $arg2
);
}
$result = bcdiv(
$arg1,
$arg2,
self::SCALE
);
if ($result === null) {
throw new DivisionByZeroError();
}
return $result;
}
public static function round(string $arg, int $precision = 0): string
{
if (!function_exists('bcadd')) {
return (string) round((float) $arg, $precision);
}
$addition = '0.' . str_repeat('0', $precision) . '5';
if ($arg[0] === '-') {
$addition = '-' . $addition;
}
return bcadd(
$arg,
$addition,
$precision
);
}
}
+32 -34
View File
@@ -52,21 +52,42 @@ class Converter
*/
public function convert(Currency $value, string $targetCurrencyCode): Currency
{
$amount = $value->getAmount();
if (!$this->configDataProvider->hasCurrency($targetCurrencyCode)) {
throw new RuntimeException("Can't convert currency to unknown currency '{$targetCurrencyCode}.");
}
$rate = $this->configDataProvider->getCurrencyRate($value->getCode());
$targetRate = $this->configDataProvider->getCurrencyRate($targetCurrencyCode);
$amount *= $rate;
$convertedAmount = $this->convertAmount($value->getAmountAsString(), $rate, $targetRate);
$amount /= $targetRate;
return new Currency($convertedAmount, $targetCurrencyCode);
}
return new Currency($amount, $targetCurrencyCode);
/**
* Convert a currency value to a specific currency with specific rates.
* Base currency should has rate equal to `1.0`.
*
* @throws RuntimeException
*/
public function convertWithRates(Currency $value, string $targetCurrencyCode, Rates $rates): Currency
{
$currencyCode = $value->getCode();
if (!$rates->hasRate($currencyCode)) {
throw new RuntimeException("No rate for the currency '{$currencyCode}.");
}
if (!$rates->hasRate($targetCurrencyCode)) {
throw new RuntimeException("No rate for the currency '{$targetCurrencyCode}.");
}
$rate = $rates->getRate($currencyCode);
$targetRate = $rates->getRate($targetCurrencyCode);
$convertedAmount = $this->convertAmount($value->getAmountAsString(), $rate, $targetRate);
return new Currency($convertedAmount, $targetCurrencyCode);
}
/**
@@ -79,34 +100,11 @@ class Converter
return $this->convert($value, $targetCurrencyCode);
}
/**
* Convert a currency value to a specific currency with specific rates.
* Base currency should has rate equal to `1.0`.
*
* @throws RuntimeException
*/
public function convertWithRates(Currency $value, string $targetCurrencyCode, Rates $rates): Currency
private function convertAmount(string $amount, float $rate, float $targetRate): string
{
$amount = $value->getAmount();
$currencyCode = $value->getCode();
if (!$rates->hasRate($currencyCode)) {
throw new RuntimeException("No rate for the currency '{$currencyCode}.");
}
if (!$rates->hasRate($targetCurrencyCode)) {
throw new RuntimeException("No rate for the currency '{$targetCurrencyCode}.");
}
$rate = $rates->getRate($currencyCode);
$targetRate = $rates->getRate($targetCurrencyCode);
$amount *= $rate;
$amount /= $targetRate;
return new Currency($amount, $targetCurrencyCode);
return CalculatorUtil::divide(
CalculatorUtil::multiply($amount, (string) $rate),
(string) $targetRate
);
}
}
+48 -18
View File
@@ -29,33 +29,56 @@
namespace Espo\Core\Field;
use Espo\Core\Currency\CalculatorUtil;
use RuntimeException;
use InvalidArgumentException;
/**
* A currency value object. Immutable.
*/
class Currency
{
private $amount;
private string $amount;
private $code;
private string $code;
public function __construct(float $amount, string $code)
/**
* @param string|float $amount
* @throws RuntimeException
*/
public function __construct($amount, string $code)
{
if (!is_string($amount) && !is_float($amount)) {
throw new InvalidArgumentException();
}
if (strlen($code) !== 3) {
throw new RuntimeException("Bad currency code.");
}
if (is_float($amount)) {
$amount = (string) $amount;
}
$this->amount = $amount;
$this->code = $code;
}
/**
* Get an amount as string.
*/
public function getAmountAsString(): string
{
return $this->amount;
}
/**
* Get an amount.
*/
public function getAmount(): float
{
return $this->amount;
return (float) $this->amount;
}
/**
@@ -71,13 +94,14 @@ class Currency
*/
public function add(self $value): self
{
$amount = $this->getAmount();
if ($this->getCode() !== $value->getCode()) {
throw new RuntimeException("Can't add a currency value with a different code.");
}
$amount += $value->getAmount();
$amount = CalculatorUtil::add(
$this->getAmountAsString(),
$value->getAmountAsString()
);
return new self($amount, $this->getCode());
}
@@ -87,13 +111,14 @@ class Currency
*/
public function subtract(self $value): self
{
$amount = $this->getAmount();
if ($this->getCode() !== $value->getCode()) {
throw new RuntimeException("Can't substract a currency value with a different code.");
}
$amount -= $value->getAmount();
$amount = CalculatorUtil::subtract(
$this->getAmountAsString(),
$value->getAmountAsString()
);
return new self($amount, $this->getCode());
}
@@ -103,9 +128,10 @@ class Currency
*/
public function multiply(float $multiplier): self
{
$amount = $this->getAmount();
$amount *= $multiplier;
$amount = CalculatorUtil::multiply(
$this->getAmountAsString(),
(string) $multiplier
);
return new self($amount, $this->getCode());
}
@@ -115,9 +141,10 @@ class Currency
*/
public function divide(float $divider): self
{
$amount = $this->getAmount();
$amount /= $divider;
$amount = CalculatorUtil::divide(
$this->getAmountAsString(),
(string) $divider
);
return new self($amount, $this->getCode());
}
@@ -127,15 +154,18 @@ class Currency
*/
public function round(int $precision = 0): self
{
$amount = round($this->getAmount(), $precision);
$amount = CalculatorUtil::round($this->getAmountAsString(), $precision);
return new self($amount, $this->getCode());
}
/**
* Create from an amount and code.
*
* @param string|float $amount
* @throws RuntimeException
*/
public static function create(float $amount, string $code): self
public static function create($amount, string $code): self
{
return new self($amount, $code);
}
@@ -38,11 +38,6 @@ use Espo\Core\{
class CurrencyConverterTest extends \PHPUnit\Framework\TestCase
{
protected function setUp() : void
{
}
public function testConvert1()
{
$currencyConfigDataProvider = $this->createMock(CurrencyConfigDataProvider::class);
@@ -95,15 +95,42 @@ class CurrencyTest extends \PHPUnit\Framework\TestCase
$this->assertEquals('USD', $value->getCode());
}
public function testRound()
public function testRound1()
{
$value = (new Currency(2.306, 'USD'))->round(2);
$this->assertEquals(2.31, $value->getAmount());
$this->assertEquals('USD', $value->getCode());
}
public function testRound2()
{
$value = (new Currency(2.306, 'USD'))->round(4);
$this->assertEquals(2.306, $value->getAmount());
}
public function testRound3()
{
$value = (new Currency(2.306, 'USD'))->round(0);
$this->assertEquals(2, $value->getAmount());
}
public function testRound4()
{
$value = (new Currency(-2.306, 'USD'))->round(2);
$this->assertEquals(-2.31, $value->getAmount());
}
public function testRound5()
{
$value = (new Currency(-2.5, 'USD'))->round(0);
$this->assertEquals(-3, $value->getAmount());
}
public function testBadAdd()
{
$this->expectException(RuntimeException::class);