Skip to content

Never drop a digit without being told how - #27

Merged
dereuromark merged 1 commit into
nextfrom
next-2.0-strictness
Oct 11, 2026
Merged

dereuromark merged 1 commit into
nextfrom
next-2.0-strictness

Conversation

@dereuromark

Copy link
Copy Markdown
Contributor

The remaining 2.0 behavior, built on one rule: the library never drops a digit unless the caller says how. A result is exact, or a rounding mode is named, or the call throws.

No silent digit loss

Call 1.x 2.0
Decimal::create('10')->divide('3', 4) 3.3333 RoundingNecessaryException
Decimal::create('10')->divide('3', 4, Decimal::ROUND_HALF_UP) 3.3333 3.3333
Decimal::create('10')->divide('4', 2) 2.50 2.50
Decimal::create('18')->sqrt() 4 RoundingNecessaryException
Decimal::create('1.5')->pow(2) 2.2 2.25
Decimal::create('19.99')->multiply('0.19', 2) 3.79 RoundingNecessaryException
Decimal::create('0.129', 2) 0.12 RoundingNecessaryException
Decimal::create('0.120', 2) 0.12 0.12
  • New ROUND_UNNECESSARY mode, the default of divide(), sqrt(), average() and of the scale argument on add(), subtract(), multiply(), mod() and pow(). Dropping zeros is never an error.
  • ROUND_DOWN reproduces the 1.x cut-off wherever it is wanted.
  • sqrt() and pow() round in every mode.
  • The $strict argument is removed, its check is always on.

Scale and string agree

Decimal::create('0.1', 4) prints 0.1000. The scale of an object is always the number of decimals in its string.

Floats

The constructor, create() and every method taking a value reject floats with an exception that points to createFromFloat().

The parameter types still list float. Without it, PHP converts a float to an integer for callers that do not declare strict types, so Decimal::create(1.5) would become 1 without any error. Keeping the type and throwing is the only way to reject it for all callers.

Exceptions

Six classes under PhpCollective\DecimalObject\Exception, all implementing DecimalException:

Exception Extends
InvalidNumberException \InvalidArgumentException
InvalidArgumentException \InvalidArgumentException
RoundingNecessaryException \RuntimeException
DivisionByZeroException \DivisionByZeroError
NegativeNumberException \DomainException
OverflowException \OverflowException

bcmath's ValueError no longer surfaces from sqrt() of a negative value or a fractional pow() exponent. toInt() out of range throws OverflowException where it threw TypeError.

Native RoundingMode

Every mode argument is typed RoundingMode|int and accepts PHP 8.4's enum next to the constants:

Decimal::create('2')->divide('3', 2, \RoundingMode::HalfAwayFromZero);

The enum is matched by case name, so the class loads and works on PHP 8.2 and 8.3, where only the constants exist. Code written with the constants needs no change later. A copy of the enum in tests/stubs is scanned by PHPStan only, so the type can be analysed on those versions.

Other

  • toFloat() returns the nearest float for long fractions and throws only beyond the float range. isBigDecimal() is removed.
  • __serialize() / __unserialize() store the decimal string. Data serialized by 1.x is still read, including a declared scale.
  • pow() exponents are limited to MAX_EXPONENT in either direction.

The class is not final and the properties stay protected.

Documentation is updated throughout, and docs/reference/upgrading.md lists every change with the 1.x and 2.0 result and what to do about it.

One note for dereuromark/cakephp-decimal: it passes database and request values straight to Decimal::create(). Any place that can hand it a float needs to go through createFromFloat() or a string before the plugin allows ^2.0.

2.0 gets one rule: a result is exact, or the caller names the rounding.

Arithmetic
- add(), subtract(), multiply() and mod() return the exact result. A
  scale argument that would lose a digit other than zero needs a
  rounding mode as third argument, otherwise
  RoundingNecessaryException is thrown. 1.x cut off.
- divide(), sqrt() and average() default to the new ROUND_UNNECESSARY,
  so an inexact result has to be rounded on purpose. ROUND_DOWN gives
  the 1.x result.
- pow() returns the exact power for a positive exponent and divides
  for a negative one. sqrt() rounds in any mode, decided on the exact
  comparison of the candidate with the value.

Values
- A scale passed to the constructor pads the string and refuses to
  lose digits. The string therefore always shows the scale, and the
  strict flag is gone because its check is always on.
- Floats are rejected by the constructor and by every method that
  takes a value. The types still list float: without it PHP converts a
  float to an integer for callers not using strict types, which would
  truncate 1.5 to 1 without any error. createFromFloat() is the one
  way in.
- toFloat() returns the nearest float instead of refusing long
  fractions. isBigDecimal() is removed.

Exceptions
- Own classes under Exception\, all implementing DecimalException and
  each extending the SPL type thrown before where there was one.
  bcmath's ValueError no longer surfaces from sqrt() of a negative
  value or from a fractional pow() exponent.

Rounding modes
- Every mode argument also accepts PHP 8.4's native RoundingMode. The
  enum is matched by case name, so the class loads on 8.2 and 8.3. A
  copy of the enum under tests/stubs is scanned by PHPStan only, to
  analyse the type on those versions.

Serialization
- __serialize() stores the decimal string. __unserialize() still reads
  the property based format of 1.x, including a declared scale larger
  than the stored decimals.
@dereuromark dereuromark added this to the 2.0.0 milestone Oct 11, 2026
@dereuromark dereuromark added the enhancement New feature or request label Oct 11, 2026
@codecov

codecov Bot commented Oct 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 86.66667% with 18 lines in your changes missing coverage. Please review.
✅ Project coverage is 95.59%. Comparing base (613a622) to head (f4a3e59).
⚠️ Report is 1 commits behind head on next.

Files with missing lines Patch % Lines
src/Decimal.php 86.66% 18 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##               next      #27      +/-   ##
============================================
- Coverage     98.00%   95.59%   -2.42%     
- Complexity      190      201      +11     
============================================
  Files             1        1              
  Lines           401      454      +53     
============================================
+ Hits            393      434      +41     
- Misses            8       20      +12     

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

@dereuromark
dereuromark merged commit a11497a into next Oct 11, 2026
5 of 7 checks passed
@dereuromark
dereuromark deleted the next-2.0-strictness branch October 11, 2026 15:39
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.

1 participant