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μs (±4.0%)** | 0.301μs (±8.9%) | 1.15x |
| instantiate string | **0.244μs (±36.6%)** | 0.678μs (±4.5%) | 2.78x |
| add | **0.632μs (±9.7%)** | 2.278μs (±3.0%) | 3.60x |
| add variadic | **1.406μs (±5.6%)** | 6.653μs (±1.3%) | 4.73x |
| sub | **0.612μs (±1.7%)** | 2.325μs (±4.8%) | 3.80x |
| mul | **0.665μs (±2.5%)** | 2.183μs (±4.9%) | 3.28x |
| div | **0.762μs (±4.1%)** | 4.664μs (±3.6%) | 6.12x |
| pow | **0.986μs (±47.0%)** | 1.416μs (±45.5%) | 1.44x |
| mod | **0.851μs (±2.3%)** | 2.851μs (±42.2%) | 3.35x |
| sqrt | **2.160μs (±3.6%)** | 4.536μs (±11.3%) | 2.10x |
| abs | **0.344μs (±4.0%)** | 0.919μs (±35.8%) | 2.67x |
| negate | **0.379μs (±10.5%)** | 1.036μs (±93.7%) | 2.73x |
| clamp | **0.956μs (±64.5%)** | 4.325μs (±32.5%) | 4.53x |
| quotient & remainder | **0.894μs (±10.7%)** | 2.878μs (±4.6%) | 3.22x |
| inspection | **1.943μs (±5.7%)** | 4.672μs (±3.7%) | 2.40x |
| round | **0.596μs (±31.8%)** | 3.495μs (±10.2%) | 5.86x |
| ceil | **0.528μs (±8.1%)** | 2.965μs (±39.6%) | 5.62x |
| floor | **0.469μs (±6.2%)** | 2.504μs (±4.0%) | 5.34x |
| comparisons | **1.400μs (±7.5%)** | 6.171μs (±4.7%) | 4.41x |
| to string | **0.529μs (±6.6%)** | 0.789μs (±3.4%) | 1.49x |
| chained workflow | **1.810μs (±3.2%)** | 8.519μs (±2.0%) | 4.71x |
| large number ops | **1.792μs (±5.9%)** | 8.273μs (±1.8%) | 4.62x |
| accumulate 100 additions | **41.182μs (±3.0%)** | 147.875μs (±11.8%) | 3.59x |
| base convert to 62 | **1.162μs (±23.8%)** | 6.888μs (±5.6%) | 5.93x |
| base convert to 16 | 1.081μs (±15.3%) | **0.965μs (±7.6%)** | 0.89x |
| integer mul | **0.937μs (±11.6%)** | 1.873μs (±5.3%) | 2.00x |
| integer powmod | **1.263μs (±10.0%)** | 2.810μs (±7.9%) | 2.22x |
| create 1000 instances | **301.794μs (±4.2%)** | 731.200μs (±2.6%) | 2.42x |
| instantiate int | **0.117μs (±15.1%)** | 0.134μs (±8.7%) | 1.14x |
| instantiate string | **0.107μs (±2.4%)** | 0.351μs (±1.5%) | 3.29x |
| add | **0.343μs (±0.8%)** | 1.177μs (±1.7%) | 3.43x |
| add variadic | **0.674μs (±2.4%)** | 3.261μs (±1.3%) | 4.84x |
| sub | **0.345μs (±4.7%)** | 1.155μs (±0.5%) | 3.35x |
| mul | **0.382μs (±2.2%)** | 1.162μs (±1.6%) | 3.04x |
| div | **0.365μs (±6.5%)** | 2.055μs (±5.6%) | 5.63x |
| pow | **0.406μs (±24.3%)** | 0.728μs (±0.8%) | 1.79x |
| mod | **0.416μs (±0.8%)** | 1.270μs (±7.8%) | 3.06x |
| sqrt | **1.123μs (±0.9%)** | 1.380μs (±1.3%) | 1.23x |
| abs | **0.185μs (±0.6%)** | 0.517μs (±1.1%) | 2.80x |
| negate | **0.215μs (±1.8%)** | 0.466μs (±1.0%) | 2.17x |
| clamp | **0.478μs (±0.7%)** | 2.117μs (±5.6%) | 4.43x |
| quotient & remainder | **0.453μs (±0.9%)** | 1.306μs (±0.8%) | 2.89x |
| inspection | **1.065μs (±3.1%)** | 1.460μs (±2.5%) | 1.37x |
| round | **0.342μs (±3.3%)** | 1.483μs (±1.1%) | 4.33x |
| ceil | **0.320μs (±0.5%)** | 1.298μs (±3.1%) | 4.06x |
| floor | **0.289μs (±1.9%)** | 1.174μs (±1.2%) | 4.06x |
| comparisons | **0.644μs (±1.5%)** | 2.777μs (±1.2%) | 4.31x |
| to string | **0.326μs (±1.6%)** | 0.511μs (±0.5%) | 1.57x |
| chained workflow | **0.965μs (±1.2%)** | 3.683μs (±0.9%) | 3.82x |
| large number ops | **0.903μs (±1.7%)** | 3.433μs (±1.1%) | 3.80x |
| accumulate 100 additions | **21.421μs (±1.8%)** | 73.261μs (±1.2%) | 3.42x |
| base convert to 62 | **0.615μs (±1.3%)** | 2.580μs (±1.5%) | 4.19x |
| base convert to 16 | 0.607μs (±1.3%) | **0.458μs (±1.9%)** | 0.75x |
| integer mul | **0.424μs (±1.4%)** | 1.014μs (±11.0%) | 2.39x |
| integer powmod | **0.551μs (±0.7%)** | 1.410μs (±1.3%) | 2.56x |
| create 1000 instances | **127.530μs (±1.8%)** | 385.754μs (±1.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μs (±8.5%)** | 2.294μs (±5.0%) | 14.149μs (±3.9%) |
| accumulate 100 | **38.086μs (±2.0%)** | 41.339μs (±2.5%) | 147.063μs (±0.4%) |
| branch | 2.795μs (±4.0%) | **2.580μs (±3.3%)** | 12.885μs (±20.7%) |
| chained workflow | **1.018μs (±0.2%)** | 1.189μs (±3.0%) | 5.352μs (±1.2%) |
| accumulate 100 | **17.499μs (±1.1%)** | 20.765μs (±2.3%) | 74.043μs (±0.5%) |
| branch | 1.457μs (±6.2%) | **1.388μs (±2.7%)** | 5.596μs (±1.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