Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 72 additions & 36 deletions docs/guide/arithmetic.md
Original file line number Diff line number Diff line change
@@ -1,51 +1,70 @@
# Arithmetic

Every operand can be a `Decimal`, a numeric string, an integer or a float. The result is always a new `Decimal`.
Every operand can be a `Decimal`, a numeric string or an integer. The result is always a new `Decimal`.

```php
use PhpCollective\DecimalObject\Decimal;

$a = Decimal::create('1.50');
```

## Exact by default

No operation drops a digit on its own. Addition, subtraction and multiplication return the exact result. Where the exact result cannot be written down, as in most divisions, you say how to round. If you do not, the call throws a `RoundingNecessaryException`.

> [!TIP] Changed in 2.0
> Results used to be cut off at the scale without notice. See [Upgrading](../reference/upgrading#from-1-x-to-2-0).

## Add and subtract

```php
echo $a->add('2.125'); // 3.625
echo $a->add('2.125'); // 3.625
echo Decimal::create('5')->subtract('7.5'); // -2.5
```

The result has the larger scale of the two operands.
The result has the larger scale of the two operands, which is always exact.

## Multiply

```php
echo Decimal::create('1.55')->multiply('2.00'); // 3.1000
echo Decimal::create('1.55')->multiply('2.00', 2); // 3.10
echo Decimal::create('1.55')->multiply('2.00'); // 3.1000
```

The scales of both operands are added, which is always enough to hold the exact product.

## Asking for a scale

`add()`, `subtract()`, `multiply()` and `mod()` take a scale as second argument and a [rounding mode](./rounding#rounding-modes) as third:

```php
echo Decimal::create('1.55')->multiply('2.00', 2); // 3.10
echo Decimal::create('19.99')->multiply('0.19', 2, Decimal::ROUND_HALF_UP); // 3.80
```

Without a scale argument the scales of both operands are added, which is always enough to hold the exact product.
The first line needs no mode because only zeros are dropped. The second would throw without one, since `3.7981` does not fit two decimals.

## Divide

`divide()` requires the scale, because a quotient can have endless decimals:

```php
echo Decimal::create('10')->divide('3', 4); // 3.3333
echo Decimal::create('2')->divide('3', 2); // 0.66
echo Decimal::create('10')->divide('4', 2); // 2.50
```

The result is cut off at the scale, not rounded. Pass a rounding mode as third argument to round it:
That quotient fits. Most do not, and then a rounding mode is needed:

```php
echo Decimal::create('2')->divide('3', 2, Decimal::ROUND_HALF_UP); // 0.67
echo Decimal::create('1')->divide('8', 2, Decimal::ROUND_HALF_EVEN); // 0.12
Decimal::create('10')->divide('3', 4);
// RoundingNecessaryException: The result does not fit the scale.

echo Decimal::create('10')->divide('3', 4, Decimal::ROUND_HALF_UP); // 3.3333
echo Decimal::create('2')->divide('3', 2, Decimal::ROUND_HALF_UP); // 0.67
echo Decimal::create('2')->divide('3', 2, Decimal::ROUND_DOWN); // 0.66
```

The rounding is decided on the exact quotient. All [rounding modes](./rounding#rounding-modes) work here at the scale, including floor and ceil.
The rounding is decided on the exact quotient, so ties are detected correctly at any scale. `ROUND_DOWN` gives the cut-off result of earlier versions.

> [!TIP] Added in 1.4
> The rounding mode argument. Without it `divide()` cuts off as before.
Dividing by zero throws a `DivisionByZeroException`.

### Quotient and remainder

Expand All @@ -61,10 +80,6 @@ The rounding is decided on the exact quotient. All [rounding modes](./rounding#r

The quotient is cut off towards zero, so the remainder has the sign of the value, like `mod()`: `-7` by `2` gives `-3` and `-1`. Quotient times divisor plus remainder always equals the value.

> [!TIP] Added in 1.4

Dividing by zero throws a `DivisionByZeroError`.

## Modulo

```php
Expand All @@ -73,23 +88,46 @@ echo Decimal::create('-7')->mod(2); // -1
echo Decimal::create('10')->mod('0.3'); // 0.1
```

The sign follows the left operand, as with PHP's `%`. The result has the larger scale of both operands. A zero operand throws a `DivisionByZeroError`.
The sign follows the left operand, as with PHP's `%`. The result has the larger scale of both operands. A zero operand throws a `DivisionByZeroException`.

## Power

```php
echo Decimal::create('8')->pow(2); // 64
echo Decimal::create('1.5')->pow(2); // 2.25
echo Decimal::create('1.5')->pow(3); // 3.375
```

A positive exponent gives the exact result. Pass a scale and a mode to shorten it:

## Power and square root
```php
echo Decimal::create('1.5')->pow(2, 1, Decimal::ROUND_HALF_UP); // 2.3
```

A negative exponent is a division. It uses the scale you pass, or the scale of the value, and needs a mode unless the result fits:

```php
echo Decimal::create('8')->pow(2); // 64
echo Decimal::create('1.5')->pow(2); // 2.2
echo Decimal::create('1.5')->pow(2, 2); // 2.25
echo Decimal::create('2')->pow(-1, 2); // 0.50
echo Decimal::create('2')->pow(-1, 2); // 0.50
echo Decimal::create('3')->pow(-1, 4, Decimal::ROUND_HALF_UP); // 0.3333
```

echo Decimal::create('18')->sqrt(); // 4
echo Decimal::create('18')->sqrt(5); // 4.24264
The exponent must be a whole number between -10000 and 10000 (`Decimal::MAX_EXPONENT`). Anything else throws an `InvalidArgumentException`.

## Square root

```php
echo Decimal::create('64')->sqrt(); // 8
echo Decimal::create('2.25')->sqrt(); // 1.50
```

Both default to the scale of the value they are called on and cut off further digits, so pass a scale when the result needs more decimals than the input has.
The result has the scale of the value unless you pass one. A root that does not fit needs a mode:

The exponent of `pow()` must be a whole number, and `sqrt()` of a negative value is not defined. bcmath reports both with a `ValueError`.
```php
echo Decimal::create('18')->sqrt(5, Decimal::ROUND_HALF_UP); // 4.24264
echo Decimal::create('18')->sqrt(3, Decimal::ROUND_HALF_UP); // 4.243
```

A negative value throws a `NegativeNumberException`.

## Sign

Expand All @@ -111,13 +149,14 @@ echo Decimal::create('12.34')->shift(-3); // 0.01234
## Several values at once

```php
echo Decimal::sum(['1.5', 2, Decimal::create('3.25')]); // 6.75
echo Decimal::average(['1', '2', '4'], 2); // 2.33
echo Decimal::min(['2.5', '-1', 7]); // -1
echo Decimal::max(['2.5', '-1', 7]); // 7
echo Decimal::sum(['1.5', 2, Decimal::create('3.25')]); // 6.75
echo Decimal::average(['1', '2', '3'], 2); // 2.00
echo Decimal::average(['1', '2', '4'], 2, Decimal::ROUND_HALF_UP); // 2.33
echo Decimal::min(['2.5', '-1', 7]); // -1
echo Decimal::max(['2.5', '-1', 7]); // 7
```

All four take any iterable. `sum()` of nothing is `0`. `average()`, `min()` and `max()` throw an `InvalidArgumentException` for an empty list. `average()` needs the scale for the same reason `divide()` does, and cuts off there.
All four take any iterable. `sum()` of nothing is `0`. `average()`, `min()` and `max()` throw an `InvalidArgumentException` for an empty list. `average()` is a division: it needs the scale, and a mode when the mean does not fit.

## Splitting an amount

Expand All @@ -138,9 +177,6 @@ The shares always sum to the original value. They have its scale, which also set

Ratios can be integers or decimals such as `'0.7'` and `'0.3'`. Negative ratios, an empty list and ratios that sum to zero throw an `InvalidArgumentException`.

> [!TIP] Added in 1.4
> `shift()`, `sum()`, `average()`, `min()`, `max()`, `split()` and `allocate()`.

## Chaining

```php
Expand Down
2 changes: 1 addition & 1 deletion docs/guide/comparison.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ var_dump($value->greaterThanOrEquals('1.5')); // bool(true)
var_dump($value->lessThanOrEquals('1.5')); // bool(true)
```

Each method accepts a `Decimal`, a numeric string, an integer or a float.
Each method accepts a `Decimal`, a numeric string or an integer.

`compareTo()` returns `-1`, `0` or `1` and fits `usort()`:

Expand Down
24 changes: 22 additions & 2 deletions docs/guide/conversion.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ A JSON number would be read back as a float by most clients, which defeats the p
echo Decimal::create('-23.74')->toInt(); // -23
```

Values outside the range of a PHP integer throw a `TypeError`. Check with `isBigInteger()` first when the value may be large:
Values outside the range of a PHP integer throw an `OverflowException`. Check with `isBigInteger()` first when the value may be large:

```php
$value = Decimal::create('9223372036854775808');
Expand All @@ -53,7 +53,16 @@ if (!$value->isBigInteger()) {
var_dump(Decimal::create('-23.44')->toFloat()); // float(-23.44)
```

A float is an approximation. Use it for display libraries and charts, not for further calculation. `toFloat()` throws a `TypeError` when `isBigDecimal()` is true, which is the case when the integral or the fractional digits exceed `PHP_INT_MAX`.
A float holds about 15 significant digits, so longer values come back as the nearest float:

```php
var_dump(Decimal::create('0.12345678901234567890')->toFloat()); // float(0.12345678901234568)
```

Use it for display libraries and charts, not for further calculation. A value beyond the float range throws an `OverflowException`.

> [!TIP] Changed in 2.0
> `toFloat()` used to refuse values with long fractions, and both conversions threw a `TypeError`. See [Upgrading](../reference/upgrading#from-1-x-to-2-0).

## Scientific notation

Expand All @@ -66,6 +75,17 @@ echo Decimal::create('0')->toScientific(); // 0e0

Decimals are kept as written, so the scale survives a round trip through `Decimal::create()`. Call `trim()` first for the shortest form.

## Serialization

`serialize()` stores the decimal string only:

```php
echo serialize(Decimal::create('19.90'));
// O:35:"PhpCollective\DecimalObject\Decimal":1:{s:5:"value";s:5:"19.90";}
```

Objects serialized by 1.x are still read. For storage that other systems read too, prefer the plain string.

## Localized output

The object has no formatting of its own. Pass the string to `NumberFormatter` or to your framework's number helper. Most formatters convert to float internally, so check their precision limits for large values.
Expand Down
78 changes: 34 additions & 44 deletions docs/guide/creating.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@ Both forms take the same arguments. `create()` reads better in a chain and also
| --- | --- | --- |
| Decimal string | `'-5.000067'` | `-5.000067` |
| Integer | `-25000` | `-25000` |
| Float | `1.1` | `1.1` |
| Scientific notation | `'6.22e8'`, `'2.2e-6'` | `622000000`, `0.0000022` |
| `Stringable` object with a numeric string | another `Decimal` | same value |

Floats are not accepted, see [Floats](#floats).

Strings are normalized, the value stays the same:

| Input | Result |
Expand All @@ -35,47 +36,34 @@ Trailing zeros are kept. `'5.10'` has a scale of 2, and the object will not drop

## Invalid input

Anything that is not a plain number throws an `InvalidArgumentException`:
Anything that is not a plain number throws an `InvalidNumberException`:

```php
Decimal::create('abc'); // Invalid non numeric value
Decimal::create('1,5'); // Invalid non numeric value
Decimal::create(INF); // Invalid non finite value
```

Localized strings such as `'1.018,90'` are rejected. Parse them with `NumberFormatter` or your framework's locale tools first.

::: warning Floats
A float is converted with PHP's own float to string rules, which round to the `precision` ini setting (14 digits by default). That hides small errors but also drops real digits. For money and other exact values, read the number as a string from the request or the database.
::: info Exponent limit
Exponents beyond `Decimal::MAX_EXPONENT` (10000 in either direction) throw an `InvalidNumberException`. Without a limit, the twelve characters of `'1e1000000000'` would expand to a string of a billion digits. A subclass can redefine the constant.
:::

## Floats with all their digits

When a float is all you have, `createFromFloat()` keeps every digit needed to get the same float back, independent of ini settings:

```php
echo Decimal::create(1234567890.1234567); // 1234567890.1235
echo Decimal::createFromFloat(1234567890.1234567); // 1234567890.1234567

echo Decimal::create(0.1 + 0.2); // 0.3
echo Decimal::createFromFloat(0.1 + 0.2); // 0.30000000000000004
```
## Floats

The second pair shows the trade: the result is what the float really holds, including its error.
A float passed to the constructor, to `create()` or to any arithmetic method throws an `InvalidArgumentException`. A float has usually lost digits before it arrives, and the library refuses to guess which ones matter.

## Zero and one
Read exact values as strings from the request or the database. When a float is all you have, convert it on purpose:

```php
$total = Decimal::zero();
$factor = Decimal::one();
echo Decimal::createFromFloat(1234567890.1234567); // 1234567890.1234567
echo Decimal::createFromFloat(0.1 + 0.2); // 0.30000000000000004
```

> [!TIP] Added in 1.4
> `createFromFloat()`, `zero()` and `one()`.
`createFromFloat()` keeps every digit needed to get the same float back, independent of ini settings and locale. The second line shows what that means: you get what the float really holds, including its binary error. Round the result to the scale you expect.

::: info Exponent limit
Exponents beyond `Decimal::MAX_EXPONENT` (10000 in either direction) throw an `InvalidArgumentException`. Without a limit, the twelve characters of `'1e1000000000'` would expand to a string of a billion digits. A subclass can redefine the constant.
:::
> [!TIP] Changed in 2.0
> Floats were accepted everywhere and cut to 14 significant digits. See [Upgrading](../reference/upgrading#from-1-x-to-2-0).

## Optional input

Expand All @@ -88,41 +76,43 @@ Decimal::tryCreate(''); // null
Decimal::tryCreate(null); // null
```

This fits optional form fields and nullable columns.
This fits optional form fields and nullable columns. A float still throws, because that is a mistake in the code and not in the input.

## Zero and one

> [!TIP] Added in 1.4
> `tryCreate()` and the exponent limit.
```php
$total = Decimal::zero();
$factor = Decimal::one();
```

## Setting a scale

The second argument fixes the number of decimal places:
The second argument fixes the number of decimal places. The string always shows it:

```php
echo Decimal::create('0.129', 2); // 0.12
echo Decimal::create('0.1', 4); // 0.1000
echo Decimal::create('5', 2); // 5.00
echo Decimal::create('0.1200', 2); // 0.12
```

Digits beyond the scale are cut off, not rounded. A scale of `0` cuts off all decimals. Use [`round()`](./rounding) when you want rounding.

A scale larger than the input records the scale without padding the string. Results of later calculations use it:
A smaller scale is accepted as long as only zeros are dropped. Losing any other digit throws a `RoundingNecessaryException`:

```php
$value = Decimal::create('0.1', 4);

echo $value; // 0.1
echo $value->scale(); // 4
echo $value->add('0.2'); // 0.3000
Decimal::create('0.129', 2);
// RoundingNecessaryException: The value has more than `2` decimals.
```

### Strict mode

Pass `true` as the third argument to refuse input that would lose digits:
Say how to round instead:

```php
Decimal::create('0.123', 2, true);
// InvalidArgumentException: Loss of precision detected. Detected scale `3` > `2` as defined.
echo Decimal::create('0.129')->withScale(2); // 0.13
echo Decimal::create('0.129')->withScale(2, Decimal::ROUND_DOWN); // 0.12
```

This is useful when the scale mirrors a database column and silent truncation would hide a bug.
This matters when the scale mirrors a database column: input with too many decimals is reported instead of being shortened without notice.

> [!TIP] Changed in 2.0
> A scale used to cut off surplus digits and did not pad the string. The `$strict` argument is gone; its check is now always on. See [Upgrading](../reference/upgrading#from-1-x-to-2-0).

## Subclassing

Expand Down
10 changes: 5 additions & 5 deletions docs/guide/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,18 +42,18 @@ if ($gross->greaterThan('20')) {
}
```

`multiply()` returned `3.7981` here, with the scales of both operands added up. `round(2)` brings it back to two decimal places. [Precision and scale](./scale) explains the rules for each operation.
`multiply()` returned the exact product `3.7981` here, with the scales of both operands added up. `round(2)` brings it back to two decimal places. Nothing is rounded unless you ask for it. [Precision and scale](./scale) explains the rules for each operation.

## Use strings for input

Pass decimal strings whenever you can. A float has already lost digits before the object sees it:
Pass decimal strings. A float has usually lost digits before the object could see it, so floats are rejected:

```php
echo Decimal::create('1234567890.1234567'); // 1234567890.1234567
echo Decimal::create(1234567890.1234567); // 1234567890.1235
Decimal::create(19.90); // InvalidArgumentException
Decimal::create('19.90'); // 19.90
```

See [Creating decimals](./creating) for all accepted input.
See [Creating decimals](./creating) for all accepted input and for converting a float on purpose.

## Where to go next

Expand Down
Loading
Loading