Skip to content

Commit 13d1653

Browse files
authored
Merge branch 'main' into dependabot/github_actions/stefanzweifel/git-auto-commit-action-7.2.0
2 parents e32e904 + 3b6ddcd commit 13d1653

7 files changed

Lines changed: 491 additions & 260 deletions

File tree

.github/workflows/ci.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,8 @@ jobs:
9898
# scale_ini test skipped: bcmath.scale INI setting ignored without native extension
9999
# gh20006 test skipped: requires BcMath\Number OOP API (not supported by polyfill)
100100
# bcround_precision_bounds test skipped: requires BcMath\Number OOP API (not supported by polyfill)
101-
docker run --rm -v $PWD:/app bcmath-phpt-test:${{ matrix.php-version }} --skip bcdivmod,bug54598,bug72093,bug75178,gh15968,gh16262,scale_ini,bcmul_check_overflow,bug78878,bcpow_error1,bcpow_error2,bcpow_error3,bcpowmod_error,bcpowmod_with_mod_1,bcpowmod_zero_modulus,bcround_all,bcround_away_from_zero,bcround_ceiling,bcround_early_return,bcround_floor,bcround_toward_zero,bcscale_variation003,bcdivmod_by_zero,gh20006,bcround_precision_bounds
101+
# bcround directional modes + bcdivmod are now implemented, so those phpt tests no longer need skipping.
102+
docker run --rm -v $PWD:/app bcmath-phpt-test:${{ matrix.php-version }} --skip bug54598,bug72093,bug75178,gh15968,gh16262,scale_ini,bcmul_check_overflow,bug78878,bcpow_error1,bcpow_error2,bcpow_error3,bcpowmod_error,bcpowmod_with_mod_1,bcpowmod_zero_modulus,bcscale_variation003,gh20006,bcround_precision_bounds
102103
strategy:
103104
fail-fast: false
104105
matrix:

README.md

Lines changed: 60 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
## 🚀 Features
1818

1919
- ✅ Complete bcmath extension polyfill for PHP 8.1+
20-
- ✅ Supports all PHP 8.4+ bcmath functions including `bcfloor()`, `bcceil()`, and `bcround()`
20+
- ✅ Supports all PHP 8.4+ bcmath functions including `bcfloor()`, `bcceil()`, `bcround()`, and `bcdivmod()`
2121
- ✅ PHP 8.5 compatibility tested and verified
2222
- ✅ Zero dependencies in production (uses phpseclib for arbitrary precision math)
2323
- ✅ Seamless fallback when bcmath extension is not available
@@ -54,6 +54,13 @@ echo bcround('2.5', 0, \RoundingMode::HalfAwayFromZero); // 3
5454
echo bcround('2.5', 0, \RoundingMode::HalfTowardsZero); // 2
5555
echo bcround('2.5', 0, \RoundingMode::HalfEven); // 2
5656
echo bcround('2.5', 0, \RoundingMode::HalfOdd); // 3
57+
echo bcround('2.5', 0, \RoundingMode::TowardsZero); // 2
58+
echo bcround('2.5', 0, \RoundingMode::AwayFromZero); // 3
59+
echo bcround('2.5', 0, \RoundingMode::PositiveInfinity); // 3
60+
echo bcround('2.5', 0, \RoundingMode::NegativeInfinity); // 2
61+
62+
// bcdivmod() returns [quotient, remainder]
63+
[$quot, $rem] = bcdivmod('17', '5'); // ['3', '2']
5764
```
5865

5966
## 📋 Supported Functions
@@ -74,20 +81,39 @@ echo bcround('2.5', 0, \RoundingMode::HalfOdd); // 3
7481
- `bcfloor()` - Round down to the nearest integer
7582
- `bcceil()` - Round up to the nearest integer
7683
- `bcround()` - Round to a specified precision with configurable rounding modes
84+
- `bcdivmod()` - Get the quotient and remainder of a division in a single call
7785

7886
#### RoundingMode Enum Support
79-
The `bcround()` function supports PHP 8.4's `RoundingMode` enum through a polyfill for PHP 8.1-8.3:
87+
The `bcround()` function supports PHP 8.4's `RoundingMode` enum through a polyfill for PHP 8.1-8.3.
88+
All eight native modes are implemented with exact, arbitrary-precision string arithmetic (no float
89+
fallback), so results match the native `bcround()` even for very large or high-precision numbers:
8090

81-
**Supported Modes:**
8291
- `RoundingMode::HalfAwayFromZero` (equivalent to `PHP_ROUND_HALF_UP`)
8392
- `RoundingMode::HalfTowardsZero` (equivalent to `PHP_ROUND_HALF_DOWN`)
8493
- `RoundingMode::HalfEven` (equivalent to `PHP_ROUND_HALF_EVEN`)
8594
- `RoundingMode::HalfOdd` (equivalent to `PHP_ROUND_HALF_ODD`)
95+
- `RoundingMode::TowardsZero`
96+
- `RoundingMode::AwayFromZero`
97+
- `RoundingMode::NegativeInfinity`
98+
- `RoundingMode::PositiveInfinity`
99+
100+
The legacy integer `PHP_ROUND_*` constants are also accepted for the four half-modes.
101+
102+
> **Note:** The polyfilled `RoundingMode` is a *pure* enum, matching native PHP 8.4 (it has no
103+
> backing value; `->value` / `from()` / `tryFrom()` are intentionally unavailable).
104+
105+
#### Interoperability with `symfony/polyfill-php84`
106+
107+
`symfony/polyfill-php84` also declares `bcceil()`, `bcfloor()`, `bcround()` and `bcdivmod()`, but only
108+
when the native bcmath extension is **already loaded** (its purpose is to backport the new PHP 8.4
109+
functions to older PHP that already has bcmath). This package instead provides the full bcmath API for
110+
environments **without** the extension.
86111

87-
**Not Yet Supported:**
88-
- `RoundingMode::TowardsZero` - Throws `ValueError` (planned for future release)
89-
- `RoundingMode::AwayFromZero` - Throws `ValueError` (planned for future release)
90-
- `RoundingMode::NegativeInfinity` - Throws `ValueError` (planned for future release)
112+
When both packages are installed on an environment that *does* have the bcmath extension and runs
113+
**PHP 8.2/8.3**, both declare `bcceil()`/`bcfloor()`/`bcround()`; whichever autoloads first wins (the
114+
`function_exists()` guards prevent a fatal redeclaration). This package's rounding is implemented with a
115+
native-compatible string algorithm, so the numeric results are identical regardless of which
116+
implementation is used. On environments without the extension only this package is active.
91117

92118
## ⚡ Performance
93119

@@ -193,34 +219,33 @@ While the polyfill is significantly slower than native bcmath:
193219
- PHP >= 7.3.0: Use `bcscale()` without arguments
194220
- PHP < 7.3.0: Use `max(0, strlen(bcadd('0', '0')) - 2)`
195221

196-
### RoundingMode Enum Limitations
197-
- Three `RoundingMode` enum values are not yet implemented:
198-
- `RoundingMode::TowardsZero` - Will throw `ValueError`
199-
- `RoundingMode::AwayFromZero` - Will throw `ValueError`
200-
- `RoundingMode::NegativeInfinity` - Will throw `ValueError`
201-
- These modes are planned for implementation in a future release
202-
- Use traditional `PHP_ROUND_*` constants as alternatives when needed
203-
204-
## 🔄 Key Differences from phpseclib/bcmath_compat
205-
206-
| Feature | phpseclib/bcmath_compat | bcmath-polyfill |
207-
|---------------------------|--------------------------------|-------------------------------------|
208-
| **PHP 8.4 functions** | ❌ Not supported | ✅ Full support |
209-
| `bcfloor()` |||
210-
| `bcceil()` |||
211-
| `bcround()` |||
212-
| **RoundingMode enum** | ❌ Not supported | ✅ Partial (4/7 modes) |
213-
| `HalfAwayFromZero` |||
214-
| `HalfTowardsZero` |||
215-
| `HalfEven` |||
216-
| `HalfOdd` |||
217-
| `TowardsZero` || ⏳ Planned |
218-
| `AwayFromZero` || ⏳ Planned |
219-
| `NegativeInfinity` || ⏳ Planned |
220-
| **PHP 8.2+ deprecations** | ⚠️ Warnings | ✅ Fixed |
221-
| **Test suite pollution** | ⚠️ Issues | ✅ Fixed |
222-
| **Active maintenance** | ❌ Limited | ✅ Active |
223-
| **CI/CD (PHP versions)** | GitHub Actions (8.1, 8.2, 8.3) | GitHub Actions (8.1, 8.2, 8.3, 8.4, 8.5) |
222+
## 🔄 Key Differences from phpseclib/bcmath_compat and symfony/polyfill-php84
223+
224+
The three libraries target different goals. `phpseclib/bcmath_compat` is the classic pre-8.4 polyfill.
225+
`symfony/polyfill-php84` backports **only** the new PHP 8.4 functions and activates them **only when the
226+
native bcmath extension is already loaded**. `bcmath-polyfill` provides the complete bcmath API —
227+
including the PHP 8.4 additions — for environments **with or without** the extension.
228+
229+
| Feature | phpseclib/bcmath_compat | symfony/polyfill-php84 | bcmath-polyfill |
230+
|------------------------------------------------|--------------------------------|---------------------------------|------------------------------------------|
231+
| Classic bc functions (`bcadd`, `bcmul`, …) || ❌ Not provided ||
232+
| Works **without** the bcmath extension || ❌ Requires the extension ||
233+
| `bcfloor()` / `bcceil()` / `bcround()` || ✅ (extension required) ||
234+
| `bcdivmod()` || ✅ (delegates to native bc) | ✅ (works without the extension) |
235+
| **RoundingMode enum** (all 8 modes) ||||
236+
| Arbitrary-precision rounding (no float fallback)|| ✅ (pure string algorithm) | ✅ (pure string algorithm) |
237+
| `RoundingMode` is a *pure* enum (native shape) ||||
238+
| Implementation of rounding || Pure string digits | Pure string digits (phpseclib elsewhere) |
239+
| Extra dependency | phpseclib | none | phpseclib |
240+
| **PHP 8.2+ deprecations** | ⚠️ Warnings | ✅ Fixed | ✅ Fixed |
241+
| **Active maintenance** | ❌ Limited | ✅ Active | ✅ Active |
242+
| **CI/CD (PHP versions)** | GitHub Actions (8.1, 8.2, 8.3) | 7.2+ | GitHub Actions (8.1, 8.2, 8.3, 8.4, 8.5) |
243+
244+
> When `bcmath-polyfill` and `symfony/polyfill-php84` are installed together on an environment that has
245+
> the bcmath extension and runs PHP 8.2/8.3, both declare `bcceil()`/`bcfloor()`/`bcround()` and whichever
246+
> autoloads first wins (guarded by `function_exists()`, so no fatal redeclaration). Because this package's
247+
> rounding uses a native-compatible string algorithm, the results are identical either way. See
248+
> [Interoperability with `symfony/polyfill-php84`](#interoperability-with-symfonypolyfill-php84).
224249
225250
### Migration from phpseclib/bcmath_compat
226251

lib/RoundingMode.php

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33
/**
44
* RoundingMode enum polyfill for PHP 8.1-8.3.
55
*
6-
* This enum provides the same interface as PHP 8.4's native RoundingMode enum,
7-
* but TowardsZero, AwayFromZero, NegativeInfinity, and PositiveInfinity will throw exceptions
8-
* when used in PHP < 8.4 to maintain compatibility expectations.
6+
* This enum mirrors PHP 8.4's native RoundingMode, which is a *pure* enum
7+
* (no backing type). Keeping it backing-less here ensures the polyfill behaves
8+
* identically to the native enum on 8.4+ (e.g. no `->value`, `from()`/`tryFrom()`),
9+
* avoiding an 8.1-8.3 vs. 8.4+ inconsistency.
910
*
1011
* The enum is only defined if:
1112
* - PHP version is 8.1 or higher (enum support)
@@ -18,15 +19,15 @@
1819
// PHP to >=8.1, so PHPStan always infers this as always-true; silence that.
1920
// @phpstan-ignore-next-line booleanAnd.rightAlwaysTrue
2021
if (!class_exists('RoundingMode', false) && version_compare(PHP_VERSION, '8.1', '>=')) {
21-
enum RoundingMode: string
22+
enum RoundingMode
2223
{
23-
case HalfAwayFromZero = 'half_away_from_zero';
24-
case HalfTowardsZero = 'half_towards_zero';
25-
case HalfEven = 'half_even';
26-
case HalfOdd = 'half_odd';
27-
case TowardsZero = 'towards_zero';
28-
case AwayFromZero = 'away_from_zero';
29-
case NegativeInfinity = 'negative_infinity';
30-
case PositiveInfinity = 'positive_infinity';
24+
case HalfAwayFromZero;
25+
case HalfTowardsZero;
26+
case HalfEven;
27+
case HalfOdd;
28+
case TowardsZero;
29+
case AwayFromZero;
30+
case NegativeInfinity;
31+
case PositiveInfinity;
3132
}
3233
}

lib/bcmath.php

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -136,19 +136,19 @@ function bcsub(string $left_operand, string $right_operand, ?int $scale = null):
136136
/**
137137
* Round down to the nearest integer (PHP 8.4+).
138138
*/
139-
function bcfloor(string $operand): string
139+
function bcfloor(string $num): string
140140
{
141-
return BCMath::floor($operand);
141+
return BCMath::floor($num);
142142
}
143143
}
144144

145145
if (!function_exists('bcceil')) {
146146
/**
147147
* Round up to the nearest integer (PHP 8.4+).
148148
*/
149-
function bcceil(string $operand): string
149+
function bcceil(string $num): string
150150
{
151-
return BCMath::ceil($operand);
151+
return BCMath::ceil($num);
152152
}
153153
}
154154

@@ -157,11 +157,25 @@ function bcceil(string $operand): string
157157
* Round to a given decimal place (PHP 8.4+).
158158
*
159159
* @param int $precision optional
160-
* @param int $mode optional
160+
* @param int|\RoundingMode $mode optional
161161
*/
162-
function bcround(string $operand, int $precision = 0, $mode = PHP_ROUND_HALF_UP): string
162+
function bcround(string $num, int $precision = 0, $mode = PHP_ROUND_HALF_UP): string
163163
{
164-
return BCMath::round($operand, $precision, $mode);
164+
return BCMath::round($num, $precision, $mode);
165+
}
166+
}
167+
168+
if (!function_exists('bcdivmod')) {
169+
/**
170+
* Get the quotient and remainder of dividing two arbitrary precision numbers (PHP 8.4+).
171+
*
172+
* @param null|int $scale optional
173+
*
174+
* @return string[] the quotient (index 0) and remainder (index 1)
175+
*/
176+
function bcdivmod(string $num1, string $num2, ?int $scale = null): array
177+
{
178+
return BCMath::divmod($num1, $num2, $scale);
165179
}
166180
}
167181

phpstan.neon.dist

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ parameters:
2929
message: "#Match arm comparison between RoundingMode and '(halfawayfromzero|halftowardszero|halfeven|halfodd|towardszero|awayfromzero|negativeinfinity|positiveinfinity)' is always false\\.#"
3030
-
3131
identifier: argument.type
32-
message: "#Parameter \\#3 \\$mode of (static method bcmath_compat\\\\BCMath::round\\(\\)|function bcround) expects int\\|RoundingMode, (string|int) given\\.#"
32+
message: "#Parameter \\#3 \\$mode of (static method bcmath_compat\\\\BCMath::round\\(\\)|function bcround) expects (int\\|RoundingMode|RoundingMode), (string|int) given\\.#"
3333
paths:
3434
- src/BCMath.php
3535
- tests/BCMathTest.php
@@ -61,3 +61,10 @@ parameters:
6161
identifier: property.nonObject
6262
path: tests/BCMathTest.php
6363
message: "#Cannot access property \\$name on string\\.#"
64+
# On PHP 8.1 PHPStan narrows $function to the six non-pow operations and
65+
# reports this in_array() guard as always-true, while PHP 8.5 does not.
66+
# The guard deliberately excludes bcpow() from the second-argument check,
67+
# so it must stay. Pre-existing on main; ignored to keep the 8.1 matrix green.
68+
-
69+
path: tests/BCMathTest.php
70+
message: "#Call to function in_array\\(\\) with arguments .* and true will always evaluate to true\\.#"

0 commit comments

Comments
 (0)