Skip to content

Apply the behavior changes held back from 1.x - #23

Merged
dereuromark merged 36 commits into
nextfrom
next-bc-changes
Oct 11, 2026
Merged

dereuromark merged 36 commits into
nextfrom
next-bc-changes

Conversation

@dereuromark

@dereuromark dereuromark commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Brings next level with master (the 1.4 fixes, API additions and documentation site) and applies the corrections that were held back from 1.x because they change results or output for ordinary input without an error.

next was 24 commits behind master, so most of the diff is that merge. The 2.0 changes themselves:

Call 1.x 2.0
new Decimal('1.5', 0) 1.5, scale 0 1, throws in strict mode
round(0, PHP_ROUND_HALF_EVEN) rounds half up InvalidArgumentException
Decimal::create('1.25')->round(1, Decimal::ROUND_FLOOR) 1 1.2
Decimal::create('15')->round(-1, Decimal::ROUND_FLOOR) InvalidArgumentException 10
Decimal::create('10')->mod('0.3') 0 0.1
Decimal::create('10')->mod('3.00') 1 1.00
new Decimal('1.0e-5') scale 5 scale 6
new Decimal('1e3', 2) 1000 1000.00
Decimal::create('5')->toScientific() 5.e0 5e0

The deprecated greatherThanOrEquals() stays removed, as it already was on next.

The documentation on this branch describes the 2.0 behavior, and docs/reference/upgrading.md has a "From 1.x to 2.0" section with the table above and what to do for each row.

Not included and still open for 2.0: toFloat() refuses values with more fractional digits than PHP_INT_MAX has (0.12345678901234567890 throws), which the tests pin on purpose.

dereuromark and others added 30 commits November 25, 2023 16:44
* Test stringable classes for create
* Fix up rounding for small values.

* Fix up rounding for small values.
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.
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.
These correct real defects but change results or output for ordinary
input without raising an error, so they wait for the major release:

- A scale of 0 in the constructor cuts off decimals like any other
  scale, and throws in strict mode. 1.x reads 0 as "no scale".
- round() throws on an unsupported mode. 1.x silently rounds half up.
- ROUND_FLOOR and ROUND_CEIL round at the requested scale. 1.x always
  rounds to a whole number.
- mod() defaults to the larger scale of both operands, like add().
  1.x uses the scale of the left operand, so 10 mod 0.3 is 0.
- A string mantissa keeps a written zero: "1.0e-5" has a scale of 6.
- Scientific input with a scale is padded to that scale for positive
  exponents too.
- toScientific() drops the radix mark when the mantissa has no
  decimals: "5e0" where 1.x prints "5.e0".
@codecov

codecov Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
⚠️ Please upload report for BASE (next@622abf5). Learn more about missing BASE report.

Additional details and impacted files
@@           Coverage Diff           @@
##             next      #23   +/-   ##
=======================================
  Coverage        ?   97.73%           
  Complexity      ?      171           
=======================================
  Files           ?        1           
  Lines           ?      353           
  Branches        ?        0           
=======================================
  Hits            ?      345           
  Misses          ?        8           
  Partials        ?        0           

☔ 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.

…es (#20)

* Fix parsing, negative zero, rounding and scientific notation edge cases

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.

* Require PHP 8.2 and update the dev tooling (#21)

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.

* Keep four behavior changes out of 1.x

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.

* Keep three more output changes out of 1.x

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.
The docs were a single README listing method names. This adds a
VitePress site under docs/, deployed to GitHub Pages by a workflow on
pushes to master, with the same setup dereuromark/cakephp-decimal uses.

Guide pages cover installation, creating decimals, arithmetic, scale,
rounding, comparison and conversion. Reference pages hold the full API
table, integrations, alternatives (moved over from the wiki) and
upgrade notes for the behavior changes since 1.3.

The root README now points at the site and keeps a short summary. The
contributing notes moved there from docs/README.md. docs/ is excluded
from the Composer dist archive.
All additions. A run of the previous and the new class over the same
10,000 calls differs in one place only: round() with a negative scale
returned an exception and now rounds to tens, hundreds and so on.

Rounding
- ROUND_HALF_DOWN, ROUND_HALF_EVEN, ROUND_HALF_ODD, ROUND_UP and
  ROUND_DOWN. They get their own values (9 to 13) and not those of the
  PHP_ROUND_* constants: round() treats unknown modes as half up in
  1.x, so reusing 2, 3 and 4 would silently change the result for
  callers that pass PHP_ROUND_HALF_EVEN today.
- withScale() pads or rounds to an exact scale and applies every mode
  at that scale, including floor and ceil, which round() cannot do in
  1.x.
- round() accepts a negative scale for the non-directed modes.

Input
- Scientific notation is limited to exponents up to MAX_EXPONENT
  (10000). "1e1000000000" used to allocate a billion digits.
- tryCreate() returns null for null and non numeric input.

Calculation
- shift() moves the decimal point.
- sum(), average(), min() and max() over any iterable.
- allocate() and split() divide a value into shares that sum to it
  exactly, at the scale of the value.
- between(), clamp(), isEven() and isOdd().

All bcmath calls in the new code pass their scale. A global bcscale()
set by the application would otherwise leak into the results.
All additions. A run of the previous and the new class over the same
10,000 calls differs in one place only: round() with a negative scale
returned an exception and now rounds to tens, hundreds and so on.

Rounding
- ROUND_HALF_DOWN, ROUND_HALF_EVEN, ROUND_HALF_ODD, ROUND_UP and
  ROUND_DOWN. They get their own values (9 to 13) and not those of the
  PHP_ROUND_* constants: round() treats unknown modes as half up in
  1.x, so reusing 2, 3 and 4 would silently change the result for
  callers that pass PHP_ROUND_HALF_EVEN today.
- withScale() pads or rounds to an exact scale and applies every mode
  at that scale, including floor and ceil, which round() cannot do in
  1.x.
- round() accepts a negative scale for the non-directed modes.

Input
- Scientific notation is limited to exponents up to MAX_EXPONENT
  (10000). "1e1000000000" used to allocate a billion digits.
- tryCreate() returns null for null and non numeric input.

Calculation
- shift() moves the decimal point.
- sum(), average(), min() and max() over any iterable.
- allocate() and split() divide a value into shares that sum to it
  exactly, at the scale of the value.
- between(), clamp(), isEven() and isOdd().

All bcmath calls in the new code pass their scale. A global bcscale()
set by the application would otherwise leak into the results.
@dereuromark
dereuromark merged commit a204053 into next Oct 11, 2026
5 checks passed
@dereuromark
dereuromark deleted the next-bc-changes branch October 11, 2026 14:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants