Файл: vendor/brick/math/src/Internal/DecimalHelper.php
Строк: 209
<?php
declare(strict_types=1);
namespace BrickMathInternal;
use BrickMathRoundingMode;
use function ltrim;
use function rtrim;
use function str_pad;
use function str_repeat;
use function strlen;
use function substr;
use const STR_PAD_LEFT;
/**
* Shared helper for decimal operations.
*
* @internal
*/
final class DecimalHelper
{
private function __construct()
{
}
/**
* Computes the scale needed to represent the exact decimal result of a reduced fraction.
*
* Returns null if the denominator has prime factors other than 2 or 5.
*
* @param string $denominator The denominator of the reduced fraction. Must be strictly positive.
*
* @return non-negative-int|null
*
* @pure
*/
public static function computeScaleFromReducedFractionDenominator(string $denominator): ?int
{
$calculator = CalculatorRegistry::get();
$d = rtrim($denominator, '0');
/** @var non-negative-int $scale rtrim can only shorten a string */
$scale = strlen($denominator) - strlen($d);
foreach ([5, 2] as $prime) {
for (; ;) {
$lastDigit = (int) $d[-1];
if ($lastDigit % $prime !== 0) {
break;
}
$d = $calculator->divQ($d, (string) $prime);
$scale++;
}
}
return $d === '1' ? $scale : null;
}
/**
* Scales an unscaled decimal value to the requested scale.
*
* Returns null when rounding is necessary and the rounding mode is Unnecessary.
*
* @param string $value The unscaled value.
* @param int $currentScale The current scale.
* @param int $targetScale The target scale.
* @param RoundingMode $roundingMode The rounding mode.
*
* @return string|null The unscaled value at the target scale, or null if RoundingMode::Unnecessary is used and rounding is necessary.
*
* @pure
*/
public static function scale(string $value, int $currentScale, int $targetScale, RoundingMode $roundingMode): ?string
{
$scaled = self::tryScaleExactly($value, $currentScale, $targetScale);
if ($scaled !== null) {
return $scaled;
}
if ($roundingMode === RoundingMode::Unnecessary) {
return null;
}
$divisor = '1' . str_repeat('0', $currentScale - $targetScale);
return CalculatorRegistry::get()->divRound($value, $divisor, $roundingMode);
}
/**
* Adds leading zeros if necessary to represent the full decimal number.
*
* @param string $value The unscaled value.
* @param int $scale The current scale.
*
* @pure
*/
public static function padUnscaledValue(string $value, int $scale): string
{
$targetLength = $scale + 1;
$negative = ($value[0] === '-');
$length = strlen($value);
if ($negative) {
$length--;
}
if ($length >= $targetLength) {
return $value;
}
if ($negative) {
$value = substr($value, 1);
}
$value = str_pad($value, $targetLength, '0', STR_PAD_LEFT);
if ($negative) {
$value = '-' . $value;
}
return $value;
}
/**
* Tries to scale exactly without rounding, returning null when rounding would be required.
*
* @param string $value The unscaled value.
* @param int $currentScale The current scale.
* @param int $targetScale The target scale.
*
* @return string|null The unscaled value at the target scale, or null if rounding would be required.
*
* @pure
*/
public static function tryScaleExactly(string $value, int $currentScale, int $targetScale): ?string
{
if ($value === '0' || $targetScale === $currentScale) {
return $value;
}
if ($targetScale > $currentScale) {
return $value . str_repeat('0', $targetScale - $currentScale);
}
$negative = ($value[0] === '-');
if ($negative) {
$value = substr($value, 1);
}
$value = self::padUnscaledValue($value, $currentScale);
$discardedDigits = $currentScale - $targetScale;
if (substr($value, -$discardedDigits) !== str_repeat('0', $discardedDigits)) {
return null;
}
$value = substr($value, 0, -$discardedDigits);
$value = ltrim($value, '0');
if ($value === '') {
return '0';
}
if ($negative) {
$value = '-' . $value;
}
return $value;
}
}