This commit is contained in:
+389
@@ -0,0 +1,389 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tez\Utils\Num;
|
||||
|
||||
use Tez\Utils\Validate\Guard;
|
||||
|
||||
/**
|
||||
* Arbitrary-precision decimal arithmetic helpers wrapping PHP's bcmath extension.
|
||||
*
|
||||
* All methods default to 8 decimal places, which is appropriate for financial
|
||||
* calculations. Every input must be a numeric string (e.g. "1.50000000").
|
||||
* Passing a non-numeric string to any method throws \InvalidArgumentException.
|
||||
*/
|
||||
final class Bc
|
||||
{
|
||||
/**
|
||||
* Add two arbitrary-precision decimals.
|
||||
*
|
||||
* @param numeric-string $a
|
||||
* @param numeric-string $b
|
||||
* @throws \Throwable
|
||||
* @return numeric-string
|
||||
*/
|
||||
public static function add(string $a, string $b, int $scale = 8): string
|
||||
{
|
||||
return bcadd(
|
||||
Guard::isNumeric($a),
|
||||
Guard::isNumeric($b),
|
||||
$scale,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sum an array of arbitrary-precision decimals.
|
||||
*
|
||||
* Returns a correctly scaled zero ('0.00000000') for an empty array.
|
||||
*
|
||||
* @param numeric-string[] $operands
|
||||
* @throws \Throwable
|
||||
* @return numeric-string
|
||||
*/
|
||||
public static function sum(array $operands, int $scale = 8): string
|
||||
{
|
||||
if ($operands === []) {
|
||||
return self::add('0', '0', $scale);
|
||||
}
|
||||
|
||||
return array_reduce(
|
||||
$operands,
|
||||
static fn(string $carry, string $item) => self::add($carry, (string) $item, $scale),
|
||||
'0',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subtract $b from $a.
|
||||
*
|
||||
* @param numeric-string $a
|
||||
* @param numeric-string $b
|
||||
* @throws \Throwable
|
||||
* @return numeric-string
|
||||
*/
|
||||
public static function sub(string $a, string $b, int $scale = 8): string
|
||||
{
|
||||
return bcsub(
|
||||
Guard::isNumeric($a),
|
||||
Guard::isNumeric($b),
|
||||
$scale,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Multiply two arbitrary-precision decimals.
|
||||
*
|
||||
* @param numeric-string $a
|
||||
* @param numeric-string $b
|
||||
* @throws \Throwable
|
||||
* @return numeric-string
|
||||
*/
|
||||
public static function mul(string $a, string $b, int $scale = 8): string
|
||||
{
|
||||
return bcmul(
|
||||
Guard::isNumeric($a),
|
||||
Guard::isNumeric($b),
|
||||
$scale,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Divide $a by $b.
|
||||
*
|
||||
* @param numeric-string $a
|
||||
* @param numeric-string $b
|
||||
* @throws \Throwable
|
||||
* @throws \DivisionByZeroError if $b is zero
|
||||
* @return numeric-string
|
||||
*/
|
||||
public static function div(string $a, string $b, int $scale = 8): string
|
||||
{
|
||||
Guard::that(
|
||||
!self::isZero($b, $scale + 1),
|
||||
static fn() => new \DivisionByZeroError('Division by zero.'),
|
||||
);
|
||||
|
||||
return bcdiv(Guard::isNumeric($a), $b, $scale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a numeric string to exactly $scale decimal places (truncates, does not round).
|
||||
*
|
||||
* Example: Bc::scale('1.5', 8) → '1.50000000'
|
||||
*
|
||||
* @param numeric-string $value
|
||||
* @throws \Throwable
|
||||
* @return numeric-string
|
||||
*/
|
||||
public static function scale(string $value, int $scale = 8): string
|
||||
{
|
||||
return self::add(
|
||||
$value,
|
||||
'0',
|
||||
$scale,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the absolute value.
|
||||
*
|
||||
* @param numeric-string $value
|
||||
* @throws \Throwable
|
||||
* @return numeric-string
|
||||
*/
|
||||
public static function abs(string $value, int $scale = 8): string
|
||||
{
|
||||
return self::isNegative($value, $scale)
|
||||
? self::sub('0', $value, $scale)
|
||||
: self::add($value, '0', $scale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the larger of two values.
|
||||
*
|
||||
* @param numeric-string $a
|
||||
* @param numeric-string $b
|
||||
* @throws \Throwable
|
||||
* @return numeric-string
|
||||
*/
|
||||
public static function max(string $a, string $b, int $scale = 8): string
|
||||
{
|
||||
return self::gte($a, $b, $scale)
|
||||
? self::add($a, '0', $scale)
|
||||
: self::add($b, '0', $scale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the smaller of two values.
|
||||
*
|
||||
* @param numeric-string $a
|
||||
* @param numeric-string $b
|
||||
* @throws \Throwable
|
||||
* @return numeric-string
|
||||
*/
|
||||
public static function min(string $a, string $b, int $scale = 8): string
|
||||
{
|
||||
return self::lte($a, $b, $scale)
|
||||
? self::add($a, '0', $scale)
|
||||
: self::add($b, '0', $scale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two values. Returns -1, 0, or 1.
|
||||
*
|
||||
* @param numeric-string $a
|
||||
* @param numeric-string $b
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public static function compare(string $a, string $b, int $scale = 8): int
|
||||
{
|
||||
return bccomp(
|
||||
Guard::isNumeric($a),
|
||||
Guard::isNumeric($b),
|
||||
$scale,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true when the value is exactly zero at the given scale.
|
||||
*
|
||||
* @param numeric-string $value
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public static function isZero(string $value, int $scale = 8): bool
|
||||
{
|
||||
return 0 === self::compare($value, '0', $scale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true when $a is greater than $b.
|
||||
*
|
||||
* @param numeric-string $a
|
||||
* @param numeric-string $b
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public static function gt(string $a, string $b, int $scale = 8): bool
|
||||
{
|
||||
return 1 === self::compare($a, $b, $scale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true when $a is less than $b.
|
||||
*
|
||||
* @param numeric-string $a
|
||||
* @param numeric-string $b
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public static function lt(string $a, string $b, int $scale = 8): bool
|
||||
{
|
||||
return -1 === self::compare($a, $b, $scale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true when $a is greater than or equal to $b.
|
||||
*
|
||||
* @param numeric-string $a
|
||||
* @param numeric-string $b
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public static function gte(string $a, string $b, int $scale = 8): bool
|
||||
{
|
||||
return self::compare($a, $b, $scale) >= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true when $a is less than or equal to $b.
|
||||
*
|
||||
* @param numeric-string $a
|
||||
* @param numeric-string $b
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public static function lte(string $a, string $b, int $scale = 8): bool
|
||||
{
|
||||
return self::compare($a, $b, $scale) <= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true when $a equals $b at the given scale.
|
||||
*
|
||||
* @param numeric-string $a
|
||||
* @param numeric-string $b
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public static function eq(string $a, string $b, int $scale = 8): bool
|
||||
{
|
||||
return 0 === self::compare($a, $b, $scale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true when $value is strictly greater than zero.
|
||||
*
|
||||
* @param numeric-string $value
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public static function isPositive(string $value, int $scale = 8): bool
|
||||
{
|
||||
return 1 === self::compare($value, '0', $scale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true when $value is strictly less than zero.
|
||||
*
|
||||
* @param numeric-string $value
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public static function isNegative(string $value, int $scale = 8): bool
|
||||
{
|
||||
return -1 === self::compare($value, '0', $scale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Negate a value (equivalent to multiplying by -1).
|
||||
*
|
||||
* @param numeric-string $value
|
||||
* @throws \Throwable
|
||||
* @return numeric-string
|
||||
*/
|
||||
public static function negate(string $value, int $scale = 8): string
|
||||
{
|
||||
return self::sub(
|
||||
'0',
|
||||
$value,
|
||||
$scale,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate $percent % of $value (i.e. value × percent ÷ 100).
|
||||
*
|
||||
* Example: Bc::percent('200', '15') → '30.00000000'
|
||||
*
|
||||
* @param numeric-string $value
|
||||
* @param numeric-string $percent
|
||||
* @throws \Throwable
|
||||
* @return numeric-string
|
||||
*/
|
||||
public static function percent(string $value, string $percent, int $scale = 8): string
|
||||
{
|
||||
return self::div(
|
||||
self::mul(
|
||||
$value,
|
||||
$percent,
|
||||
$scale + 2,
|
||||
),
|
||||
'100',
|
||||
$scale,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Multiply an array of arbitrary-precision decimals together.
|
||||
*
|
||||
* Returns '1.00000000' for an empty array (neutral element of multiplication).
|
||||
*
|
||||
* @param numeric-string[] $operands
|
||||
* @throws \Throwable
|
||||
* @return numeric-string
|
||||
*/
|
||||
public static function product(array $operands, int $scale = 8): string
|
||||
{
|
||||
if ($operands === []) {
|
||||
return self::add('1', '0', $scale);
|
||||
}
|
||||
|
||||
return array_reduce(
|
||||
$operands,
|
||||
static fn(string $carry, string $item) => self::mul($carry, (string) $item, $scale),
|
||||
'1',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subtract all subsequent operands from the first.
|
||||
*
|
||||
* Returns '0.00000000' for an empty array.
|
||||
*
|
||||
* @param numeric-string[] $operands
|
||||
* @throws \Throwable
|
||||
* @return numeric-string
|
||||
*/
|
||||
public static function subAll(array $operands, int $scale = 8): string
|
||||
{
|
||||
if ($operands === []) {
|
||||
return self::add('0', '0', $scale);
|
||||
}
|
||||
|
||||
$first = (string) array_shift($operands);
|
||||
|
||||
return array_reduce(
|
||||
$operands,
|
||||
static fn(string $carry, string $item) => self::sub($carry, (string) $item, $scale),
|
||||
$first,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Divide the first operand by each subsequent operand in turn.
|
||||
*
|
||||
* Returns '1.00000000' for an empty array (neutral element of division).
|
||||
*
|
||||
* @param numeric-string[] $operands
|
||||
* @throws \Throwable
|
||||
* @return numeric-string
|
||||
*/
|
||||
public static function divAll(array $operands, int $scale = 8): string
|
||||
{
|
||||
if ($operands === []) {
|
||||
return self::add('1', '0', $scale);
|
||||
}
|
||||
|
||||
$first = (string) array_shift($operands);
|
||||
|
||||
return array_reduce(
|
||||
$operands,
|
||||
static fn(string $carry, string $item) => self::div($carry, (string) $item, $scale),
|
||||
$first,
|
||||
);
|
||||
}
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tez\Utils\Num;
|
||||
|
||||
/**
|
||||
* Number formatting helpers: decimal, byte sizes, percentages, and rounding.
|
||||
*/
|
||||
final class Num
|
||||
{
|
||||
private const BYTE_UNITS = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
|
||||
|
||||
/**
|
||||
* Format a number with grouped thousands and a configurable decimal separator.
|
||||
*/
|
||||
public static function format(
|
||||
int|float $value,
|
||||
int $decimals = 2,
|
||||
string $dec = '.',
|
||||
string $thousands = ',',
|
||||
): string {
|
||||
return number_format((float) $value, $decimals, $dec, $thousands);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a byte count as a human-readable string using binary units (1 KB = 1024 B).
|
||||
*
|
||||
* @param int<0, max> $bytes
|
||||
*/
|
||||
public static function bytes(int $bytes, int $precision = 2): string
|
||||
{
|
||||
if ($bytes < 1024) {
|
||||
return self::format($bytes, 0) . ' B';
|
||||
}
|
||||
|
||||
$exp = (int) floor(log($bytes, 1024));
|
||||
$exp = min($exp, count(self::BYTE_UNITS) - 1);
|
||||
$value = $bytes / (1024 ** $exp);
|
||||
$unit = self::BYTE_UNITS[$exp];
|
||||
|
||||
return self::format($value, $precision) . ' ' . $unit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a ratio (0.0–1.0) as a percentage string.
|
||||
*
|
||||
* Example: 0.1234 → '12.3%'
|
||||
*/
|
||||
public static function percent(int|float $ratio, int $decimals = 1): string
|
||||
{
|
||||
return self::format((float) $ratio * 100, $decimals) . '%';
|
||||
}
|
||||
|
||||
/**
|
||||
* Clamp a value to the inclusive range [$min, $max].
|
||||
*/
|
||||
public static function clamp(int|float $value, int|float $min, int|float $max): int|float
|
||||
{
|
||||
if ($value < $min) {
|
||||
return $min;
|
||||
}
|
||||
|
||||
if ($value > $max) {
|
||||
return $max;
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Round a value to the nearest multiple of $multiple.
|
||||
*
|
||||
* Examples: roundTo(17, 5) → 15, roundTo(18, 5) → 20, roundTo(1.7, 0.5) → 1.5
|
||||
*/
|
||||
public static function roundTo(int|float $value, int|float $multiple): int|float
|
||||
{
|
||||
if ($multiple === 0 || $multiple === 0.0) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
$rounded = round((float) $value / (float) $multiple) * (float) $multiple;
|
||||
|
||||
if (is_int($value) && is_int($multiple)) {
|
||||
return (int) $rounded;
|
||||
}
|
||||
|
||||
return $rounded;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user