intdiv(PHP_INT_MAX - (int) $fraction, 100)) { throw new \InvalidArgumentException('balance_decimal_overflow'); } return new self(AssetType::BALANCE, ($whole * 100) + (int) $fraction); } public static function balance_minor(string $decimal): int { return self::balance($decimal)->minor(); } public static function decimal_from_minor(int $minor): string { return self::from_minor(AssetType::BALANCE, $minor)->decimal(); } /** * Apply an integer percentage using half-up rounding without floats. */ public static function percentage(int $minor, int $percent): int { if ($minor < 0) { throw new \InvalidArgumentException('asset_minor_must_be_non_negative'); } if ($percent < 0 || $percent > 100) { throw new \InvalidArgumentException('asset_percentage_invalid'); } $whole = intdiv($minor, 100) * $percent; $remainder = ($minor % 100) * $percent; return $whole + intdiv($remainder + 50, 100); } public static function from_minor(AssetType|string $asset, int $minor): self { if ($minor < 0) { throw new \InvalidArgumentException('asset_minor_must_be_non_negative'); } return new self(AssetType::from_value($asset), $minor); } public function asset(): AssetType { return $this->asset; } public function minor(): int { return $this->minor; } public function decimal(): string { if ($this->asset === AssetType::POINTS) { return (string) $this->minor; } return sprintf('%d.%02d', intdiv($this->minor, 100), $this->minor % 100); } public function formatted(): string { if ($this->asset === AssetType::POINTS) { return number_format_i18n($this->minor); } global $wp_locale; $decimal_point = isset($wp_locale->number_format['decimal_point']) ? $wp_locale->number_format['decimal_point'] : '.'; return number_format_i18n(intdiv($this->minor, 100)) . $decimal_point . sprintf('%02d', $this->minor % 100); } }