Calculate with Prices
As floating point numbers (
double) are not able to represent numbers exactly (see here
if you want to know details), and exact numbers are a strict demand to e-commerce applications the E-Commerce Framrwork
value objects instead of floats to represent prices. These value objects internally store the represented value as integer
by defining a fixed amount of supported digits (so-called
scale) after the comma and by multiplying the actual value
with the given scale on construction. The scale is set to 4 by default, but can be changed globally in the
An example: Given a scale of 4, a
Decimal will internally represent a number of
1234500 by calculating
123.45 * 10^4 = 1234500.
To calculate with these values, the
Decimal class exposes methods like
div() and others
to run calculations without having to deal with the internal scale representation. For details see the Decimal class definition
and the corresponding test
which contains a lot of usage examples and describes the
Decimal behaviour quite well.
Decimalis designed as immutable value object. Every operation yields a new instance of a
Decimalrepresenting the new value. Please be aware that this needs to be considered when calculating with
Decimals and/or when extending the
Decimalclass with custom functionality.
On the DB side, all E-Commerce Framework class definitions supporting price values were updated to store prices as
data type. This means, when fetching a price value from an ecommerce-object (e.g. an order), it will be a string instead
float. This string can directly be passed to the
Decimal value object's
A usage example:
$a = Decimal::create(10);
$b = Decimal::create(20);
$c = $a->add($b);
var_dump($c->asString()); // 30.0000 as the default string representation contains all digits depending on the scale
// regarding immutability:
// $a is still 10 and will never be changed - the add() method returns a new object ($c in the example above)
$a->equals(Decimal::create(30)); // false
$a->equals(Decimal::create(10)); // true
In most cases, you should be able to build a
Decimal object by using the static
create() method which is able to build
Decimal from strings, integers and floating point numbers. If needed, you can also make use of a number of
directly exposing costruction logic for a specific type (e.g.
Decimal object will try to avoid rounding and calculating with floats if possible. If you pass a string to the
method, it will try to convert the numeric string to an integer with string operations before falling back to converting
it into a float which is rounded.
Examples (given a scale of 4):
Decimal::create('123')will just add 4 chars of
0to the string and cast it to an integer (no floats involved) - results in
Decimal::create('123.45')will move the dot to the right by 2 places and add 2 chars of
0- results in
However, if the given value exceeds the maximum scale or is a float value, the
create() method needs to fall back to
float calculations and rounding to generate the integer representation:
Decimal::create('123.1234567')with a string value will fall back to float calculations as the amount of digits after the comma exceed the scale of
4. The value is first multiplied with
10^4, resulting in PHP casting the string to float. The float
1231234.567is then passed to PHP's
round()function with a
0and a default rounding mode of
PHP_ROUND_HALF_UP, resulting in an integer representation of
1231235(note the rounding on the last digit).
Decimal::create(123.1234567)with a float value will have the same behaviour as above (skipping the string conversion).
You can influence how rounding is applied by specifying the
$roundingMode parameter on the
var_dump(Decimal::create('123.55555', 4, PHP_ROUND_HALF_DOWN)->asString()); // 123.5555
var_dump(Decimal::create('123.55555', 4, PHP_ROUND_HALF_UP)->asString()); // 123.5556
Please be aware that as rounding is applied only when exceeding the scale, the following can happen:
// supported by scale -> 1.9999
// rounding is applied -> 2.0000
This is important to know as it could lead to unexpected calculation results. The
DECIMAL mysql data type has the same
behaviour - if you calculate with
Decimals at a higher scale you still need to update your class definitions to match
that scale as otherwise the same rounding logic will be applied by the database. However, while doing calculations (e.g.
tax or discount calculations, reports), you're free to calculate at a higher scale by passing the
scale parameter to the
create() method and just setting the resulting value to the scale represented on the DB. Example:
// operate at a high scale supporting 8 digits after the comma
$a = Decimal::create(10, 8);
// do calculations
$b = $a->toPercentage(15);
// create a representation with a lower scale - at this point rounding logic will be applied
$result = $b->withScale(4);
// an order object
Extending the Decimal class
You're free to add custom calculation logic to the Decimal class, but please make sure every operation returns a new instance holding the calculated value. An object can never change its internal state (value and scale) after construction!
Caveats/aspects good to know
- In order to have sufficient number of digits in integer datatype, your system should run on 64 bit infrastructure.
On 32 bit systems, you would be able to handle prices up to 214,748.3647 only (since max int value with 32 bit is 2,147,483,647).
Decimalimplementation contains logic to throw an exception on an integer overflow in case the used numbers are too large/small.