diff --git a/.github/workflows/acceptance.yml b/.github/workflows/acceptance.yml index c604970..4378918 100644 --- a/.github/workflows/acceptance.yml +++ b/.github/workflows/acceptance.yml @@ -3,80 +3,36 @@ name: Acceptance on: [push, pull_request] jobs: - test-7-4: - runs-on: ubuntu-latest - name: Test PHP 7.4 - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Set up PHP 7.4 - uses: shivammathur/setup-php@v2 - with: - php-version: '7.4' - - name: Composer - run: make deps-install - - name: Test - run: make test - - test-8-0: - runs-on: ubuntu-latest - name: Test PHP 8.0 - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Set up PHP 8.0 - uses: shivammathur/setup-php@v2 - with: - php-version: '8.0' - - name: Composer - run: make deps-install - - name: Test - run: make test - test-8-1: + test: + strategy: + matrix: + php-versions: ["7.4", "8.0", "8.1", "8.2", "8.3", "8.4"] runs-on: ubuntu-latest - name: Test PHP 8.1 + name: Unit test steps: - name: Checkout uses: actions/checkout@v3 - - name: Set up PHP 8.1 + - name: Set up PHP uses: shivammathur/setup-php@v2 with: - php-version: '8.1' - - name: Composer - run: make deps-install - - name: Test - run: make test - - test-8-2: - runs-on: ubuntu-latest - name: Test PHP 8.2 - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Set up PHP 8.2 - uses: shivammathur/setup-php@v2 - with: - php-version: '8.2' - - name: Composer - run: make deps-install - - name: Test - run: make test - - test-8-3: - runs-on: ubuntu-latest - name: Test PHP 8.3 - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Set up PHP 8.3 - uses: shivammathur/setup-php@v2 + php-version: ${{ matrix.php-versions }} + coverage: none + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Get composer cache directory + id: composer-cache + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + - name: Cache dependencies + uses: actions/cache@v3 with: - php-version: '8.3' - - name: Composer - run: make deps-install + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} + restore-keys: ${{ runner.os }}-composer- + - name: Install dependencies + run: composer install --prefer-dist - name: Test - run: make test + run: vendor/bin/phpunit cs-check: runs-on: ubuntu-latest @@ -84,14 +40,26 @@ jobs: steps: - name: Checkout uses: actions/checkout@v3 - - name: Set up PHP 8.2 + - name: Set up PHP uses: shivammathur/setup-php@v2 with: - php-version: '8.2' - - name: Composer - run: make deps-install + php-version: "8.2" + coverage: none + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Get composer cache directory + id: composer-cache + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + - name: Cache dependencies + uses: actions/cache@v3 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} + restore-keys: ${{ runner.os }}-composer- + - name: Install dependencies + run: composer install --prefer-dist - name: Code standard - run: make cs-check + run: vendor/bin/phpcs --standard=PSR1,PSR12 --encoding=UTF-8 --report=full --colors src tests coverage: runs-on: ubuntu-latest @@ -99,14 +67,27 @@ jobs: steps: - name: Checkout uses: actions/checkout@v3 - - name: Set up PHP 8.2 + - name: Set up PHP uses: shivammathur/setup-php@v2 with: - php-version: '8.2' - extensions: xdebug - - name: Composer - run: make deps-install - - name: Code coverage + php-version: "8.2" + coverage: xdebug + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Get composer cache directory + id: composer-cache + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + - name: Cache dependencies + uses: actions/cache@v3 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} + restore-keys: ${{ runner.os }}-composer- + - name: Install dependencies + run: composer install --prefer-dist + - name: Code coverage build + run: XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-clover build/logs/clover.xml + - name: Code coverage upload env: COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: make coverage + run: vendor/bin/php-coveralls -v diff --git a/README.md b/README.md index 17745ac..46ee5b8 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ [![Build Status](https://github.com/sirn-se/phrity-util-numerics/actions/workflows/acceptance.yml/badge.svg)](https://github.com/sirn-se/phrity-util-numerics/actions) -[![Coverage Status](https://coveralls.io/repos/github/sirn-se/phrity-util-numerics/badge.svg?branch=master)](https://coveralls.io/github/sirn-se/phrity-util-numerics?branch=master) +[![Coverage Status](https://coveralls.io/repos/github/sirn-se/phrity-util-numerics/badge.svg?branch=main)](https://coveralls.io/github/sirn-se/phrity-util-numerics?branch=main) # Numerics utility Utility library for numerics. Float versions of `ceil()`, `floor()` and `rand()` with precision. An open minded numeric parser, formatter, plus some additional functions. -Current version supports PHP `^7.1|^8.0`. +Current version supports PHP `^7.4|^8.0`. ## Installation @@ -22,15 +22,15 @@ composer require phrity/util-numerics Phrity\Util\Numerics { /* Methods */ - public __construct(int $precision = 0) - public ceil(number $number, int $precision = null) : float - public floor(number $number, int $precision = null) : float - public round(number $number, int $precision = null) : float - public parse(mixed $numeric) : float - public format(number $number, int $precision = null) : string - public rand(number $min = 0, number $max = 0, int $precision = null) : float - public precision(number $number) : int - public setLocale(string $locale) : void + public __construct(int|null $precision = null, string|null $locale = null); + public ceil(float $number, int|null $precision = null): float; + public floor(float $number, int|null $precision = null): float; + public round(float $number, int|null $precision = null): float; + public parse(int|float|string $numeric): float|null; + public format(float $number, int|null $precision = null): string; + public rand(float $min = 0, float[null $max = null, int|null $precision = null): float|null; + public precision(float $number, bool $wide = false): int; + public setLocale(string $locale): void; } ``` @@ -51,16 +51,16 @@ Round fractions up, according to precision specifier. A precision of `0` corresp ```php // Precison specified on each call $numerics = new Numerics(); -$numerics->ceil(1234.5678, 2) // 1234.57 -$numerics->ceil(1234.5678, 1) // 1234.60 -$numerics->ceil(1234.5678, 0) // 1235.00 -$numerics->ceil(1234.5678, -1) // 1240.00 -$numerics->ceil(1234.5678, -2) // 1300.00 +$numerics->ceil(1234.5678, 2); // 1234.57 +$numerics->ceil(1234.5678, 1); // 1234.60 +$numerics->ceil(1234.5678, 0); // 1235.00 +$numerics->ceil(1234.5678, -1); // 1240.00 +$numerics->ceil(1234.5678, -2); // 1300.00 // Precison specified as default, override is possible $numerics = new Numerics(2); -$numerics->ceil(1234.5678) // 1234.57 -$numerics->ceil(1234.5678, 0) // 1235.00 +$numerics->ceil(1234.5678); // 1234.57 +$numerics->ceil(1234.5678, 0); // 1235.00 ``` ### Floor method @@ -70,16 +70,16 @@ Round fractions down, according to precision specifier. A precision of `0` corre ```php // Precison specified on each call $numerics = new Numerics(); -$numerics->floor(1234.5678, 2) // 1234.56 -$numerics->floor(1234.5678, 1) // 1234.50 -$numerics->floor(1234.5678, 0) // 1234.00 -$numerics->floor(1234.5678, -1) // 1230.00 -$numerics->floor(1234.5678, -2) // 1200.00 +$numerics->floor(1234.5678, 2); // 1234.56 +$numerics->floor(1234.5678, 1); // 1234.50 +$numerics->floor(1234.5678, 0); // 1234.00 +$numerics->floor(1234.5678, -1); // 1230.00 +$numerics->floor(1234.5678, -2); // 1200.00 // Precison specified as default, override is possible $numerics = new Numerics(2); -$numerics->floor(1234.5678) // 1234.56 -$numerics->floor(1234.5678, 0) // 1234.00 +$numerics->floor(1234.5678); // 1234.56 +$numerics->floor(1234.5678, 0); // 1234.00 ``` ### Round method @@ -89,16 +89,16 @@ Standard round, according to precision specifier. Precision can also be negative ```php // Precison specified on each call $numerics = new Numerics(); -$numerics->round(1234.5678, 2) // 1234.57 -$numerics->round(1234.5678, 1) // 1234.60 -$numerics->round(1234.5678, 0) // 1235.00 -$numerics->round(1234.5678, -1) // 1230.00 -$numerics->round(1234.5678, -2) // 1200.00 +$numerics->round(1234.5678, 2); // 1234.57 +$numerics->round(1234.5678, 1); // 1234.60 +$numerics->round(1234.5678, 0); // 1235.00 +$numerics->round(1234.5678, -1); // 1230.00 +$numerics->round(1234.5678, -2); // 1200.00 // Precison specified as default, override is possible $numerics = new Numerics(2); -$numerics->round(1234.5678) // 1234.57 -$numerics->round(1234.5678, 0) // 1235.00 +$numerics->round(1234.5678); // 1234.57 +$numerics->round(1234.5678, 0); // 1235.00 ``` ### Parse method @@ -109,49 +109,61 @@ Numeric parser. Parses number by evaluating input rather than using locale or ma $numerics = new Numerics(); // Integer and float input -$numerics->parse(1234.56) // 1234.56 -$numerics->parse(1234) // 1234.00 +$numerics->parse(1234.56); // 1234.56 +$numerics->parse(1234); // 1234.00 // String input -$numerics->parse('1234.56') // 1234.56 -$numerics->parse('1234,56') // 1234.56 -$numerics->parse('1 234.56') // 1234.56 -$numerics->parse('1 234,56') // 1234.56 -$numerics->parse('1,234.56') // 1234.56 -$numerics->parse('1.234,56') // 1234.56 +$numerics->parse('1234.56'); // 1234.56 +$numerics->parse('1234,56'); // 1234.56 +$numerics->parse('1 234.56'); // 1234.56 +$numerics->parse('1 234,56'); // 1234.56 +$numerics->parse('1,234.56'); // 1234.56 +$numerics->parse('1.234,56'); // 1234.56 // Evaluated string input -$numerics->parse(' 1 234.56 ') // 1234.56 -$numerics->parse('-1,234.56') // -1234.56 -$numerics->parse('+1.234,56') // 1234.56 -$numerics->parse('.56') // 0.56 -$numerics->parse(',56') // 0.56 -$numerics->parse('12.') // 12.0 -$numerics->parse('12,') // 12.0 +$numerics->parse(' 1 234.56 '); // 1234.56 +$numerics->parse('-1,234.56'); // -1234.56 +$numerics->parse('+1.234,56'); // 1234.56 +$numerics->parse('.56'); // 0.56 +$numerics->parse(',56'); // 0.56 +$numerics->parse('12.'); // 12.0 +$numerics->parse('12,'); // 12.0 // Locale support for fringe cases $numerics->setLocale('sv_SE'); -$numerics->parse('123,456') // 123.456 +$numerics->parse('123,456'); // 123.456 $numerics->setLocale('en_US'); -$numerics->parse('123,456') // 123456.0 +$numerics->parse('123,456'); // 123456.0 ``` ### Format method -Numeric formatter. Formats numbers according to precision (rounding and padding) and locale. +Numeric formatter. Formats numbers according to precision (rounding and padding) and locale. Precision can also be negative. ```php $numerics = new Numerics(); $numerics->setLocale('sv_SE'); -$numerics->format(1234.5678, 2) // "1 234,56" -$numerics->format(1234, 2) // "1 234,00" -$numerics->format(1234.5678, 0) // "1 234" +$numerics->format(1234.5678, 2); // "1 234,56" +$numerics->format(1234, 2); // "1 234,00" +$numerics->format(1234.5678, 0); // "1 234" +$numerics->format(1234, -2); // "1 200" $numerics->setLocale('en_US'); -$numerics->format(1234.5678, 2) // "1,234.56" -$numerics->format(1234, 2) // "1,234.00" -$numerics->format(1234.5678, 0) // "1,234" +$numerics->format(1234.5678, 2); // "1,234.56" +$numerics->format(1234, 2); // "1,234.00" +$numerics->format(1234.5678, 0); // "1,234" +$numerics->format(1234, -2); // "1,200" +``` + +Using format with many decimals. + +```php +$big = 0.123456789123456789; +$numerics->format($big); // "0.123457" - same as below +$numerics->format($big, $numerics->precision($big)); // "0.123457" - php decimal precision +$numerics->format($big, $numerics->precision($big, true)); // "0.123456789123457" - without precision loss +$numerics->format($big, 50); // "0.12345678912345678379658409085095627233386039733887" - with precision loss ``` ### Rand method @@ -161,27 +173,36 @@ Float random number with precision. Precision can also be negative. Returns `flo ```php // Precison specified on each call $numerics = new Numerics(); -$numerics->rand(0, 10) // 0.0 … 10.0 -$numerics->rand(0, 100, 2) // 0.00 … 100.00 -$numerics->rand(-100, 100, 4) // -100.0000 … 100.0000 -$numerics->rand(0.01, 0.97, 4) // 0.0100 … 0.9700 -$numerics->rand(9, 11, -1) // 10.0 -$numerics->rand(90, 110, -2) // 100.0 +$numerics->rand(0, 10); // 0.0 … 10.0 +$numerics->rand(0, 100, 2); // 0.00 … 100.00 +$numerics->rand(-100, 100, 4); // -100.0000 … 100.0000 +$numerics->rand(0.01, 0.97, 4); // 0.0100 … 0.9700 +$numerics->rand(9, 11, -1); // 10.0 +$numerics->rand(90, 110, -2); // 100.0 // Precison specified as default, override is possible $numerics = new Numerics(2); -$numerics->rand(0, 100) // 0.00 … 100.00 +$numerics->rand(0, 100); // 0.00 … 100.00 ``` ### Precision method Count number of relevant decimals in a number. +By default the number of serializable decimals (as of php evaluation) is returned. + +```php +$numerics = new Numerics(); +$numerics->precision(12); // 0 +$numerics->precision(12.0); // 0 +$numerics->precision(12.34); // 2 +$numerics->precision(0.123456789123456789); // 6 +``` + +By setting the `wide` option, number of decimals usable without precision loss is returned. ```php $numerics = new Numerics(); -$numerics->precision(12) // 0 -$numerics->precision(12.0) // 0 -$numerics->precision(12.34) // 2 +$numerics->precision(0.123456789123456789, true); // 15 ``` ### setLocale method @@ -197,6 +218,7 @@ $numerics->setLocale('sv_SE'); // Set to Swedish | Version | PHP | | | --- | --- | --- | +| `2.3` | `^7.4\|^8.0` | Precision imrovements, negative precision in format() | | `2.2` | `^7.4\|^8.0` | Default locale | | `2.1` | `^7.1\|^8.0` | | | `2.0` | `^7.1` | Instanceable, `format()` method, ability to specify locale | diff --git a/composer.json b/composer.json index 8213fb8..39eca07 100644 --- a/composer.json +++ b/composer.json @@ -1,33 +1,28 @@ { - "name": "phrity/util-numerics", - "type": "library", - "description": "Utility library for numerics. Float versions of ceil(), floor() and rand() with precision. Open minded numeric parser and formatter.", - "homepage": "https://phrity.sirn.se/util-numerics", - "keywords": ["numbers", "numerics", "float", "ceil", "floor", "precision", "parse", "format"], - "license": "MIT", - "authors": [ - { - "name": "Sören Jensen", - "email": "sirn@sirn.se", - "homepage": "https://phrity.sirn.se" + "name": "phrity/util-numerics", + "type": "library", + "description": "Utility library for numerics. Float versions of ceil(), floor() and rand() with precision. Open minded numeric parser and formatter.", + "homepage": "https://phrity.sirn.se/util-numerics", + "keywords": ["numbers", "numerics", "float", "ceil", "floor", "precision", "parse", "format"], + "license": "MIT", + "authors": [ + { + "name": "Sören Jensen", + "email": "sirn@sirn.se", + "homepage": "https://phrity.sirn.se" + } + ], + "autoload": { + "psr-4": { + "Phrity\\Util\\": "src/" + } + }, + "require": { + "php": "^7.4|^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.0|^10.0", + "php-coveralls/php-coveralls": "^2.4", + "squizlabs/php_codesniffer": "^3.5" } - ], - "autoload": { - "psr-4": { - "": "src/" - } - }, - "autoload-dev": { - "psr-4": { - "": "tests/" - } - }, - "require": { - "php": "^7.4|^8.0" - }, - "require-dev": { - "phpunit/phpunit": "^9.0|^10.0", - "php-coveralls/php-coveralls": "^2.4", - "squizlabs/php_codesniffer": "^3.5" - } } diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 2b5a122..2dd0e16 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,13 +1,13 @@ - - - - ./src/ - - + ./tests/ + + + ./src/ + + diff --git a/src/Phrity/Util/Numerics.php b/src/Numerics.php similarity index 72% rename from src/Phrity/Util/Numerics.php rename to src/Numerics.php index 0a843a0..cb2d5a9 100644 --- a/src/Phrity/Util/Numerics.php +++ b/src/Numerics.php @@ -14,23 +14,22 @@ */ class Numerics { - /** - * @var int $precision Default precision - */ + /** @var int $precision Default precision */ private $precision; - /** - * @var array $localization Localization data - */ + /** @var int $digits Relevant digits for floats */ + private $digits; + /** @var array $localization Localization data */ private $localization; /** * Constructor for this class - * @param integer $precision Default precision - * @param string $locale Default locale to use as string + * @param int|null $precision Default precision + * @param string|null $locale Default locale to use as string */ public function __construct(?int $precision = null, ?string $locale = null) { $this->precision = $precision; + $this->digits = PHP_FLOAT_DIG; $this->localization = localeconv(); if ($locale) { $this->setLocale($locale); @@ -39,7 +38,7 @@ public function __construct(?int $precision = null, ?string $locale = null) /** * Set locale on instance, used for format and parse method - * @param string $locale The locale to use as string + * @param string $locale The locale to use as string */ public function setLocale(string $locale): void { @@ -51,8 +50,8 @@ public function setLocale(string $locale): void /** * Floor function with precision. - * @param number $number The number to apply floor to - * @param integer $precision Precision to apply + * @param float $number The number to apply floor to + * @param int|null $precision Precision to apply * @return float Return floor with precision */ public function floor(float $number, ?int $precision = null): float @@ -63,8 +62,8 @@ public function floor(float $number, ?int $precision = null): float /** * Ceil function with precision. - * @param number $number The number to apply ceil to - * @param integer $precision Precision to apply + * @param float $number The number to apply ceil to + * @param int|null $precision Precision to apply * @return float Return ceil with precision */ public function ceil(float $number, ?int $precision = null): float @@ -75,9 +74,9 @@ public function ceil(float $number, ?int $precision = null): float /** * Round function with precision. - * @param number $number The number to apply round to - * @param integer $precision Precision to apply - * @return float Return round with precision + * @param float $number The number to apply round to + * @param int|null $precision Precision to apply + * @return float Return round with precision */ public function round(float $number, ?int $precision = null): float { @@ -86,10 +85,10 @@ public function round(float $number, ?int $precision = null): float /** * Random float number generator with precision. - * @param number $min Lowest result - * @param number $max Highest result - * @param integer $precision Precision to use - * @return float Random number with precision (null if not solvable) + * @param float $min Lowest result + * @param float|null $max Highest result + * @param int|null $precision Precision to use + * @return float|null Random number with precision (null if not solvable) */ public function rand(float $min = 0, ?float $max = null, ?int $precision = null): ?float { @@ -115,20 +114,22 @@ public function rand(float $min = 0, ?float $max = null, ?int $precision = null) /** * Count number of relevant decimals in a number. - * @param number $number The number to count decimals on - * @return int Number of decimals + * @param float $number The number to count decimals on + * @param bool $wide If all relevant decimals should be considered + * @return int Number of decimals */ - public function precision(float $number): int + public function precision(float $number, bool $wide = false): int { - $pos = strrchr((string)$number, '.'); + $numstr = $wide ? sprintf("%.{$this->digits}f", $number) : sprintf("%f", $number); + $pos = strrchr(rtrim($numstr, 0), '.'); return $pos ? max(0, strlen($pos) - 1) : 0; } /** * Numeric parser. * Identifies decimal/thousand separator from input rather than assumptions. - * @param mixed $numeric A numeric representation to parse - * @return float Return as float (null if parsing failed) + * @param int|float|string $numeric A numeric representation to parse + * @return float|null Return as float (null if parsing failed) */ public function parse($numeric): ?float { @@ -195,15 +196,16 @@ public function parse($numeric): ?float /** * Numeric formatter. - * @param number $number The number to count decimals on - * @param integer $precision Precision to use, no rounding by default - * @return string Numeric string + * @param float $number The number to count decimals on + * @param int|null $precision Precision to use, no rounding by default + * @return string Numeric string */ public function format(float $number, ?int $precision = null): string { + $precision = $precision ?? $this->precision ?? $this->precision($number); return number_format( - $number, - $precision ?? $this->precision ?? $this->precision($number), + $this->round($number, $precision), + $precision, $this->localization['decimal_point'], $this->localization['thousands_sep'] ); diff --git a/tests/FormatTest.php b/tests/FormatTest.php index bd757e7..4c61832 100644 --- a/tests/FormatTest.php +++ b/tests/FormatTest.php @@ -38,6 +38,9 @@ public function testFormat(): void $this->assertEquals('12.35', $numerics->format(12.345678, 2)); $this->assertEquals('-12.34', $numerics->format(-12.34, 2)); $this->assertEquals('1234.56', $numerics->format(1234.56, 2)); + $this->assertEquals('1200', $numerics->format(1234.56, -2)); + $this->assertEquals('1234.5600000000', $numerics->format(1234.56, 10)); + $this->assertEquals('0.12345678912345678380', $numerics->format(0.123456789123456789, 20)); } /** diff --git a/tests/PrecisionTest.php b/tests/PrecisionTest.php index d3a6024..9f56b1a 100644 --- a/tests/PrecisionTest.php +++ b/tests/PrecisionTest.php @@ -39,6 +39,9 @@ public function testValidPrecision(): void $this->assertEquals(1, $numerics->precision(12.30)); $this->assertEquals(2, $numerics->precision(12.34)); $this->assertEquals(2, $numerics->precision(12.340)); + $this->assertEquals(6, $numerics->precision(0.123456789123456789)); + $this->assertEquals(15, $numerics->precision(0.123456789123456789, true)); + $this->assertEquals(0, $numerics->precision(1.0E+25)); } /**