diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ad86dc2..1602569 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 @@ -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 diff --git a/.github/workflows/qa.yml b/.github/workflows/qa.yml index a5d2d31..41b779d 100644 --- a/.github/workflows/qa.yml +++ b/.github/workflows/qa.yml @@ -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 @@ -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 @@ -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 }} diff --git a/.gitignore b/.gitignore index 2dde6dd..82ab364 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ .phpstan composer.lock /cov +/.* diff --git a/CHANGELOG.md b/CHANGELOG.md index 824fa2e..55c5116 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 740f640..e657311 100644 --- a/README.md +++ b/README.md @@ -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) @@ -368,38 +368,38 @@ 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). @@ -407,9 +407,9 @@ All operations above use immutable `Math` (the default). fab2s/math wins every o | 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: diff --git a/composer.json b/composer.json index 40d6fc5..94d271a 100644 --- a/composer.json +++ b/composer.json @@ -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": { diff --git a/phpunit.xml b/phpunit.xml index 8256e2e..2ceccd2 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,5 +1,5 @@ - + ./tests/ diff --git a/pint.json b/pint.json index 4da5b43..6eb5bab 100644 --- a/pint.json +++ b/pint.json @@ -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" diff --git a/src/Math.php b/src/Math.php index 56f3a5d..d0be842 100644 --- a/src/Math.php +++ b/src/Math.php @@ -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; } @@ -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