Skip to content

Fix parsing, negative zero, rounding and scientific notation edge cases - #20

Merged
dereuromark merged 4 commits into
masterfrom
bugfix/edge-cases
Oct 11, 2026
Merged

dereuromark merged 4 commits into
masterfrom
bugfix/edge-cases

Conversation

@dereuromark

@dereuromark dereuromark commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

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 Decimal for 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

Input Before Now
'0010', '-007', '+007' 0010, -007, 007 10, -7, 7
'+.5' .5 0.5
'12.5e-1' 0.125 1.25
'100e-2' 0.0100 1.00
'.5e1' 5.5 5
'0.00012e3' 0.00012 0.12
'1e+5' exception 100000
1e25 (float) exception 10000000000000000000000000
INF, NAN stored as INF / NAN InvalidArgumentException
new Decimal('1.5', -1) scale -1 InvalidArgumentException

Negative zero

'-0.0', negate() on zero and truncate() / trim() of small negative values produced -0. Those objects reported isNegative() === true while sign() returned 0. Zero no longer carries a sign.

Rounding

Call Before Now
Decimal::create('-1.0')->floor() -2 -1
Decimal::create('1.0')->ceil() 2 1
Decimal::create('0.00')->ceil() 1 0
round(-1) ValueError from bcmath, or an object with scale -1 InvalidArgumentException

Conversion

Call Before Now
Decimal::create('0')->toScientific() .e-1 0e0
Decimal::create('120.50')->toScientific() 1.250e2 1.2050e2
Decimal::create('12345678901234567')->toScientific() 1.2345678901235e16 1.2345678901234567e16
Decimal::create((string)PHP_INT_MIN)->toInt() TypeError PHP_INT_MIN

Operands

mod() and pow() handed the operand to bcmath unparsed, so mod('1e1') failed. Both now parse it like the other operations do.

The protected method signatures are unchanged.

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.
@dereuromark dereuromark added the bug Something isn't working label Oct 11, 2026
Copilot AI balanced review requested due to automatic review settings October 11, 2026 14:27

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@codecov

codecov Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 95.65%. Comparing base (49612bb) to head (763e5bc).

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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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.
Copilot AI balanced review requested due to automatic review settings October 11, 2026 14:31

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

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.
Copilot AI balanced review requested due to automatic review settings October 11, 2026 14:40

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

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.
Copilot AI balanced review requested due to automatic review settings October 11, 2026 14:41

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@dereuromark
dereuromark merged commit 5fd1e0a into master Oct 11, 2026
7 checks passed
@dereuromark
dereuromark deleted the bugfix/edge-cases branch October 11, 2026 14:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants