diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0cf3aea..f13c05c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,6 +8,9 @@ on: - next workflow_dispatch: +permissions: + contents: read + jobs: testsuite: runs-on: ubuntu-latest @@ -18,6 +21,8 @@ jobs: steps: - uses: actions/checkout@v6 + with: + persist-credentials: false - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -51,6 +56,8 @@ jobs: steps: - uses: actions/checkout@v6 + with: + persist-credentials: false - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -67,3 +74,24 @@ jobs: - name: Run phpcs run: composer cs-check + + docs: + name: Documentation + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + - uses: actions/setup-node@v6 + with: + node-version: '24' + cache: npm + cache-dependency-path: docs/package-lock.json + - run: npm ci + working-directory: docs + - run: npm audit --audit-level=low + working-directory: docs + - run: npm run docs:build + working-directory: docs diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index af440c4..4c607b0 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -23,7 +23,7 @@ jobs: - name: Checkout uses: actions/checkout@v6 with: - fetch-depth: 0 + persist-credentials: false - name: Setup Node uses: actions/setup-node@v6 diff --git a/README.md b/README.md index 1d2a9ae..483328b 100644 --- a/README.md +++ b/README.md @@ -39,13 +39,12 @@ var_dump($total->equals('0.3')); // bool(true) - Arithmetic, rounding, comparison and conversion. - Immutable. - Handles numbers beyond the integer and float range. -- No custom PHP extension, only `bcmath`. +- Calculates with PHP's `bcmath` extension. ## Installation -``` -composer require php-collective/decimal-object -``` +Follow [Installation](https://php-collective.github.io/decimal-object/guide/installation) +to install the package and enable bcmath for the PHP process that runs it. Requires PHP 8.2+ and the `bcmath` extension. The 1.x API is maintained in the 1.4 releases, see [Upgrading](https://php-collective.github.io/decimal-object/reference/upgrading) for the changes. PHP 8.1 is covered by the 1.3 releases. For anything older, use the [legacy library](https://github.com/spryker/decimal-object). diff --git a/docs/.vitepress/theme/custom.css b/docs/.vitepress/theme/custom.css index 0320544..9f5efbb 100644 --- a/docs/.vitepress/theme/custom.css +++ b/docs/.vitepress/theme/custom.css @@ -19,7 +19,8 @@ .vp-doc table code, .vp-doc p code, .vp-doc li code { - white-space: nowrap; + white-space: normal; + overflow-wrap: anywhere; } .vp-doc .custom-block.tip { diff --git a/docs/README.md b/docs/README.md index 25b6e28..c8b402c 100644 --- a/docs/README.md +++ b/docs/README.md @@ -6,6 +6,6 @@ To preview the site locally: ```sh cd docs -npm install +npm ci npm run docs:dev ``` diff --git a/docs/guide/creating.md b/docs/guide/creating.md index f54a032..fed87ab 100644 --- a/docs/guide/creating.md +++ b/docs/guide/creating.md @@ -7,7 +7,7 @@ $a = Decimal::create('1.234'); $b = new Decimal('1.234'); ``` -Both forms take the same arguments. `create()` reads better in a chain and also accepts an existing `Decimal`. +Both forms accept numeric values and `Stringable` objects, including an existing `Decimal`. `create()` reads better in a chain, and returns a `Decimal` of the called class as is when no scale is given. ## Accepted input diff --git a/docs/guide/getting-started.md b/docs/guide/getting-started.md index 8b6cf72..6d0331b 100644 --- a/docs/guide/getting-started.md +++ b/docs/guide/getting-started.md @@ -1,5 +1,7 @@ # Getting started +Complete [Installation](./installation) first. + Floats cannot represent most decimal fractions exactly: ```php @@ -7,17 +9,24 @@ var_dump(0.1 + 0.2 === 0.3); // bool(false) var_dump(0.1 + 0.2 - 0.3); // float(5.551115123125783E-17) ``` -`Decimal` stores the number as a string of digits and calculates with bcmath, so the result is the one you would get on paper: +`Decimal` stores the number as a string of digits and calculates with bcmath, so decimal fractions can be added exactly. + +Save this as `example.php` in the directory containing `composer.json`: ```php +add('0.2'); -echo $sum; // 0.3 +echo $sum, PHP_EOL; // 0.3 var_dump($sum->equals('0.3')); // bool(true) ``` +Run `php example.php`. Later snippets use the same Composer autoloader and `Decimal` import. + ## Immutability A `Decimal` never changes. Each operation returns a new object, so assign the result: diff --git a/docs/guide/installation.md b/docs/guide/installation.md index b852048..a2d42d8 100644 --- a/docs/guide/installation.md +++ b/docs/guide/installation.md @@ -4,15 +4,12 @@ composer require php-collective/decimal-object ``` -## Requirements +## Compatibility -- PHP 8.2 or newer -- The `bcmath` extension - -Composer checks both. Enable `ext-bcmath` for the CLI and for the web server, as they often load different `php.ini` files. +Composer checks PHP and extension compatibility. Enable `ext-bcmath` for the CLI and for the web server, as they often load different `php.ini` files. ::: tip Older PHP versions -PHP 8.1 is served by the 1.3 releases, which Composer selects on its own. For PHP 7 use the original [spryker/decimal-object](https://github.com/spryker/decimal-object). +PHP 8.1 is served by the 1.3 releases, which Composer selects on its own. For PHP versions older than 8.1, use the original [spryker/decimal-object](https://github.com/spryker/decimal-object). ::: Continue with [Getting started](./getting-started). diff --git a/docs/guide/rounding.md b/docs/guide/rounding.md index 59d13be..97fc8fc 100644 --- a/docs/guide/rounding.md +++ b/docs/guide/rounding.md @@ -16,7 +16,7 @@ echo Decimal::create('-12.5')->round(); // -13 echo Decimal::create('11')->round(2); // 11.00 ``` -The first argument is the number of decimal places, `0` by default. The result always has exactly that scale, padded with zeros if needed. +The first argument is the number of decimal places, `0` by default. The result always has exactly that scale, padded with zeros if needed. Negative scales round to positions before the decimal point. The default mode rounds half away from zero, like PHP's `round()`. It works on the decimal digits and never passes through a float. diff --git a/docs/index.md b/docs/index.md index d30346a..ffd9896 100644 --- a/docs/index.md +++ b/docs/index.md @@ -3,7 +3,7 @@ layout: home hero: name: Decimal Object text: Exact decimal arithmetic for PHP - tagline: An immutable value object on top of bcmath. No custom extension, no float rounding error. + tagline: An immutable value object on top of bcmath. Uses bcmath, no float rounding error. image: src: /logo.svg alt: Decimal Object logo diff --git a/docs/reference/alternatives.md b/docs/reference/alternatives.md index 6ad0f22..03de982 100644 --- a/docs/reference/alternatives.md +++ b/docs/reference/alternatives.md @@ -4,7 +4,7 @@ This library is one way to handle decimals in PHP. Depending on the project anot | Library | Notes | | --- | --- | -| [php-decimal](https://php-decimal.github.io/) | A PHP extension. Faster, and it supports the arithmetic operators, but it has to be installed on every server. Works with precision (significant digits) where this library works with scale. | +| [php-decimal](https://php-decimal.github.io/) | A PHP extension with arithmetic operators, installed separately on each server. Works with precision (significant digits) where this library works with scale. | | [brick/math](https://github.com/brick/math) | `BigDecimal`, `BigInteger` and `BigRational` with several calculation engines and more rounding modes. | | [brick/money](https://github.com/brick/money), [moneyphp/money](https://github.com/moneyphp/money) | Money with currencies, allocation and formatting. Choose one of these when you need currency handling and not only a number. | @@ -16,10 +16,19 @@ This library is one way to handle decimals in PHP. Depending on the project anot | `abs()` | `absolute()` | | `signum()` | `sign()` | | `sub()`, `mul()`, `div()` | `subtract()`, `multiply()`, `divide()` | +| `shift()`, `isEven()`, `isOdd()` | Same method names | +| `rem($value)` | `mod($value)` | +| `mod($value)` | Integer modulo in php-decimal; see the different [modulo behavior](../guide/arithmetic#modulo) here | +| `toFixed($places)` | `(string)$decimal->withScale($places)` | +| `Decimal::sum()` | `Decimal::sum()` | +| `Decimal::avg()` | `Decimal::average($values, $scale, $roundMode)` | | Precision as significant digits | Scale as decimal places | -Not available here: `rem()`, `shift()`, `ln()`, `exp()`, `log10()`, `parity()`, `isEven()`, `isOdd()`, `toFixed()`, `Decimal::sum()` and `Decimal::avg()`. +There are no methods named `rem()`, `ln()`, `exp()`, `log10()` or `parity()` here. +For a quotient and remainder, use [`divideWithRemainder()`](../guide/arithmetic#quotient-and-remainder). +Precision and scale differ between the libraries, so review rounding and division +when migrating calculations. ## History -The library was written in 2019 for the [Spryker](https://spryker.com/) commerce framework and has been used there in order and price calculations since. This repository is the independently maintained continuation for PHP 8. The original for older PHP versions is [spryker/decimal-object](https://github.com/spryker/decimal-object). +The library was written in 2019 for the [Spryker](https://spryker.com/) commerce framework and has been used there in order and price calculations since. This repository is the independently maintained continuation for the PHP ecosystem. The original for older PHP versions is [spryker/decimal-object](https://github.com/spryker/decimal-object). diff --git a/docs/reference/integrations.md b/docs/reference/integrations.md index af31833..f33e343 100644 --- a/docs/reference/integrations.md +++ b/docs/reference/integrations.md @@ -4,9 +4,7 @@ [dereuromark/cakephp-decimal](https://dereuromark.github.io/cakephp-decimal/) maps `DECIMAL` database columns to `Decimal` objects in the CakePHP ORM. Entities receive value objects on read, and form input is converted during marshalling. -```sh -composer require dereuromark/cakephp-decimal -``` +Follow the plugin's [Installation](https://dereuromark.github.io/cakephp-decimal/guide/installation) for setup. ```php $order = $orders->get($id); @@ -14,7 +12,7 @@ $order->total = $order->total->add('0.10'); $orders->saveOrFail($order); ``` -The plugin documentation covers setup, forms, localized input and formatting. +The plugin documentation also covers forms, localized input and formatting. ## Your own framework diff --git a/docs/reference/upgrading.md b/docs/reference/upgrading.md index 809295b..a7544f3 100644 --- a/docs/reference/upgrading.md +++ b/docs/reference/upgrading.md @@ -164,3 +164,11 @@ Zero has no sign. `'-0.0'` becomes `0.0`, `isNegative()` is false for it, and `n #### Operands `mod()` and `pow()` accept operands in scientific notation. + +## 1.4.1 + +`mod()` and `pow()` now accept float operands. Subclasses overriding these methods must include `float` in their operand type to keep their signatures compatible. + +## 1.4.2 + +Comparisons use every stored decimal. `Decimal::create('1.9', 0)->compareTo(1)` returned `0` and now returns `1`. In 2.0 that value cannot be created without a rounding mode. diff --git a/src/Decimal.php b/src/Decimal.php index 37ac74d..32a44ed 100644 --- a/src/Decimal.php +++ b/src/Decimal.php @@ -186,9 +186,8 @@ protected function normalizeValue(string $value): string /** * Returns a Decimal instance from the given value. * - * If the value is already a Decimal instance, then (since immutable) return it unmodified. - * Otherwise, create a new Decimal instance from the given value and return - * it. + * If the value is already an instance of this class and no scale is supplied, + * return it as is. Otherwise, construct a new instance from the value. * * @param \Stringable|string|float|int $value * @param int|null $scale Decimal places in the result. Omit to enable auto-detection. @@ -356,12 +355,12 @@ protected static function extreme(iterable $values, int $direction): static /** * Equality * - * This method is equivalent to the `==` operator. + * Compares numeric values, ignoring differences in scale. * * @param self|string|float|int $value * * @return bool TRUE if this decimal is considered equal to the given value. - * Equal decimal values tie-break on precision. + * Trailing zeros do not affect equality. */ public function equals(self|string|float|int $value): bool { @@ -539,7 +538,7 @@ public function isInteger(): bool } /** - * Returns if truly zero. + * Returns whether the value is zero. * * @return bool */ @@ -549,7 +548,7 @@ public function isZero(): bool } /** - * Returns if truly negative (not zero). + * Returns whether the value is negative. * * @return bool */ @@ -559,7 +558,7 @@ public function isNegative(): bool } /** - * Returns if truly positive (not zero). + * Returns whether the value is positive. * * @return bool */ @@ -754,7 +753,7 @@ protected function roundedQuotient(self $divisor, int $scale, RoundingMode|int $ } /** - * This method is equivalent to the ** operator. + * Raises this decimal to a whole-number exponent. * * @param self|string|float|int $exponent A whole number up to MAX_EXPONENT in either direction * @param int|null $scale Decimal places in the result. Omit for the exact result of a positive @@ -831,7 +830,7 @@ public function sqrt(?int $scale = null, RoundingMode|int $roundMode = self::ROU } /** - * This method is equivalent to the % operator. + * Returns the remainder. * * @param self|string|float|int $value * @param int|null $scale Decimal places in the result. Omit for the larger scale of both, which is exact.