From 601e0497f28ef8ab53a2142dd724eb572fcded26 Mon Sep 17 00:00:00 2001 From: Pablo Borowicz Date: Mon, 15 Jul 2019 19:36:18 -0300 Subject: [PATCH 1/4] Extract builder class --- src/Builder.php | 57 +++++++++++++++++++++++++++++++++++++++++++++++++ src/Number.php | 41 +++++------------------------------ 2 files changed, 62 insertions(+), 36 deletions(-) create mode 100644 src/Builder.php diff --git a/src/Builder.php b/src/Builder.php new file mode 100644 index 0000000..8dd45a8 --- /dev/null +++ b/src/Builder.php @@ -0,0 +1,57 @@ + + * @license https://opensource.org/licenses/MIT MIT License + */ + +namespace PrestaShop\Decimal; + +use PrestaShop\Decimal\Number; + + +/** + * Builds Number instances + */ +class Builder +{ + + const NUMBER_PATTERN = "/^(?[-+])?(?\d+)(?:\.(?\d+))?(?[eE](?[-+])(?\d+))?$/"; + + /** + * Builds a Number from a string + * + * @param string $number + * + * @return Number + */ + public static function parseNumber($number) + { + if (!preg_match(self::NUMBER_PATTERN, $number, $parts)) { + throw new \InvalidArgumentException( + sprintf('"%s" cannot be interpreted as a number', print_r($number, true)) + ); + } + + // extract the integer part and remove leading zeroes and plus sign + $integerPart = ltrim($parts['integerPart'], '0'); + + $fractionalPart = ''; + if (array_key_exists('fractionalPart', $parts)) { + // extract the fractional part and remove trailing zeroes + $fractionalPart = rtrim($parts['fractionalPart'], '0'); + } + + $exponent = strlen($fractionalPart); + $coefficient = $integerPart . $fractionalPart; + + // when coefficient is '0' or a sequence of '0' + if ('' === $coefficient) { + $coefficient = '0'; + } + + return new Number($parts['sign'] . $coefficient, $exponent); + } + +} diff --git a/src/Number.php b/src/Number.php index 85b1e15..55f5758 100644 --- a/src/Number.php +++ b/src/Number.php @@ -69,11 +69,13 @@ public function __construct($number, $exponent = null) } if (null === $exponent) { - $this->initFromString($number); - } else { - $this->initFromScientificNotation($number, $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; @@ -455,39 +457,6 @@ public function toMagnitude($exponent) return (new Operation\MagnitudeChange())->compute($this, $exponent); } - /** - * Initializes the number using a string - * - * @param string $number - */ - private function initFromString($number) - { - if (!preg_match("/^(?[-+])?(?\d+)(?:\.(?\d+))?$/", $number, $parts)) { - throw new \InvalidArgumentException( - sprintf('"%s" cannot be interpreted as a number', print_r($number, true)) - ); - } - - $this->isNegative = ('-' === $parts['sign']); - - // extract the integer part and remove leading zeroes and plus sign - $integerPart = ltrim($parts['integerPart'], '0'); - - $fractionalPart = ''; - if (array_key_exists('fractionalPart', $parts)) { - // extract the fractional part and remove trailing zeroes - $fractionalPart = rtrim($parts['fractionalPart'], '0'); - } - - $this->exponent = strlen($fractionalPart); - $this->coefficient = $integerPart . $fractionalPart; - - // when coefficient is '0' or a sequence of '0' - if ('' === $this->coefficient) { - $this->coefficient = '0'; - } - } - /** * Initializes the number using a coefficient and exponent * From ad97f15560831a20c3f6a4d55d7ab8fb95bd4637 Mon Sep 17 00:00:00 2001 From: Pablo Borowicz Date: Tue, 16 Jul 2019 18:51:49 -0300 Subject: [PATCH 2/4] Clarify exponent in Number constructor --- src/Number.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Number.php b/src/Number.php index 55f5758..1093bf6 100644 --- a/src/Number.php +++ b/src/Number.php @@ -54,11 +54,11 @@ class Number * (string) new Number('123456', 6); // -> '0.123456' * ``` * - * Note: exponents are always positive. + * Note: decimal positions must always be a positive number. * * @param string $number Number or coefficient - * @param int $exponent [default=null] If provided, the number is considered a coefficient of - * the scientific notation. + * @param int $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) { From 9e2656aec501ce26c6559691fc2b42dc9063e7e1 Mon Sep 17 00:00:00 2001 From: Pablo Borowicz Date: Tue, 16 Jul 2019 18:54:26 -0300 Subject: [PATCH 3/4] Make Builder parse more formats, like: - 0.1e-1 - 1E+10 - .01 - .1E-10 --- src/Builder.php | 60 +++++++++++++++--- tests/NumberTest.php | 146 ++++++++++++++++++++++++++----------------- 2 files changed, 139 insertions(+), 67 deletions(-) diff --git a/src/Builder.php b/src/Builder.php index 8dd45a8..5587aa7 100644 --- a/src/Builder.php +++ b/src/Builder.php @@ -17,7 +17,15 @@ class Builder { - const NUMBER_PATTERN = "/^(?[-+])?(?\d+)(?:\.(?\d+))?(?[eE](?[-+])(?\d+))?$/"; + /** + * Pattern for most numbers + */ + const NUMBER_PATTERN = "/^(?[-+])?(?\d+)?(?:\.(?\d+)(?[eE](?[-+])(?\d+))?)?$/"; + + /** + * Pattern for integer numbers in scientific notation (rare but supported by spec) + */ + const INT_EXPONENTIAL_PATTERN = "/^(?[-+])?(?\d+)(?[eE](?[-+])(?\d+))$/"; /** * Builds a Number from a string @@ -28,22 +36,25 @@ class Builder */ public static function parseNumber($number) { - if (!preg_match(self::NUMBER_PATTERN, $number, $parts)) { + if (!self::itLooksLikeANumber($number, $numberParts)) { throw new \InvalidArgumentException( sprintf('"%s" cannot be interpreted as a number', print_r($number, true)) ); } - // extract the integer part and remove leading zeroes and plus sign - $integerPart = ltrim($parts['integerPart'], '0'); + $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', $parts)) { + if (array_key_exists('fractionalPart', $numberParts)) { // extract the fractional part and remove trailing zeroes - $fractionalPart = rtrim($parts['fractionalPart'], '0'); + $fractionalPart = rtrim($numberParts['fractionalPart'], '0'); } - $exponent = strlen($fractionalPart); + $fractionalDigits = strlen($fractionalPart); $coefficient = $integerPart . $fractionalPart; // when coefficient is '0' or a sequence of '0' @@ -51,7 +62,40 @@ public static function parseNumber($number) $coefficient = '0'; } - return new Number($parts['sign'] . $coefficient, $exponent); + // 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 Number($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) + ) + ); } } diff --git a/tests/NumberTest.php b/tests/NumberTest.php index 5c54a2c..4cf8c7d 100644 --- a/tests/NumberTest.php +++ b/tests/NumberTest.php @@ -8,7 +8,7 @@ namespace PrestaShop\Decimal\tests\Unit\Core\Decimal; -use PrestaShop\Decimal\Number as DecimalNumber; +use PrestaShop\Decimal\Number; use PrestaShop\Decimal\Operation\Rounding; class NumberTest extends \PHPUnit_Framework_TestCase @@ -16,7 +16,7 @@ class NumberTest extends \PHPUnit_Framework_TestCase /** * Given a valid number in a string - * When constructing a DecimalNumber with it + * When constructing a Number with it * Then it should interpret the sign, decimal and fractional parts correctly * * @param string $number @@ -29,7 +29,7 @@ class NumberTest extends \PHPUnit_Framework_TestCase */ public function testItInterpretsNumbers($number, $expectedSign, $expectedInteger, $expectedFraction, $expectedStr) { - $decimalNumber = new DecimalNumber($number); + $decimalNumber = new Number($number); $this->assertSame($expectedSign, $decimalNumber->getSign(), 'The sign is not as expected'); $this->assertSame($expectedInteger, $decimalNumber->getIntegerPart(), 'The integer part is not as expected'); $this->assertSame( @@ -42,7 +42,7 @@ public function testItInterpretsNumbers($number, $expectedSign, $expectedInteger /** * Given a valid coefficient and exponent - * When constructing a DecimalNumber with them + * When constructing a Number with them * Then it should convert them to the expected string * * @param string $coefficient @@ -53,13 +53,13 @@ public function testItInterpretsNumbers($number, $expectedSign, $expectedInteger */ public function testItInterpretsExponents($coefficient, $exponent, $expectedStr) { - $decimalNumber = new DecimalNumber($coefficient, $exponent); + $decimalNumber = new Number($coefficient, $exponent); $this->assertSame($expectedStr, (string) $decimalNumber); } /** * Given an invalid number - * When constructing a DecimalNumber with it + * When constructing a Number with it * Then an InvalidArgumentException should be thrown * * @param mixed $number @@ -69,12 +69,12 @@ public function testItInterpretsExponents($coefficient, $exponent, $expectedStr) */ public function testItThrowsExceptionWhenGivenInvalidNumber($number) { - new DecimalNumber($number); + new Number($number); } /** * Given an invalid coefficient or exponent - * When constructing a DecimalNumber with them + * When constructing a Number with them * Then an InvalidArgumentException should be thrown * * @param mixed $coefficient @@ -85,11 +85,11 @@ public function testItThrowsExceptionWhenGivenInvalidNumber($number) */ public function testItThrowsExceptionWhenGivenInvalidCoefficientOrExponent($coefficient, $exponent) { - new DecimalNumber($coefficient, $exponent); + new Number($coefficient, $exponent); } /** - * Given a DecimalNumber constructed with a valid number + * Given a Number constructed with a valid number * When casting the number to string * The resulting string should not include leading nor trailing zeroes * @@ -100,17 +100,17 @@ public function testItThrowsExceptionWhenGivenInvalidCoefficientOrExponent($coef */ public function testItDropsNonSignificantDigits($number, $expected) { - $decimalNumber = new DecimalNumber($number); + $decimalNumber = new Number($number); $this->assertSame($expected, (string) $decimalNumber); } /** - * Given a DecimalNumber constructed with a valid number + * Given a Number constructed with a valid number * When rounding it to a specific precision, using a specific rounding mode * The returned string should match the expectation * * @param string $number - * @param int $precision DecimalNumber of decimal characters + * @param int $precision Number of decimal characters * @param string $mode Rounding mode * @param string $expected Expected result * @@ -118,17 +118,17 @@ public function testItDropsNonSignificantDigits($number, $expected) */ public function testPrecision($number, $precision, $mode, $expected) { - $decimalNumber = new DecimalNumber($number); + $decimalNumber = new Number($number); $this->assertSame($expected, (string) $decimalNumber->toPrecision($precision, $mode)); } /** - * Given a DecimalNumber constructed with a valid number + * Given a Number constructed with a valid number * When rounding it to a specific precision, using a specific rounding mode * The returned string should match the expectation * * @param string $number - * @param int $precision DecimalNumber of decimal characters + * @param int $precision Number of decimal characters * @param string $mode Rounding mode * @param string $expected Expected result * @@ -136,7 +136,7 @@ public function testPrecision($number, $precision, $mode, $expected) */ public function testRounding($number, $precision, $mode, $expected) { - $decimalNumber = new DecimalNumber($number); + $decimalNumber = new Number($number); $this->assertSame( $expected, (string) $decimalNumber->round($precision, $mode), @@ -145,7 +145,7 @@ public function testRounding($number, $precision, $mode, $expected) } /** - * Given a DecimalNumber constructed with a valid number + * Given a Number constructed with a valid number * When rounding it to a greater precision than its current one * The returned string should have been padded with the proper number of trailing zeroes * @@ -157,7 +157,7 @@ public function testRounding($number, $precision, $mode, $expected) */ public function testItExtendsPrecisionAsNeeded($number, $precision, $expected) { - $decimalNumber = new DecimalNumber($number); + $decimalNumber = new Number($number); $this->assertSame( $expected, (string) $decimalNumber->toPrecision($precision), @@ -166,12 +166,12 @@ public function testItExtendsPrecisionAsNeeded($number, $precision, $expected) } /** - * Given two instances of DecimalNumber + * Given two instances of Number * When comparing the first one with the second one * Then the result should be true if the instances are equal, and false otherwise * - * @param DecimalNumber $number1 - * @param DecimalNumber $number2 + * @param Number $number1 + * @param Number $number2 * @param string $expected * * @dataProvider provideEqualityTestCases @@ -199,8 +199,8 @@ public function testItIsAbleToTellIfEqual($number1, $number2, $expected) public function testIsAbleToTellIfGreaterThan($number1, $number2, $expected) { $shouldBeGreater = (1 === $expected); - $number1 = new DecimalNumber($number1); - $number2 = new DecimalNumber($number2); + $number1 = new Number($number1); + $number2 = new Number($number2); $this->assertSame($number1->isGreaterThan($number2), $shouldBeGreater); } @@ -219,8 +219,8 @@ public function testIsAbleToTellIfGreaterThan($number1, $number2, $expected) public function testIsAbleToTellIfLowerThan($number1, $number2, $expected) { $shouldBeLower = (-1 === $expected); - $number1 = new DecimalNumber($number1); - $number2 = new DecimalNumber($number2); + $number1 = new Number($number1); + $number2 = new Number($number2); $this->assertSame($number1->isLowerThan($number2), $shouldBeLower); } @@ -237,7 +237,7 @@ public function testIsAbleToTellIfLowerThan($number1, $number2, $expected) */ public function testItTransformsPositiveToNegative($number, $expected) { - $number = (new DecimalNumber($number)) + $number = (new Number($number)) ->toNegative(); $this->assertSame((string) $number, $expected); @@ -255,7 +255,7 @@ public function testItTransformsPositiveToNegative($number, $expected) */ public function testItTransformsNegativeToPositive($number, $expected) { - $number = (new DecimalNumber($number)) + $number = (new Number($number)) ->toPositive(); $this->assertSame((string) $number, $expected); @@ -264,7 +264,13 @@ public function testItTransformsNegativeToPositive($number, $expected) public function provideValidNumbers() { return [ - ['0.0', '', '0', '0', '0'], + [ + 'number' => '0.0', + 'expectedSign' => '', + 'expectedInteger' => '0', + 'expectedFraction' => '0', + 'expectedStr' => '0' + ], ['00000.0', '', '0', '0', '0'], ['0.00000', '', '0', '0', '0'], ['00000.00000', '', '0', '0', '0'], @@ -278,20 +284,41 @@ public function provideValidNumbers() ['01.0', '', '1', '0', '1'], ['01.01', '', '1', '01', '1.01'], ['10.2345', '', '10', '2345', '10.2345'], - [ - '123917549171231.12451028401824', - '', - '123917549171231', - '12451028401824', - '123917549171231.12451028401824' - ], - ['+12351.49273592', '', '12351', '49273592', '12351.49273592'], - ['-12351.49273592', '-', '12351', '49273592', '-12351.49273592'], - ['-12351', '-', '12351', '0', '-12351'], - ['-0', '', '0', '0', '0'], - ['-01', '-', '1', '0', '-1'], - ['-01.0', '-', '1', '0', '-1'], - ['-01.01', '-', '1', '01', '-1.01'], + '123917549171231.12451028401824' => ['123917549171231.12451028401824', '', '123917549171231', '12451028401824', '123917549171231.12451028401824'], + '+12351.49273592' => ['+12351.49273592', '', '12351', '49273592', '12351.49273592'], + '-12351.49273592' => ['-12351.49273592', '-', '12351', '49273592', '-12351.49273592'], + '-12351' => ['-12351', '-', '12351', '0', '-12351'], + '-0' => ['-0', '', '0', '0', '0'], + '-01' => ['-01', '-', '1', '0', '-1'], + '-01.0' => ['-01.0', '-', '1', '0', '-1'], + '-01.01' => ['-01.01', '-', '1', '01', '-1.01'], + '0.1e-1' => ['0.1e-1', '', '0', '01', '0.01'], + '0.1e-2' => ['0.1e-2', '', '0', '001', '0.001'], + '0.1e-3' => ['0.1e-3', '', '0', '0001', '0.0001'], + '0.1e-4' => ['0.1e-4', '', '0', '00001', '0.00001'], + '0.1e-5' => ['0.1e-5', '', '0', '000001', '0.000001'], + '0.01e-1' => ['0.01e-1', '', '0', '001', '0.001'], + '123.01e-1' => ['123.01e-1', '', '12', '301', '12.301'], + '12301e-4' => ['12301e-4', '', '1', '2301', '1.2301'], + '12301e-5' => ['12301e-5', '', '0', '12301', '0.12301'], + '12301e-6' => ['12301e-6', '', '0', '012301', '0.012301'], + '12301e-7' => ['12301e-7', '', '0', '0012301', '0.0012301'], + '12301e-10' => ['12301e-10', '', '0', '0000012301', '0.0000012301'], + '12301e+3' => ['12301e+3', '', '12301000', '0', '12301000'], + '0.1e+1' => ['0.1e+1', '', '1', '0', '1'], + '0.1e+2' => ['0.1e+2', '', '10', '0', '10'], + '0.1e+3' => ['0.1e+3', '', '100', '0', '100'], + '0.1e+4' => ['0.1e+4', '', '1000', '0', '1000'], + '0.1e+5' => ['0.1e+5', '', '10000', '0', '10000'], + '123.01e+1' => ['123.01e+1', '', '1230', '1', '1230.1'], + '123.01e+5' => ['123.01e+5', '', '12301000', '0', '12301000'], + '1.0E+15' => ['1.0E+15', '', '1000000000000000', '0', '1000000000000000'], + '-123.0456E+15' => ['-123.0456E+15', '-', '123045600000000000', '0', '-123045600000000000'], + '-123.04560E+15' => ['-123.04560E+15', '-', '123045600000000000', '0', '-123045600000000000'], + '.1e+2' => ['.1e+2', '', '10', '0', '10'], + '-.1e+2' => ['-.1e+2', '-', '10', '0', '-10'], + '+.1e+2' => ['+.1e+2', '', '10', '0', '10'], + '.01' => ['.01', '', '0', '01', '0.01'], ]; } @@ -340,6 +367,7 @@ public function provideInvalidNumbers() 'NaN with comma' => ['asd,foo'], 'array' => [array()], 'null' => [null], + '1.' => ['1.'], ]; } @@ -482,43 +510,43 @@ public function provideEqualityTestCases() { return [ [ - new DecimalNumber('0'), - new DecimalNumber('0', 5), + new Number('0'), + new Number('0', 5), true ], [ - new DecimalNumber('0.1234'), - new DecimalNumber('1234', 4), + new Number('0.1234'), + new Number('1234', 4), true ], [ - new DecimalNumber('1234.01'), - new DecimalNumber('123401', 2), + new Number('1234.01'), + new Number('123401', 2), true ], [ - new DecimalNumber('-0'), - new DecimalNumber('0'), + new Number('-0'), + new Number('0'), true ], [ - new DecimalNumber('-1234.01'), - new DecimalNumber('-123401', 2), + new Number('-1234.01'), + new Number('-123401', 2), true ], [ - new DecimalNumber('-1234.01'), - new DecimalNumber('123401', 2), + new Number('-1234.01'), + new Number('123401', 2), false ], [ - new DecimalNumber('1234.01'), - new DecimalNumber('-123401', 2), + new Number('1234.01'), + new Number('-123401', 2), false ], [ - new DecimalNumber('1234.01'), - new DecimalNumber('-1234.01'), + new Number('1234.01'), + new Number('-1234.01'), false ], ]; From 35e437ccd290919db1f5f23dbb8420d4976b5001 Mon Sep 17 00:00:00 2001 From: Pablo Borowicz Date: Tue, 16 Jul 2019 19:07:57 -0300 Subject: [PATCH 4/4] Activate tests for PHP 7.2 and 7.3 --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index e9d0c43..b12708e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,6 +8,8 @@ php: - 5.6 - 7.0 - 7.1 + - 7.2 + - 7.3 before_script: - travis_retry composer install --no-interaction