Download project

This commit is contained in:
Roman Pyrih
2024-11-20 09:09:44 +01:00
parent 547a138d6a
commit 5ff041757f
40737 changed files with 7766183 additions and 0 deletions

View File

@@ -0,0 +1,35 @@
# IntelliJ - PhpStorm and PyCharm
.idea
*.iml
*.ipr
*.iws
# Netbeans
nbproject
.nbproject
.nbproject/*
nbproject/*
nbproject/private/
build/
nbbuild/
dist/
nbdist/
nbactions.xml
nb-configuration.xml
# Mac OSX
.DS_Store
# Thumbnails
._*
# Files that might appear on external disk
.Spotlight-V100
.Trashes
# SublimeText project files
/*.sublime-project
*.sublime-workspace
build
composer.lock
docs
vendor

View File

@@ -0,0 +1,21 @@
dist: trusty
language: php
php:
- 5.4
- 5.5
- 5.6
- 7.0
- 7.1
- 7.2
- 7.3
before_script:
- travis_retry composer install --no-interaction
script:
- vendor/bin/phpunit --coverage-text
after_script:
- php vendor/bin/codacycoverage clover build/clover.xml

View File

@@ -0,0 +1,35 @@
# Contributing
Contributions are **welcome** and will be fully **credited**.
We accept contributions via Pull Requests on [Github](https://github.com/prestashop/decimal).
## Compatibility
Decimal is compatible with PHP >= 5.4. Please don't break backwards compatibility :)
## Pull Requests
- **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](http://pear.php.net/package/PHP_CodeSniffer).
- **Add tests!** - Your patch won't be accepted if it doesn't have tests.
- **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date.
- **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option.
- **Create feature branches** - Don't ask us to pull from your master branch.
- **One pull request per feature** - If you want to do more than one thing, send multiple pull requests.
- **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please squash them before submitting.
## Running Tests
``` bash
$ vendor/bin/phpunit
```
**Happy coding**!

View File

@@ -0,0 +1,21 @@
# The MIT License (MIT)
Copyright (c) 2017 PrestaShop
> 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.

View File

@@ -0,0 +1,288 @@
# Decimal
[![Build Status](https://api.travis-ci.org/PrestaShop/decimal.svg?branch=master)](https://travis-ci.org/PrestaShop/decimal)
[![Codacy Badge](https://api.codacy.com/project/badge/Grade/985899efeb83453babcc507def66c90e)](https://www.codacy.com/app/PrestaShop/decimal?utm_source=github.com&utm_medium=referral&utm_content=PrestaShop/decimal&utm_campaign=Badge_Grade)
[![Codacy Badge](https://api.codacy.com/project/badge/Coverage/985899efeb83453babcc507def66c90e)](https://www.codacy.com/app/PrestaShop/decimal?utm_source=github.com&utm_medium=referral&utm_content=PrestaShop/decimal&utm_campaign=Badge_Coverage)
[![Total Downloads](https://img.shields.io/packagist/dt/prestashop/decimal.svg?style=flat-square)](https://packagist.org/packages/prestashop/decimal)
An object-oriented [BC Math extension](http://php.net/manual/en/book.bc.php) wrapper/shim.
**Decimal** offers a stateless, fluent object-oriented implementation of basic arbitrary-precision arithmetic, using BC Math if available.
You can find out more about floating point precision [here](http://php.net/float).
Example:
```php
use PrestaShop\Decimal\DecimalNumber;
use PrestaShop\Decimal\Operation\Rounding;
echo (new DecimalNumber('0.1'))
->plus(new DecimalNumber('0.7'))
->times(new DecimalNumber('10'))
->round(0, Rounding::ROUND_FLOOR)
// echoes '8'
```
## Install
Via Composer
``` bash
$ composer require prestashop/decimal
```
## Usage reference
Quick links:
- [Instantiation](#instantiation)
- [Addition](#addition)
- [Subtraction](#subtraction)
- [Multiplication](#multiplication)
- [Division](#division)
- [Comparison](#comparison)
- [Fixed precision](#fixed-precision)
- [Rounding](#rounding)
- [Dot shifting](#dot-shifting)
- [Useful methods](#useful-methods)
### Instantiation
Creates a new Decimal number.
```php
public __construct ( string $number [, int $exponent = null ] ): DecimalNumber
```
There are two ways to instantiate a Decimal\DecimalNumber:
``` php
// create a number from string
$number = new PrestaShop\Decimal\DecimalNumber('123.456');
echo $number; // echoes '123.456'
```
``` php
// exponent notation
$number = new PrestaShop\Decimal\DecimalNumber('123456', -3);
echo $number; // echoes '123.456'
```
### Addition
Returns the computed result of adding another number to the current one.
```php
public DecimalNumber::plus ( DecimalNumber $addend ): DecimalNumber
```
Examples:
```php
$a = new PrestaShop\Decimal\DecimalNumber('123.456');
$b = new PrestaShop\Decimal\DecimalNumber('654.321');
echo $a->plus($b); // echoes '777.777'
```
### Subtraction
Returns the computed result of subtracting another number to the current one.
```php
public DecimalNumber::minus ( DecimalNumber $subtrahend ): DecimalNumber
```
Examples:
```php
$a = new PrestaShop\Decimal\DecimalNumber('777.777');
$b = new PrestaShop\Decimal\DecimalNumber('654.321');
echo $a->minus($b); // echoes '123.456'
```
### Multiplication
Returns the computed result of multiplying the current number with another one.
```php
public DecimalNumber::times ( DecimalNumber $factor ): DecimalNumber
```
Examples:
```php
$a = new PrestaShop\Decimal\DecimalNumber('777.777');
$b = new PrestaShop\Decimal\DecimalNumber('654.321');
echo $a->times($b); // echoes '508915.824417'
```
### Division
Returns the computed result of dividing the current number by another one, with up to a certain number of decimal positions (6 by default).
```php
public DecimalNumber::dividedBy ( DecimalNumber $divisor [, int $precision = Operation\Division::DEFAULT_PRECISION ] )
```
Examples:
```php
$a = new PrestaShop\Decimal\DecimalNumber('777.777');
$b = new PrestaShop\Decimal\DecimalNumber('654.321');
echo $a->dividedBy($b, 0); // echoes '1'
echo $a->dividedBy($b, 5); // echoes '1.18867'
echo $a->dividedBy($b, 10); // echoes '1.1886780341'
echo $a->dividedBy($b, 15); // echoes '1.188678034175886'
```
### Comparison
Returns the result of the comparison assertion.
```php
$a = new PrestaShop\Decimal\DecimalNumber('777.777');
$b = new PrestaShop\Decimal\DecimalNumber('654.321');
$a->equals($b); // returns false
$a->isLowerThan($b); // returns false
$a->isLowerOrEqualThan($b); // returns false
$a->isGreaterThan($b); // returns true
$a->isGreaterOrEqualThan($b); // returns true
// shortcut methods
$a->equalsZero(); // returns false
$a->isLowerThanZero(); // returns false
$a->isLowerOrEqualThanZero(); // returns false
$a->isGreaterThanZero(); // returns true
$a->isGreaterOrEqualThanZero(); // returns true
```
### Fixed precision
Returns the number as a string, optionally rounded, with an exact number of decimal positions.
```php
public DecimalNumber::toPrecision ( int $precision [, string $roundingMode = Rounding::ROUND_TRUNCATE ] ): string
```
Examples:
```php
$a = new PrestaShop\Decimal\DecimalNumber('123.456');
$a = new PrestaShop\Decimal\DecimalNumber('-123.456');
// truncate / pad
$a->toPrecision(0); // '123'
$a->toPrecision(1); // '123.4'
$a->toPrecision(2); // '123.45'
$a->toPrecision(3); // '123.456'
$a->toPrecision(4); // '123.4560'
$b->toPrecision(0); // '-123'
$b->toPrecision(1); // '-123.4'
$b->toPrecision(2); // '-123.45'
$b->toPrecision(3); // '-123.456'
$b->toPrecision(4); // '-123.4560'
// ceil (round up)
$a->toPrecision(0, PrestaShop\Decimal\Operation\Rounding::ROUND_CEIL); // '124'
$a->toPrecision(1, PrestaShop\Decimal\Operation\Rounding::ROUND_CEIL); // '123.5'
$a->toPrecision(2, PrestaShop\Decimal\Operation\Rounding::ROUND_CEIL); // '123.46'
$b->toPrecision(0, PrestaShop\Decimal\Operation\Rounding::ROUND_CEIL); // '-122'
$b->toPrecision(1, PrestaShop\Decimal\Operation\Rounding::ROUND_CEIL); // '-123.3'
$b->toPrecision(2, PrestaShop\Decimal\Operation\Rounding::ROUND_CEIL); // '-123.44'
// floor (round down)
$a->toPrecision(0, PrestaShop\Decimal\Operation\Rounding::ROUND_FLOOR); // '123'
$a->toPrecision(1, PrestaShop\Decimal\Operation\Rounding::ROUND_FLOOR); // '123.4'
$a->toPrecision(2, PrestaShop\Decimal\Operation\Rounding::ROUND_FLOOR); // '123.45'
$b->toPrecision(0, PrestaShop\Decimal\Operation\Rounding::ROUND_FLOOR); // '-124'
$b->toPrecision(1, PrestaShop\Decimal\Operation\Rounding::ROUND_FLOOR); // '-123.5'
$b->toPrecision(2, PrestaShop\Decimal\Operation\Rounding::ROUND_FLOOR); // '-123.46'
// half-up (symmetric half-up)
$a->toPrecision(0, PrestaShop\Decimal\Operation\Rounding::ROUND_HALF_UP); // '123'
$a->toPrecision(1, PrestaShop\Decimal\Operation\Rounding::ROUND_HALF_UP); // '123.5'
$a->toPrecision(2, PrestaShop\Decimal\Operation\Rounding::ROUND_HALF_UP); // '123.46'
$b->toPrecision(0, PrestaShop\Decimal\Operation\Rounding::ROUND_HALF_UP); // '-123'
$b->toPrecision(1, PrestaShop\Decimal\Operation\Rounding::ROUND_HALF_UP); // '-123.5'
$b->toPrecision(2, PrestaShop\Decimal\Operation\Rounding::ROUND_HALF_UP); // '-123.46'
// half-down (symmetric half-down)
$a->toPrecision(0, PrestaShop\Decimal\Operation\Rounding::ROUND_HALF_DOWN); // '123'
$a->toPrecision(1, PrestaShop\Decimal\Operation\Rounding::ROUND_HALF_DOWN); // '123.4'
$a->toPrecision(2, PrestaShop\Decimal\Operation\Rounding::ROUND_HALF_DOWN); // '123.46'
$a->toPrecision(0, PrestaShop\Decimal\Operation\Rounding::ROUND_HALF_DOWN); // '-123'
$a->toPrecision(1, PrestaShop\Decimal\Operation\Rounding::ROUND_HALF_DOWN); // '-123.4'
$a->toPrecision(2, PrestaShop\Decimal\Operation\Rounding::ROUND_HALF_DOWN); // '-123.46'
// half-even
$a->toPrecision(0, PrestaShop\Decimal\Operation\Rounding::ROUND_HALF_EVEN); // '123'
$a->toPrecision(1, PrestaShop\Decimal\Operation\Rounding::ROUND_HALF_EVEN); // '123.4'
$a->toPrecision(2, PrestaShop\Decimal\Operation\Rounding::ROUND_HALF_EVEN); // '123.46'
$a->toPrecision(0, PrestaShop\Decimal\Operation\Rounding::ROUND_HALF_EVEN); // '-123'
$a->toPrecision(1, PrestaShop\Decimal\Operation\Rounding::ROUND_HALF_EVEN); // '-123.4'
$a->toPrecision(2, PrestaShop\Decimal\Operation\Rounding::ROUND_HALF_EVEN); // '-123.46'
$a = new PrestaShop\Decimal\DecimalNumber('1.1525354556575859505');
$a->toPrecision(0, PrestaShop\Decimal\Operation\Rounding::ROUND_HALF_EVEN); // '1'
$a->toPrecision(1, PrestaShop\Decimal\Operation\Rounding::ROUND_HALF_EVEN); // '1.2'
$a->toPrecision(2, PrestaShop\Decimal\Operation\Rounding::ROUND_HALF_EVEN); // '1.15'
$a->toPrecision(3, PrestaShop\Decimal\Operation\Rounding::ROUND_HALF_EVEN); // '1.152'
$a->toPrecision(4, PrestaShop\Decimal\Operation\Rounding::ROUND_HALF_EVEN); // '1.1525'
$a->toPrecision(5, PrestaShop\Decimal\Operation\Rounding::ROUND_HALF_EVEN); // '1.15255'
$a->toPrecision(6, PrestaShop\Decimal\Operation\Rounding::ROUND_HALF_EVEN); // '1.152535'
$a->toPrecision(7, PrestaShop\Decimal\Operation\Rounding::ROUND_HALF_EVEN); // '1.1525354'
$a->toPrecision(8, PrestaShop\Decimal\Operation\Rounding::ROUND_HALF_EVEN); // '1.15253546'
$a->toPrecision(9, PrestaShop\Decimal\Operation\Rounding::ROUND_HALF_EVEN); // '1.152535456'
$a->toPrecision(10, PrestaShop\Decimal\Operation\Rounding::ROUND_HALF_EVEN); // '1.1525354556'
```
### Rounding
Rounding behaves like `toPrecision`, but provides "up to" a certain number of decimal positions
(it does not add trailing zeroes).
```php
public DecimalNumber::round ( int $maxDecimals [, string $roundingMode = Rounding::ROUND_TRUNCATE ] ): string
```
Examples:
```php
$a = new PrestaShop\Decimal\DecimalNumber('123.456');
$a = new PrestaShop\Decimal\DecimalNumber('-123.456');
// truncate / pad
$a->round(0); // '123'
$a->round(1); // '123.4'
$a->round(2); // '123.45'
$a->round(3); // '123.456'
$a->round(4); // '123.456'
$b->round(0); // '-123'
$b->round(1); // '-123.4'
$b->round(2); // '-123.45'
$b->round(3); // '-123.456'
$b->round(4); // '-123.456'
```
### Dot shifting
Creates a new copy of this number multiplied by 10^exponent
```php
public DecimalNumber::toMagnitude ( int $exponent ): DecimalNumber
```
Examples:
```php
$a = new PrestaShop\Decimal\DecimalNumber('123.456789');
// shift 3 digits to the left
$a->toMagnitude(-3); // 0.123456789
// shift 3 digits to the right
$a->toMagnitude(3); // 123456.789
```
### Useful methods
```php
$number = new PrestaShop\Decimal\DecimalNumber('123.45');
$number->getIntegerPart(); // '123'
$number->getFractionalPart(); // '45'
$number->getPrecision(); // '2' (number of decimals)
$number->getSign(); // '' ('-' if the number was negative)
$number->getExponent(); // '2' (always positive)
$number->getCoefficient(); // '123456'
$number->isPositive(); // true
$number->isNegative(); // false
$number->invert(); // new Decimal\DecimalNumber('-123.45')
```
## Testing
``` bash
$ composer install
$ vendor/bin/phpunit
```
## Contributing
Please see [CONTRIBUTING](CONTRIBUTING.md) for details.
## Credits
- [All Contributors](https://github.com/prestashop/decimal/contributors)
## License
The MIT License (MIT). Please see [License File](LICENSE.md) for more information.

View File

@@ -0,0 +1,40 @@
{
"name": "prestashop/decimal",
"description": "Object-oriented wrapper/shim for BC Math PHP extension. Allows for arbitrary-precision math operations.",
"type": "library",
"keywords": [
"decimal",
"math",
"precision",
"bcmath",
"prestashop"
],
"homepage": "https://github.com/prestashop/decimal",
"license": "MIT",
"authors": [
{
"name": "PrestaShop SA",
"email": "contact@prestashop.com"
},{
"name": "Pablo Borowicz",
"email": "pablo.borowicz@prestashop.com"
}
],
"require": {
"php" : ">=5.4"
},
"require-dev": {
"phpunit/phpunit" : "4.*",
"codacy/coverage": "dev-master"
},
"autoload": {
"psr-4": {
"PrestaShop\\Decimal\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"PrestaShop\\Decimal\\Test\\": "tests"
}
}
}

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="vendor/autoload.php"
backupGlobals="false"
backupStaticAttributes="false"
colors="true"
verbose="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false">
<testsuites>
<testsuite name="Decimal Test Suite">
<directory>tests</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory suffix=".php">src/</directory>
</whitelist>
</filter>
<logging>
<log type="tap" target="build/report.tap"/>
<log type="junit" target="build/report.junit.xml"/>
<log type="coverage-html" target="build/coverage" charset="UTF-8" yui="true" highlight="true"/>
<log type="coverage-text" target="build/coverage.txt"/>
<log type="coverage-clover" target="build/clover.xml"/>
</logging>
</phpunit>

View File

@@ -0,0 +1,101 @@
<?php
/**
* This file is part of the PrestaShop\Decimal package
*
* @author PrestaShop SA <contact@prestashop.com>
* @license https://opensource.org/licenses/MIT MIT License
*/
namespace PrestaShop\Decimal;
use PrestaShop\Decimal\DecimalNumber;
/**
* Builds Number instances
*/
class Builder
{
/**
* Pattern for most numbers
*/
const NUMBER_PATTERN = "/^(?<sign>[-+])?(?<integerPart>\d+)?(?:\.(?<fractionalPart>\d+)(?<exponentPart>[eE](?<exponentSign>[-+])(?<exponent>\d+))?)?$/";
/**
* Pattern for integer numbers in scientific notation (rare but supported by spec)
*/
const INT_EXPONENTIAL_PATTERN = "/^(?<sign>[-+])?(?<integerPart>\d+)(?<exponentPart>[eE](?<exponentSign>[-+])(?<exponent>\d+))$/";
/**
* Builds a Number from a string
*
* @param string $number
*
* @return DecimalNumber
*/
public static function parseNumber($number)
{
if (!self::itLooksLikeANumber($number, $numberParts)) {
throw new \InvalidArgumentException(
sprintf('"%s" cannot be interpreted as a number', print_r($number, true))
);
}
$integerPart = '';
if (array_key_exists('integerPart', $numberParts)) {
// extract the integer part and remove leading zeroes
$integerPart = ltrim($numberParts['integerPart'], '0');
}
$fractionalPart = '';
if (array_key_exists('fractionalPart', $numberParts)) {
// extract the fractional part and remove trailing zeroes
$fractionalPart = rtrim($numberParts['fractionalPart'], '0');
}
$fractionalDigits = strlen($fractionalPart);
$coefficient = $integerPart . $fractionalPart;
// when coefficient is '0' or a sequence of '0'
if ('' === $coefficient) {
$coefficient = '0';
}
// when the number has been provided in scientific notation
if (array_key_exists('exponentPart', $numberParts)) {
$givenExponent = (int) ($numberParts['exponentSign'] . $numberParts['exponent']);
// we simply add or subtract fractional digits from the given exponent (depending if it's positive or negative)
$fractionalDigits -= $givenExponent;
if ($fractionalDigits < 0) {
// if the resulting fractional digits is negative, it means there is no fractional part anymore
// we need to add trailing zeroes as needed
$coefficient = str_pad($coefficient, strlen($coefficient) - $fractionalDigits, '0');
// there's no fractional part anymore
$fractionalDigits = 0;
}
}
return new DecimalNumber($numberParts['sign'] . $coefficient, $fractionalDigits);
}
/**
* @param string $number
* @param array $numberParts
*
* @return bool
*/
private static function itLooksLikeANumber($number, &$numberParts)
{
return (
strlen((string) $number) > 0
&& (
preg_match(self::NUMBER_PATTERN, $number, $numberParts)
|| preg_match(self::INT_EXPONENTIAL_PATTERN, $number, $numberParts)
)
);
}
}

View File

@@ -0,0 +1,574 @@
<?php
/**
* This file is part of the PrestaShop\Decimal package
*
* @author PrestaShop SA <contact@prestashop.com>
* @license https://opensource.org/licenses/MIT MIT License
*/
namespace PrestaShop\Decimal;
use InvalidArgumentException;
use PrestaShop\Decimal\Operation\Rounding;
/**
* Decimal number.
*
* Allows for arbitrary precision math operations.
*/
class DecimalNumber
{
/**
* Indicates if the number is negative
* @var bool
*/
private $isNegative = false;
/**
* Integer representation of this number
* @var string
*/
private $coefficient = '';
/**
* Scientific notation exponent. For practical reasons, it's always stored as a positive value.
* @var int
*/
private $exponent = 0;
/**
* Number constructor.
*
* This constructor can be used in two ways:
*
* 1) With a number string:
*
* ```php
* (string) new Number('0.123456'); // -> '0.123456'
* ```
*
* 2) With an integer string as coefficient and an exponent
*
* ```php
* // 123456 * 10^(-6)
* (string) new Number('123456', 6); // -> '0.123456'
* ```
*
* Note: decimal positions must always be a positive number.
*
* @param string $number Number or coefficient
* @param int|null $exponent [default=null] If provided, the number can be considered as the negative
* exponent of the scientific notation, or the number of fractional digits.
*/
public function __construct($number, $exponent = null)
{
if (!is_string($number)) {
throw new InvalidArgumentException(
sprintf('Invalid type - expected string, but got (%s) "%s"', gettype($number), print_r($number, true))
);
}
if (null === $exponent) {
$decimalNumber = Builder::parseNumber($number);
$number = $decimalNumber->getSign() . $decimalNumber->getCoefficient();
$exponent = $decimalNumber->getExponent();
}
$this->initFromScientificNotation($number, $exponent);
if ('0' === $this->coefficient) {
// make sure the sign is always positive for zero
$this->isNegative = false;
}
}
/**
* Returns the integer part of the number.
* Note that this does NOT include the sign.
*
* @return string
*/
public function getIntegerPart()
{
if ('0' === $this->coefficient) {
return $this->coefficient;
}
if (0 === $this->exponent) {
return $this->coefficient;
}
if ($this->exponent >= strlen($this->coefficient)) {
return '0';
}
return substr($this->coefficient, 0, -$this->exponent);
}
/**
* Returns the fractional part of the number.
* Note that this does NOT include the sign.
*
* @return string
*/
public function getFractionalPart()
{
if (0 === $this->exponent || '0' === $this->coefficient) {
return '0';
}
if ($this->exponent > strlen($this->coefficient)) {
return str_pad($this->coefficient, $this->exponent, '0', STR_PAD_LEFT);
}
return substr($this->coefficient, -$this->exponent);
}
/**
* Returns the number of digits in the fractional part.
*
* @see self::getExponent() This method is an alias of getExponent().
*
* @return int
*/
public function getPrecision()
{
return $this->getExponent();
}
/**
* Returns the number's sign.
* Note that this method will return an empty string if the number is positive!
*
* @return string '-' if negative, empty string if positive
*/
public function getSign()
{
return $this->isNegative ? '-' : '';
}
/**
* Returns the exponent of this number. For practical reasons, this exponent is always >= 0.
*
* This value can also be interpreted as the number of significant digits on the fractional part.
*
* @return int
*/
public function getExponent()
{
return $this->exponent;
}
/**
* Returns the raw number as stored internally. This coefficient is always an integer.
*
* It can be transformed to float by computing:
* ```
* getCoefficient() * 10^(-getExponent())
* ```
*
* @return string
*/
public function getCoefficient()
{
return $this->coefficient;
}
/**
* Returns a string representation of this object
*
* @return string
*/
public function __toString()
{
$output = $this->getSign() . $this->getIntegerPart();
$fractionalPart = $this->getFractionalPart();
if ('0' !== $fractionalPart) {
$output .= '.' . $fractionalPart;
}
return $output;
}
/**
* Returns the number as a string, with exactly $precision decimals
*
* Example:
* ```
* $n = new Number('123.4560');
* (string) $n->round(1); // '123.4'
* (string) $n->round(2); // '123.45'
* (string) $n->round(3); // '123.456'
* (string) $n->round(4); // '123.4560' (trailing zeroes are added)
* (string) $n->round(5); // '123.45600' (trailing zeroes are added)
* ```
*
* @param int $precision Exact number of desired decimals
* @param string $roundingMode [default=Rounding::ROUND_TRUNCATE] Rounding algorithm
*
* @return string
*/
public function toPrecision($precision, $roundingMode = Rounding::ROUND_TRUNCATE)
{
$currentPrecision = $this->getPrecision();
if ($precision === $currentPrecision) {
return (string) $this;
}
$return = $this;
if ($precision < $currentPrecision) {
$return = (new Operation\Rounding())->compute($this, $precision, $roundingMode);
}
if ($precision > $return->getPrecision()) {
return (
$return->getSign()
.$return->getIntegerPart()
.'.'
.str_pad($return->getFractionalPart(), $precision, '0')
);
}
return (string) $return;
}
/**
* Returns the number as a string, with up to $maxDecimals significant digits.
*
* Example:
* ```
* $n = new Number('123.4560');
* (string) $n->round(1); // '123.4'
* (string) $n->round(2); // '123.45'
* (string) $n->round(3); // '123.456'
* (string) $n->round(4); // '123.456' (does not add trailing zeroes)
* (string) $n->round(5); // '123.456' (does not add trailing zeroes)
* ```
*
* @param int $maxDecimals Maximum number of decimals
* @param string $roundingMode [default=Rounding::ROUND_TRUNCATE] Rounding algorithm
*
* @return string
*/
public function round($maxDecimals, $roundingMode = Rounding::ROUND_TRUNCATE)
{
$currentPrecision = $this->getPrecision();
if ($maxDecimals < $currentPrecision) {
return (string) (new Operation\Rounding())->compute($this, $maxDecimals, $roundingMode);
}
return (string) $this;
}
/**
* Returns this number as a positive number
*
* @return self
*/
public function toPositive()
{
if (!$this->isNegative) {
return $this;
}
return $this->invert();
}
/**
* Returns this number as a negative number
*
* @return self
*/
public function toNegative()
{
if ($this->isNegative) {
return $this;
}
return $this->invert();
}
/**
* Returns the computed result of adding another number to this one
*
* @param self $addend Number to add
*
* @return self
*/
public function plus(self $addend)
{
return (new Operation\Addition())->compute($this, $addend);
}
/**
* Returns the computed result of subtracting another number to this one
*
* @param self $subtrahend Number to subtract
*
* @return self
*/
public function minus(self $subtrahend)
{
return (new Operation\Subtraction())->compute($this, $subtrahend);
}
/**
* Returns the computed result of multiplying this number with another one
*
* @param self $factor
*
* @return self
*/
public function times(self $factor)
{
return (new Operation\Multiplication())->compute($this, $factor);
}
/**
* Returns the computed result of dividing this number by another one, with up to $precision number of decimals.
*
* A target maximum precision is required in order to handle potential infinite number of decimals
* (e.g. 1/3 = 0.3333333...).
*
* If the division yields more decimal positions than the requested precision,
* the remaining decimals are truncated, with **no rounding**.
*
* @param self $divisor
* @param int $precision [optional] By default, up to Operation\Division::DEFAULT_PRECISION number of decimals.
*
* @return self
*
* @throws Exception\DivisionByZeroException
*/
public function dividedBy(self $divisor, $precision = Operation\Division::DEFAULT_PRECISION)
{
return (new Operation\Division())->compute($this, $divisor, $precision);
}
/**
* Indicates if this number equals zero
*
* @return bool
*/
public function equalsZero()
{
return '0' == $this->getCoefficient();
}
/**
* Indicates if this number is greater than the provided one
*
* @param self $number
*
* @return bool
*/
public function isGreaterThan(self $number)
{
return (1 === (new Operation\Comparison())->compare($this, $number));
}
/**
* Indicates if this number is greater than zero
*
* @return bool
*/
public function isGreaterThanZero()
{
return $this->isPositive() && !$this->equalsZero();
}
/**
* Indicates if this number is greater or equal than zero
*
* @return bool
*/
public function isGreaterOrEqualThanZero()
{
return $this->isPositive();
}
/**
* Indicates if this number is greater or equal compared to the provided one
*
* @param self $number
*
* @return bool
*/
public function isGreaterOrEqualThan(self $number)
{
return (0 <= (new Operation\Comparison())->compare($this, $number));
}
/**
* Indicates if this number is lower than zero
*
* @return bool
*/
public function isLowerThanZero()
{
return $this->isNegative() && !$this->equalsZero();
}
/**
* Indicates if this number is lower or equal than zero
*
* @return bool
*/
public function isLowerOrEqualThanZero()
{
return $this->isNegative() || $this->equalsZero();
}
/**
* Indicates if this number is greater than the provided one
*
* @param self $number
*
* @return bool
*/
public function isLowerThan(self $number)
{
return (-1 === (new Operation\Comparison())->compare($this, $number));
}
/**
* Indicates if this number is lower or equal compared to the provided one
*
* @param self $number
*
* @return bool
*/
public function isLowerOrEqualThan(self $number)
{
return (0 >= (new Operation\Comparison())->compare($this, $number));
}
/**
* Indicates if this number is positive
*
* @return bool
*/
public function isPositive()
{
return !$this->isNegative;
}
/**
* Indicates if this number is negative
*
* @return bool
*/
public function isNegative()
{
return $this->isNegative;
}
/**
* Indicates if this number equals another one
*
* @param self $number
*
* @return bool
*/
public function equals(self $number)
{
return (
$this->isNegative === $number->isNegative
&& $this->coefficient === $number->getCoefficient()
&& $this->exponent === $number->getExponent()
);
}
/**
* Returns the additive inverse of this number (that is, N * -1).
*
* @return static
*/
public function invert()
{
// invert sign
$sign = $this->isNegative ? '' : '-';
return new static($sign . $this->getCoefficient(), $this->getExponent());
}
/**
* Creates a new copy of this number multiplied by 10^$exponent
*
* @param int $exponent
*
* @return static
*/
public function toMagnitude($exponent)
{
return (new Operation\MagnitudeChange())->compute($this, $exponent);
}
/**
* Initializes the number using a coefficient and exponent
*
* @param string $coefficient
* @param int $exponent
*/
private function initFromScientificNotation($coefficient, $exponent)
{
if ($exponent < 0) {
throw new InvalidArgumentException(
sprintf('Invalid value for exponent. Expected a positive integer or 0, but got "%s"', $coefficient)
);
}
if (!preg_match("/^(?<sign>[-+])?(?<integerPart>\d+)$/", $coefficient, $parts)) {
throw new InvalidArgumentException(
sprintf('"%s" cannot be interpreted as a number', $coefficient)
);
}
$this->isNegative = ('-' === $parts['sign']);
$this->exponent = (int) $exponent;
// trim leading zeroes
$this->coefficient = ltrim($parts['integerPart'], '0');
// when coefficient is '0' or a sequence of '0'
if ('' === $this->coefficient) {
$this->exponent = 0;
$this->coefficient = '0';
return;
}
$this->removeTrailingZeroesIfNeeded();
}
/**
* Removes trailing zeroes from the fractional part and adjusts the exponent accordingly
*/
private function removeTrailingZeroesIfNeeded()
{
$exponent = $this->getExponent();
$coefficient = $this->getCoefficient();
// trim trailing zeroes from the fractional part
// for example 1000e-1 => 100.0
if (0 < $exponent && '0' === substr($coefficient, -1)) {
$fractionalPart = $this->getFractionalPart();
$trailingZeroesToRemove = 0;
for ($i = $exponent - 1; $i >= 0; $i--) {
if ('0' !== $fractionalPart[$i]) {
break;
}
$trailingZeroesToRemove++;
}
if ($trailingZeroesToRemove > 0) {
$this->coefficient = substr($coefficient, 0, -$trailingZeroesToRemove);
$this->exponent = $exponent - $trailingZeroesToRemove;
}
}
}
}

View File

@@ -0,0 +1,16 @@
<?php
/**
* This file is part of the PrestaShop\Decimal package
*
* @author PrestaShop SA <contact@prestashop.com>
* @license https://opensource.org/licenses/MIT MIT License
*/
namespace PrestaShop\Decimal\Exception;
/**
* Thrown when attempting to divide by zero
*/
class DivisionByZeroException extends \Exception
{
}

View File

@@ -0,0 +1,27 @@
<?php
/**
* This file is part of the PrestaShop\Decimal package
*
* @author PrestaShop SA <contact@prestashop.com>
* @license https://opensource.org/licenses/MIT MIT License
*/
namespace PrestaShop\Decimal;
/**
* Retrocompatible name for DecimalNumber
*
* @deprecated use DecimalNumber instead
*/
class Number extends DecimalNumber
{
/**
* {@inheritdoc}
*/
public function __construct($number, $exponent = null)
{
@trigger_error(__FUNCTION__ . 'is deprecated since version 1.4. Use DecimalNumber instead.', E_USER_DEPRECATED);
parent::__construct($number, $exponent);
}
}

View File

@@ -0,0 +1,215 @@
<?php
/**
* This file is part of the PrestaShop\Decimal package
*
* @author PrestaShop SA <contact@prestashop.com>
* @license https://opensource.org/licenses/MIT MIT License
*/
namespace PrestaShop\Decimal\Operation;
use PrestaShop\Decimal\DecimalNumber;
/**
* Computes the addition of two decimal numbers
*/
class Addition
{
/**
* Maximum safe string size in order to be confident
* that it won't overflow the max int size when operating with it
* @var int
*/
private $maxSafeIntStringSize;
/**
* Constructor
*/
public function __construct()
{
$this->maxSafeIntStringSize = strlen((string) PHP_INT_MAX) - 1;
}
/**
* Performs the addition
*
* @param DecimalNumber $a Base number
* @param DecimalNumber $b Addend
*
* @return DecimalNumber Result of the addition
*/
public function compute(DecimalNumber $a, DecimalNumber $b)
{
if (function_exists('bcadd')) {
return $this->computeUsingBcMath($a, $b);
}
return $this->computeWithoutBcMath($a, $b);
}
/**
* Performs the addition using BC Math
*
* @param DecimalNumber $a Base number
* @param DecimalNumber $b Addend
*
* @return DecimalNumber Result of the addition
*/
public function computeUsingBcMath(DecimalNumber $a, DecimalNumber $b)
{
$precision1 = $a->getPrecision();
$precision2 = $b->getPrecision();
return new DecimalNumber((string) bcadd($a, $b, max($precision1, $precision2)));
}
/**
* Performs the addition without BC Math
*
* @param DecimalNumber $a Base number
* @param DecimalNumber $b Addend
*
* @return DecimalNumber Result of the addition
*/
public function computeWithoutBcMath(DecimalNumber $a, DecimalNumber $b)
{
if ($a->isNegative()) {
if ($b->isNegative()) {
// if both numbers are negative,
// we can just add them as positive numbers and then invert the sign
// f(x, y) = -(|x| + |y|)
// eg. f(-1, -2) = -(|-1| + |-2|) = -3
// eg. f(-2, -1) = -(|-2| + |-1|) = -3
return $this
->computeWithoutBcMath($a->toPositive(), $b->toPositive())
->invert();
}
// if the number is negative and the addend positive,
// perform an inverse subtraction by inverting the terms
// f(x, y) = y - |x|
// eg. f(-2, 1) = 1 - |-2| = -1
// eg. f(-1, 2) = 2 - |-1| = 1
// eg. f(-1, 1) = 1 - |-1| = 0
return $b->minus(
$a->toPositive()
);
}
if ($b->isNegative()) {
// if the number is positive and the addend is negative
// perform subtraction instead: 2 - 1
// f(x, y) = x - |y|
// f(2, -1) = 2 - |-1| = 1
// f(1, -2) = 1 - |-2| = -1
// f(1, -1) = 1 - |-1| = 0
return $a->minus(
$b->toPositive()
);
}
// optimization: 0 + x = x
if ('0' === (string) $a) {
return $b;
}
// optimization: x + 0 = x
if ('0' === (string) $b) {
return $a;
}
// pad coefficients with leading/trailing zeroes
list($coeff1, $coeff2) = $this->normalizeCoefficients($a, $b);
// compute the coefficient sum
$sum = $this->addStrings($coeff1, $coeff2);
// both signs are equal, so we can use either
$sign = $a->getSign();
// keep the bigger exponent
$exponent = max($a->getExponent(), $b->getExponent());
return new DecimalNumber($sign . $sum, $exponent);
}
/**
* Normalizes coefficients by adding leading or trailing zeroes as needed so that both are the same length
*
* @param DecimalNumber $a
* @param DecimalNumber $b
*
* @return array An array containing the normalized coefficients
*/
private function normalizeCoefficients(DecimalNumber $a, DecimalNumber $b)
{
$exp1 = $a->getExponent();
$exp2 = $b->getExponent();
$coeff1 = $a->getCoefficient();
$coeff2 = $b->getCoefficient();
// add trailing zeroes if needed
if ($exp1 > $exp2) {
$coeff2 = str_pad($coeff2, strlen($coeff2) + $exp1 - $exp2, '0', STR_PAD_RIGHT);
} elseif ($exp1 < $exp2) {
$coeff1 = str_pad($coeff1, strlen($coeff1) + $exp2 - $exp1, '0', STR_PAD_RIGHT);
}
$len1 = strlen($coeff1);
$len2 = strlen($coeff2);
// add leading zeroes if needed
if ($len1 > $len2) {
$coeff2 = str_pad($coeff2, $len1, '0', STR_PAD_LEFT);
} elseif ($len1 < $len2) {
$coeff1 = str_pad($coeff1, $len2, '0', STR_PAD_LEFT);
}
return [$coeff1, $coeff2];
}
/**
* Adds two integer numbers as strings.
*
* @param string $number1
* @param string $number2
* @param bool $fractional [default=false]
* If true, the numbers will be treated as the fractional part of a number (padded with trailing zeroes).
* Otherwise, they will be treated as the integer part (padded with leading zeroes).
*
* @return string
*/
private function addStrings($number1, $number2, $fractional = false)
{
// optimization - numbers can be treated as integers as long as they don't overflow the max int size
if ('0' !== $number1[0]
&& '0' !== $number2[0]
&& strlen($number1) <= $this->maxSafeIntStringSize
&& strlen($number2) <= $this->maxSafeIntStringSize
) {
return (string) ((int) $number1 + (int) $number2);
}
// find out which of the strings is longest
$maxLength = max(strlen($number1), strlen($number2));
// add leading or trailing zeroes as needed
$number1 = str_pad($number1, $maxLength, '0', $fractional ? STR_PAD_RIGHT : STR_PAD_LEFT);
$number2 = str_pad($number2, $maxLength, '0', $fractional ? STR_PAD_RIGHT : STR_PAD_LEFT);
$result = '';
$carryOver = 0;
for ($i = $maxLength - 1; 0 <= $i; $i--) {
$sum = $number1[$i] + $number2[$i] + $carryOver;
$result .= $sum % 10;
$carryOver = (int) ($sum >= 10);
}
if ($carryOver > 0) {
$result .= '1';
}
return strrev($result);
}
}

View File

@@ -0,0 +1,168 @@
<?php
/**
* This file is part of the PrestaShop\Decimal package
*
* @author PrestaShop SA <contact@prestashop.com>
* @license https://opensource.org/licenses/MIT MIT License
*/
namespace PrestaShop\Decimal\Operation;
use PrestaShop\Decimal\DecimalNumber;
/**
* Compares two decimal numbers
*/
class Comparison
{
/**
* Compares two decimal numbers.
*
* @param DecimalNumber $a
* @param DecimalNumber $b
*
* @return int Returns 1 if $a > $b, -1 if $a < $b, and 0 if they are equal.
*/
public function compare(DecimalNumber $a, DecimalNumber $b)
{
if (function_exists('bccomp')) {
return $this->compareUsingBcMath($a, $b);
}
return $this->compareWithoutBcMath($a, $b);
}
/**
* Compares two decimal numbers using BC Math
*
* @param DecimalNumber $a
* @param DecimalNumber $b
*
* @return int Returns 1 if $a > $b, -1 if $a < $b, and 0 if they are equal.
*/
public function compareUsingBcMath(DecimalNumber $a, DecimalNumber $b)
{
return bccomp((string) $a, (string) $b, max($a->getExponent(), $b->getExponent()));
}
/**
* Compares two decimal numbers without using BC Math
*
* @param DecimalNumber $a
* @param DecimalNumber $b
*
* @return int Returns 1 if $a > $b, -1 if $a < $b, and 0 if they are equal.
*/
public function compareWithoutBcMath(DecimalNumber $a, DecimalNumber $b)
{
$signCompare = $this->compareSigns($a->getSign(), $b->getSign());
if ($signCompare !== 0) {
return $signCompare;
}
// signs are equal, compare regardless of sign
$result = $this->positiveCompare($a, $b);
// inverse the result if the signs are negative
if ($a->isNegative()) {
return -$result;
}
return $result;
}
/**
* Compares two decimal numbers as positive regardless of sign.
*
* @param DecimalNumber $a
* @param DecimalNumber $b
*
* @return int Returns 1 if $a > $b, -1 if $a < $b, and 0 if they are equal.
*/
private function positiveCompare(DecimalNumber $a, DecimalNumber $b)
{
// compare integer length
$intLengthCompare = $this->compareNumeric(
strlen($a->getIntegerPart()),
strlen($b->getIntegerPart())
);
if ($intLengthCompare !== 0) {
return $intLengthCompare;
}
// integer parts are equal in length, compare integer part
$intPartCompare = $this->compareBinary($a->getIntegerPart(), $b->getIntegerPart());
if ($intPartCompare !== 0) {
return $intPartCompare;
}
// integer parts are equal, compare fractional part
return $this->compareBinary($a->getFractionalPart(), $b->getFractionalPart());
}
/**
* Compares positive/negative signs.
*
* @param string $a
* @param string $b
*
* @return int Returns 0 if both signs are equal, 1 if $a is positive, and -1 if $b is positive
*/
private function compareSigns($a, $b)
{
if ($a === $b) {
return 0;
}
// empty string means positive sign
if ($a === '') {
return 1;
}
return -1;
}
/**
* Compares two values numerically.
*
* @param mixed $a
* @param mixed $b
*
* @return int Returns 1 if $a > $b, -1 if $a < $b, and 0 if they are equal.
*/
private function compareNumeric($a, $b)
{
if ($a < $b) {
return -1;
}
if ($a > $b) {
return 1;
}
return 0;
}
/**
* Compares two strings binarily.
*
* @param string $a
* @param string $b
*
* @return int Returns 1 if $a > $b, -1 if $a < $b, and 0 if they are equal.
*/
private function compareBinary($a, $b)
{
$comparison = strcmp($a, $b);
if ($comparison > 0) {
return 1;
}
if ($comparison < 0) {
return -1;
}
return 0;
}
}

View File

@@ -0,0 +1,175 @@
<?php
/**
* This file is part of the PrestaShop\Decimal package
*
* @author PrestaShop SA <contact@prestashop.com>
* @license https://opensource.org/licenses/MIT MIT License
*/
namespace PrestaShop\Decimal\Operation;
use PrestaShop\Decimal\Exception\DivisionByZeroException;
use PrestaShop\Decimal\DecimalNumber;
/**
* Computes the division between two decimal numbers.
*/
class Division
{
const DEFAULT_PRECISION = 6;
/**
* Performs the division.
*
* A target maximum precision is required in order to handle potential infinite number of decimals
* (e.g. 1/3 = 0.3333333...).
*
* If the division yields more decimal positions than the requested precision,
* the remaining decimals are truncated, with **no rounding**.
*
* @param DecimalNumber $a Dividend
* @param DecimalNumber $b Divisor
* @param int $precision Maximum decimal precision
*
* @return DecimalNumber Result of the division
* @throws DivisionByZeroException
*/
public function compute(DecimalNumber $a, DecimalNumber $b, $precision = self::DEFAULT_PRECISION)
{
if (function_exists('bcdiv')) {
return $this->computeUsingBcMath($a, $b, $precision);
}
return $this->computeWithoutBcMath($a, $b, $precision);
}
/**
* Performs the division using BC Math
*
* @param DecimalNumber $a Dividend
* @param DecimalNumber $b Divisor
* @param int $precision Maximum decimal precision
*
* @return DecimalNumber Result of the division
* @throws DivisionByZeroException
*/
public function computeUsingBcMath(DecimalNumber $a, DecimalNumber $b, $precision = self::DEFAULT_PRECISION)
{
if ((string) $b === '0') {
throw new DivisionByZeroException();
}
return new DecimalNumber((string) bcdiv($a, $b, $precision));
}
/**
* Performs the division without BC Math
*
* @param DecimalNumber $a Dividend
* @param DecimalNumber $b Divisor
* @param int $precision Maximum decimal precision
*
* @return DecimalNumber Result of the division
* @throws DivisionByZeroException
*/
public function computeWithoutBcMath(DecimalNumber $a, DecimalNumber $b, $precision = self::DEFAULT_PRECISION)
{
$bString = (string) $b;
if ('0' === $bString) {
throw new DivisionByZeroException();
}
$aString = (string) $a;
// 0 as dividend always yields 0
if ('0' === $aString) {
return $a;
}
// 1 as divisor always yields the dividend
if ('1' === $bString) {
return $a;
}
// -1 as divisor always yields the the inverted dividend
if ('-1' === $bString) {
return $a->invert();
}
// if dividend and divisor are equal, the result is always 1
if ($a->equals($b)) {
return new DecimalNumber('1');
}
$aPrecision = $a->getPrecision();
$bPrecision = $b->getPrecision();
$maxPrecision = max($aPrecision, $bPrecision);
if ($maxPrecision > 0) {
// make $a and $b integers by multiplying both by 10^(maximum number of decimals)
$a = $a->toMagnitude($maxPrecision);
$b = $b->toMagnitude($maxPrecision);
}
$result = $this->integerDivision($a, $b, max($precision, $aPrecision));
return $result;
}
/**
* Computes the division between two integer DecimalNumbers
*
* @param DecimalNumber $a Dividend
* @param DecimalNumber $b Divisor
* @param int $precision Maximum number of decimals to try
*
* @return DecimalNumber
*/
private function integerDivision(DecimalNumber $a, DecimalNumber $b, $precision)
{
$dividend = $a->getCoefficient();
$divisor = new DecimalNumber($b->getCoefficient());
$dividendLength = strlen($dividend);
$result = '';
$exponent = 0;
$currentSequence = '';
for ($i = 0; $i < $dividendLength; $i++) {
// append digits until we get a number big enough to divide
$currentSequence .= $dividend[$i];
if ($currentSequence < $divisor) {
if (!empty($result)) {
$result .= '0';
}
} else {
// subtract divisor as many times as we can
$remainder = new DecimalNumber($currentSequence);
$multiple = 0;
do {
$multiple++;
$remainder = $remainder->minus($divisor);
} while ($remainder->isGreaterOrEqualThan($divisor));
$result .= (string) $multiple;
// reset sequence to the reminder
$currentSequence = (string) $remainder;
}
// add up to $precision decimals
if ($currentSequence > 0 && $i === $dividendLength - 1 && $precision > 0) {
// "borrow" up to $precision digits
--$precision;
$dividend .= '0';
$dividendLength++;
$exponent++;
}
}
$sign = ($a->isNegative() xor $b->isNegative()) ? '-' : '';
return new DecimalNumber($sign . $result, $exponent);
}
}

View File

@@ -0,0 +1,60 @@
<?php
/**
* This file is part of the PrestaShop\Decimal package
*
* @author PrestaShop SA <contact@prestashop.com>
* @license https://opensource.org/licenses/MIT MIT License
*/
namespace PrestaShop\Decimal\Operation;
use PrestaShop\Decimal\DecimalNumber;
/**
* Computes relative magnitude changes on a decimal number
*/
class MagnitudeChange
{
/**
* Multiplies a number by 10^$exponent.
*
* Examples:
* ```php
* $n = new Decimal\Number('123.45678');
* $o = new Decimal\Operation\MagnitudeChange();
* $o->compute($n, 2); // 12345.678
* $o->compute($n, 6); // 123456780
* $o->compute($n, -2); // 1.2345678
* $o->compute($n, -6); // 0.00012345678
* ```
*
* @param DecimalNumber $number
* @param int $exponent
*
* @return DecimalNumber
*/
public function compute(DecimalNumber $number, $exponent)
{
$exponent = (int) $exponent;
if ($exponent === 0) {
return $number;
}
$resultingExponent = $exponent - $number->getExponent();
if ($resultingExponent <= 0) {
return new DecimalNumber(
$number->getSign() . $number->getCoefficient(),
abs($resultingExponent)
);
}
// add zeroes
$targetLength = strlen($number->getCoefficient()) + $resultingExponent;
return new DecimalNumber(
$number->getSign() . str_pad($number->getCoefficient(), $targetLength, '0')
);
}
}

View File

@@ -0,0 +1,155 @@
<?php
/**
* This file is part of the PrestaShop\Decimal package
*
* @author PrestaShop SA <contact@prestashop.com>
* @license https://opensource.org/licenses/MIT MIT License
*/
namespace PrestaShop\Decimal\Operation;
use PrestaShop\Decimal\DecimalNumber;
/**
* Computes the multiplication between two decimal numbers
*/
class Multiplication
{
/**
* Performs the multiplication
*
* @param DecimalNumber $a Left operand
* @param DecimalNumber $b Right operand
*
* @return DecimalNumber Result of the multiplication
*/
public function compute(DecimalNumber $a, DecimalNumber $b)
{
if (function_exists('bcmul')) {
return $this->computeUsingBcMath($a, $b);
}
return $this->computeWithoutBcMath($a, $b);
}
/**
* Performs the multiplication using BC Math
*
* @param DecimalNumber $a Left operand
* @param DecimalNumber $b Right operand
*
* @return DecimalNumber Result of the multiplication
*/
public function computeUsingBcMath(DecimalNumber $a, DecimalNumber $b)
{
$precision1 = $a->getPrecision();
$precision2 = $b->getPrecision();
return new DecimalNumber((string) bcmul($a, $b, $precision1 + $precision2));
}
/**
* Performs the multiplication without BC Math
*
* @param DecimalNumber $a Left operand
* @param DecimalNumber $b Right operand
*
* @return DecimalNumber Result of the multiplication
*/
public function computeWithoutBcMath(DecimalNumber $a, DecimalNumber $b)
{
$aAsString = (string) $a;
$bAsString = (string) $b;
// optimization: if either one is zero, the result is zero
if ('0' === $aAsString || '0' === $bAsString) {
return new DecimalNumber('0');
}
// optimization: if either one is one, the result is the other one
if ('1' === $aAsString) {
return $b;
}
if ('1' === $bAsString) {
return $a;
}
$result = $this->multiplyStrings(
ltrim($a->getCoefficient(), '0'),
ltrim($b->getCoefficient(), '0')
);
$sign = ($a->isNegative() xor $b->isNegative()) ? '-' : '';
// a multiplication has at most as many decimal figures as the sum
// of the number of decimal figures the factors have
$exponent = $a->getExponent() + $b->getExponent();
return new DecimalNumber($sign . $result, $exponent);
}
/**
* Multiplies two integer numbers as strings.
*
* This method implements a naive "long multiplication" algorithm.
*
* @param string $topNumber
* @param string $bottomNumber
*
* @return string
*/
private function multiplyStrings($topNumber, $bottomNumber)
{
$topNumberLength = strlen($topNumber);
$bottomNumberLength = strlen($bottomNumber);
if ($topNumberLength < $bottomNumberLength) {
// multiplication is commutative, and this algorithm
// performs better if the bottom number is shorter.
return $this->multiplyStrings($bottomNumber, $topNumber);
}
$stepNumber = 0;
$result = new DecimalNumber('0');
for ($i = $bottomNumberLength - 1; $i >= 0; $i--) {
$carryOver = 0;
$partialResult = '';
// optimization: we don't need to bother multiplying by zero
if ($bottomNumber[$i] === '0') {
$stepNumber++;
continue;
}
if ($bottomNumber[$i] === '1') {
// multiplying by one is the same as copying the top number
$partialResult = strrev($topNumber);
} else {
// digit-by-digit multiplication using carry-over
for ($j = $topNumberLength - 1; $j >= 0; $j--) {
$multiplicationResult = ($bottomNumber[$i] * $topNumber[$j]) + $carryOver;
$carryOver = floor($multiplicationResult / 10);
$partialResult .= $multiplicationResult % 10;
}
if ($carryOver > 0) {
$partialResult .= $carryOver;
}
}
// pad the partial result with as many zeros as performed steps
$padding = str_pad('', $stepNumber, '0');
$partialResult = $padding . $partialResult;
// add to the result
$result = $result->plus(
new DecimalNumber(strrev($partialResult))
);
$stepNumber++;
}
return (string) $result;
}
}

View File

@@ -0,0 +1,420 @@
<?php
/**
* This file is part of the PrestaShop\Decimal package
*
* @author PrestaShop SA <contact@prestashop.com>
* @license https://opensource.org/licenses/MIT MIT License
*/
namespace PrestaShop\Decimal\Operation;
use PrestaShop\Decimal\DecimalNumber;
/**
* Allows transforming a decimal number's precision
*/
class Rounding
{
const ROUND_TRUNCATE = 'truncate';
const ROUND_CEIL = 'ceil';
const ROUND_FLOOR = 'floor';
const ROUND_HALF_UP = 'up';
const ROUND_HALF_DOWN = 'down';
const ROUND_HALF_EVEN = 'even';
/**
* Rounds a decimal number to a specified precision
*
* @param DecimalNumber $number Number to round
* @param int $precision Maximum number of decimals
* @param string $roundingMode Rounding algorithm
*
* @return DecimalNumber
*/
public function compute(DecimalNumber $number, $precision, $roundingMode)
{
switch ($roundingMode) {
case self::ROUND_HALF_UP:
return $this->roundHalfUp($number, $precision);
break;
case self::ROUND_CEIL:
return $this->ceil($number, $precision);
break;
case self::ROUND_FLOOR:
return $this->floor($number, $precision);
break;
case self::ROUND_HALF_DOWN:
return $this->roundHalfDown($number, $precision);
break;
case self::ROUND_TRUNCATE:
return $this->truncate($number, $precision);
break;
case self::ROUND_HALF_EVEN:
return $this->roundHalfEven($number, $precision);
break;
}
throw new \InvalidArgumentException(sprintf("Invalid rounding mode: %s", print_r($roundingMode, true)));
}
/**
* Truncates a number to a target number of decimal digits.
*
* @param DecimalNumber $number Number to round
* @param int $precision Maximum number of decimals
*
* @return DecimalNumber
*/
public function truncate(DecimalNumber $number, $precision)
{
$precision = $this->sanitizePrecision($precision);
if ($number->getPrecision() <= $precision) {
return $number;
}
if (0 === $precision) {
return new DecimalNumber($number->getSign() . $number->getIntegerPart());
}
return new DecimalNumber(
$number->getSign()
. $number->getIntegerPart()
. '.'
. substr($number->getFractionalPart(), 0, $precision)
);
}
/**
* Rounds a number up if its precision is greater than the target one.
*
* Ceil always rounds towards positive infinity.
*
* Examples:
*
* ```
* $n = new Decimal\Number('123.456');
* $this->ceil($n, 0); // '124'
* $this->ceil($n, 1); // '123.5'
* $this->ceil($n, 2); // '123.46'
*
* $n = new Decimal\Number('-123.456');
* $this->ceil($n, 0); // '-122'
* $this->ceil($n, 1); // '-123.3'
* $this->ceil($n, 2); // '-123.44'
* ```
*
* @param DecimalNumber $number Number to round
* @param int $precision Maximum number of decimals
*
* @return DecimalNumber
*/
public function ceil(DecimalNumber $number, $precision)
{
$precision = $this->sanitizePrecision($precision);
if ($number->getPrecision() <= $precision) {
return $number;
}
if ($number->isNegative()) {
// ceil works exactly as truncate for negative numbers
return $this->truncate($number, $precision);
}
/**
* The principle for ceil is the following:
*
* let X = number to round
* P = number of decimal digits that we want
* D = digit from the fractional part at index P
*
* if D > 0, ceil(X, P) = truncate(X + 10^(-P), P)
* if D = 0, ceil(X, P) = truncate(X, P)
*/
if ($precision > 0) {
// we know that D > 0, because we have already checked that the number's precision
// is greater than the target precision
$numberToAdd = '0.' . str_pad('1', $precision, '0', STR_PAD_LEFT);
} else {
$numberToAdd = '1';
}
return $this
->truncate($number, $precision)
->plus(new DecimalNumber($numberToAdd));
}
/**
* Rounds a number down if its precision is greater than the target one.
*
* Floor always rounds towards negative infinity.
*
* Examples:
*
* ```
* $n = new Decimal\Number('123.456');
* $this->floor($n, 0); // '123'
* $this->floor($n, 1); // '123.4'
* $this->floor($n, 2); // '123.45'
*
* $n = new Decimal\Number('-123.456');
* $this->floor($n, 0); // '-124'
* $this->floor($n, 1); // '-123.5'
* $this->floor($n, 2); // '-123.46'
* ```
*
* @param DecimalNumber $number Number to round
* @param int $precision Maximum number of decimals
*
* @return DecimalNumber
*/
public function floor(DecimalNumber $number, $precision)
{
$precision = $this->sanitizePrecision($precision);
if ($number->getPrecision() <= $precision) {
return $number;
}
if ($number->isPositive()) {
// floor works exactly as truncate for positive numbers
return $this->truncate($number, $precision);
}
/**
* The principle for ceil is the following:
*
* let X = number to round
* P = number of decimal digits that we want
* D = digit from the fractional part at index P
*
* if D < 0, ceil(X, P) = truncate(X - 10^(-P), P)
* if D = 0, ceil(X, P) = truncate(X, P)
*/
if ($precision > 0) {
// we know that D > 0, because we have already checked that the number's precision
// is greater than the target precision
$numberToSubtract = '0.' . str_pad('1', $precision, '0', STR_PAD_LEFT);
} else {
$numberToSubtract = '1';
}
return $this
->truncate($number, $precision)
->minus(new DecimalNumber($numberToSubtract));
}
/**
* Rounds the number according to the digit D located at precision P.
* - It rounds away from zero if D >= 5
* - It rounds towards zero if D < 5
*
* Examples:
*
* ```
* $n = new Decimal\Number('123.456');
* $this->roundHalfUp($n, 0); // '123'
* $this->roundHalfUp($n, 1); // '123.5'
* $this->roundHalfUp($n, 2); // '123.46'
*
* $n = new Decimal\Number('-123.456');
* $this->roundHalfUp($n, 0); // '-123'
* $this->roundHalfUp($n, 1); // '-123.5'
* $this->roundHalfUp($n, 2); // '-123.46'
* ```
*
* @param DecimalNumber $number Number to round
* @param int $precision Maximum number of decimals
*
* @return DecimalNumber
*/
public function roundHalfUp(DecimalNumber $number, $precision)
{
return $this->roundHalf($number, $precision, 5);
}
/**
* Rounds the number according to the digit D located at precision P.
* - It rounds away from zero if D > 5
* - It rounds towards zero if D <= 5
*
* Examples:
*
* ```
* $n = new Decimal\Number('123.456');
* $this->roundHalfUp($n, 0); // '123'
* $this->roundHalfUp($n, 1); // '123.4'
* $this->roundHalfUp($n, 2); // '123.46'
*
* $n = new Decimal\Number('-123.456');
* $this->roundHalfUp($n, 0); // '-123'
* $this->roundHalfUp($n, 1); // '-123.4'
* $this->roundHalfUp($n, 2); // '-123.46'
* ```
*
* @param DecimalNumber $number Number to round
* @param int $precision Maximum number of decimals
*
* @return DecimalNumber
*/
public function roundHalfDown(DecimalNumber $number, $precision)
{
return $this->roundHalf($number, $precision, 6);
}
/**
* Rounds a number according to "banker's rounding".
*
* The number is rounded according to the digit D located at precision P.
* - Away from zero if D > 5
* - Towards zero if D < 5
* - if D = 5, then
* - If the last significant digit is even, the number is rounded away from zero
* - If the last significant digit is odd, the number is rounded towards zero.
*
* Examples:
*
* ```
* $n = new Decimal\Number('123.456');
* $this->roundHalfUp($n, 0); // '123'
* $this->roundHalfUp($n, 1); // '123.4'
* $this->roundHalfUp($n, 2); // '123.46'
*
* $n = new Decimal\Number('-123.456');
* $this->roundHalfUp($n, 0); // '-123'
* $this->roundHalfUp($n, 1); // '-123.4'
* $this->roundHalfUp($n, 2); // '-123.46'
*
* $n = new Decimal\Number('1.1525354556575859505');
* $this->roundHalfEven($n, 0); // '1'
* $this->roundHalfEven($n, 1); // '1.2'
* $this->roundHalfEven($n, 2); // '1.15'
* $this->roundHalfEven($n, 3); // '1.152'
* $this->roundHalfEven($n, 4); // '1.1525'
* $this->roundHalfEven($n, 5); // '1.15255'
* $this->roundHalfEven($n, 6); // '1.152535'
* $this->roundHalfEven($n, 7); // '1.1525354'
* $this->roundHalfEven($n, 8); // '1.15253546'
* $this->roundHalfEven($n, 9); // '1.152535456'
* $this->roundHalfEven($n, 10); // '1.1525354556'
* ```
*
* @param DecimalNumber $number Number to round
* @param int $precision Maximum number of decimals
*
* @return DecimalNumber
*/
public function roundHalfEven(DecimalNumber $number, $precision)
{
$precision = $this->sanitizePrecision($precision);
if ($number->getPrecision() <= $precision) {
return $number;
}
/**
* The principle for roundHalfEven is the following:
*
* let X = number to round
* P = number of decimal digits that we want
* D = digit from the fractional part at index P
* E = digit to the left of D
*
* if D != 5, roundHalfEven(X, P) = roundHalfUp(X, P)
* if D = 5 and E is even, roundHalfEven(X, P) = truncate(X, P)
* if D = 5 and E is odd and X is positive, roundHalfUp(X, P) = ceil(X, P)
* if D = 5 and E is odd and X is negative, roundHalfUp(X, P) = floor(X, P)
*/
$fractionalPart = $number->getFractionalPart();
$digit = (int) $fractionalPart[$precision];
if ($digit !== 5) {
return $this->roundHalfUp($number, $precision);
}
// retrieve the digit to the left of it
if ($precision === 0) {
$referenceDigit = (int) substr($number->getIntegerPart(), -1);
} else {
$referenceDigit = (int) $fractionalPart[$precision - 1];
}
// truncate if even
$isEven = $referenceDigit % 2 === 0;
if ($isEven) {
return $this->truncate($number, $precision);
}
// round away from zero
$method = ($number->isPositive()) ? self::ROUND_CEIL : self::ROUND_FLOOR;
return $this->compute($number, $precision, $method);
}
/**
* Rounds the number according to the digit D located at precision P.
* - It rounds away from zero if D >= $halfwayValue
* - It rounds towards zero if D < $halfWayValue
*
* @param DecimalNumber $number Number to round
* @param int $precision Maximum number of decimals
* @param int $halfwayValue Threshold upon which the rounding will be performed
* away from zero instead of towards zero.
*
* @return DecimalNumber
*/
private function roundHalf(DecimalNumber $number, $precision, $halfwayValue)
{
$precision = $this->sanitizePrecision($precision);
if ($number->getPrecision() <= $precision) {
return $number;
}
/**
* The principle for roundHalf is the following:
*
* let X = number to round
* P = number of decimal digits that we want
* D = digit from the fractional part at index P
* Y = digit considered as the half-way value on which we round up (usually 5 or 6)
*
* if D >= Y, roundHalf(X, P) = ceil(X, P)
* if D < Y, roundHalf(X, P) = truncate(X, P)
*/
$fractionalPart = $number->getFractionalPart();
$digit = (int) $fractionalPart[$precision];
if ($digit >= $halfwayValue) {
// round away from zero
$mode = ($number->isPositive()) ? self::ROUND_CEIL : self::ROUND_FLOOR;
return $this->compute($number, $precision, $mode);
}
// round towards zero
return $this->truncate($number, $precision);
}
/**
* Ensures that precision is a positive int
*
* @param mixed $precision
*
* @return int Precision
*
* @throws \InvalidArgumentException if precision is not a positive integer
*/
private function sanitizePrecision($precision)
{
if (!is_numeric($precision) || $precision < 0) {
throw new \InvalidArgumentException(sprintf('Invalid precision: %s', print_r($precision, true)));
}
return (int) $precision;
}
}

View File

@@ -0,0 +1,203 @@
<?php
/**
* This file is part of the PrestaShop\Decimal package
*
* @author PrestaShop SA <contact@prestashop.com>
* @license https://opensource.org/licenses/MIT MIT License
*/
namespace PrestaShop\Decimal\Operation;
use PrestaShop\Decimal\DecimalNumber;
/**
* Computes the subtraction of two decimal numbers
*/
class Subtraction
{
/**
* Maximum safe string size in order to be confident
* that it won't overflow the max int size when operating with it
* @var int
*/
private $maxSafeIntStringSize;
/**
* Constructor
*/
public function __construct()
{
$this->maxSafeIntStringSize = strlen((string) PHP_INT_MAX) - 1;
}
/**
* Performs the subtraction
*
* @param DecimalNumber $a Minuend
* @param DecimalNumber $b Subtrahend
*
* @return DecimalNumber Result of the subtraction
*/
public function compute(DecimalNumber $a, DecimalNumber $b)
{
if (function_exists('bcsub')) {
return $this->computeUsingBcMath($a, $b);
}
return $this->computeWithoutBcMath($a, $b);
}
/**
* Performs the subtraction using BC Math
*
* @param DecimalNumber $a Minuend
* @param DecimalNumber $b Subtrahend
*
* @return DecimalNumber Result of the subtraction
*/
public function computeUsingBcMath(DecimalNumber $a, DecimalNumber $b)
{
$precision1 = $a->getPrecision();
$precision2 = $b->getPrecision();
return new DecimalNumber((string) bcsub($a, $b, max($precision1, $precision2)));
}
/**
* Performs the subtraction without using BC Math
*
* @param DecimalNumber $a Minuend
* @param DecimalNumber $b Subtrahend
*
* @return DecimalNumber Result of the subtraction
*/
public function computeWithoutBcMath(DecimalNumber $a, DecimalNumber $b)
{
if ($a->isNegative()) {
if ($b->isNegative()) {
// if both minuend and subtrahend are negative
// perform the subtraction with inverted coefficients position and sign
// f(x, y) = |y| - |x|
// eg. f(-1, -2) = |-2| - |-1| = 2 - 1 = 1
// e.g. f(-2, -1) = |-1| - |-2| = 1 - 2 = -1
return $this->computeWithoutBcMath($b->toPositive(), $a->toPositive());
} else {
// if the minuend is negative and the subtrahend is positive,
// we can just add them as positive numbers and then invert the sign
// f(x, y) = -(|x| + y)
// eg. f(1, 2) = -(|-1| + 2) = -3
// eg. f(-2, 1) = -(|-2| + 1) = -3
return $a
->toPositive()
->plus($b)
->toNegative();
}
} else if ($b->isNegative()) {
// if the minuend is positive subtrahend is negative, perform an addition
// f(x, y) = x + |y|
// eg. f(2, -1) = 2 + |-1| = 2 + 1 = 3
return $a->plus($b->toPositive());
}
// optimization: 0 - x = -x
if ('0' === (string) $a) {
return (!$b->isNegative()) ? $b->toNegative() : $b;
}
// optimization: x - 0 = x
if ('0' === (string) $b) {
return $a;
}
// pad coefficients with leading/trailing zeroes
list($coeff1, $coeff2) = $this->normalizeCoefficients($a, $b);
// compute the coefficient subtraction
if ($a->isGreaterThan($b)) {
$sub = $this->subtractStrings($coeff1, $coeff2);
$sign = '';
} else {
$sub = $this->subtractStrings($coeff2, $coeff1);
$sign = '-';
}
// keep the bigger exponent
$exponent = max($a->getExponent(), $b->getExponent());
return new DecimalNumber($sign . $sub, $exponent);
}
/**
* Normalizes coefficients by adding leading or trailing zeroes as needed so that both are the same length
*
* @param DecimalNumber $a
* @param DecimalNumber $b
*
* @return array An array containing the normalized coefficients
*/
private function normalizeCoefficients(DecimalNumber $a, DecimalNumber $b)
{
$exp1 = $a->getExponent();
$exp2 = $b->getExponent();
$coeff1 = $a->getCoefficient();
$coeff2 = $b->getCoefficient();
// add trailing zeroes if needed
if ($exp1 > $exp2) {
$coeff2 = str_pad($coeff2, strlen($coeff2) + $exp1 - $exp2, '0', STR_PAD_RIGHT);
} elseif ($exp1 < $exp2) {
$coeff1 = str_pad($coeff1, strlen($coeff1) + $exp2 - $exp1, '0', STR_PAD_RIGHT);
}
$len1 = strlen($coeff1);
$len2 = strlen($coeff2);
// add leading zeroes if needed
if ($len1 > $len2) {
$coeff2 = str_pad($coeff2, $len1, '0', STR_PAD_LEFT);
} elseif ($len1 < $len2) {
$coeff1 = str_pad($coeff1, $len2, '0', STR_PAD_LEFT);
}
return [$coeff1, $coeff2];
}
/**
* Subtracts $number2 to $number1.
* For this algorithm to work, $number1 has to be >= $number 2.
*
* @param string $number1
* @param string $number2
* @param bool $fractional [default=false]
* If true, the numbers will be treated as the fractional part of a number (padded with trailing zeroes).
* Otherwise, they will be treated as the integer part (padded with leading zeroes).
*
* @return string
*/
private function subtractStrings($number1, $number2, $fractional = false)
{
// find out which of the strings is longest
$maxLength = max(strlen($number1), strlen($number2));
// add leading or trailing zeroes as needed
$number1 = str_pad($number1, $maxLength, '0', $fractional ? STR_PAD_RIGHT : STR_PAD_LEFT);
$number2 = str_pad($number2, $maxLength, '0', $fractional ? STR_PAD_RIGHT : STR_PAD_LEFT);
$result = '';
$carryOver = 0;
for ($i = $maxLength -1; 0 <= $i; $i--) {
$operand1 = $number1[$i] - $carryOver;
$operand2 = $number2[$i];
if ($operand1 >= $operand2) {
$result .= $operand1 - $operand2;
$carryOver = 0;
} else {
$result .= 10 + $operand1 - $operand2;
$carryOver = 1;
}
}
return strrev($result);
}
}