self::MAX_THOUSANDTHS)) { throw new InvalidArgumentException('Количество позиции чека вне границ кассы.'); } $this->thousandths = $thousandths; } public static function pieces(int $pieces): self { return new self($pieces * self::SCALE); } public static function fromThousandths(int $thousandths): self { return new self($thousandths); } /** * Количество из корзины заказа (`num`): число или числовая строка, округляется до тысячных. * Ноль, отрицательное, не число или больше потолка кассы — null. * * @param mixed $raw */ public static function fromCart($raw): ?self { if (is_string($raw)) { $raw = trim($raw); } if (!is_int($raw) && !is_float($raw) && !(is_string($raw) && is_numeric($raw))) { return null; } $thousandths = (int) round((float) $raw * self::SCALE); if (($thousandths < 1) || ($thousandths > self::MAX_THOUSANDTHS)) { return null; } return new self($thousandths); } public function getThousandths(): int { return $this->thousandths; } /** * Целое ли количество: только такую строку можно резать на две с разницей цен в копейку. */ public function isWhole(): bool { return $this->thousandths % self::SCALE === 0; } /** * Целое число штук; только для {@see self::isWhole()}. */ public function getPieces(): int { return intdiv($this->thousandths, self::SCALE); } /** * Десятичная запись без лишних нулей: `2`, `2.5`, `0.125`. */ public function toDecimal(): string { if ($this->isWhole()) { return (string) $this->getPieces(); } return rtrim( sprintf('%d.%03d', intdiv($this->thousandths, self::SCALE), $this->thousandths % self::SCALE), '0', ); } /** * Сумма позиции: цена x количество, округление до копейки (половина — вверх). */ public function totalOf(Money $price): Money { return Money::fromMinorUnits( intdiv(($price->toMinorUnits() * $this->thousandths) + intdiv(self::SCALE, 2), self::SCALE), ); } }