Repository navigation
Fix parsing, negative zero, rounding and scientific notation edge cases - #20
Merged
Merged
Conversation
Parsing
- Leading zeros survived in several forms: "0010", "-007", "+007" and
"-00" kept them, and "+.5" lost its leading zero.
- Scientific notation was wrong for any mantissa with more than one
integer digit and a negative exponent ("12.5e-1" became 0.125,
"100e-2" became 0.0100) and for mantissas below one with a positive
exponent (".5e1" became 5.5, "0.00012e3" stayed 0.00012).
- An explicit plus in the exponent ("1e+5") was rejected. PHP renders
large floats that way, so new Decimal(1e25) threw.
- "1.0e0" dropped the written zero. A trailing ".0" is now only removed
from the string PHP produces for a float.
- INF and NAN were accepted and stored as the strings "INF" and "NAN".
- A scale of 0 was treated as "no scale": new Decimal('1.5', 0) kept
the fraction and strict mode did not throw. Negative scales were
accepted.
Negative zero
- "-0.0", negate() on zero, truncate() and trim() of small negative
values produced "-0" and reported isNegative() as true while sign()
returned 0. Zero now never carries a sign.
Rounding
- floor() and ceil() moved whole numbers with a fraction of zeros:
floor('-1.0') returned -2 and ceil('1.0') returned 2.
- ROUND_FLOOR and ROUND_CEIL ignored the requested scale.
- An unknown round mode silently fell back to half up. It now throws,
as does a negative scale.
Conversion
- toScientific() returned ".e-1" for zero, went through float division
and so lost digits beyond 15 for large integers, and returned
"1.250e2" for 120.50.
- toInt() refused PHP_INT_MIN.
Operands
- mod() and pow() passed the operand to bcmath unparsed, so values in
scientific notation failed. mod() now defaults to the larger scale of
both operands, like add(); 10 mod 0.3 returned 0 before.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #20 +/- ##
============================================
+ Coverage 94.14% 95.65% +1.50%
- Complexity 104 114 +10
============================================
Files 1 1
Lines 239 230 -9
============================================
- Hits 225 220 -5
+ Misses 14 10 -4 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
PHP 8.1 has been out of security support since the end of 2025. The floor stops at 8.2 because dereuromark/cakephp-decimal supports 8.2 and should keep receiving new releases of this library. - PHPUnit ^11.5 || ^12 || ^13, the 10.x line was only needed for 8.1. - php-collective/code-sniffer 0.2 to 0.6. The new rules want attributes imported and docblock types that agree with the native hint, so the operand docblocks now say self like the signatures do. - CI runs 8.2, 8.3, 8.4 and 8.5 on ubuntu-latest. - The Docker image no longer downloads PHPUnit 10.4 and PHPStan 1.10 phars. The Makefile runs the Composer scripts and never used them. - composer.json gains keywords and support links.
A run of the old and new class over the same 10,000 calls showed four of the earlier changes altering results for ordinary input, with no error to point at the cause. They move to the next major and 1.x keeps its behavior: - round() with an unsupported mode rounds half up again, where the fix made every such call throw. - ROUND_FLOOR and ROUND_CEIL round to a whole number whatever scale is passed, as the docblock has always said. - A scale of 0 in the constructor is read as no scale and does not cut off decimals. - mod() defaults to the scale of the value it is called on. The fixes for wrong results stay: floor() and ceil() on whole numbers with decimals, operand parsing in mod() and pow(), the negative scale check.
Same reasoning as the previous commit, applied to the changes that
only alter the string a caller gets back:
- A string mantissa of "1.0" is read as "1" again, so "1.0e-5" keeps
a scale of 5.
- Scientific input with a positive exponent and a scale pads the way
it did before.
- toScientific() keeps the radix mark on a mantissa without decimals
("5.e0"). The wrong results stay fixed: zero, lost digits on large
integers, and values such as 120.50.
All three move to the next major.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This branch also carries #21 (PHP 8.2 requirement, PHPUnit and code sniffer updates, CI matrix 8.2 to 8.5), which was merged into it.
A pass over
Decimalfor wrong results on valid input. Each row below was reproduced on master first and has a regression test.Old and new class were run side by side over about 10,000 calls. Every remaining difference is either a wrong result that is now right, or an exception where garbage came back before. Corrections that would change results or output for ordinary input are not in here; they are in #23 for the next major.
Parsing
'0010','-007','+007'0010,-007,00710,-7,7'+.5'.50.5'12.5e-1'0.1251.25'100e-2'0.01001.00'.5e1'5.55'0.00012e3'0.000120.12'1e+5'1000001e25(float)10000000000000000000000000INF,NANINF/NANInvalidArgumentExceptionnew Decimal('1.5', -1)InvalidArgumentExceptionNegative zero
'-0.0',negate()on zero andtruncate()/trim()of small negative values produced-0. Those objects reportedisNegative() === truewhilesign()returned0. Zero no longer carries a sign.Rounding
Decimal::create('-1.0')->floor()-2-1Decimal::create('1.0')->ceil()21Decimal::create('0.00')->ceil()10round(-1)ValueErrorfrom bcmath, or an object with scale -1InvalidArgumentExceptionConversion
Decimal::create('0')->toScientific().e-10e0Decimal::create('120.50')->toScientific()1.250e21.2050e2Decimal::create('12345678901234567')->toScientific()1.2345678901235e161.2345678901234567e16Decimal::create((string)PHP_INT_MIN)->toInt()TypeErrorPHP_INT_MINOperands
mod()andpow()handed the operand to bcmath unparsed, somod('1e1')failed. Both now parse it like the other operations do.The protected method signatures are unchanged.