currency bcmath
This commit is contained in:
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user