value = $value; } /** * Строгий разбор десятичной строки; всё, что не `^\d+\.\d{2}$`, отклоняется. */ public static function fromDecimal(string $value): self { if (preg_match(self::PATTERN, $value) !== 1) { throw new InvalidArgumentException('Сумма должна быть десятичной строкой с двумя знаками после точки.'); } $money = new self($value); if ($money->toMinorUnits() > (new self(self::MAX))->toMinorUnits()) { throw new InvalidArgumentException('Сумма превышает допустимый максимум.'); } return $money; } /** * Сумма в копейках. */ public static function fromMinorUnits(int $minorUnits): self { if ($minorUnits < 0) { throw new InvalidArgumentException('Сумма не может быть отрицательной.'); } return self::fromDecimal(sprintf('%d.%02d', intdiv($minorUnits, 100), $minorUnits % 100)); } /** * Граница с хостом: округление до копеек делается здесь один раз, дальше `float` не используется. */ public static function fromFloat(float $amount): self { if (($amount < 0) || !is_finite($amount)) { throw new InvalidArgumentException('Сумма хоста должна быть конечным неотрицательным числом.'); } return self::fromDecimal(number_format($amount, 2, '.', '')); } public function toDecimal(): string { return $this->value; } public function toMinorUnits(): int { [$units, $cents] = explode('.', $this->value); return ((int) $units * 100) + (int) $cents; } public function isPositive(): bool { return ($this->value !== '0.00'); } public function equals(self $other): bool { return ($this->value === $other->value); } public function add(self $other): self { return self::fromMinorUnits($this->toMinorUnits() + $other->toMinorUnits()); } public function multiply(int $quantity): self { if ($quantity < 0) { throw new InvalidArgumentException('Количество не может быть отрицательным.'); } return self::fromMinorUnits($this->toMinorUnits() * $quantity); } public function __toString(): string { return $this->value; } }