Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,8 @@ jobs:
# scale_ini test skipped: bcmath.scale INI setting ignored without native extension
# gh20006 test skipped: requires BcMath\Number OOP API (not supported by polyfill)
# bcround_precision_bounds test skipped: requires BcMath\Number OOP API (not supported by polyfill)
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
# bcround directional modes + bcdivmod are now implemented, so those phpt tests no longer need skipping.
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
strategy:
fail-fast: false
matrix:
Expand Down
95 changes: 60 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
## 🚀 Features

- ✅ Complete bcmath extension polyfill for PHP 8.1+
- ✅ Supports all PHP 8.4+ bcmath functions including `bcfloor()`, `bcceil()`, and `bcround()`
- ✅ Supports all PHP 8.4+ bcmath functions including `bcfloor()`, `bcceil()`, `bcround()`, and `bcdivmod()`
- ✅ PHP 8.5 compatibility tested and verified
- ✅ Zero dependencies in production (uses phpseclib for arbitrary precision math)
- ✅ Seamless fallback when bcmath extension is not available
Expand Down Expand Up @@ -54,6 +54,13 @@ echo bcround('2.5', 0, \RoundingMode::HalfAwayFromZero); // 3
echo bcround('2.5', 0, \RoundingMode::HalfTowardsZero); // 2
echo bcround('2.5', 0, \RoundingMode::HalfEven); // 2
echo bcround('2.5', 0, \RoundingMode::HalfOdd); // 3
echo bcround('2.5', 0, \RoundingMode::TowardsZero); // 2
echo bcround('2.5', 0, \RoundingMode::AwayFromZero); // 3
echo bcround('2.5', 0, \RoundingMode::PositiveInfinity); // 3
echo bcround('2.5', 0, \RoundingMode::NegativeInfinity); // 2

// bcdivmod() returns [quotient, remainder]
[$quot, $rem] = bcdivmod('17', '5'); // ['3', '2']
```

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

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

**Supported Modes:**
- `RoundingMode::HalfAwayFromZero` (equivalent to `PHP_ROUND_HALF_UP`)
- `RoundingMode::HalfTowardsZero` (equivalent to `PHP_ROUND_HALF_DOWN`)
- `RoundingMode::HalfEven` (equivalent to `PHP_ROUND_HALF_EVEN`)
- `RoundingMode::HalfOdd` (equivalent to `PHP_ROUND_HALF_ODD`)
- `RoundingMode::TowardsZero`
- `RoundingMode::AwayFromZero`
- `RoundingMode::NegativeInfinity`
- `RoundingMode::PositiveInfinity`

The legacy integer `PHP_ROUND_*` constants are also accepted for the four half-modes.

> **Note:** The polyfilled `RoundingMode` is a *pure* enum, matching native PHP 8.4 (it has no
> backing value; `->value` / `from()` / `tryFrom()` are intentionally unavailable).

#### Interoperability with `symfony/polyfill-php84`

`symfony/polyfill-php84` also declares `bcceil()`, `bcfloor()`, `bcround()` and `bcdivmod()`, but only
when the native bcmath extension is **already loaded** (its purpose is to backport the new PHP 8.4
functions to older PHP that already has bcmath). This package instead provides the full bcmath API for
environments **without** the extension.

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

## ⚡ Performance

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

### RoundingMode Enum Limitations
- Three `RoundingMode` enum values are not yet implemented:
- `RoundingMode::TowardsZero` - Will throw `ValueError`
- `RoundingMode::AwayFromZero` - Will throw `ValueError`
- `RoundingMode::NegativeInfinity` - Will throw `ValueError`
- These modes are planned for implementation in a future release
- Use traditional `PHP_ROUND_*` constants as alternatives when needed

## 🔄 Key Differences from phpseclib/bcmath_compat

| Feature | phpseclib/bcmath_compat | bcmath-polyfill |
|---------------------------|--------------------------------|-------------------------------------|
| **PHP 8.4 functions** | ❌ Not supported | ✅ Full support |
| `bcfloor()` | ❌ | ✅ |
| `bcceil()` | ❌ | ✅ |
| `bcround()` | ❌ | ✅ |
| **RoundingMode enum** | ❌ Not supported | ✅ Partial (4/7 modes) |
| `HalfAwayFromZero` | ❌ | ✅ |
| `HalfTowardsZero` | ❌ | ✅ |
| `HalfEven` | ❌ | ✅ |
| `HalfOdd` | ❌ | ✅ |
| `TowardsZero` | ❌ | ⏳ Planned |
| `AwayFromZero` | ❌ | ⏳ Planned |
| `NegativeInfinity` | ❌ | ⏳ Planned |
| **PHP 8.2+ deprecations** | ⚠️ Warnings | ✅ Fixed |
| **Test suite pollution** | ⚠️ Issues | ✅ Fixed |
| **Active maintenance** | ❌ Limited | ✅ Active |
| **CI/CD (PHP versions)** | GitHub Actions (8.1, 8.2, 8.3) | GitHub Actions (8.1, 8.2, 8.3, 8.4, 8.5) |
## 🔄 Key Differences from phpseclib/bcmath_compat and symfony/polyfill-php84

The three libraries target different goals. `phpseclib/bcmath_compat` is the classic pre-8.4 polyfill.
`symfony/polyfill-php84` backports **only** the new PHP 8.4 functions and activates them **only when the
native bcmath extension is already loaded**. `bcmath-polyfill` provides the complete bcmath API —
including the PHP 8.4 additions — for environments **with or without** the extension.

| Feature | phpseclib/bcmath_compat | symfony/polyfill-php84 | bcmath-polyfill |
|------------------------------------------------|--------------------------------|---------------------------------|------------------------------------------|
| Classic bc functions (`bcadd`, `bcmul`, …) | ✅ | ❌ Not provided | ✅ |
| Works **without** the bcmath extension | ✅ | ❌ Requires the extension | ✅ |
| `bcfloor()` / `bcceil()` / `bcround()` | ❌ | ✅ (extension required) | ✅ |
| `bcdivmod()` | ❌ | ✅ (delegates to native bc) | ✅ (works without the extension) |
| **RoundingMode enum** (all 8 modes) | ❌ | ✅ | ✅ |
| Arbitrary-precision rounding (no float fallback)| ❌ | ✅ (pure string algorithm) | ✅ (pure string algorithm) |
| `RoundingMode` is a *pure* enum (native shape) | — | ✅ | ✅ |
| Implementation of rounding | — | Pure string digits | Pure string digits (phpseclib elsewhere) |
| Extra dependency | phpseclib | none | phpseclib |
| **PHP 8.2+ deprecations** | ⚠️ Warnings | ✅ Fixed | ✅ Fixed |
| **Active maintenance** | ❌ Limited | ✅ Active | ✅ Active |
| **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) |

> When `bcmath-polyfill` and `symfony/polyfill-php84` are installed together on an environment that has
> the bcmath extension and runs PHP 8.2/8.3, both declare `bcceil()`/`bcfloor()`/`bcround()` and whichever
> autoloads first wins (guarded by `function_exists()`, so no fatal redeclaration). Because this package's
> rounding uses a native-compatible string algorithm, the results are identical either way. See
> [Interoperability with `symfony/polyfill-php84`](#interoperability-with-symfonypolyfill-php84).

### Migration from phpseclib/bcmath_compat

Expand Down
25 changes: 13 additions & 12 deletions lib/RoundingMode.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
/**
* RoundingMode enum polyfill for PHP 8.1-8.3.
*
* This enum provides the same interface as PHP 8.4's native RoundingMode enum,
* but TowardsZero, AwayFromZero, NegativeInfinity, and PositiveInfinity will throw exceptions
* when used in PHP < 8.4 to maintain compatibility expectations.
* This enum mirrors PHP 8.4's native RoundingMode, which is a *pure* enum
* (no backing type). Keeping it backing-less here ensures the polyfill behaves
* identically to the native enum on 8.4+ (e.g. no `->value`, `from()`/`tryFrom()`),
* avoiding an 8.1-8.3 vs. 8.4+ inconsistency.
*
* The enum is only defined if:
* - PHP version is 8.1 or higher (enum support)
Expand All @@ -18,15 +19,15 @@
// PHP to >=8.1, so PHPStan always infers this as always-true; silence that.
// @phpstan-ignore-next-line booleanAnd.rightAlwaysTrue
if (!class_exists('RoundingMode', false) && version_compare(PHP_VERSION, '8.1', '>=')) {
enum RoundingMode: string
enum RoundingMode
{
case HalfAwayFromZero = 'half_away_from_zero';
case HalfTowardsZero = 'half_towards_zero';
case HalfEven = 'half_even';
case HalfOdd = 'half_odd';
case TowardsZero = 'towards_zero';
case AwayFromZero = 'away_from_zero';
case NegativeInfinity = 'negative_infinity';
case PositiveInfinity = 'positive_infinity';
case HalfAwayFromZero;
case HalfTowardsZero;
case HalfEven;
case HalfOdd;
case TowardsZero;
case AwayFromZero;
case NegativeInfinity;
case PositiveInfinity;
}
}
28 changes: 21 additions & 7 deletions lib/bcmath.php
Original file line number Diff line number Diff line change
Expand Up @@ -136,19 +136,19 @@ function bcsub(string $left_operand, string $right_operand, ?int $scale = null):
/**
* Round down to the nearest integer (PHP 8.4+).
*/
function bcfloor(string $operand): string
function bcfloor(string $num): string
{
return BCMath::floor($operand);
return BCMath::floor($num);
}
}

if (!function_exists('bcceil')) {
/**
* Round up to the nearest integer (PHP 8.4+).
*/
function bcceil(string $operand): string
function bcceil(string $num): string
{
return BCMath::ceil($operand);
return BCMath::ceil($num);
}
}

Expand All @@ -157,11 +157,25 @@ function bcceil(string $operand): string
* Round to a given decimal place (PHP 8.4+).
*
* @param int $precision optional
* @param int $mode optional
* @param int|\RoundingMode $mode optional
*/
function bcround(string $operand, int $precision = 0, $mode = PHP_ROUND_HALF_UP): string
function bcround(string $num, int $precision = 0, $mode = PHP_ROUND_HALF_UP): string
{
return BCMath::round($operand, $precision, $mode);
return BCMath::round($num, $precision, $mode);
}
}

if (!function_exists('bcdivmod')) {
/**
* Get the quotient and remainder of dividing two arbitrary precision numbers (PHP 8.4+).
*
* @param null|int $scale optional
*
* @return string[] the quotient (index 0) and remainder (index 1)
*/
function bcdivmod(string $num1, string $num2, ?int $scale = null): array
{
return BCMath::divmod($num1, $num2, $scale);
}
}

Expand Down
9 changes: 8 additions & 1 deletion phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ parameters:
message: "#Match arm comparison between RoundingMode and '(halfawayfromzero|halftowardszero|halfeven|halfodd|towardszero|awayfromzero|negativeinfinity|positiveinfinity)' is always false\\.#"
-
identifier: argument.type
message: "#Parameter \\#3 \\$mode of (static method bcmath_compat\\\\BCMath::round\\(\\)|function bcround) expects int\\|RoundingMode, (string|int) given\\.#"
message: "#Parameter \\#3 \\$mode of (static method bcmath_compat\\\\BCMath::round\\(\\)|function bcround) expects (int\\|RoundingMode|RoundingMode), (string|int) given\\.#"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
paths:
- src/BCMath.php
- tests/BCMathTest.php
Expand Down Expand Up @@ -61,3 +61,10 @@ parameters:
identifier: property.nonObject
path: tests/BCMathTest.php
message: "#Cannot access property \\$name on string\\.#"
# On PHP 8.1 PHPStan narrows $function to the six non-pow operations and
# reports this in_array() guard as always-true, while PHP 8.5 does not.
# The guard deliberately excludes bcpow() from the second-argument check,
# so it must stay. Pre-existing on main; ignored to keep the 8.1 matrix green.
-
path: tests/BCMathTest.php
message: "#Call to function in_array\\(\\) with arguments .* and true will always evaluate to true\\.#"
Loading
Loading