Calculator.php in Commerce Core 8.2
Namespace
Drupal\commerce_priceFile
modules/price/src/Calculator.phpView source
<?php
namespace Drupal\commerce_price;
/**
* Provides helpers for bcmath-based arithmetic.
*
* The bcmath extension provides support for arbitrary precision arithmetic,
* which does not suffer from the precision loses that make floating point
* arithmetic unsafe for eCommerce.
*
* Important: All numbers must be passed as strings.
*/
final class Calculator {
/**
* Adds the second number to the first number.
*
* @param string $first_number
* The first number.
* @param string $second_number
* The second number.
* @param int $scale
* The maximum number of digits after the decimal place.
* Any digit after $scale will be truncated.
*
* @return string
* The result.
*/
public static function add(string $first_number, string $second_number, int $scale = 6) : string {
self::assertNumberFormat($first_number);
self::assertNumberFormat($second_number);
$result = bcadd($first_number, $second_number, $scale);
return self::trim($result);
}
/**
* Subtracts the second number from the first number.
*
* @param string $first_number
* The first number.
* @param string $second_number
* The second number.
* @param int $scale
* The maximum number of digits after the decimal place.
* Any digit after $scale will be truncated.
*
* @return string
* The result.
*/
public static function subtract(string $first_number, string $second_number, int $scale = 6) : string {
self::assertNumberFormat($first_number);
self::assertNumberFormat($second_number);
$result = bcsub($first_number, $second_number, $scale);
return self::trim($result);
}
/**
* Multiplies the first number by the second number.
*
* @param string $first_number
* The first number.
* @param string $second_number
* The second number.
* @param int $scale
* The maximum number of digits after the decimal place.
* Any digit after $scale will be truncated.
*
* @return string
* The result.
*/
public static function multiply(string $first_number, string $second_number, int $scale = 6) : string {
self::assertNumberFormat($first_number);
self::assertNumberFormat($second_number);
$result = bcmul($first_number, $second_number, $scale);
return self::trim($result);
}
/**
* Divides the first number by the second number.
*
* @param string $first_number
* The first number.
* @param string $second_number
* The second number.
* @param int $scale
* The maximum number of digits after the decimal place.
* Any digit after $scale will be truncated.
*
* @return string
* The result.
*/
public static function divide(string $first_number, string $second_number, int $scale = 6) : string {
self::assertNumberFormat($first_number);
self::assertNumberFormat($second_number);
$result = bcdiv($first_number, $second_number, $scale);
return self::trim($result);
}
/**
* Calculates the next highest whole value of a number.
*
* @param string $number
* A numeric string value.
*
* @return string
* The result.
*/
public static function ceil(string $number) : string {
if (self::compare($number, 0) == 1) {
$result = bcadd($number, '1', 0);
}
else {
$result = bcadd($number, '0', 0);
}
return $result;
}
/**
* Calculates the next lowest whole value of a number.
*
* @param string $number
* The number.
*
* @return string
* The result.
*/
public static function floor(string $number) : string {
if (self::compare($number, 0) == 1) {
$result = bcadd($number, '0', 0);
}
else {
$result = bcadd($number, '-1', 0);
}
return $result;
}
/**
* Rounds the given number.
*
* Replicates PHP's support for rounding to the nearest even/odd number
* even if that number is decimal ($precision > 0).
*
* @param string $number
* The number.
* @param int $precision
* The number of decimals to round to.
* @param int $mode
* The rounding mode. One of the following constants: PHP_ROUND_HALF_UP,
* PHP_ROUND_HALF_DOWN, PHP_ROUND_HALF_EVEN, PHP_ROUND_HALF_ODD.
*
* @return string
* The rounded number.
*
* @throws \InvalidArgumentException
* Thrown when an invalid (non-numeric or negative) precision is given.
*/
public static function round(string $number, int $precision = 0, int $mode = PHP_ROUND_HALF_UP) : string {
self::assertNumberFormat($number);
if (!is_numeric($precision) || $precision < 0) {
throw new \InvalidArgumentException('The provided precision should be a positive number');
}
// Round the number in both directions (up/down) before choosing one.
$rounding_increment = bcdiv('1', pow(10, $precision), $precision);
if (self::compare($number, '0') == 1) {
$rounded_up = bcadd($number, $rounding_increment, $precision);
}
else {
$rounded_up = bcsub($number, $rounding_increment, $precision);
}
$rounded_down = bcsub($number, 0, $precision);
// The rounding direction is based on the first decimal after $precision.
$number_parts = explode('.', $number);
$decimals = !empty($number_parts[1]) ? $number_parts[1] : '0';
$relevant_decimal = isset($decimals[$precision]) ? $decimals[$precision] : 0;
if ($relevant_decimal < 5) {
$number = $rounded_down;
}
elseif ($relevant_decimal == 5) {
if ($mode == PHP_ROUND_HALF_UP) {
$number = $rounded_up;
}
elseif ($mode == PHP_ROUND_HALF_DOWN) {
$number = $rounded_down;
}
elseif ($mode == PHP_ROUND_HALF_EVEN) {
$integer = bcmul($rounded_up, pow(10, $precision), 0);
$number = bcmod($integer, '2') == 0 ? $rounded_up : $rounded_down;
}
elseif ($mode == PHP_ROUND_HALF_ODD) {
$integer = bcmul($rounded_up, pow(10, $precision), 0);
$number = bcmod($integer, '2') != 0 ? $rounded_up : $rounded_down;
}
}
elseif ($relevant_decimal > 5) {
$number = $rounded_up;
}
return $number;
}
/**
* Compares the first number to the second number.
*
* @param string $first_number
* The first number.
* @param string $second_number
* The second number.
* @param int $scale
* The maximum number of digits after the decimal place.
* Any digit after $scale will be truncated.
*
* @return int
* 0 if both numbers are equal, 1 if the first one is greater, -1 otherwise.
*/
public static function compare(string $first_number, string $second_number, int $scale = 6) : int {
self::assertNumberFormat($first_number);
self::assertNumberFormat($second_number);
return bccomp($first_number, $second_number, $scale);
}
/**
* Trims the given number.
*
* By default bcmath returns numbers with the number of digits according
* to $scale. This means that bcadd('2', '2', 6) will return '4.00000'.
* Trimming the number removes the excess zeroes.
*
* @param string $number
* The number to trim.
*
* @return string
* The trimmed number.
*/
public static function trim(string $number) : string {
if (strpos($number, '.') != FALSE) {
// The number is decimal, strip trailing zeroes.
// If no digits remain after the decimal point, strip it as well.
$number = rtrim($number, '0');
$number = rtrim($number, '.');
}
return $number;
}
/**
* Assert that the given number is a numeric string value.
*
* @param string $number
* The number to check.
*
* @throws \InvalidArgumentException
* Thrown when the given number is not a numeric string value.
*/
public static function assertNumberFormat($number) {
if (is_float($number)) {
throw new \InvalidArgumentException(sprintf('The provided value "%s" must be a string, not a float.', $number));
}
if (!is_numeric($number)) {
throw new \InvalidArgumentException(sprintf('The provided value "%s" is not a numeric value.', $number));
}
}
}
Classes
Name | Description |
---|---|
Calculator | Provides helpers for bcmath-based arithmetic. |