Skip to content

Commit a428318

Browse files
authored
Update README.md
1 parent 11aad4a commit a428318

File tree

1 file changed

+57
-8
lines changed

1 file changed

+57
-8
lines changed

README.md

Lines changed: 57 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@
22

33
[![run-tests](https://github.com/whitecube/php-prices/actions/workflows/tests.yml/badge.svg)](https://github.com/whitecube/php-prices/actions/workflows/tests.yml)
44

5-
> 💸 **Version 2.x**
5+
> 💸 **Version 3.x**
66
>
7-
> This new major version is shifting the package towards more flexibility and configuration possibilities in general.
7+
> This new major version aims to avoid rounding errors when working with division and multiplication.
88
>
9-
> One of the main differences is that we replaced [`moneyphp/money`](https://github.com/moneyphp/money) with [`brick/money`](https://github.com/brick/money) under the hood. This introduces a ton of **breaking changes**, mainly on the instantiation methods that now reflect brick/money's API in order to keep things developer friendly. The `1.x` branch will still be available and maintained for a while, but we strongly recommend updating to `2.x`.
9+
> We have replaced almost all `Brick\Money\Money` typehints to `Brick\Money\AbstractMoney` in order to allow usage of `Brick\Money\RationalMoney` instances as the base for the price object, in order to allow rounding-free division and multiplication. See [the new chapter on rounding errors](#handling-rounding-properly-when-using-division-and-multiplication) in this documentation for more information.
10+
> We have also added type definitions everywhere we could. This introduces some **breaking changes**.
1011
1112
Using the underlying [`brick/money`](https://github.com/brick/money) library, this simple Price object allows to work with complex composite monetary values which include exclusive, inclusive, VAT (and other potential taxes) and discount amounts. It makes it safer and easier to compute final displayable prices without having to worry about their construction.
1213

@@ -18,7 +19,7 @@ composer require whitecube/php-prices
1819

1920
## Getting started
2021

21-
Each `Price` object has a `Brick\Money\Money` instance which is considered to be the item's unchanged, per-unit & exclusive amount. All the composition operations, such as adding VAT or applying discounts, are added on top of this base value.
22+
Each `Price` object has a base `Brick\Money\Money` or `Brick\Money\RationalMoney` instance which is considered to be the item's unchanged, per-unit & exclusive amount. All the composition operations, such as adding VAT or applying discounts, are added on top of this base value.
2223

2324
```php
2425
use Whitecube\Price\Price;
@@ -36,7 +37,7 @@ There are several convenient ways to obtain a `Price` instance:
3637

3738
| Method | Using major values | Using minor values | Defining units |
3839
| :----------------------------------------------- | :-------------------------------- | :---------------------------------- | :---------------------------------------- |
39-
| [Constructor](#from-constructor) | `new Price(Money $base)` | `new Price(Money $base)` | `new Price(Money $base, $units)` |
40+
| [Constructor](#from-constructor) | `new Price(AbstractMoney $base)` | `new Price(AbstractMoney $base)` | `new Price(AbstractMoney $base, $units)` |
4041
| [Brick/Money API](#from-brickmoney-like-methods) | `Price::of($major, $currency)` | `Price::ofMinor($minor, $currency)` | - |
4142
| [Currency API](#from-currency-code-methods) | - | `Price::EUR($minor)` | `Price::USD($minor, $units)` |
4243
| [Parsed strings](#from-parsed-string-values) | `Price::parse($value, $currency)` | - | `Price::parse($value, $currency, $units)` |
@@ -104,7 +105,9 @@ Parsing formatted strings is a tricky subject. More information on [parsing stri
104105

105106
## Accessing the Money objects (getters)
106107

107-
Once set, the **base amount** can be accessed using the `base()` method.
108+
Once set, the **base amount** can be accessed using the `base()` method.
109+
110+
> **Note** If you give an instance of `Brick\Money\Money` as a parameter when instanciating the price object, you will get a `Brick\Money\Money` instance back. Similarly, instanciating with `Brick\Money\RationalMoney` will give you a `RationalMoney` object back.
108111
109112
```php
110113
$perUnit = $price->base(); // Brick\Money\Money
@@ -181,11 +184,10 @@ $price->compareBaseTo(Price::USD(500, 4)); // 0
181184

182185
The price object will forward all the `Brick\Money\Money` API method calls to its base value.
183186

184-
> ⚠️ **Warning**: In opposition to [Money](https://github.com/brick/money) objects, Price objects are not immutable. Therefore, operations like plus, minus, etc. will directly modify the price's base value instead of returning a new instance.
187+
> **Warning**: In opposition to [Money](https://github.com/brick/money) objects, Price objects are not immutable. Therefore, operations like plus, minus, etc. will directly modify the price's base value instead of returning a new instance.
185188
186189
```php
187190
use Whitecube\Price\Price;
188-
use Brick\Money\Money;
189191

190192
$price = Price::ofMinor(500, 'USD')->setUnits(2); // 2 x $5.00
191193

@@ -200,6 +202,53 @@ Please refer to [`brick/money`'s documentation](https://github.com/brick/money)
200202

201203
> 💡 **Nice to know**: Whenever possible, you should prefer using modifiers to alter a price since its base value is meant to be constant. For more information on modifiers, please take at the ["Adding modifiers" section](#adding-modifiers) below.
202204
205+
### Handling rounding properly when using division and multiplication
206+
207+
When creating a price object from a `Brick\Money\Money` instance, rounding errors can occur when doing division and multiplication.
208+
209+
An example of the problem: we have a base price of 1000 minor units, that we need to divide by 12 and then multiply by 11.
210+
211+
`1000 / 12 * 11 = 916,6666666666...`
212+
213+
Using the regular `Brick\Money\Money` class forces us to specify a rounding mode when doing the division, which means we have a rounded result before doing the multiplication, which introduces an error in the result:
214+
215+
216+
```php
217+
use \Brick\Money\Money;
218+
use \Whitecube\Price\Price;
219+
use \Brick\Math\RoundingMode;
220+
221+
$base = Money::ofMinor(1000, 'EUR');
222+
$price = new Price($base);
223+
224+
// A rounding mode is mandatory in order to do the division,
225+
// which causes rounding errors down the line
226+
$price->dividedBy(12, RoundingMode::HALF_UP)->multipliedBy(11);
227+
228+
$price->getMinorAmount(); // 913 minor units ❌
229+
```
230+
231+
The solution is to build the Price instance with a base `Brick\Money\RationalMoney` instance instead, which represents the amount as a fraction and thus does not require rounding.
232+
233+
234+
```php
235+
use \Brick\Money\Money;
236+
use \Whitecube\Price\Price;
237+
use \Brick\Math\RoundingMode;
238+
use \Brick\Money\Context\CustomContext;
239+
240+
$base = Money::ofMinor(1000, 'EUR')->toRational();
241+
$price = new Price($base);
242+
243+
// With RationalMoney, rounding is not necessary at this stage
244+
$price->dividedBy(12)->multipliedBy(11);
245+
246+
// But rounding can occur at the very end
247+
$price->to(new CustomContext(2), RoundingMode::HALF_UP)->getMinorAmount(); // 917 minor units ✅
248+
```
249+
250+
For more information, see [brick/money's documentation on the matter](https://github.com/brick/money#advanced-calculations).
251+
203252
## Setting units (quantities)
204253

205254
This package's default behavior is to consider its base price as the "per unit" price. When no units have been specified, it defaults to `1`. Since "units" can be anything from a number of undividable products to a measurement, they are always converted to floats.

0 commit comments

Comments
 (0)