Add HL3D branding, Etsy Live/Draft paging and Projects lifecycle
Deploy HickLab3D Dashboard / Deploy to production (push) Failing after 23s
Deploy HickLab3D Dashboard / Deploy to production (push) Failing after 23s
Replace the old touchmark/PNG identity with the supplied lockup and monogram, segmented role-aware navigation, Live/Draft listing filters with generation-based cache invalidation, and admin Projects covering immutable file revisions, listing live pointers, and exact-decimal filament/packaging/shipping costing. Additive schema only; no Etsy publication. Isolated clone tests: 301 tests, 1162 assertions.
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use OverflowException;
|
||||
|
||||
/**
|
||||
* Exact decimal fixed-point arithmetic using bounded native integers.
|
||||
*
|
||||
* Neither the bcmath nor gmp extensions are available in this application's
|
||||
* runtime, so costing math is done by scaling decimal strings into integers
|
||||
* (never floats) and checking every operation for overflow against the
|
||||
* platform's native integer range.
|
||||
*/
|
||||
class FixedPoint
|
||||
{
|
||||
private const MAX_INT = PHP_INT_MAX;
|
||||
|
||||
private const MIN_INT = PHP_INT_MIN;
|
||||
|
||||
/**
|
||||
* Parse a decimal string (or int/numeric string) into an integer scaled by 10^$scale.
|
||||
*/
|
||||
public static function toScaled(string $decimal, int $scale, ?int $maxAbs = null, bool $allowNegative = true): int
|
||||
{
|
||||
$decimal = trim($decimal);
|
||||
if ($decimal === '' || ! preg_match('/^-?\d+(\.\d+)?$/', $decimal)) {
|
||||
throw new InvalidArgumentException("Invalid decimal value: {$decimal}");
|
||||
}
|
||||
|
||||
$negative = $decimal[0] === '-';
|
||||
if ($negative) {
|
||||
if (! $allowNegative) {
|
||||
throw new InvalidArgumentException("Negative values are not allowed: {$decimal}");
|
||||
}
|
||||
$decimal = substr($decimal, 1);
|
||||
}
|
||||
|
||||
[$whole, $fraction] = array_pad(explode('.', $decimal, 2), 2, '');
|
||||
if (strlen($fraction) > $scale) {
|
||||
throw new InvalidArgumentException("Value has more than {$scale} decimal places: {$decimal}");
|
||||
}
|
||||
$fraction = str_pad($fraction, $scale, '0');
|
||||
$whole = $whole === '' ? '0' : $whole;
|
||||
|
||||
$combined = ltrim($whole.$fraction, '0');
|
||||
$combined = $combined === '' ? '0' : $combined;
|
||||
|
||||
if (strlen($combined) > 18) {
|
||||
throw new InvalidArgumentException("Value out of bounds: {$decimal}");
|
||||
}
|
||||
|
||||
$value = (int) $combined;
|
||||
if ($value < 0 || $combined !== (string) $value) {
|
||||
throw new InvalidArgumentException("Value out of bounds: {$decimal}");
|
||||
}
|
||||
|
||||
if ($maxAbs !== null && $value > $maxAbs * (10 ** $scale)) {
|
||||
throw new InvalidArgumentException("Value exceeds the allowed maximum: {$decimal}");
|
||||
}
|
||||
|
||||
return $negative ? -$value : $value;
|
||||
}
|
||||
|
||||
public static function add(int $a, int $b): int
|
||||
{
|
||||
if (($b > 0 && $a > self::MAX_INT - $b) || ($b < 0 && $a < self::MIN_INT - $b)) {
|
||||
throw new OverflowException('Fixed-point addition overflowed.');
|
||||
}
|
||||
|
||||
return $a + $b;
|
||||
}
|
||||
|
||||
public static function subtract(int $a, int $b): int
|
||||
{
|
||||
return self::add($a, -$b);
|
||||
}
|
||||
|
||||
public static function multiply(int $a, int $b): int
|
||||
{
|
||||
if ($a === 0 || $b === 0) {
|
||||
return 0;
|
||||
}
|
||||
$result = $a * $b;
|
||||
if (! is_int($result) || intdiv($result, $b) !== $a) {
|
||||
throw new OverflowException('Fixed-point multiplication overflowed.');
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Divide a scaled value by a plain positive integer (e.g. batch units), rounding half away from zero.
|
||||
*/
|
||||
public static function divideByInt(int $value, int $divisor): int
|
||||
{
|
||||
if ($divisor === 0) {
|
||||
throw new InvalidArgumentException('Cannot divide by zero.');
|
||||
}
|
||||
|
||||
return self::roundDivide($value, $divisor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rescale a value from one decimal scale to another, rounding half away from zero.
|
||||
*/
|
||||
public static function rescale(int $value, int $fromScale, int $toScale): int
|
||||
{
|
||||
if ($toScale === $fromScale) {
|
||||
return $value;
|
||||
}
|
||||
if ($toScale > $fromScale) {
|
||||
$factor = 10 ** ($toScale - $fromScale);
|
||||
$result = $value * $factor;
|
||||
if ($factor !== 0 && intdiv($result, $factor) !== $value) {
|
||||
throw new OverflowException('Fixed-point rescale overflowed.');
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
return self::roundDivide($value, 10 ** ($fromScale - $toScale));
|
||||
}
|
||||
|
||||
public static function format(int $scaled, int $scale): string
|
||||
{
|
||||
$negative = $scaled < 0;
|
||||
$abs = (string) abs($scaled);
|
||||
$abs = str_pad($abs, $scale + 1, '0', STR_PAD_LEFT);
|
||||
$whole = substr($abs, 0, strlen($abs) - $scale);
|
||||
$fraction = $scale > 0 ? substr($abs, strlen($abs) - $scale) : '';
|
||||
|
||||
$formatted = $scale > 0 ? "{$whole}.{$fraction}" : $whole;
|
||||
|
||||
return ($negative && $scaled !== 0 ? '-' : '').$formatted;
|
||||
}
|
||||
|
||||
private static function roundDivide(int $value, int $divisor): int
|
||||
{
|
||||
$sign = ($value < 0) !== ($divisor < 0) ? -1 : 1;
|
||||
$numerator = abs($value);
|
||||
$denominator = abs($divisor);
|
||||
$quotient = intdiv($numerator, $denominator);
|
||||
$remainder = $numerator % $denominator;
|
||||
|
||||
if ($remainder * 2 >= $denominator) {
|
||||
$quotient++;
|
||||
}
|
||||
|
||||
return $sign * $quotient;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user