├── .gitignore ├── phpstan.neon ├── ISSUE_TEMPLATE.md ├── ecs.php ├── phpunit.xml ├── LICENSE ├── composer.json ├── .github └── workflows │ └── tests.yml ├── badge-coverage.svg ├── README.md ├── CODE_OF_CONDUCT.md ├── clover.xml ├── src └── BCMathExtended │ └── BC.php └── tests └── Unit └── BCTest.php /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | /vendor 3 | composer.lock 4 | .cache/ 5 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 9 3 | phpVersion: 80400 4 | tmpDir: .cache/phpstan/ 5 | paths: 6 | - src 7 | - tests 8 | ignoreErrors: 9 | - identifier: missingType.iterableValue 10 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | > Please provide the following details. 2 | 3 | * *Operating System*: 4 | * *PHP Version*: <7.4 | 8.2 | ...> 5 | 6 | > Steps required to reproduce the problem. 7 | 8 | 1. 9 | 2. 10 | 3. 11 | 12 | > Expected Result. 13 | 14 | * 15 | 16 | > Actual Result. 17 | 18 | * 19 | -------------------------------------------------------------------------------- /ecs.php: -------------------------------------------------------------------------------- 1 | paths([__DIR__ . '/src', __DIR__ . '/tests',]); 10 | $ecsConfig->sets([ 11 | SetList::PSR_12, 12 | SetList::CLEAN_CODE, 13 | SetList::STRICT, 14 | SetList::ARRAY, 15 | SetList::PHPUNIT 16 | ]); 17 | 18 | $ecsConfig->fileExtensions(['php']); 19 | $ecsConfig->cacheDirectory('.cache/ecs'); 20 | }; 21 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | ./tests/ 8 | 9 | 10 | 11 | 12 | src/ 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 krowinski 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "krowinski/bcmath-extended", 3 | "description": "Extends php BCMath lib for missing functions like floor, ceil, round, abs, min, max, rand for big numbers. Also wraps existing BCMath functions. (more http://php.net/manual/en/book.bc.php) Supports scientific notations", 4 | "keywords": [ 5 | "bc", 6 | "bcmath", 7 | "round", 8 | "ceil", 9 | "round", 10 | "php", 11 | "precision", 12 | "bignumber", 13 | "math", 14 | "rand", 15 | "floor", 16 | "abs", 17 | "arbitrary-precision", 18 | "complex-numbers", 19 | "scientific-notation" 20 | ], 21 | "type": "library", 22 | "require": { 23 | "php": "^8.4", 24 | "ext-bcmath": "*" 25 | }, 26 | "require-dev": { 27 | "phpunit/phpunit": "^12.1", 28 | "phpstan/phpstan": "^2.1", 29 | "symplify/easy-coding-standard": "^12.5" 30 | }, 31 | "license": "MIT", 32 | "authors": [ 33 | { 34 | "name": "Kacper Rowiński", 35 | "email": "kacper.rowinski@gmail.com" 36 | } 37 | ], 38 | "autoload": { 39 | "psr-4": { 40 | "BCMathExtended\\": "src/BCMathExtended/" 41 | } 42 | }, 43 | "autoload-dev": { 44 | "psr-4": { 45 | "BCMathExtended\\Tests\\Unit\\": "tests/Unit/" 46 | } 47 | }, 48 | "scripts": { 49 | "cs:check": "ecs check", 50 | "cs:fix": "ecs check --fix", 51 | "phpstan:analyse": "phpstan analyse -cphpstan.neon" 52 | }, 53 | "minimum-stability": "stable" 54 | } 55 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: PHP Tests 2 | on: [push, pull_request] 3 | jobs: 4 | build: 5 | 6 | runs-on: ubuntu-latest 7 | 8 | strategy: 9 | matrix: 10 | php: [ '8.4' ] 11 | 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | 16 | - name: Setup PHP, with composer and extensions 17 | uses: shivammathur/setup-php@v2 18 | with: 19 | php-version: ${{ matrix.php }} 20 | coverage: xdebug 21 | 22 | - name: Validate composer.json and composer.lock 23 | run: composer validate 24 | 25 | - name: Cache Composer packages 26 | id: composer-cache 27 | uses: actions/cache@v4 28 | with: 29 | path: vendor 30 | key: ${{ runner.os }}-${{ matrix.php }}-${{ hashFiles('**/composer.lock') }} 31 | restore-keys: | 32 | ${{ runner.os }}-${{ matrix.php }}- 33 | 34 | - name: Install dependencies 35 | if: steps.composer-cache.outputs.cache-hit != 'true' 36 | run: composer install --prefer-dist --no-progress --no-suggest 37 | 38 | - name: Run tests 39 | run: vendor/bin/phpunit --coverage-clover clover.xml 40 | 41 | - name: PHPStan analyse 42 | run: composer phpstan:analyse 43 | 44 | - name: Coding standards check 45 | run: composer cs:check 46 | 47 | - name: Generate test coverage badge 48 | uses: timkrase/phpunit-coverage-badge@v1.2.0 49 | with: 50 | coverage_badge_path: 'badge-coverage.svg' 51 | push_badge: true 52 | repo_token: ${{ secrets.GITHUB_TOKEN }} 53 | -------------------------------------------------------------------------------- /badge-coverage.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | coverage 10 | coverage 11 | 12 | 13 | 99 % 14 | 99 % 15 | 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bcmath-extended 2 | [![PHP Tests](https://github.com/krowinski/bcmath-extended/actions/workflows/tests.yml/badge.svg)](https://github.com/krowinski/bcmath-extended/actions/workflows/tests.yml) 3 | [![Code Coverage](https://raw.githubusercontent.com/krowinski/bcmath-extended/master/badge-coverage.svg)](https://github.com/krowinski/bcmath-extended/actions) 4 | [![Latest Stable Version](https://poser.pugx.org/krowinski/bcmath-extended/v/stable)](https://packagist.org/packages/krowinski/bcmath-extended) 5 | [![Total Downloads](https://poser.pugx.org/krowinski/bcmath-extended/downloads)](https://packagist.org/packages/krowinski/bcmath-extended) 6 | [![License](https://poser.pugx.org/krowinski/bcmath-extended/license)](https://packagist.org/packages/krowinski/bcmath-extended) 7 | 8 | Extends php BCMath lib for missing functions like abs, min, max, rand, fact, log for big numbers. 9 | Handles scientific notation like 1.0E+35 to be used with BCMath. 10 | Also wraps existing BCMath functions. (more http://php.net/manual/en/book.bc.php) 11 | 12 | Installation 13 | === 14 | 15 | ```sh 16 | composer require krowinski/bcmath-extended 17 | ``` 18 | 19 | Features 20 | === 21 | - config 22 | - setTrimTrailingZeroes - disable|enable trailing zeros (default trimming is enabled) 23 | - new tool methods 24 | - convertToNumber - converts scientific notation, string and int to [BcMath\Number](https://www.php.net/manual/en/class.bcmath-number.php) 25 | - getScale - gets current global scale 26 | - getDecimalsLengthFromNumber - gets amount of decimals 27 | - hexdec - converting from hexadecimal to decimal 28 | - dechex - converting from decimal to hexadecimal 29 | - bin2dec - converting from binary to decimal 30 | - dec2bin - converting from decimal to binary 31 | - new math functions 32 | - round 33 | - abs 34 | - rand 35 | - max 36 | - min 37 | - roundDown 38 | - roundUp 39 | - roundHalfEven 40 | - ceil 41 | - exp 42 | - log 43 | - fact 44 | - pow (supports fractional) 45 | - mod (supports fractional + scale) 46 | - bitwise operators 47 | - bitXor 48 | - bitOr 49 | - bitAnd 50 | - proxy for [original functions](https://www.php.net/manual/en/book.bc.php) 51 | - all functions supports scientific notation 52 | - all functions are static, so it can be easily replaced by this lib 53 | 54 | Info 55 | === 56 | As of 7.2 float can be passed to bcmod, but they don't return correct values (IMO) 57 | 58 | I created bug for this in https://bugs.php.net/bug.php?id=76287, but it was commented as documentation issue not a bug. 59 | 60 | ``` 61 | bcmod() doesn't use floor() but rather truncates towards zero, 62 | which is also defined this way for POSIX fmod(), so that the 63 | result always has the same sign as the dividend. Therefore, this 64 | is not a bug, but rather a documentation issue. 65 | ``` 66 | 67 | But I still will use floor not truncated for mod in this lib. 68 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at kacper.rowinski@gmial.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /clover.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | -------------------------------------------------------------------------------- /src/BCMathExtended/BC.php: -------------------------------------------------------------------------------- 1 | sub($min)->add(1); 35 | $randPercent = static::div(mt_rand(), mt_getrandmax(), 8); 36 | 37 | return $difference->mul($randPercent, 8)->add(1, 0); 38 | } 39 | 40 | public static function convertToNumber(int|string|Number $number): Number 41 | { 42 | if ($number instanceof Number) { 43 | return $number; 44 | } 45 | 46 | if (is_int($number)) { 47 | return new Number($number); 48 | } 49 | 50 | // check if number is in scientific notation, first use stripos as is faster than preg_match 51 | if (stripos($number, 'E') !== false && preg_match('/(-?(\d+\.)?\d+)E([+-]?)(\d+)/i', $number, $regs)) { 52 | // calculate final scale of number 53 | $scale = (int)$regs[4] + static::getDecimalsLength($regs[1]); 54 | $pow = static::pow(10, $regs[4], $scale); 55 | if ($regs[3] === '-') { 56 | $number = static::div($regs[1], $pow, $scale); 57 | } else { 58 | $number = static::mul($pow, $regs[1], $scale); 59 | } 60 | $number = static::formatTrailingZeroes($number); 61 | } 62 | 63 | return static::parseToNumber($number); 64 | } 65 | 66 | public static function getDecimalsLength(int|string|Number $number): int 67 | { 68 | if (static::isFloat($number)) { 69 | return strcspn(strrev((string)$number), '.'); 70 | } 71 | 72 | return 0; 73 | } 74 | 75 | protected static function isFloat(int|string|Number $number): bool 76 | { 77 | return str_contains((string)$number, '.'); 78 | } 79 | 80 | public static function pow( 81 | int|string|Number $base, 82 | int|string|Number $exponent, 83 | ?int $scale = null 84 | ): Number { 85 | $base = static::convertToNumber($base); 86 | $exponent = static::convertToNumber($exponent); 87 | 88 | if (static::isFloat($exponent)) { 89 | $r = static::powFractional($base, $exponent, self::getScaleForMethod($scale)); 90 | } else { 91 | $r = $base->pow($exponent, self::getScaleForMethod($scale)); 92 | } 93 | 94 | return static::formatTrailingZeroes($r); 95 | } 96 | 97 | protected static function powFractional( 98 | int|string|Number $base, 99 | int|string|Number $exponent, 100 | ?int $scale = null 101 | ): Number { 102 | // we need to increased scale to get correct results and avoid rounding error 103 | $currentScale = $scale ?? static::getScale(); 104 | $increasedScale = $currentScale * 2; 105 | 106 | // add zero to trim scale 107 | return static::parseToNumber( 108 | static::add( 109 | static::exp(static::mul($exponent, static::log($base), $increasedScale)), 110 | 0, 111 | $currentScale 112 | ) 113 | ); 114 | } 115 | 116 | public static function getScale(): int 117 | { 118 | return bcscale(); 119 | } 120 | 121 | protected static function parseToNumber(int|string|Number $number): Number 122 | { 123 | if ($number instanceof Number) { 124 | return $number; 125 | } 126 | 127 | if (is_int($number)) { 128 | return new Number($number); 129 | } 130 | 131 | $number = str_replace( 132 | '+', 133 | '', 134 | (string)filter_var( 135 | $number, 136 | FILTER_SANITIZE_NUMBER_FLOAT, 137 | FILTER_FLAG_ALLOW_FRACTION 138 | ) 139 | ); 140 | if ($number === '-0' || !is_numeric($number)) { 141 | $number = 0; 142 | } 143 | return new Number($number); 144 | } 145 | 146 | public static function add( 147 | int|string|Number $leftOperand, 148 | int|string|Number $rightOperand, 149 | ?int $scale = null 150 | ): Number { 151 | $leftOperand = static::convertToNumber($leftOperand); 152 | $rightOperand = static::convertToNumber($rightOperand); 153 | 154 | $r = $leftOperand->add($rightOperand, self::getScaleForMethod($scale)); 155 | 156 | return static::formatTrailingZeroes($r); 157 | } 158 | 159 | protected static function formatTrailingZeroes(Number $number): Number 160 | { 161 | if (self::$trimTrailingZeroes) { 162 | return static::trimTrailingZeroes($number); 163 | } 164 | 165 | return $number; 166 | } 167 | 168 | protected static function trimTrailingZeroes(int|string|Number $number): Number 169 | { 170 | $number = (string)$number; 171 | 172 | if (static::isFloat($number)) { 173 | $number = rtrim($number, '0'); 174 | } 175 | 176 | $number = rtrim($number, '.') ?: '0'; 177 | 178 | return new Number($number); 179 | } 180 | 181 | public static function exp(int|string|Number $number): Number 182 | { 183 | $number = static::convertToNumber($number); 184 | $scale = static::DEFAULT_SCALE; 185 | $result = new Number(1); 186 | for ($i = 299; $i > 0; --$i) { 187 | $result = $result->div($i, $scale)->mul($number, $scale)->add(1); 188 | } 189 | 190 | return self::trimTrailingZeroes($result); 191 | } 192 | 193 | public static function mul( 194 | int|string|Number $leftOperand, 195 | int|string|Number $rightOperand, 196 | ?int $scale = null 197 | ): Number { 198 | $leftOperand = static::convertToNumber($leftOperand); 199 | $rightOperand = static::convertToNumber($rightOperand); 200 | 201 | $r = $leftOperand->mul($rightOperand, self::getScaleForMethod($scale)); 202 | 203 | return static::formatTrailingZeroes($r); 204 | } 205 | 206 | public static function div( 207 | int|string|Number $dividend, 208 | int|string|Number $divisor, 209 | ?int $scale = null 210 | ): Number { 211 | $divisor = static::convertToNumber($divisor); 212 | 213 | if ((string)static::trimTrailingZeroes($divisor) === '0') { 214 | throw new InvalidArgumentException('Division by zero'); 215 | } 216 | 217 | $r = static::convertToNumber($dividend)->div($divisor, self::getScaleForMethod($scale)); 218 | 219 | return static::formatTrailingZeroes($r); 220 | } 221 | 222 | public static function log(int|string|Number $number): string|Number 223 | { 224 | $number = static::convertToNumber($number); 225 | if ((string)$number === '0') { 226 | return '-INF'; 227 | } 228 | if ($number->compare('0') === static::COMPARE_RIGHT_GRATER) { 229 | return 'NAN'; 230 | } 231 | 232 | $scale = static::DEFAULT_SCALE; 233 | $m = (string)log((float)(string)$number); 234 | $x = $number->div(static::exp($m), $scale)->sub(1, $scale); 235 | 236 | $res = new Number(0); 237 | $pow = new Number(1); 238 | 239 | $i = 1; 240 | do { 241 | $pow = $pow->mul($x, $scale); 242 | $sum = $pow->div($i, $scale); 243 | 244 | if ($i % 2 === 1) { 245 | $res = $res->add($sum, $scale); 246 | } else { 247 | $res = $res->sub($sum, $scale); 248 | } 249 | ++$i; 250 | } while ($sum->compare(0, $scale)); 251 | 252 | return self::trimTrailingZeroes($res->add($m, $scale)); 253 | } 254 | 255 | public static function compare( 256 | int|string|Number $leftOperand, 257 | int|string|Number $rightOperand, 258 | ?int $scale = null 259 | ): int { 260 | $leftOperand = static::convertToNumber($leftOperand); 261 | $rightOperand = static::convertToNumber($rightOperand); 262 | 263 | return $leftOperand->compare($rightOperand, self::getScaleForMethod($scale)); 264 | } 265 | 266 | public static function sub( 267 | int|string|Number $leftOperand, 268 | int|string|Number $rightOperand, 269 | ?int $scale = null 270 | ): Number { 271 | $leftOperand = static::convertToNumber($leftOperand); 272 | $rightOperand = static::convertToNumber($rightOperand); 273 | 274 | $r = $leftOperand->sub($rightOperand, self::getScaleForMethod($scale)); 275 | 276 | return static::formatTrailingZeroes($r); 277 | } 278 | 279 | public static function sqrt(int|string|Number $number, ?int $scale = null): Number 280 | { 281 | $number = static::convertToNumber($number); 282 | 283 | $r = $number->sqrt(self::getScaleForMethod($scale)); 284 | 285 | return static::formatTrailingZeroes($r); 286 | } 287 | 288 | public static function setTrimTrailingZeroes(bool $flag): void 289 | { 290 | self::$trimTrailingZeroes = $flag; 291 | } 292 | 293 | /** 294 | * @param mixed $values 295 | */ 296 | public static function max(...$values): null|Number 297 | { 298 | $max = null; 299 | foreach (static::parseValues($values) as $number) { 300 | $number = static::convertToNumber((string)$number); 301 | if ($max === null) { 302 | $max = $number; 303 | } elseif ($max->compare($number) === static::COMPARE_RIGHT_GRATER) { 304 | $max = $number; 305 | } 306 | } 307 | 308 | return $max; 309 | } 310 | 311 | protected static function parseValues(array $values): array 312 | { 313 | if (is_array($values[0])) { 314 | $values = $values[0]; 315 | } 316 | 317 | return $values; 318 | } 319 | 320 | /** 321 | * @param mixed $values 322 | */ 323 | public static function min(...$values): null|Number 324 | { 325 | $min = null; 326 | foreach (static::parseValues($values) as $number) { 327 | $number = static::convertToNumber((string)$number); 328 | if ($min === null) { 329 | $min = $number; 330 | } elseif ($min->compare($number) === static::COMPARE_LEFT_GRATER) { 331 | $min = $number; 332 | } 333 | } 334 | 335 | return $min; 336 | } 337 | 338 | public static function powMod( 339 | int|string|Number $base, 340 | int|string|Number $exponent, 341 | int|string|Number $modulus, 342 | ?int $scale = null 343 | ): Number { 344 | $base = static::convertToNumber($base); 345 | $exponent = static::convertToNumber($exponent); 346 | 347 | if (static::isNegative($exponent)) { 348 | throw new InvalidArgumentException('Exponent can\'t be negative'); 349 | } 350 | 351 | if ((string)static::trimTrailingZeroes($modulus) === '0') { 352 | throw new InvalidArgumentException('Modulus can\'t be zero'); 353 | } 354 | 355 | // bcpowmod don't support floats 356 | if (static::isFloat($base) || static::isFloat($exponent) || static::isFloat($modulus)) { 357 | $r = static::mod( 358 | static::pow( 359 | $base, 360 | $exponent, 361 | self::getScaleForMethod($scale) 362 | ), 363 | $modulus, 364 | self::getScaleForMethod($scale) 365 | ); 366 | } else { 367 | $r = $base->powmod($exponent, $modulus, self::getScaleForMethod($scale)); 368 | } 369 | 370 | return static::formatTrailingZeroes($r); 371 | } 372 | 373 | protected static function isNegative(int|string|Number $number): bool 374 | { 375 | return strncmp('-', (string)$number, 1) === 0; 376 | } 377 | 378 | public static function mod( 379 | int|string|Number $dividend, 380 | int|string|Number $divisor, 381 | ?int $scale = null 382 | ): Number { 383 | // bcmod is not working properly - for example bcmod(9.9999E-10, -0.00056, 9) should return '-0.000559999' but returns 0.0000000 384 | // let use this $x - floor($x/$y) * $y; 385 | return static::formatTrailingZeroes( 386 | static::sub( 387 | $dividend, 388 | static::mul( 389 | static::floor( 390 | static::div( 391 | $dividend, 392 | $divisor, 393 | self::getScaleForMethod($scale) 394 | ) 395 | ), 396 | $divisor, 397 | self::getScaleForMethod($scale) 398 | ), 399 | $scale 400 | ) 401 | ); 402 | } 403 | 404 | public static function floor(int|string|Number $number): Number 405 | { 406 | return static::convertToNumber($number)->floor(); 407 | } 408 | 409 | public static function fact(int|string|Number $number): Number 410 | { 411 | $number = static::convertToNumber($number); 412 | 413 | if (static::isFloat($number)) { 414 | throw new InvalidArgumentException('Number has to be an integer'); 415 | } 416 | if (static::isNegative($number)) { 417 | throw new InvalidArgumentException('Number has to be greater than or equal to 0'); 418 | } 419 | 420 | $return = new Number(1); 421 | for ($i = 2; $i <= (int)(string)$number; ++$i) { 422 | $return = $return->mul($i); 423 | } 424 | 425 | return $return; 426 | } 427 | 428 | public static function hexdec(string $hex): string 429 | { 430 | $remainingDigits = str_replace('0x', '', substr($hex, 0, -1)); 431 | $lastDigitToDecimal = (string)hexdec(substr($hex, -1)); 432 | 433 | if ($remainingDigits === '') { 434 | return $lastDigitToDecimal; 435 | } 436 | 437 | return (string)static::add( 438 | static::mul( 439 | 16, 440 | static::hexdec($remainingDigits) 441 | ), 442 | $lastDigitToDecimal, 443 | 0 444 | ); 445 | } 446 | 447 | public static function dechex(int|string|Number $decimal): string 448 | { 449 | $quotient = static::div($decimal, 16, 0); 450 | $remainderToHex = dechex((int)(string)static::mod($decimal, 16)); 451 | 452 | if ($quotient->compare(0) === static::COMPARE_EQUAL) { 453 | return $remainderToHex; 454 | } 455 | 456 | return static::dechex($quotient) . $remainderToHex; 457 | } 458 | 459 | public static function bitAnd(int|string|Number $leftOperand, int|string|Number $rightOperand): Number 460 | { 461 | return static::bitOperatorHelper($leftOperand, $rightOperand, static::BIT_OPERATOR_AND); 462 | } 463 | 464 | protected static function bitOperatorHelper( 465 | int|string|Number $leftOperand, 466 | int|string|Number $rightOperand, 467 | string $operator 468 | ): Number { 469 | $leftOperand = static::convertToNumber($leftOperand); 470 | $rightOperand = static::convertToNumber($rightOperand); 471 | 472 | if (static::isFloat($leftOperand)) { 473 | throw new InvalidArgumentException('Left operator has to be an integer'); 474 | } 475 | if (static::isFloat($rightOperand)) { 476 | throw new InvalidArgumentException('Right operator has to be an integer'); 477 | } 478 | 479 | $leftOperandNegative = static::isNegative($leftOperand); 480 | $rightOperandNegative = static::isNegative($rightOperand); 481 | 482 | $leftOperand = static::dec2bin((string)static::abs($leftOperand)); 483 | $rightOperand = static::dec2bin((string)static::abs($rightOperand)); 484 | 485 | $maxLength = max(strlen($leftOperand), strlen($rightOperand)); 486 | 487 | $leftOperand = static::alignBinLength($leftOperand, $maxLength); 488 | $rightOperand = static::alignBinLength($rightOperand, $maxLength); 489 | 490 | if ($leftOperandNegative) { 491 | $leftOperand = static::recalculateNegative($leftOperand); 492 | } 493 | if ($rightOperandNegative) { 494 | $rightOperand = static::recalculateNegative($rightOperand); 495 | } 496 | 497 | $isNegative = false; 498 | $result = ''; 499 | if (static::BIT_OPERATOR_AND === $operator) { 500 | $result = $leftOperand & $rightOperand; 501 | $isNegative = ($leftOperandNegative and $rightOperandNegative); 502 | } elseif (static::BIT_OPERATOR_OR === $operator) { 503 | $result = $leftOperand | $rightOperand; 504 | $isNegative = ($leftOperandNegative or $rightOperandNegative); 505 | } elseif (static::BIT_OPERATOR_XOR === $operator) { 506 | $result = $leftOperand ^ $rightOperand; 507 | $isNegative = ($leftOperandNegative xor $rightOperandNegative); 508 | } 509 | 510 | if ($isNegative) { 511 | $result = static::recalculateNegative($result); 512 | } 513 | 514 | $result = static::bin2dec($result); 515 | 516 | return new Number($isNegative ? '-' . $result : $result); 517 | } 518 | 519 | public static function dec2bin(string $number, int $base = self::MAX_BASE): string 520 | { 521 | return static::decBaseHelper( 522 | $base, 523 | static function (int $base) use ($number): string { 524 | $value = ''; 525 | if ($number === '0') { 526 | return chr((int)$number); 527 | } 528 | 529 | while (self::compare($number, 0) !== self::COMPARE_EQUAL) { 530 | $rest = self::mod($number, $base); 531 | $number = self::div($number, $base); 532 | $value = chr((int)(string)$rest) . $value; 533 | } 534 | 535 | return $value; 536 | } 537 | ); 538 | } 539 | 540 | /** 541 | * @param Closure(int): string $closure 542 | */ 543 | protected static function decBaseHelper(int $base, Closure $closure): string 544 | { 545 | if ($base < 2 || $base > static::MAX_BASE) { 546 | throw new InvalidArgumentException('Invalid Base: ' . $base); 547 | } 548 | $orgScale = static::getScale(); 549 | static::setScale(0); 550 | 551 | $value = $closure($base); 552 | 553 | static::setScale($orgScale); 554 | 555 | return $value; 556 | } 557 | 558 | public static function setScale(int $scale): void 559 | { 560 | bcscale($scale); 561 | self::$currentScale = $scale; 562 | } 563 | 564 | protected static function getScaleForMethod(?int $scale): ?int 565 | { 566 | if ($scale !== null) { 567 | return $scale; 568 | } 569 | 570 | return self::$currentScale; 571 | } 572 | 573 | public static function abs(int|string|Number $number): Number 574 | { 575 | $number = static::convertToNumber($number); 576 | 577 | if (static::isNegative($number)) { 578 | $number = substr((string)$number, 1); 579 | } 580 | 581 | return static::parseToNumber($number); 582 | } 583 | 584 | protected static function alignBinLength(string $string, int $length): string 585 | { 586 | return str_pad($string, $length, static::dec2bin('0'), STR_PAD_LEFT); 587 | } 588 | 589 | protected static function recalculateNegative(string $number): string 590 | { 591 | $xor = str_repeat(static::dec2bin((string)(static::MAX_BASE - 1)), strlen($number)); 592 | $number ^= $xor; 593 | for ($i = strlen($number) - 1; $i >= 0; --$i) { 594 | $byte = ord($number[$i]); 595 | if (++$byte !== static::MAX_BASE) { 596 | $number[$i] = chr($byte); 597 | break; 598 | } 599 | } 600 | 601 | return $number; 602 | } 603 | 604 | public static function bin2dec(int|string|Number $binary, int $base = self::MAX_BASE): string 605 | { 606 | $binary = (string)$binary; 607 | 608 | return static::decBaseHelper( 609 | $base, 610 | static function (int $base) use ($binary): string { 611 | $size = strlen($binary); 612 | $return = '0'; 613 | 614 | for ($i = 0; $i < $size; ++$i) { 615 | $element = ord($binary[$i]); 616 | $power = self::pow($base, $size - $i - 1); 617 | $return = self::add($return, self::mul($element, $power)); 618 | } 619 | 620 | return (string)$return; 621 | } 622 | ); 623 | } 624 | 625 | public static function bitOr(int|string|Number $leftOperand, int|string|Number $rightOperand): Number 626 | { 627 | return static::bitOperatorHelper($leftOperand, $rightOperand, static::BIT_OPERATOR_OR); 628 | } 629 | 630 | public static function bitXor(int|string|Number $leftOperand, int|string|Number $rightOperand): Number 631 | { 632 | return static::bitOperatorHelper($leftOperand, $rightOperand, static::BIT_OPERATOR_XOR); 633 | } 634 | 635 | public static function roundHalfEven(int|string|Number $number, int $precision = 0): Number 636 | { 637 | return static::convertToNumber($number)->round($precision, RoundingMode::HalfEven); 638 | } 639 | 640 | public static function round( 641 | int|string|Number $number, 642 | int $precision = 0, 643 | RoundingMode $mode = RoundingMode::HalfAwayFromZero 644 | ): Number { 645 | $number = static::convertToNumber($number); 646 | if (static::isFloat($number)) { 647 | $number = static::formatTrailingZeroes($number->round($precision, $mode)); 648 | } 649 | 650 | return static::parseToNumber($number); 651 | } 652 | 653 | public static function roundUp(int|string|Number $number, int $precision = 0): Number 654 | { 655 | return static::convertToNumber($number)->round($precision, RoundingMode::PositiveInfinity); 656 | } 657 | 658 | public static function ceil(int|string|Number $number): Number 659 | { 660 | return static::convertToNumber($number)->ceil(); 661 | } 662 | 663 | public static function roundDown(int|string|Number $number, int $precision = 0): Number 664 | { 665 | return static::convertToNumber($number)->round($precision, RoundingMode::NegativeInfinity); 666 | } 667 | } 668 | -------------------------------------------------------------------------------- /tests/Unit/BCTest.php: -------------------------------------------------------------------------------- 1 | = $left); 313 | self::assertTrue($rand <= $right); 314 | } 315 | 316 | public function testMax(): void 317 | { 318 | self::assertSame('3', (string)BC::max(1, 2, 3)); 319 | self::assertSame('6', (string)BC::max(6, 3, 2)); 320 | self::assertSame('999', (string)BC::max(100, 999, 5)); 321 | 322 | self::assertSame('677', (string)BC::max([3, 5, 677])); 323 | self::assertSame('-3', (string)BC::max([-3, -5, -677])); 324 | 325 | self::assertSame( 326 | '999999999999999999999999999999999999999999', 327 | (string)BC::max( 328 | '432423432423423423423423432432423423423', 329 | '999999999999999999999999999999999999999999', 330 | '321312312423435657' 331 | ) 332 | ); 333 | self::assertSame('0.00000000099999', (string)BC::max(9.9999E-10, -5.6E-4)); 334 | } 335 | 336 | public function testMin(): void 337 | { 338 | self::assertSame('7.20', (string)BC::min('7.30', '7.20')); 339 | self::assertSame('3', (string)BC::min([3, 5, 677])); 340 | self::assertSame('-677', (string)BC::min([-3, -5, -677])); 341 | 342 | self::assertSame( 343 | '321312312423435657', 344 | (string)BC::min( 345 | '432423432423423423423423432432423423423', 346 | '999999999999999999999999999999999999999999', 347 | '321312312423435657' 348 | ) 349 | ); 350 | 351 | self::assertSame('-0.00056', (string)BC::min(9.9999E-10, -5.6E-4)); 352 | } 353 | 354 | public static function setScaleProvider(): array 355 | { 356 | return [[50, '3', '1', '2'], [0, '3', '1', '2'], [13, '3', '1', '2']]; 357 | } 358 | 359 | #[DataProvider('setScaleProvider')] 360 | public function testSetScale(int $scale, string $expected, string $left, string $right): void 361 | { 362 | BC::setScale($scale); 363 | self::assertSame($expected, (string)BC::add($left, $right)); 364 | } 365 | 366 | public static function roundUpProvider(): array 367 | { 368 | return [ 369 | ['663', '662.79'], 370 | ['662.8', '662.79', 1], 371 | ['60', '54.1', -1], 372 | ['60', '55.1', -1], 373 | ['-23.6', '-23.62', 1], 374 | ['4', '3.2'], 375 | ['77', '76.9'], 376 | ['3.142', '3.14159', 3], 377 | ['-3.1', '-3.14159', 1], 378 | ['31500', '31415.92654', -2], 379 | ['31420', '31415.92654', -1], 380 | ['0.0119', '0.0119', 4], 381 | ['0', '-0'], 382 | ['0', ''], 383 | ['0', 'null'], 384 | ['0', '0-'], 385 | ['1', '9.9999E-10'], 386 | ]; 387 | } 388 | 389 | #[DataProvider('roundUpProvider')] 390 | public function testRoundUp(string $expected, string $number, int $precision = 0): void 391 | { 392 | self::assertSame($expected, (string)BC::roundUp($number, $precision)); 393 | } 394 | 395 | public static function roundDownProvider(): array 396 | { 397 | return [ 398 | ['662', '662.79'], 399 | ['662.7', '662.79', 1], 400 | ['50', '54.1', -1], 401 | ['50', '55.1', -1], 402 | ['-23.7', '-23.62', 1], 403 | ['3', '3.2'], 404 | ['76', '76.9'], 405 | ['3.141', '3.14159', 3], 406 | ['-3.2', '-3.14159', 1], 407 | ['31400', '31415.92654', -2], 408 | ['31410', '31415.92654', -1], 409 | ['0.0119', '0.0119', 4], 410 | ['0', '-0'], 411 | ['0', ''], 412 | ['0', 'null'], 413 | ['0', '0-'], 414 | ['0', '9.9999E-10'], 415 | ['1.12', '1.1259', 2], 416 | ]; 417 | } 418 | 419 | #[DataProvider('roundDownProvider')] 420 | public function testRoundDown(string $expected, string $number, int $precision = 0): void 421 | { 422 | self::assertSame($expected, (string)BC::roundDown($number, $precision)); 423 | } 424 | 425 | public static function addProvider(): array 426 | { 427 | return [ 428 | ['3', '1', '2'], 429 | ['2', '1', '1'], 430 | ['15', '10', '5'], 431 | ['2.05', '1', '1.05', 2], 432 | ['4', '-1', '5', 4], 433 | ['8728932003911564969352217864684', '1928372132132819737213', '8728932001983192837219398127471', 2], 434 | ['-0.00055999', '9.9999E-10', '-5.6E-4', 8], 435 | ['15.000000000000311', '3.11e-13', '15', 15], 436 | ['3110000015', '3.11e9', '15', 0], 437 | ]; 438 | } 439 | 440 | #[DataProvider('addProvider')] 441 | public function testAdd(string $expected, string $left, string $right, ?int $scale = 0): void 442 | { 443 | self::assertSame($expected, (string)BC::add($left, $right, $scale)); 444 | } 445 | 446 | public function testAddUsingGlobalScale(): void 447 | { 448 | BC::setScale(0); 449 | self::assertSame('2', (string)BC::add('1', '1.05')); 450 | self::assertSame('2.05', (string)BC::add('1', '1.05', 2)); 451 | BC::setScale(2); 452 | self::assertSame('2', (string)BC::add('1', '1.05', 0)); 453 | self::assertSame('2.05', (string)BC::add('1', '1.05')); 454 | } 455 | 456 | public function testSubUsingGlobalScale(): void 457 | { 458 | BC::setScale(0); 459 | self::assertSame('-1', (string)BC::sub('1', '2.5')); 460 | self::assertSame('-1.5', (string)BC::sub('1', '2.5', 2)); 461 | BC::setScale(2); 462 | self::assertSame('-1', (string)BC::sub('1', '2.5', 0)); 463 | self::assertSame('-1.5', (string)BC::sub('1', '2.5')); 464 | } 465 | 466 | public static function subProvider(): array 467 | { 468 | return [ 469 | ['-1', '1', '2'], 470 | ['0', '1', '1'], 471 | ['5', '10', '5'], 472 | ['-1.5', '1', '2.5', 2], 473 | ['-6', '-1', '5', 4], 474 | ['8728932000054820705086578390258', '8728932001983192837219398127471', '1928372132132819737213', 2], 475 | ['0.00056', '9.9999E-10', '-5.6E-4', 8], 476 | ]; 477 | } 478 | 479 | #[DataProvider('subProvider')] 480 | public function testSub(string $expected, string $left, string $right, ?int $scale = 0): void 481 | { 482 | self::assertSame($expected, (string)BC::sub($left, $right, $scale)); 483 | } 484 | 485 | public static function compProvider(): array 486 | { 487 | return [ 488 | ['-1', '5', BC::COMPARE_RIGHT_GRATER, 4], 489 | ['1928372132132819737213', '8728932001983192837219398127471', BC::COMPARE_RIGHT_GRATER, 1], 490 | ['1.00000000000000000001', '2', BC::COMPARE_RIGHT_GRATER, 1], 491 | ['97321', '1', BC::COMPARE_LEFT_GRATER, 2], 492 | ['1', '0', BC::COMPARE_LEFT_GRATER, 0], 493 | ['1', '1', BC::COMPARE_EQUAL, 0], 494 | ['0', '1', BC::COMPARE_RIGHT_GRATER, 0], 495 | ['1', '0', BC::COMPARE_LEFT_GRATER, 0], 496 | ['1', '1', BC::COMPARE_EQUAL, 0], 497 | ['0', '1', BC::COMPARE_RIGHT_GRATER, 0], 498 | ['1', '0.0005', BC::COMPARE_LEFT_GRATER, 4], 499 | ['1', '0.000000000000000000000000005', BC::COMPARE_LEFT_GRATER, 2], 500 | ]; 501 | } 502 | 503 | #[DataProvider('compProvider')] 504 | public function testComp(string $left, string $right, int $expected, int $scale): void 505 | { 506 | self::assertSame($expected, BC::compare($left, $right, $scale)); 507 | } 508 | 509 | public static function getScaleProvider(): array 510 | { 511 | return [[10], [25], [0]]; 512 | } 513 | 514 | #[DataProvider('getScaleProvider')] 515 | public function testGetScale(int $expected): void 516 | { 517 | BC::setScale($expected); 518 | 519 | self::assertSame($expected, BC::getScale()); 520 | } 521 | 522 | public static function divProvider(): array 523 | { 524 | return [ 525 | ['0.5', '1', '2', 2], 526 | ['-0.2', '-1', '5', 4], 527 | ['4526580661.75', '8728932001983192837219398127471', '1928372132132819737213', 2], 528 | ['0.000000000099999', '9.9999E-10', '10', 15], 529 | ]; 530 | } 531 | 532 | #[DataProvider('divProvider')] 533 | public function testDiv(string $expected, string $left, string $right, ?int $scale): void 534 | { 535 | self::assertSame($expected, (string)BC::div($left, $right, $scale)); 536 | } 537 | 538 | public function testThrowDivByZero(): void 539 | { 540 | $this->expectExceptionMessage('Division by zero'); 541 | $this->expectException(InvalidArgumentException::class); 542 | BC::div('1', '0'); 543 | } 544 | 545 | public function testDivUsingGlobalScale(): void 546 | { 547 | BC::setScale(0); 548 | self::assertSame('0', (string)BC::div('1', '2')); 549 | self::assertSame('0.5', (string)BC::div('1', '2', 2)); 550 | BC::setScale(2); 551 | self::assertSame('0', (string)BC::div('1', '2', 0)); 552 | self::assertSame('0.5', (string)BC::div('1', '2')); 553 | } 554 | 555 | public static function modProvider(): array 556 | { 557 | return [ 558 | ['1', '11', '2', 0], 559 | ['-1', '-1', '5', 0], 560 | ['1459434331351930289678', '8728932001983192837219398127471', '1928372132132819737213', 0], 561 | ['0', '9.9999E-10', '1', 0], 562 | ['0.5', '10.5', '2.5', 2], 563 | ['0.5', '10.5', '2.5', 2], 564 | ['0.8', '10', '9.2', 1], 565 | ['0', '20', '4.0', 1], 566 | ['0', '10.5', '3.5', 1], 567 | ['0.3', '10.2', '3.3', 1], 568 | ['-0.000559999', '9.9999E-10', '-5.6E-4', 9], 569 | ]; 570 | } 571 | 572 | #[DataProvider('modProvider')] 573 | public function testMod(string $expected, string $left, string $right, int $scale): void 574 | { 575 | self::assertSame($expected, (string)BC::mod($left, $right, $scale)); 576 | } 577 | 578 | public static function mulProvider(): array 579 | { 580 | return [ 581 | ['1', '1.5', '1.5', 1], 582 | ['10', '1.2500', '12.5', 2], 583 | ['100', '0.29', '29', 0], 584 | ['100', '0.029', '2.9', 1], 585 | ['100', '0.0029', '0.29', 2], 586 | ['1000', '0.29', '290', 0], 587 | ['1000', '0.029', '29', 0], 588 | ['1000', '0.0029', '2.9', 1], 589 | ['2000', '0.0029', '5.8', 1], 590 | ['1', '2', '2', 2], 591 | ['-3', '5', '-15', 2], 592 | ['1234567890', '9876543210', '12193263111263526900', 2], 593 | ['2.5', '1.5', '3.75', 2], 594 | ['2.555', '1.555', '3.97', 2], 595 | ['9.9999E-2', '-5.6E-2', '-0.005599944', 9], 596 | ]; 597 | } 598 | 599 | #[DataProvider('mulProvider')] 600 | public function testMul(string $leftOperand, string $rightOperand, string $expected, ?int $scale): void 601 | { 602 | self::assertSame($expected, (string)BC::mul($leftOperand, $rightOperand, $scale)); 603 | } 604 | 605 | public static function powProvider(): array 606 | { 607 | return [ 608 | ['256', '2', '8', 0], 609 | ['74.08', '4.2', '3', 2], 610 | ['-32', '-2', '5', 4], 611 | ['18446744073709551616', '2', '64', 0], 612 | ['-108.88', '-2.555', '5', 2], 613 | ['63998080023999840000.5999988', '19.9999E+2', '6', 9], 614 | [ 615 | '1229984803535237425357460579824952453848609953896821302286319065669207712270213276022808840210306942692366529569453244416', 616 | '66', 617 | '66', 618 | 0, 619 | ], 620 | ['1', '0', '0', 0], 621 | ['0.1', '10', '-1', 1], 622 | ['1.0837983867343681398392334849264865554733', '5', '0.05', 40], 623 | ['59.3839', '8', '1.964', 4], 624 | ['1', '10', '0.0000001', 0], 625 | ['1', '10', '0.0000001', 2], 626 | ['36.029', '5.1', '2.2', 3], 627 | ['0.5492944034820568190269179209773099881437', '1.005', '-120.12345678', 40], 628 | ]; 629 | } 630 | 631 | #[DataProvider('powProvider')] 632 | public function testPow(string $expected, string $left, string $right, ?int $scale = 0): void 633 | { 634 | self::assertSame($expected, (string)BC::pow($left, $right, $scale)); 635 | } 636 | 637 | public static function logProvider(): array 638 | { 639 | return [ 640 | [ 641 | '0.6931471805599453094172321214581765680755001343602552541206800094933936219696947156058633269964186879', 642 | '2', 643 | ], 644 | [ 645 | '2.3025850929940456840179914546843642076011014886287729760333279009675726096773524802359972050895982985', 646 | '10', 647 | ], 648 | ['-INF', '0'], 649 | ['0', '1'], 650 | ['NAN', '-1'], 651 | ]; 652 | } 653 | 654 | #[DataProvider('logProvider')] 655 | public function testLog(string $expected, string $value): void 656 | { 657 | self::assertSame($expected, (string)BC::log($value)); 658 | } 659 | 660 | public static function expProvider(): array 661 | { 662 | return [ 663 | [ 664 | '298.8674009670602326720280305552958844792720557285859930698483175291494073935732314440167229005529804941', 665 | '5.7', 666 | ], 667 | [ 668 | '5009718959.9179776145898629945755344777367448017822374820837617183949259570367010764387804812833108859845756998', 669 | '22.334645654645', 670 | ], 671 | [ 672 | '0.3678794411714423215955237701614608674458111310317678345078368016974614957448998033571472743459196437', 673 | '-1', 674 | ], 675 | ['1', '0'], 676 | [ 677 | '1021450427617659.4094516982518620090788645038742627301331924304676748729927177787220453326541582910229003291582467933', 678 | '3.456e1', 679 | ], 680 | ]; 681 | } 682 | 683 | #[DataProvider('expProvider')] 684 | public function testExp(string $expected, string $arg): void 685 | { 686 | self::assertSame($expected, (string)BC::exp($arg)); 687 | } 688 | 689 | public static function factProvider(): array 690 | { 691 | return [ 692 | ['1', 'FOO'], 693 | ['6', '3'], 694 | ['24', '4'], 695 | ['120', '5'], 696 | ['12696403353658275925965100847566516959580321051449436762275840000000000000', '55'], 697 | [ 698 | '1352120101970733773067011614274819697122599483131811908183737047628177265994034132291368713461649497349302589856143133435229510116396294837506877692006315661432915834997174910826068673360566341497288989997209954743556681697531481588638027960571240359783423531512393979498023752359924002706644565826435712121847113827344984006487098528182570814184286177369204400602363840908701270850750232905962380555370342454089529954739412757066091123167095526161838186578820110955607100824438507032320782992357615916749432532447527368676631859661521238760870735527498653795183539783564690884483063180835764379492982520378977182244324512341018150509625525378905237230283986134834548153407352954715849859625768547943765161546418930836848064948451279093982894200819727754989070021704775797730750238107994872656828695516417881633934002199666960973645243375443246651301541333655652238296995689775354768542007545075142984211576541365557119557540627643329393683436387728118677828867857740757132233113516693527211814574739338293271460885570441349231351752383433352765975545204375912432594596113332741119354939353014503322387908651048263510102330371491105379688834342735823028755445146907083524895708398217607871080941341652914407671398367518509530325036889967085438794341130203490432090168747436904916918651173414472969992936827322290441286162500190109938519460450826861557230028805295622910014088032828346719679517421247715020534537700965849660883032302717019644562433150024635604762052002534028696629046990072858757588435951741887858588348113453649702435529712275328734772200798824647783552298277793615161059963699445498256000077540527301059550783529092098564446519833883557504895944421503363619527476998068362404201929345421647714763941354568414779882124491156177862842542815506817685490004739467429347446618659579953462063079757736600753871877067083965237845213240010848581504831053975434425143171746631341762195342736634638129582085373475793944488694512602143694792948257271921140153640603343096248881341215081509400288581408435215549715915260473296819413135019859948374962046712488759726342119726826901506105396218407578855410289091823224308767092452169221737093493587297758858417573680905241101368865449581132523234774846630787586927672276856400694036146591236722557130579954111692826164834542054936860327798863571665477779826251029086223483796906779089487784145495378256136425915177355034898173950147490682126470336438593281560487283159506304621370530261708627129060553915509003975468090346375866276033394920320678432457143507723107122487195374519128978668296580081062332375853436617376879980116152400514000392596929419052997064108467277989862501470384743645675198143314964216720733011237794071202653538264770997343051525931760263574123279062668951096284013681437574586191015603626739281515733205332994849968660671183273822108848740739542594997209308985543929302889180711874734873521273053528846156754996593353832701400704122512249925895034727323372449016472717793021639824951160265330662841601481925693783522925057812098900177609172565210004384736787468976183572374389866970896876729283713848904742983177352243630752419854642087694908122374322997951504415719963497682903859528697835977460808683230919315768931320321515572756803021410702219574222854791625348206167001024307602312292274062783656313069811273748388957231168299593456124069326155054222350440615253925773104703909512962614769075725229805737146418789996836311534187921166588053060653570084646462567237637902199048768915146311997127035919225466123766967732669086045035741874988471283181730605371604597281737626169543071041569095277933284149606769679089217424933371700178273937138278648458324132426529709883371763283712076074348683393641921474293431751961856826381800033652192885044485408096653004376245673073573167133545715282194839672509402656047674960172191002707776969993708883002946569483638775035028406280030045680099965963352967687712780415844194344897034867035649238735417907284480143800715923875226999988182837399775375032771365049990944279400177666169025474822560599277998078660375551951259606461334237444393907826059364572995077012696500217571481359989544121387805436811766656562426437674715462409672464285545179178656627209567861141385839660668523900041470576453589931144057302653030962570219302927462587782634610438021502841017584951874606688047253850643655244112919192792819462728160774912708068851493794723457166187479660457072589244982411564768691660373121926807493884960044548017084897020763855207653681096759142002538226845404372799721685705572634438220271114429420133733951367785285045015242008542653557707317451857972154654382589317549090816442222028837987889474372606419089374814467969896617039883595672111439205446056522484144835457023481497028559622902200558019868118246599782160047265704558098918522653452115528498393826050243781982164817504322506948067517212641792764493495897458412764877555887954848260359825696806281379301363345037981603241039149654502867961958451074927305138707709064067972599358554012791947848815290502895895860449670062560156040554849129569699278138934666508929878995893935747162189359711443358863173891265526226063435539120837625829638607194040524668720159344401629952773505778079352490821105274251986635668955237486825386746806409531711107991700074469550018959692530976626932834845278877276910533814229212661693460909241564118904928212460243057459003602392795914829610139931664352827606322648351888396817703519638140804131058812545855808475244859924876181478011379330289041290637623990235765095562678716591049279750452157666526740287900476731608369483988928999686707572922693981700802433912039899996007955549400467969054090810822620301958626526282042169285939649535367207857745108450969447098437807317727774253678971042666633565535058826907799225976904526915064162232477244522769483279502195056391311845087471891767935107410797095722827132957212481902147148757254626945121374919137154163065062186579710977767318846783049909516922471026817599405710343608499199244540993787974291529714016949175605461772769321738528529439505603179052626064457271898086735983761791376964239234728566142708083390935543815061352142697561148552832340249191226591947656969923293812987346346579553020648796718123999227633510804333685665336946322035397517106771942515502711510070956498459423946691815638510496076491288834098753445908737263754155130880000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', 699 | '2.349e+3', 700 | ], 701 | ]; 702 | } 703 | 704 | #[DataProvider('factProvider')] 705 | public function testFact(string $expected, string $fact): void 706 | { 707 | self::assertSame($expected, (string)BC::fact($fact)); 708 | } 709 | 710 | public function testFactThrowErrorOnFloat(): void 711 | { 712 | $this->expectExceptionMessage('Number has to be an integer'); 713 | $this->expectException(InvalidArgumentException::class); 714 | (string)BC::fact('1.1'); 715 | } 716 | 717 | public function testFactThrowErrorOnNegative(): void 718 | { 719 | $this->expectExceptionMessage('Number has to be greater than or equal to 0'); 720 | $this->expectException(InvalidArgumentException::class); 721 | (string)BC::fact('-1'); 722 | } 723 | 724 | public function testPowUsingGlobalScale(): void 725 | { 726 | BC::setScale(0); 727 | self::assertSame('74', (string)BC::pow('4.2', '3')); 728 | self::assertSame('74.08', (string)BC::pow('4.2', '3', 2)); 729 | BC::setScale(2); 730 | self::assertSame('74', (string)BC::pow('4.2', '3', 0)); 731 | self::assertSame('74.08', (string)BC::pow('4.2', '3')); 732 | } 733 | 734 | public static function powModProvider(): array 735 | { 736 | return [ 737 | ['4', '5', '2', '7', 0], 738 | ['4', '5', '2', '7', null], 739 | ['-4', '-2', '5', '7', 0], 740 | ['790', '10', '2147483648', '2047', 0], 741 | ['790', '1E+1', '2E+8', '2047', 0], 742 | ['4', '5', '2', '7', 2], 743 | ['3.7', '5', '2', '7.1', 2], 744 | ['3.7', '5', '2', '7.1', 2], 745 | ['1', '4', '4', '3', 2], 746 | ['0.52', '5.1', '2.2', '7.1', 2], 747 | ['0.52', '5.1', '2.2', '7.1', null], 748 | ]; 749 | } 750 | 751 | #[DataProvider('powModProvider')] 752 | public function testPowMod(string $expected, string $left, string $right, string $modulus, ?int $scale): void 753 | { 754 | self::assertSame($expected, (string)BC::powMod($left, $right, $modulus, $scale)); 755 | } 756 | 757 | public static function sqrtProvider(): array 758 | { 759 | return [ 760 | ['3', '9', 0], 761 | ['3.07', '9.444', 2], 762 | ['43913234134.28826', '1928372132132819737213', 5], 763 | ['0.31', '9.9999E-2', 2], 764 | ]; 765 | } 766 | 767 | #[DataProvider('sqrtProvider')] 768 | public function testSqrt(string $expected, string $operand, int $scale): void 769 | { 770 | self::assertSame($expected, (string)BC::sqrt($operand, $scale)); 771 | } 772 | 773 | public function testSqrtUsingGlobalScale(): void 774 | { 775 | BC::setScale(0); 776 | self::assertSame('3', (string)BC::sqrt('9.444')); 777 | self::assertSame('3.07', (string)BC::sqrt('9.444', 2)); 778 | BC::setScale(2); 779 | self::assertSame('3', (string)BC::sqrt('9.444', 0)); 780 | self::assertSame('3.07', (string)BC::sqrt('9.444')); 781 | } 782 | 783 | public static function hexdecProvider(): array 784 | { 785 | return [ 786 | ['123', '7b'], 787 | ['1234567890', '499602d2'], 788 | ['12345678901234567890', 'ab54a98ceb1f0ad2'], 789 | ['123456789012345678901234567890', '18ee90ff6c373e0ee4e3f0ad2'], 790 | ['1234567890123456789012345678901234567890', '3a0c92075c0dbf3b8acbc5f96ce3f0ad2'], 791 | ['1234567890123456789012345678901234567890', '3a0c92075c0dbf3b8acbc5f96ce3f0ad2'], 792 | [ 793 | '21711016733383976335769713789619838426107890367109022384973563144062439196678', 794 | '0x300000000d2c12440c4310c20c2428c20c8330cc0ca318ca0cc330cc0c230806', 795 | ], 796 | ]; 797 | } 798 | 799 | #[DataProvider('hexdecProvider')] 800 | public function testHexdec(string $expected, string $operand): void 801 | { 802 | self::assertSame($expected, BC::hexdec($operand)); 803 | } 804 | 805 | public static function dechexProvider(): array 806 | { 807 | return [ 808 | ['7b', '123'], 809 | ['ffffffff', '4294967295'], 810 | ['200000000', '8589934592'], 811 | ['7fffffffffffffff', '9223372036854775807'], 812 | ['10000000000000000', '18446744073709551616'], 813 | ['18ee90ff6c373e0ee4e3f0ad2', '123456789012345678901234567890'], 814 | ]; 815 | } 816 | 817 | #[DataProvider('dechexProvider')] 818 | public function testDechex(string $expected, string $operand): void 819 | { 820 | self::assertSame($expected, BC::dechex($operand)); 821 | } 822 | 823 | #[DataProvider('bitAddProvider')] 824 | public function testBitAdd(string $expected, string $left, string $right): void 825 | { 826 | self::assertSame($expected, (string)BC::bitAnd($left, $right)); 827 | } 828 | 829 | public static function bitAddProvider(): array 830 | { 831 | return [ 832 | [ 833 | '2972225677459078825024027220918272', 834 | '1000000000865464564564564567867867867800000', 835 | '5000788676546456456458678760000000', 836 | ], 837 | ['610237752474644548', '543543543534543534543534543 ', '4213434324324234324'], 838 | ['0', '0', '0'], 839 | ['0', '0', '5'], 840 | ['1', '1', '5'], 841 | ['0', '2', '5'], 842 | ['4', '4', '5'], 843 | ['4', '-4', '5'], 844 | ['0', '4', '-5'], 845 | ['0', '8', '5'], 846 | ]; 847 | } 848 | 849 | #[DataProvider('bitOrProvider')] 850 | public function testBitOr(string $expected, string $left, string $right): void 851 | { 852 | self::assertSame($expected, (string)BC::bitOr($left, $right)); 853 | } 854 | 855 | public static function bitOrProvider(): array 856 | { 857 | return [ 858 | [ 859 | '1000000002894027563651942199302519406881728', 860 | '1000000000865464564564564567867867867800000', 861 | '5000788676546456456458678760000000', 862 | ], 863 | ['543543547137740106393124319', '543543543534543534543534543 ', '4213434324324234324'], 864 | ['0', '0', '0'], 865 | ['5', '0', '5'], 866 | ['5', '1', '5'], 867 | ['7', '2', '5'], 868 | ['5', '4', '5'], 869 | ['-3', '-4', '5'], 870 | ['-1', '4', '-5'], 871 | ['13', '8', '5'], 872 | ['-853289843298817', '-3213123123123123', '-999696956946954'], 873 | ['-113449967538441782579763', '-677868678631231237867786867', '-123213213123123123123123'], 874 | ]; 875 | } 876 | 877 | #[DataProvider('bitXorProvider')] 878 | public function testBitXor(string $expected, string $left, string $right): void 879 | { 880 | self::assertSame($expected, (string)BC::bitXor($left, $right)); 881 | } 882 | 883 | public static function bitXorProvider(): array 884 | { 885 | return [ 886 | ['7', '2', '5'], 887 | ['-8', '3', '-5'], 888 | ['-6', '-4', '6'], 889 | ['15', '-8', '-9'], 890 | ['32111810161015317381218', '21312831290381290382198', '10912839021839123211028'], 891 | ['-2', '999999999999999999999999999999999', '-999999999999999999999999999999999'], 892 | ['0', '999999999999999999999999999999999', '999999999999999999999999999999999'], 893 | ['-2', '-999999999999999999999999999999999', '999999999999999999999999999999999'], 894 | ]; 895 | } 896 | 897 | public function testThrowErrorOnFloatXorLeftOperator(): void 898 | { 899 | $this->expectExceptionMessage('Left operator has to be an integer'); 900 | $this->expectException(InvalidArgumentException::class); 901 | BC::bitXor('0.001', '1'); 902 | } 903 | 904 | public function testThrowErrorOnNegativeModPowExponent(): void 905 | { 906 | $this->expectExceptionMessage('Exponent can\'t be negative'); 907 | $this->expectException(InvalidArgumentException::class); 908 | BC::powMod('10000.123', '-1', '2'); 909 | } 910 | 911 | public function testThrowErrorOnZeroModPowModulus(): void 912 | { 913 | $this->expectExceptionMessage('Modulus can\'t be zero'); 914 | $this->expectException(InvalidArgumentException::class); 915 | BC::powMod('10000.123', '1', '0'); 916 | } 917 | 918 | public function testThrowErrorOnFloatOrLeftOperator(): void 919 | { 920 | $this->expectExceptionMessage('Left operator has to be an integer'); 921 | $this->expectException(InvalidArgumentException::class); 922 | BC::bitOr('0.001', '1'); 923 | } 924 | 925 | public function testThrowErrorOnFloatAndLeftOperator(): void 926 | { 927 | $this->expectException(InvalidArgumentException::class); 928 | $this->expectExceptionMessage('Left operator has to be an integer'); 929 | BC::bitAnd('0.001', '1'); 930 | } 931 | 932 | public function testThrowErrorOnFloatXorRightOperator(): void 933 | { 934 | $this->expectExceptionMessage('Right operator has to be an integer'); 935 | $this->expectException(InvalidArgumentException::class); 936 | BC::bitXor('1', '0.001'); 937 | } 938 | 939 | public function testThrowErrorOnFloatOrRightOperator(): void 940 | { 941 | $this->expectExceptionMessage('Right operator has to be an integer'); 942 | $this->expectException(InvalidArgumentException::class); 943 | BC::bitOr('1', '0.001'); 944 | } 945 | 946 | public function testThrowErrorOnFloatAndRightOperator(): void 947 | { 948 | $this->expectException(InvalidArgumentException::class); 949 | $this->expectExceptionMessage('Right operator has to be an integer'); 950 | BC::bitAnd('1', '0.001'); 951 | } 952 | 953 | #[DataProvider('convertBinaryProvider')] 954 | public function testConvertBinary(string $expected, string $base64binary): void 955 | { 956 | $decoded = (string)base64_decode($base64binary, true); 957 | self::assertSame($expected, BC::bin2dec($decoded)); 958 | self::assertSame($decoded, BC::dec2bin($expected)); 959 | } 960 | 961 | public static function convertBinaryProvider(): array 962 | { 963 | return [ 964 | ['1000000000865464564564564567867867867800000', 'C3q8Ypr74y+H5r28hvnkbmHA'], 965 | ['5000788676546456456458678760000000', '9o7TsLbE7mZg8DmCSgA'], 966 | ['543543543534543534543534543', 'AcGb0ol63k727vnP'], 967 | ['2', 'Ag=='], 968 | ['48', 'MA=='], 969 | ]; 970 | } 971 | 972 | public function testThrowErrorOnIncorrectBaseInBin2Dec(): void 973 | { 974 | $this->expectExceptionMessage('Invalid Base: 300'); 975 | $this->expectException(InvalidArgumentException::class); 976 | BC::bin2dec('', 300); 977 | } 978 | 979 | public function testThrowErrorOnIncorrectBaseInDec2Bin(): void 980 | { 981 | $this->expectException(InvalidArgumentException::class); 982 | $this->expectExceptionMessage('Invalid Base: 600'); 983 | BC::dec2bin('1', 600); 984 | } 985 | 986 | public function testCorrectFormatTrailingZeros(): void 987 | { 988 | BC::setTrimTrailingZeroes(false); 989 | 990 | self::assertSame('64.0000000000', (string)BC::pow('8', '2', 10)); 991 | self::assertSame('2.0000', (string)BC::sqrt('4', 4)); 992 | self::assertSame('14444.2230000000', (string)BC::add('10000.123', '4444.1', 10)); 993 | self::assertSame('19.524770142330000', (string)BC::mul('9.123767', '2.13999', 15)); 994 | self::assertSame('2.00', (string)BC::div('4.000', '2.0', 2)); 995 | self::assertSame('5556.0230000000', (string)BC::sub('10000.123', '4444.1', 10)); 996 | self::assertSame('1.35', (string)BC::powMod('10000.123', '4444.1', '2')); 997 | self::assertSame('1111.9230000000', (string)BC::mod('10000.123', '4444.1', 10)); 998 | self::assertSame('-50000000000.0000000000', (string)BC::convertToNumber('-5e+10')); 999 | self::assertSame('10000.123000', (string)BC::round('10000.123', 6)); 1000 | 1001 | BC::setTrimTrailingZeroes(true); 1002 | 1003 | self::assertSame('64', (string)BC::pow('8', '2', 10)); 1004 | self::assertSame('2', (string)BC::sqrt('4', 4)); 1005 | self::assertSame('14444.223', (string)BC::add('10000.123', '4444.1', 10)); 1006 | self::assertSame('19.52477014233', (string)BC::mul('9.123767', '2.13999', 15)); 1007 | self::assertSame('2', (string)BC::div('4.000', '2.0', 2)); 1008 | self::assertSame('5556.023', (string)BC::sub('10000.123', '4444.1', 10)); 1009 | self::assertSame('1.35', (string)BC::powMod('10000.123', '4444.1', '2')); 1010 | self::assertSame('1111.923', (string)BC::mod('10000.123', '4444.1', 10)); 1011 | self::assertSame('-50000000000', (string)BC::convertToNumber('-5e+10')); 1012 | self::assertSame('10000.123', (string)BC::round('10000.123', 6)); 1013 | } 1014 | } 1015 | --------------------------------------------------------------------------------