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
18 changes: 13 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
php-versions: [ '8.4', '8.3', '8.2', '8.1' ]
orchestra-versions: [ '8.0', '9.0', '10.0' ]
php-versions: [ '8.5', '8.4', '8.3', '8.2' ]
orchestra-versions: [ '11.0', '10.0', '9.0' ]
exclude:
- php-versions: 8.1
# Laravel 13 (Testbench 11) requires PHP >= 8.3
- php-versions: 8.2
orchestra-versions: 11.0
# Laravel 11 (Testbench 9) does not support PHP 8.5
- php-versions: 8.5
orchestra-versions: 9.0
- php-versions: 8.1
orchestra-versions: 10.0

steps:
- name: Checkout
Expand Down Expand Up @@ -41,6 +43,12 @@ jobs:
- name: Remove Qa / bench libs
run: composer remove "laravel/pint" "brick/math" --dev --no-update

# Laravel 11 (Testbench 9) is EOL and carries unpatched security advisories,
# so composer blocks its install by default. Math still supports it, so
# allow it in CI only (never touches the committed composer.json).
- name: Allow advisory-flagged dependencies (Laravel 11 is EOL)
run: composer config --global policy.advisories.block false

- name: Install Orchestra ${{ matrix.orchestra-versions }}
run: composer require "orchestra/testbench:^${{ matrix.orchestra-versions }}" --dev --no-update

Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/qa.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: 8.4
php-version: 8.5
extensions: mbstring, dom, fileinfo, gmp, bcmath
coverage: xdebug

Expand All @@ -39,7 +39,7 @@ jobs:
run: composer install --no-progress --prefer-dist --optimize-autoloader

- name: Check code style
run: composer fix
run: vendor/bin/pint --ansi --config pint.json --test

- name: PHPStan src
run: composer stan
Expand All @@ -51,6 +51,6 @@ jobs:
run: vendor/bin/phpunit --coverage-clover ./coverage.xml

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
.phpstan
composer.lock
/cov
/.*
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/).

## [Unreleased]

## [3.1.0] - 2026-07-31

### Changed

- Requires PHP 8.2+ (PHP 8.1 is EOL, pin `3.0.*` to stay on it)
- Tested against PHP 8.5, CI matrix is now PHP 8.2 to 8.5 against Orchestra 9, 10 and 11
- `phpunit/phpunit` `^11.0|^12.0|^13.0`, `orchestra/testbench` `^9.0|^10.0|^11.0`
- QA now actually checks code style (`pint --test`) instead of rewriting it on the runner

## [3.0.0] - 2026-02-08

### Changed
Expand Down
66 changes: 33 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ composer require fab2s/math

### Requirements

- PHP 8.1+
- PHP 8.2+
- ext-bcmath (required)
- ext-gmp (optional, faster base conversions, mod, pow and powMod)

Expand Down Expand Up @@ -368,48 +368,48 @@ protected $casts = [

## Benchmarks

Compared against [brick/math](https://github.com/brick/math) (PHP 8.4, opcache off, GMP enabled). The **bold** value is the faster one in each row, and _Factor_ shows how many times faster it is.
Compared against [brick/math](https://github.com/brick/math) (PHP 8.5, opcache off, GMP enabled). The **bold** value is the faster one in each row, and _Factor_ shows how many times faster it is.

| Operation | fab2s/math | brick/math | Factor |
|---|---:|---:|---:|
| instantiate int | **0.261μs4.0%)** | 0.301μs (±8.9%) | 1.15x |
| instantiate string | **0.244μs36.6%)** | 0.678μs4.5%) | 2.78x |
| add | **0.632μs9.7%)** | 2.278μs3.0%) | 3.60x |
| add variadic | **1.406μs5.6%)** | 6.653μs (±1.3%) | 4.73x |
| sub | **0.612μs1.7%)** | 2.325μs4.8%) | 3.80x |
| mul | **0.665μs (±2.5%)** | 2.183μs4.9%) | 3.28x |
| div | **0.762μs4.1%)** | 4.664μs3.6%) | 6.12x |
| pow | **0.986μs47.0%)** | 1.416μs45.5%) | 1.44x |
| mod | **0.851μs2.3%)** | 2.851μs42.2%) | 3.35x |
| sqrt | **2.160μs3.6%)** | 4.536μs11.3%) | 2.10x |
| abs | **0.344μs4.0%)** | 0.919μs35.8%) | 2.67x |
| negate | **0.379μs10.5%)** | 1.036μs93.7%) | 2.73x |
| clamp | **0.956μs64.5%)** | 4.325μs32.5%) | 4.53x |
| quotient & remainder | **0.894μs10.7%)** | 2.878μs4.6%) | 3.22x |
| inspection | **1.943μs5.7%)** | 4.672μs3.7%) | 2.40x |
| round | **0.596μs31.8%)** | 3.495μs10.2%) | 5.86x |
| ceil | **0.528μs8.1%)** | 2.965μs39.6%) | 5.62x |
| floor | **0.469μs6.2%)** | 2.504μs4.0%) | 5.34x |
| comparisons | **1.400μs7.5%)** | 6.171μs4.7%) | 4.41x |
| to string | **0.529μs6.6%)** | 0.789μs3.4%) | 1.49x |
| chained workflow | **1.810μs3.2%)** | 8.519μs2.0%) | 4.71x |
| large number ops | **1.792μs5.9%)** | 8.273μs (±1.8%) | 4.62x |
| accumulate 100 additions | **41.182μs3.0%)** | 147.875μs11.8%) | 3.59x |
| base convert to 62 | **1.162μs23.8%)** | 6.888μs5.6%) | 5.93x |
| base convert to 16 | 1.081μs15.3%) | **0.965μs7.6%)** | 0.89x |
| integer mul | **0.937μs11.6%)** | 1.873μs5.3%) | 2.00x |
| integer powmod | **1.263μs10.0%)** | 2.810μs7.9%) | 2.22x |
| create 1000 instances | **301.794μs4.2%)** | 731.200μs2.6%) | 2.42x |
| instantiate int | **0.117μs15.1%)** | 0.134μs (±8.7%) | 1.14x |
| instantiate string | **0.107μs2.4%)** | 0.351μs1.5%) | 3.29x |
| add | **0.343μs0.8%)** | 1.177μs1.7%) | 3.43x |
| add variadic | **0.674μs2.4%)** | 3.261μs (±1.3%) | 4.84x |
| sub | **0.345μs4.7%)** | 1.155μs0.5%) | 3.35x |
| mul | **0.382μs (±2.2%)** | 1.162μs1.6%) | 3.04x |
| div | **0.365μs6.5%)** | 2.055μs5.6%) | 5.63x |
| pow | **0.406μs24.3%)** | 0.728μs0.8%) | 1.79x |
| mod | **0.416μs0.8%)** | 1.270μs7.8%) | 3.06x |
| sqrt | **1.123μs0.9%)** | 1.380μs1.3%) | 1.23x |
| abs | **0.185μs0.6%)** | 0.517μs1.1%) | 2.80x |
| negate | **0.215μs1.8%)** | 0.466μs1.0%) | 2.17x |
| clamp | **0.478μs0.7%)** | 2.117μs5.6%) | 4.43x |
| quotient & remainder | **0.453μs0.9%)** | 1.306μs0.8%) | 2.89x |
| inspection | **1.065μs3.1%)** | 1.460μs2.5%) | 1.37x |
| round | **0.342μs3.3%)** | 1.483μs1.1%) | 4.33x |
| ceil | **0.320μs0.5%)** | 1.298μs3.1%) | 4.06x |
| floor | **0.289μs1.9%)** | 1.174μs1.2%) | 4.06x |
| comparisons | **0.644μs1.5%)** | 2.777μs1.2%) | 4.31x |
| to string | **0.326μs1.6%)** | 0.511μs0.5%) | 1.57x |
| chained workflow | **0.965μs1.2%)** | 3.683μs0.9%) | 3.82x |
| large number ops | **0.903μs1.7%)** | 3.433μs (±1.1%) | 3.80x |
| accumulate 100 additions | **21.421μs1.8%)** | 73.261μs1.2%) | 3.42x |
| base convert to 62 | **0.615μs1.3%)** | 2.580μs1.5%) | 4.19x |
| base convert to 16 | 0.607μs1.3%) | **0.458μs1.9%)** | 0.75x |
| integer mul | **0.424μs1.4%)** | 1.014μs11.0%) | 2.39x |
| integer powmod | **0.551μs0.7%)** | 1.410μs1.3%) | 2.56x |
| create 1000 instances | **127.530μs1.8%)** | 385.754μs1.1%) | 3.02x |

All operations above use immutable `Math` (the default). fab2s/math wins every operation except base-16 conversion, where brick/math delegates to GMP's native hex output. The speed advantage comes from keeping bcmath's C-level string arithmetic as the hot path for decimal operations, while brick/math pays for an extra object-wrapping layer on top of GMP. Integer-only operations (`mod`, `pow`, `powMod`, base conversion) use GMP directly when the extension is available, combining the best of both backends. Realistic workflows like chained calculations or 100-iteration accumulations show a consistent 3-5x advantage, with immutability costing only a lightweight `clone` per operation (two properties: a string and an int).

`MathMutable` eliminates the clone overhead entirely for hot loops:

| Operation | MathMutable | Math (immutable) | brick/math |
|---|---:|---:|---:|
| chained workflow | **1.964μs8.5%)** | 2.294μs5.0%) | 14.149μs3.9%) |
| accumulate 100 | **38.086μs2.0%)** | 41.339μs (±2.5%) | 147.063μs (±0.4%) |
| branch | 2.795μs4.0%) | **2.580μs3.3%)** | 12.885μs20.7%) |
| chained workflow | **1.018μs0.2%)** | 1.189μs3.0%) | 5.352μs1.2%) |
| accumulate 100 | **17.499μs1.1%)** | 20.765μs (±2.3%) | 74.043μs (±0.5%) |
| branch | 1.457μs6.2%) | **1.388μs2.7%)** | 5.596μs1.1%) |

Run benchmarks yourself:

Expand Down
12 changes: 6 additions & 6 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,17 @@
"MIT"
],
"require" : {
"php": "^8.1",
"php": "^8.2",
"ext-bcmath": "*",
"fab2s/context-exception": "^2.0|^3.0"
},
"require-dev": {
"phpunit/phpunit": "^10.0|^11.0",
"laravel/pint": "^1.11",
"orchestra/testbench": "^8.0|^9.0|^10.0",
"phpunit/phpunit": "^11.0|^12.0|^13.0",
"laravel/pint": "^1.21.2",
"orchestra/testbench": "^9.0|^10.0|^11.0",
"phpstan/phpstan": "^2.1",
"brick/math": "^0.14.7",
"phpbench/phpbench": "^1.4"
"brick/math": "^0.14.7|^0.19.0",
"phpbench/phpbench": "^1.4|^1.7"
},
"autoload": {
"psr-4": {
Expand Down
2 changes: 1 addition & 1 deletion phpunit.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" backupGlobals="false" bootstrap="vendor/autoload.php" colors="true" processIsolation="false" stopOnFailure="false" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.3/phpunit.xsd" cacheDirectory=".phpunit.cache" backupStaticProperties="false">
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" backupGlobals="false" bootstrap="vendor/autoload.php" colors="true" processIsolation="false" stopOnFailure="false" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/11.5/phpunit.xsd" cacheDirectory=".phpunit.cache" backupStaticProperties="false">
<testsuites>
<testsuite name="Application Test Suite">
<directory>./tests/</directory>
Expand Down
1 change: 1 addition & 0 deletions pint.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"preset": "laravel",
"cache-file": ".pint.cache",
"rules": {
"header_comment": {
"header": "This file is part of fab2s/Math.\n(c) Fabrice de Stefanis / https://github.com/fab2s/Math\nThis source file is licensed under the MIT license which you will\nfind in the LICENSE file or at https://opensource.org/licenses/MIT"
Expand Down
5 changes: 4 additions & 1 deletion src/Math.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ class Math extends MathOpsAbstract implements JsonSerializable, Stringable
public function __construct(string|int|float|Math $number)
{
if (isset(static::$globalPrecision)) {
/* @codeCoverageIgnore */
$this->precision = static::$globalPrecision;
}

Expand Down Expand Up @@ -173,4 +172,8 @@ public function jsonSerialize(): string
}

// OMG a dynamic static anti pattern ^^
// runs at file load, before per test coverage collection starts,
// so it can never be reported as covered
// @codeCoverageIgnoreStart
Math::gmpSupport();
// @codeCoverageIgnoreEnd
Loading