diff --git a/config/helpers.php b/config/helpers.php index 382cf51..5ba00e5 100644 --- a/config/helpers.php +++ b/config/helpers.php @@ -3,10 +3,15 @@ return [ /* |-------------------------------------------------------------------------- - | Base Configuration + | Math Helper Configuration |-------------------------------------------------------------------------- | | All notable configurations will show here. | */ + 'math' => [ + 'scale' => 10, + 'storage_scale' => 10, + 'rounding_mode' => \Brick\Math\RoundingMode::DOWN, + ], ]; diff --git a/phpstan.neon b/phpstan.neon index 6fc2426..9aa1952 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -7,6 +7,7 @@ parameters: # Level 9 is the highest level level: 9 ignoreErrors: + - '#on Brick\\Math\\BigDecimal\|float\|int\|string#' scanFiles: excludePaths: - tests/*/Feature/* diff --git a/src/Data/OptionData.php b/src/Data/OptionData.php index 164ca17..601b9f8 100644 --- a/src/Data/OptionData.php +++ b/src/Data/OptionData.php @@ -18,5 +18,6 @@ public function __construct( public Optional|null|string $groupIcon = null, /** @var Collection|null */ public Optional|Collection|null $items = null, - ) {} + ) { + } } diff --git a/src/Helpers/Math.php b/src/Helpers/Math.php deleted file mode 100644 index c6ce44b..0000000 --- a/src/Helpers/Math.php +++ /dev/null @@ -1,466 +0,0 @@ -powTen($this->floatScale); - - return $this->round( - $this->mul( - $value, - $decimalPlaces, - $this->floatScale - ) - ); - } - - /** - * Converts a big integer into a float based on the given scale - * - * @throws MathException - */ - public function intToFloat(float|int|string $value): string - { - $decimalPlaces = $this->powTen($this->floatScale); - - return $this->div($value, $decimalPlaces, $this->floatScale); - } - - /** - * Sums to big integers - * - * @throws MathException - * @throws RoundingNecessaryException - */ - public function addInteger(float|int|string $first, float|int|string $second, ?int $scale = null): string - { - return $this->add( - $this->floatToInt($first), - $this->floatToInt($second), - ); - } - - /** - * Adds a percentage to a big integer - * - * @throws MathException - * @throws RoundingNecessaryException - */ - public function addPercentageInteger(float|int|string $number, float|int|string $percentage, ?int $scale = null): string - { - $intNumber = $this->floatToInt($number); - $percentageValue = $this->div($this->mul($intNumber, $this->floatToInt($percentage)), $this->floatToInt(100), $scale); - - return $this->add($intNumber, $percentageValue); - } - - /** - * Subtracts two big integers - * - * @throws MathException - * @throws RoundingNecessaryException - */ - public function subInteger(float|int|string $first, float|int|string $second, ?int $scale = null): string - { - return $this->sub( - $this->floatToInt($first), - $this->floatToInt($second) - ); - } - - /** - * Subtracts a percentage from a big integer - * - * @throws MathException - * @throws RoundingNecessaryException - */ - public function subtractPercentageInteger(float|int|string $number, float|int|string $percentage, ?int $scale = null): string - { - $intNumber = $this->floatToInt($number); - $percentageValue = $this->div($this->mul($intNumber, $this->floatToInt($percentage)), $this->floatToInt(100), $scale); - - return $this->sub($intNumber, $percentageValue); - } - - /** - * Divides two big integers - * - * @throws MathException - */ - public function divInteger(float|int|string $first, float|int|string $second, ?int $scale = null): string - { - return $this->div( - $this->floatToInt($first), - $this->floatToInt($second), - $scale ?? $this->floatScale - ); - } - - /** - * Multiplies two big integers - * - * @throws MathException - * @throws RoundingNecessaryException - */ - public function mulInteger(float|int|string $first, float|int|string $second, ?int $scale = null): string - { - return $this->mul( - $this->floatToInt($first), - $this->floatToInt($second) - ); - } - - /** - * Raises a big integer to the power of another big integer - * - * @throws MathException - * @throws RoundingNecessaryException - */ - public function powInteger(float|int|string $first, float|int|string $second, ?int $scale = null): string - { - return $this->pow( - $this->floatToInt($first), - $this->floatToInt($second), - ); - } - - /** - * Powers a big integer to the power of ten - * - * @throws MathException - * @throws RoundingNecessaryException - */ - public function powTenInteger(float|int|string $number): string - { - return $this->powTen($this->floatToInt($number)); - } - - /** - * Ceils a big integer - * - * @throws MathException - */ - public function ceilInteger(float|int|string $number): string - { - return $this->ceil($this->floatToInt($number)); - } - - /** - * Floors a big integer - * - * @throws MathException - */ - public function floorInteger(float|int|string $number): string - { - return $this->floor($this->floatToInt($number)); - } - - /** - * Rounds a big integer - * - * @throws MathException - */ - public function roundInteger(float|int|string $number, int $precision = 0): string - { - return $this->round($this->floatToInt($number), $precision); - } - - /** - * Absolutes a big integer - * - * @throws MathException - * @throws RoundingNecessaryException - */ - public function absInteger(float|int|string $number): string - { - return $this->abs($this->floatToInt($number)); - } - - /** - * Negatives a big integer - * - * @throws MathException - * @throws RoundingNecessaryException - */ - public function negativeInteger(float|int|string $number): string - { - return $this->negative($this->floatToInt($number)); - } - - /** - * Compares two big integers - * - * @throws MathException - */ - public function compareInteger(float|int|string $first, float|int|string $second): int - { - return $this->compare($this->floatToInt($first), $this->floatToInt($second)); - } - - /** - * Ensures the scale of a big integer - * - * @throws MathException - * @throws RoundingNecessaryException - */ - public function ensureScale(float|int|string $number): string - { - return $this->mul($number, 1); - } - - /** - * Sums two floats - * - * @throws MathException - * @throws RoundingNecessaryException - */ - public function add(float|int|string $first, float|int|string $second, ?int $scale = null): string - { - return (string) BigDecimal::of($first) - ->plus(BigDecimal::of($second)) - ->toScale($scale ?? $this->floatScale, RoundingMode::DOWN); - } - - /** - * Adds a percentage to a number - * - * @throws MathException - * @throws RoundingNecessaryException - */ - public function addPercentage(float|int|string $number, float|int|string $percentage, ?int $scale = null): string - { - $percentageValue = $this->div($this->mul($number, $percentage), 100, $scale); - - return $this->add($number, $percentageValue, $scale); - } - - /** - * Subtracts two floats - * - * @throws MathException - * @throws RoundingNecessaryException - */ - public function sub(float|int|string $first, float|int|string $second, ?int $scale = null): string - { - return (string) BigDecimal::of($first) - ->minus(BigDecimal::of($second)) - ->toScale($scale ?? $this->floatScale, RoundingMode::DOWN); - } - - /** - * Subtracts a percentage from a number - * - * @throws MathException - * @throws RoundingNecessaryException - */ - public function subtractPercentage(float|int|string $number, float|int|string $percentage, ?int $scale = null): string - { - $percentageValue = $this->div($this->mul($number, $percentage), 100, $scale); - - return $this->sub($number, $percentageValue, $scale); - } - - /** - * Divides two floats - * - * @throws MathException - */ - public function div(float|int|string $first, float|int|string $second, ?int $scale = null): string - { - return (string) BigDecimal::of($first) - ->dividedBy(BigDecimal::of($second), $scale ?? $this->floatScale, RoundingMode::DOWN); - } - - /** - * Multiplies two floats - * - * @throws MathException - * @throws RoundingNecessaryException - */ - public function mul(float|int|string $first, float|int|string $second, ?int $scale = null): string - { - return (string) BigDecimal::of($first) - ->multipliedBy(BigDecimal::of($second)) - ->toScale($scale ?? $this->floatScale, RoundingMode::DOWN); - } - - /** - * Raises a float to the power of another float - * - * @throws MathException - * @throws RoundingNecessaryException - */ - public function pow(float|int|string $first, float|int|string $second, ?int $scale = null): string - { - return (string) BigDecimal::of($first) - ->power((int) $second) - ->toScale($scale ?? $this->floatScale, RoundingMode::DOWN); - } - - /** - * Powers a float to the power of ten - * - * @throws MathException - * @throws RoundingNecessaryException - */ - public function powTen(float|int|string $number): string - { - return $this->pow(10, $number); - } - - /** - * Ceils a float - * - * @throws MathException - */ - public function ceil(float|int|string $number): string - { - return (string) BigDecimal::of($number) - ->dividedBy(BigDecimal::one(), 0, RoundingMode::CEILING); - } - - /** - * Floors a float - * - * @throws MathException - */ - public function floor(float|int|string $number): string - { - return (string) BigDecimal::of($number) - ->dividedBy(BigDecimal::one(), 0, RoundingMode::FLOOR); - } - - /** - * Rounds a float - * - * @throws MathException - */ - public function round(float|int|string $number, int $precision = 0): string - { - return (string) BigDecimal::of($number) - ->dividedBy(BigDecimal::one(), $precision, RoundingMode::HALF_UP); - } - - /** - * Absolutes a float - * - * @throws MathException - * @throws RoundingNecessaryException - */ - public function abs(float|int|string $number, ?int $scale = null): string - { - return (string) BigDecimal::of($number)->abs()->toScale($scale ?? $this->floatScale, RoundingMode::DOWN); - } - - /** - * Negatives a float - * - * @throws MathException - * @throws RoundingNecessaryException - */ - public function negative(float|int|string $number, ?int $scale = null): string - { - $number = BigDecimal::of($number); - if ($number->isNegative()) { - return (string) $number->toScale($scale ?? $this->floatScale, RoundingMode::DOWN); - } - - return (string) BigDecimal::of($number) - ->toScale($scale ?? $this->floatScale, RoundingMode::DOWN) - ->negated(); - } - - /** - * Compares two floats - * - * @throws MathException - */ - public function compare(float|int|string $first, float|int|string $second): int - { - return BigDecimal::of($first)->compareTo(BigDecimal::of($second)); - } - - /** - * Check if its zero - * - * @throws DivisionByZeroException - * @throws NumberFormatException - * @throws RoundingNecessaryException - */ - public function isZero(float|int|string $number, ?int $scale = null): bool - { - return BigDecimal::of($number) - ->toScale($scale ?? $this->floatScale, RoundingMode::DOWN) - ->isZero(); - } - - /** - * Check if its not zero - * - * @throws DivisionByZeroException - * @throws NumberFormatException - * @throws RoundingNecessaryException - */ - public function isNotZero(float|int|string $number, ?int $scale = null): bool - { - return ! $this->isZero($number, $scale); - } - - /** - * Returns the representation of the number as a string - * - * @throws DivisionByZeroException - * @throws NumberFormatException - * @throws RoundingNecessaryException - */ - public function toToNumber(float|int|string $number, ?int $scale = null): BigDecimal - { - return BigDecimal::of($number) - ->toScale($scale ?? $this->floatScale, RoundingMode::DOWN); - } - - /** - * Check if two numbers are equal - * - * @throws DivisionByZeroException - * @throws NumberFormatException - * @throws RoundingNecessaryException - */ - public function isEqual(float|int|string $first, float|int|string $second, ?int $scale = null): bool - { - $firstScaled = BigDecimal::of($first) - ->toScale($scale ?? $this->floatScale, RoundingMode::DOWN); - - $secondScaled = BigDecimal::of($second) - ->toScale($scale ?? $this->floatScale, RoundingMode::DOWN); - - return $firstScaled->isEqualTo($secondScaled); - } -} diff --git a/src/Helpers/Math/Math.php b/src/Helpers/Math/Math.php new file mode 100644 index 0000000..4f3ce02 --- /dev/null +++ b/src/Helpers/Math/Math.php @@ -0,0 +1,637 @@ +number = BigDecimal::of($number); + } + } + + /** + * A static factory method to create a new instance of the class. + */ + public static function of( + float|int|string|BigDecimal $number, + ?int $scale = null, + ?int $storageScale = null, + ?RoundingMode $roundingMode = null + ): Math { + + return new Math( + $number, + // @phpstan-ignore-next-line + $scale ?? (int) config('helpers.math.scale', 10), + // @phpstan-ignore-next-line + $storageScale ?? (int) config('helpers.math.scale', 10), + // @phpstan-ignore-next-line + $roundingMode ?? config('helpers.math.rounding_mode', RoundingMode::DOWN) + ); + } + + /** + * @param int|float|string|BigDecimal ...$numbers + * + * @throws DivisionByZeroException + * @throws MathException + * @throws NumberFormatException + */ + public static function average(...$numbers): Math + { + /** @var Math $sum */ + $sum = array_reduce( + $numbers, + fn ($carry, $num) => self::of($carry)->sum(BigDecimal::of($num)), + BigDecimal::zero() + ); + + return self::of($sum->divide(count($numbers))); + } + + /** + * Converts a float, int or string to a BigDecimal + * + * @throws DivisionByZeroException + * @throws NumberFormatException + */ + public function toBigDecimal(float|int|string|BigDecimal $value): BigDecimal + { + if ($value instanceof BigDecimal) { + return $value; + } + + return BigDecimal::of($value); + } + + /** + * Sets the rounding mode up or down + * + * @return $this + */ + public function roundingMode(RoundingMode $mode): Math + { + $this->roundingMode = $mode; + + return $this; + } + + /** + * Sets the rounding mode to down + * + * @return $this + */ + public function roundDown(): Math + { + $this->roundingMode = RoundingMode::DOWN; + + return $this; + } + + /** + * Sets the rounding mode to up + * + * @return $this + */ + public function roundUp(): Math + { + $this->roundingMode = RoundingMode::UP; + + return $this; + } + + /** + * Sets the scale of the number + * + * @return $this + */ + public function scale(int $scale): Math + { + $this->scale = $scale; + + return $this; + } + + /** + * Sets the storage scale of the number + * + * @return $this + */ + public function storageScale(int $storageScale): Math + { + $this->storageScale = $storageScale; + + return $this; + } + + /** + * Adds a value to the current number + * + * @throws DivisionByZeroException + * @throws NumberFormatException + * @throws MathException + */ + public function sum(float|int|string $value): Math + { + return self::of( + $this->number->plus($this->toBigDecimal($value)), + $this->scale, + $this->storageScale, + $this->roundingMode + ); + } + + /** + * Subtracts a value from the current number + * + * @throws DivisionByZeroException + * @throws NumberFormatException + * @throws MathException + */ + public function subtract(float|int|string $value): Math + { + return self::of( + $this->number->minus($this->toBigDecimal($value)), + $this->scale, + $this->storageScale, + $this->roundingMode + ); + } + + /** + * Multiplies the current number by a value + * + * @throws DivisionByZeroException + * @throws NumberFormatException + * @throws MathException + */ + public function multiply(float|int|string $value): Math + { + return self::of( + $this->number->multipliedBy($this->toBigDecimal($value)), + $this->scale, + $this->storageScale, + $this->roundingMode + ); + } + + /** + * Divides the current number by a value + * + * @throws DivisionByZeroException + * @throws MathException + * @throws NumberFormatException + */ + public function divide(float|int|string $value): Math + { + return self::of( + $this->number->dividedBy($this->toBigDecimal($value), $this->scale, $this->roundingMode), + $this->scale, + $this->storageScale, + $this->roundingMode + ); + } + + /** + * Raises the current number to the power of an exponent + */ + public function pow(int $exponent): Math + { + return self::of( + $this->number->power($exponent), + $this->scale, + $this->storageScale, + $this->roundingMode + ); + } + + /** + * Rounds the current number to the given precision + * + * @throws RoundingNecessaryException + */ + public function round(int $precision = 0, ?RoundingMode $roundingMode = null): Math + { + return self::of( + $this->number->toScale($precision, $roundingMode ?? $this->roundingMode), + $this->scale, + $this->storageScale, + $this->roundingMode + ); + } + + /** + * Rounds the current number up to the nearest + * + * @throws MathException + */ + public function ceil(): Math + { + return self::of( + $this->number->dividedBy(BigDecimal::one(), 0, RoundingMode::CEILING), + $this->scale, + $this->storageScale, + $this->roundingMode + ); + } + + /** + * Rounds the current number down to the nearest + * + * @throws MathException + */ + public function floor(): Math + { + return self::of( + $this->number->dividedBy(BigDecimal::one(), 0, RoundingMode::FLOOR), + $this->scale, + $this->storageScale, + $this->roundingMode + ); + } + + /** + * Returns the absolute value of the current number + */ + public function absolute(): Math + { + return self::of( + $this->number->abs(), + $this->scale, + $this->storageScale, + $this->roundingMode + ); + } + + /** + * Returns the negative value of the current number + * + * @return $this|self + */ + public function negative(): Math + { + if ($this->number->isNegative()) { + return $this; + } + + return self::of( + $this->number->negated(), + $this->scale, + $this->storageScale, + $this->roundingMode + ); + } + + /** + * Adds a percentage to the current number + * + * @throws DivisionByZeroException + * @throws MathException + * @throws NumberFormatException + */ + public function addPercentage(float|int|string $percentage): Math + { + $percentageValue = $this + ->number + ->multipliedBy($this->toBigDecimal($percentage)) + ->dividedBy(100, $this->scale, $this->roundingMode); + + return self::of( + $this->number->plus($percentageValue), + $this->scale, + $this->storageScale, + $this->roundingMode + ); + } + + /** + * Subtracts a percentage from the current number + * + * @throws DivisionByZeroException + * @throws MathException + * @throws NumberFormatException + */ + public function subtractPercentage(float|int|string $percentage): Math + { + $percentageValue = $this + ->number + ->multipliedBy($this->toBigDecimal($percentage)) + ->dividedBy(100, $this->scale, $this->roundingMode); + + return self::of( + $this->number->minus($percentageValue), + $this->scale, + $this->storageScale, + $this->roundingMode + ); + } + + /** + * Compares the current number to another value + * + * @throws MathException + * @throws RoundingNecessaryException + */ + public function compare(float|int|string $value): int + { + $other = self::of( + $value, + $this->scale, + $this->storageScale, + $this->roundingMode + ); + + return $this->number->compareTo($other->number->toBigDecimal()); + } + + /** + * Checks if the current number is less than another value + * + * @throws MathException + * @throws RoundingNecessaryException + */ + public function isLessThan(float|int|string $value): bool + { + return $this->compare($value) < 0; + } + + /** + * Checks if the current number is less than or equal to another value + * + * @throws MathException + * @throws RoundingNecessaryException + */ + public function isLessThanOrEqual(float|int|string $value): bool + { + return $this->compare($value) <= 0; + } + + /** + * Checks if the current number is greater than another value + * + * @throws MathException + * @throws RoundingNecessaryException + */ + public function isGreaterThan(float|int|string $value): bool + { + return $this->compare($value) > 0; + } + + /** + * Checks if the current number is greater than or equal to another value + * + * @throws MathException + * @throws RoundingNecessaryException + */ + public function isGreaterThanOrEqual(float|int|string $value): bool + { + return $this->compare($value) >= 0; + } + + /** + * Calculates the specified percentage of the current number. + * + * @throws DivisionByZeroException + * @throws MathException + * @throws NumberFormatException + */ + public function toPercentageOf(float|int|string $percentage): Math + { + $percentageValue = $this->toBigDecimal($percentage)->dividedBy(100, $this->scale, $this->roundingMode); + + return self::of( + $this->number->multipliedBy($percentageValue), + $this->scale, + $this->storageScale, + $this->roundingMode + ); + } + + /** + * Get the percentage of the current number compared to the given total + * + * @throws DivisionByZeroException + * @throws MathException + * @throws NumberFormatException + * @throws RoundingNecessaryException + */ + public function percentageOf(float|int|string|BigDecimal $total): float + { + $percentage = $this->number + ->multipliedBy(BigDecimal::of(100)) + ->dividedBy($this->toBigDecimal($total), $this->scale, $this->roundingMode); + + return $percentage->toScale($this->scale, $this->roundingMode)->toFloat(); + } + + /** + * Calculates the percentage difference between two numbers + * + * @throws DivisionByZeroException + * @throws MathException + * @throws NumberFormatException + */ + public function differenceInPercentage(float|int|string|BigDecimal $value): float + { + $original = $this->number; + $comparisonValue = $this->toBigDecimal($value); + $difference = $original->minus($comparisonValue)->abs(); + + if ($original->isZero() && $comparisonValue->isZero()) { + return 0.0; + } + + if ($original->isZero()) { + return 100.0; + } + + $percentage = $difference + ->multipliedBy(100) + ->dividedBy($original->abs(), $this->scale, $this->roundingMode); + + return $percentage->toScale($this->scale, $this->roundingMode)->toFloat(); + } + + /** + * Checks if the current number is equal to another value + * + * @throws MathException + * @throws RoundingNecessaryException + */ + public function isEqual(float|int|string $value): bool + { + return $this->compare($value) === 0; + } + + /** + * Checks if the current number is zero + * + * @throws MathException + * @throws RoundingNecessaryException + */ + public function isZero(): bool + { + return $this->number->toScale($this->scale, $this->roundingMode)->isZero(); + } + + /** + * Checks if the current number is not zero + * + * @throws MathException + * @throws RoundingNecessaryException + */ + public function isNotZero(): bool + { + return ! $this->isZero(); + } + + /** + * Ensures the scale of the current number + * + * @throws RoundingNecessaryException + */ + public function ensureScale(): Math + { + return self::of( + $this->number->toScale($this->scale, $this->roundingMode), + $this->scale, + $this->storageScale, + $this->roundingMode, + ); + } + + /** + * Returns the current number as an integer to save on storage + * + * @throws MathException + * @throws RoundingNecessaryException + */ + public function toStorageScale(): int + { + $decimalPlaces = BigDecimal::of(10)->power($this->storageScale); + $numberRounded = $this->ensureScale(); + + return $numberRounded + ->multiply($decimalPlaces) + ->ensureScale() + ->toInt(); + } + + /** + * Returns the current number from the storage as a Math object + * + * @throws DivisionByZeroException + * @throws MathException + * @throws NumberFormatException + */ + public function fromStorage(): Math + { + $decimalPlaces = BigDecimal::of(10)->power($this->storageScale); + + return self::of( + $this->number->dividedBy($decimalPlaces, $this->scale, $this->roundingMode), + $this->scale, + $this->storageScale, + $this->roundingMode + ); + } + + /** + * Returns the current number as a BigDecimal + */ + public function toNumber(): BigDecimal + { + if ($this->number instanceof BigDecimal) { + return $this->number; + } + + return BigDecimal::of($this->number); + } + + /** + * Returns the current number as an integer + * + * @throws MathException + * @throws RoundingNecessaryException + */ + public function toInt(): int + { + return $this->number->toScale(0, $this->roundingMode)->toInt(); + } + + /** + * Returns the current number as a float + * + * @throws MathException + * @throws RoundingNecessaryException + */ + public function toFloat(): float + { + return $this->number->toScale($this->scale, $this->roundingMode)->toFloat(); + } + + /** + * Returns the current number as a string + * + * @throws RoundingNecessaryException + */ + public function toString(): string + { + return $this->number->toScale($this->scale, $this->roundingMode)->__toString(); + } + + /** + * Formats the current number to a string + * + * @throws MathException + * @throws RoundingNecessaryException + */ + public function format(string $thousandsSeparator = ',', string $decimalPoint = '.'): string + { + return number_format($this->toFloat(), $this->scale, $decimalPoint, $thousandsSeparator); + } + + /** + * Returns the current number as a string + * + * @throws RoundingNecessaryException + */ + public function __toString(): string + { + return $this->toString(); + } +} diff --git a/src/Helpers/RateLimiterHelper.php b/src/Helpers/RateLimiterHelper.php index 4ad06c1..e79129a 100644 --- a/src/Helpers/RateLimiterHelper.php +++ b/src/Helpers/RateLimiterHelper.php @@ -29,7 +29,8 @@ public function __construct( protected string $key, protected string $by, protected bool $hashed = true - ) {} + ) { + } /** * Forwards the call to the RateLimiter instance. diff --git a/src/LaravelHelpersServiceProvider.php b/src/LaravelHelpersServiceProvider.php index 023f0b5..eef3c12 100644 --- a/src/LaravelHelpersServiceProvider.php +++ b/src/LaravelHelpersServiceProvider.php @@ -19,5 +19,7 @@ public function bootingPackage(): void // Booting the package } - public function registeringPackage(): void {} + public function registeringPackage(): void + { + } } diff --git a/tests/ExampleTest.php b/tests/ExampleTest.php index 5d36321..8656ad3 100644 --- a/tests/ExampleTest.php +++ b/tests/ExampleTest.php @@ -1,5 +1,449 @@ toBeTrue(); +use Brick\Math\Exception\DivisionByZeroException; +use Brick\Math\Exception\NegativeNumberException; +use Brick\Math\RoundingMode; +use Flavorly\LaravelHelpers\Helpers\Math\Math; + +beforeEach(function () { + config()->set('helpers.math.scale', 2); + config()->set('helpers.math.storage_scale', 10); + config()->set('helpers.math.rounding_mode', RoundingMode::DOWN); +}); + +it('performs basic sum operations', function ($initial, $addend, $expected, $scale = null) { + $math = Math::of($initial, $scale); + $result = $math->sum($addend)->toFloat(); + expect($result)->toBe($expected); +})->with([ + 'integer addition' => [5, 3, 8.00], + 'float addition' => [5.5, 3.3, 8.80], + 'negative number addition' => [5, -3, 2.00], + 'string number addition' => ['5.5', '3.3', 8.80], + 'large number addition' => [1000000, 2000000, 3000000.00], + 'small number addition' => [0.001, 0.002, 0.00], // Rounds to 0.00 with default scale + 'custom scale addition' => [0.001, 0.002, 0.0030, 4], + 'different type addition' => [5, '3.3', 8.30], + 'zero addition' => [5, 0, 5.00], + 'addition resulting in negative' => [5, -10, -5.00], +]); + +it('can chain sum operations', function () { + $result = Math::of(5) + ->sum(3) + ->sum(2) + ->sum(1.5) + ->toString(); + + expect($result)->toBe('11.50'); +}); + +it('handles different scales and rounding modes correctly', function () { + // Test with HALF_DOWN (default) + $resultHalfDown = Math::of(1.23456) + ->scale(4) + ->roundDown() + ->sum(2.34567) + ->toFloat(); + expect($resultHalfDown)->toBe(3.5802); + + // Test with HALF_UP + $resultHalfUp = Math::of(1.23456) + ->scale(4) + ->roundUp() + ->sum(2.34567) + ->toFloat(); + expect($resultHalfUp)->toBe(3.5803); + + // Check the exact value before rounding + $resultExact = Math::of(1.23456) + ->scale(10) + ->sum(2.34567) + ->toString(); + expect($resultExact)->toBe('3.5802300000'); + + // Test rounding at different scales + $resultScale3 = Math::of(1.23456) + ->scale(3) + ->roundDown() + ->sum(2.34567) + ->toFloat(); + expect($resultScale3)->toBe(3.580); + + $resultScale5 = Math::of(1.23456) + ->scale(5) + ->roundDown() + ->sum(2.34567) + ->toFloat(); + expect($resultScale5)->toBe(3.58023); +}); + +it('handles edge cases correctly', function () { + // Testing numbers that are exactly at the rounding point + $resultEdge = Math::of(1.23455) + ->scale(4) + ->roundUp() + ->sum(2.34565) + ->toFloat(); + expect($resultEdge)->toBe(3.5802); + + // Testing negative numbers + $resultNegative = Math::of(-1.23456) + ->scale(4) + ->roundDown() + ->sum(-2.34567) + ->toFloat(); + expect($resultNegative)->toBe(-3.5802); +}); + +it('preserves immutability in sum operations', function () { + $initial = Math::of(5); + $result1 = $initial->sum(3); + $result2 = $initial->sum(2); + + expect($result1->toString())->toBe('8.00'); + expect($result2->toString())->toBe('7.00'); + expect($initial->toString())->toBe('5.00'); +}); + +it('handles very small numbers correctly', function () { + // Default scale (2) + $result1 = Math::of(0.001)->sum(0.002)->toString(); + expect($result1)->toBe('0.00'); + + // Scale 3 + $result2 = Math::of(0.001) + ->scale(3) + ->sum(0.002) + ->toString(); + expect($result2)->toBe('0.003'); + + // Scale 4 + $result3 = Math::of(0.0001) + ->scale(4) + ->sum(0.0002) + ->toString(); + expect($result3)->toBe('0.0003'); + + // Scale 5 with subtraction + $result4 = Math::of(0.00001) + ->scale(5) + ->subtract(0.00002) + ->toString(); + expect($result4)->toBe('-0.00001'); + + // Scale 6 with multiplication + $result5 = Math::of(0.000001) + ->scale(6) + ->multiply(1000) + ->toString(); + expect($result5)->toBe('0.001000'); + + // Scale 7 with division + $result6 = Math::of(0.0000001) + ->scale(7) + ->divide(0.1) + ->toString(); + expect($result6)->toBe('0.0000010'); + + // Handling very small numbers with rounding + $result7 = Math::of(0.0000001) + ->scale(6) + ->roundUp() + ->toString(); + expect($result7)->toBe('0.000001'); + + $result8 = Math::of(0.0000001) + ->scale(6) + ->roundDown() + ->toString(); + expect($result8)->toBe('0.000000'); + + $result9 = Math::of(0.0000005) + ->scale(6) + ->roundUp() + ->toString(); + expect($result9)->toBe('0.000001'); + + $result10 = Math::of(0.0000005) + ->scale(6) + ->roundDown() + ->toString(); + expect($result10)->toBe('0.000000'); + + // Testing the default HALF_DOWN behavior + $result11 = Math::of(0.0000005) + ->scale(6) + ->toString(); + expect($result11)->toBe('0.000000'); + + $result12 = Math::of(0.0000006) + ->scale(6) + ->toString(); + expect($result12)->toBe('0.000000'); +}); + +it('performs basic arithmetic operations correctly', function () { + // Subtraction + expect(Math::of(10)->subtract(3)->toFloat())->toBe(7.00); + expect(Math::of(5.5)->subtract(2.2)->toFloat())->toBe(3.30); + expect(Math::of(1)->subtract(1.1)->toFloat())->toBe(-0.10); + + // Multiplication + expect(Math::of(5)->multiply(3)->toFloat())->toBe(15.00); + expect(Math::of(2.5)->multiply(2.5)->toFloat())->toBe(6.25); + expect(Math::of(100)->multiply(0.1)->toFloat())->toBe(10.00); + + // Division + // Division with different scales + expect(Math::of(1)->scale(3)->divide(3)->toFloat())->toBe(0.333); + expect(Math::of(1)->scale(4)->divide(3)->toFloat())->toBe(0.3333); + expect(Math::of(1)->scale(5)->divide(3)->toFloat())->toBe(0.33333); + + // Division with rounding + expect(Math::of(1)->scale(2)->divide(3)->toFloat())->toBe(0.33); + expect(Math::of(1)->scale(2)->roundUp()->divide(3)->toFloat())->toBe(0.34); + + // More complex division scenarios + expect(Math::of(10)->scale(4)->divide(3)->toFloat())->toBe(3.3333); + expect(Math::of(2)->scale(3)->divide(3)->toFloat())->toBe(0.666); + + // Division resulting in repeating decimals + expect(Math::of(1)->scale(6)->divide(7)->toFloat())->toBe(0.142857); + expect(Math::of(1)->scale(8)->divide(6)->toFloat())->toBe(0.16666666); + + // Division by very small numbers + expect(Math::of(1)->scale(4)->divide(0.0001)->toFloat())->toBe(10000.0000); + + // Division of very small numbers + expect(Math::of(0.0001)->scale(8)->divide(0.0001)->toFloat())->toBe(1.00000000); + + // Power + expect(Math::of(2)->pow(3)->toFloat())->toBe(8.00); + expect(Math::of(3)->pow(2)->toFloat())->toBe(9.00); + expect(Math::of(10)->pow(0)->toFloat())->toBe(1.00); + + // Combining operations + $result = Math::of(10) + ->subtract(2) // 8 + ->multiply(3) // 24 + ->divide(4) // 6 + ->pow(2) // 36 + ->toFloat(); + + // Due to potential floating-point precision issues, let's use a delta + expect($result)->toBe(36.00, 2); + + // Alternatively, we can use toString() for exact comparison + expect(Math::of(10) + ->subtract(2) + ->multiply(3) + ->divide(4) + ->pow(2) + ->toString() + )->toBe('36.00'); + + // Operations with negative numbers + expect(Math::of(-5)->subtract(3)->toFloat())->toBe(-8.00); + expect(Math::of(-5)->multiply(-2)->toFloat())->toBe(10.00); + expect(Math::of(-10)->divide(2)->toFloat())->toBe(-5.00); + expect(Math::of(-2)->pow(3)->toFloat())->toBe(-8.00); + + // Operations with very small numbers + expect(Math::of(0.1)->subtract(0.09)->scale(3)->toFloat())->toBe(0.010); + expect(Math::of(0.1)->multiply(0.1)->scale(3)->toFloat())->toBe(0.010); + expect(Math::of(0.01)->scale(4)->divide(10)->toFloat())->toBe(0.0010); + + // Operations with very large numbers + expect(Math::of(1000000)->multiply(1000000)->toFloat())->toBe(1000000000000.00); + expect(Math::of(1000000000000)->divide(1000000)->toFloat())->toBe(1000000.00); +}); + +it('compares numbers correctly', function () { + // isLessThan + expect(Math::of(5)->isLessThan(10))->toBeTrue(); + expect(Math::of(10)->isLessThan(5))->toBeFalse(); + expect(Math::of(5)->isLessThan(5))->toBeFalse(); + + // isLessThanOrEqual + expect(Math::of(5)->isLessThanOrEqual(10))->toBeTrue(); + expect(Math::of(5)->isLessThanOrEqual(5))->toBeTrue(); + expect(Math::of(10)->isLessThanOrEqual(5))->toBeFalse(); + + // isGreaterThan + expect(Math::of(10)->isGreaterThan(5))->toBeTrue(); + expect(Math::of(5)->isGreaterThan(10))->toBeFalse(); + expect(Math::of(5)->isGreaterThan(5))->toBeFalse(); + + // isGreaterThanOrEqual + expect(Math::of(10)->isGreaterThanOrEqual(5))->toBeTrue(); + expect(Math::of(5)->isGreaterThanOrEqual(5))->toBeTrue(); + expect(Math::of(5)->isGreaterThanOrEqual(10))->toBeFalse(); + + // isEqual + expect(Math::of(5)->isEqual(5))->toBeTrue(); + expect(Math::of(5)->isEqual(10))->toBeFalse(); + + // Comparing with different types + expect(Math::of(5)->isEqual('5'))->toBeTrue(); + expect(Math::of(5.0)->isEqual(5))->toBeTrue(); + + // Comparing with small differences + expect(Math::of(0.1 + 0.2)->isEqual(0.3))->toBeTrue(); + + // Comparing with different scales + expect(Math::of(1)->scale(2)->isEqual(1.00))->toBeTrue(); + expect(Math::of(1)->scale(2)->isEqual(1.001))->toBeFalse(); + + // Comparing negative numbers + expect(Math::of(-5)->isLessThan(-3))->toBeTrue(); + expect(Math::of(-3)->isGreaterThan(-5))->toBeTrue(); + + expect(Math::of(10.0313131)->scale(10)->isLessThan(9.0313131))->toBeFalse(); +}); + +it('performs utility operations correctly', function () { + expect(Math::of(-5)->absolute()->toFloat())->toBe(5.0); + expect(Math::of(3)->negative()->toFloat())->toBe(-3.0); + expect(Math::of(3.7)->ceil()->toFloat())->toBe(4.0); + expect(Math::of(3.2)->floor()->toFloat())->toBe(3.0); + expect(Math::of('3.14159')->round(2)->toString())->toBe('3.14'); +}); + +it('handles percentage operations correctly', function () { + $math = Math::of(100); + + // Testing addition of percentage + $newMath = $math->addPercentage(50); // Adding 50% + expect($newMath->toFloat())->toBe(150.0); + + // Testing subtraction of percentage + $newMath = $math->subtractPercentage(50); // Subtracting 50% + expect($newMath->toFloat())->toBe(50.0); +}); + +it('can chain multiple different operations', function () { + $result = Math::of(100) + ->addPercentage(10) // 110 + ->multiply(2) // 220 + ->subtract(20) // 200 + ->divide(2) // 100 + ->sum(50) // 150 + ->roundUp() // Should round up here if needed + ->toFloat(); + + expect($result)->toBe(150.0); +}); + +it('handles errors correctly', function () { + $this->expectException(DivisionByZeroException::class); + + Math::of(100)->divide(0)->toFloat(); + + $this->expectException(TypeError::class); + + Math::of('invalid')->sum('oops'); + + $this->expectException(NegativeNumberException::class); + + Math::of(-100)->absolute()->negative()->toInt(); +}); + +it('maintains precision with very large numbers', function () { + $largeNumber = Math::of('999999999999999999999999999999')->sum('1')->scale(0); + expect($largeNumber->toString())->toBe('1000000000000000000000000000000'); + + $multiplied = $largeNumber->multiply('100000000000000000000')->scale(0); + expect($multiplied->toString())->toBe('100000000000000000000000000000000000000000000000000'); +}); + +it('converts to different formats correctly', function () { + $math = Math::of(1234.5678); + + expect($math->toInt())->toBe(1234); + expect($math->toFloat())->toBe(1234.56); + expect($math->toString())->toBe('1234.56'); +}); + +it('allows changing scale and rounding mode mid-calculation', function () { + $math = Math::of(100.123456) + ->scale(5) // Now scale is set first + ->multiply(2) + ->roundingMode(RoundingMode::UP) // Rounding after multiplication + ->sum(0.00002); // Tiny sum here + + expect($math->toString())->toBe('200.24694'); // Adjusted expected outcome +}); + +it('handles operations with mixed scales correctly', function () { + $num1 = Math::of('123.456', 3); // Scale 3 + $num2 = Math::of('0.7891', 4); // Scale 4 + + $result = $num1->sum($num2); + expect($result->toString())->toBe('124.245'); // Expecting concatenated scale 4 +}); + +it('can convert to storage scale', function () { + $storage_scale = 10; + $storage_value = Math::of(100.123456)->storageScale($storage_scale)->toStorageScale(); + $decode_value = Math::of($storage_value)->storageScale($storage_scale)->fromStorage()->toFloat(); + + expect($decode_value)->toBe(100.12) + ->and($storage_value)->toBe(1001200000000); +}); + +it('give the percentage of the number', function () { + expect(Math::of(100)->toPercentageOf(50)->toFloat())->toBe(50.0); + expect(Math::of(100)->toPercentageOf(30)->toFloat())->toBe(30.0); + expect(Math::of(123.45)->toPercentageOf(50)->toFloat())->toBe(61.72); + expect(Math::of(99.99)->toPercentageOf(10)->toFloat())->toBe(9.99); +}); + +it('calculates percentage difference correctly', function () { + + expect(Math::of(100)->differenceInPercentage(50))->toBe(50.0); + expect(Math::of(50)->differenceInPercentage(100))->toBe(100.0); + + expect(Math::of(100.5)->differenceInPercentage(50.1))->toBe(50.14); + expect(Math::of(50.1)->differenceInPercentage(100.5))->toBe(100.59); +}); + +test('average calculation', function () { + // Integers + expect(Math::average(2, 3, 4, 5)->toFloat())->toBe(3.5); + expect(Math::average(0, 100)->toFloat())->toBe(50.0); + + // Floats + expect(Math::average(2.5, 3.5)->toFloat())->toBe(3.0); + expect(Math::average(3.33, 3.33)->toFloat())->toBe(3.33); +}); + +test('percentage of calculation', function () { + // Integers + expect(Math::of(50)->percentageOf(100))->toBe(50.00); + expect(Math::of(25)->percentageOf(100))->toBe(25.00); + + // Floats + expect(Math::of(1)->percentageOf(3))->toBe(33.33); + expect(Math::of(2)->percentageOf(3))->toBe(66.66); +}); + +it('respects config scale and rounding mode', function () { + // Mock config values + config(['helpers.math.scale' => 4]); + config(['helpers.math.rounding_mode' => RoundingMode::UP]); + + $math = Math::of(1.23456789); + + expect($math->toFloat())->toBe(1.2346); + expect($math->roundingMode)->toBe(RoundingMode::UP); + + // Reset config to default + config(['helpers.math.scale' => 2]); + config(['helpers.math.rounding_mode' => RoundingMode::DOWN]); + + $defaultMath = Math::of(1.23456789); + + expect($defaultMath->toFloat())->toBe(1.23); + expect($defaultMath->roundingMode)->toBe(RoundingMode::DOWN); });