Repository navigation
Apply the behavior changes held back from 1.x - #23
Merged
Merged
Conversation
* Test stringable classes for create
Update README.md
Document meaning of scale
codecov-update
Check PHP84
* 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 Report✅ All modified and coverable lines are covered by tests. 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. 🚀 New features to boost your workflow:
|
…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.
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.
Brings
nextlevel withmaster(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.nextwas 24 commits behindmaster, so most of the diff is that merge. The 2.0 changes themselves:new Decimal('1.5', 0)1.5, scale 01, throws in strict moderound(0, PHP_ROUND_HALF_EVEN)InvalidArgumentExceptionDecimal::create('1.25')->round(1, Decimal::ROUND_FLOOR)11.2Decimal::create('15')->round(-1, Decimal::ROUND_FLOOR)InvalidArgumentException10Decimal::create('10')->mod('0.3')00.1Decimal::create('10')->mod('3.00')11.00new Decimal('1.0e-5')new Decimal('1e3', 2)10001000.00Decimal::create('5')->toScientific()5.e05e0The deprecated
greatherThanOrEquals()stays removed, as it already was onnext.The documentation on this branch describes the 2.0 behavior, and
docs/reference/upgrading.mdhas 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 thanPHP_INT_MAXhas (0.12345678901234567890throws), which the tests pin on purpose.