19declare(strict_types=1);
23use InvalidArgumentException;
59 private const array
EXTRA_SAMPLES = [1000, 1001, 1011, 100000, 1000000, 1000001, 2000000];
69 [
'<',
'<=',
'>',
'>='],
80 private readonly
int $count,
81 private readonly
string $formula,
82 private readonly array $tree
92 if (isset(self::$parsed[$header])) {
93 return self::$parsed[$header];
96 if (strlen($header) > self::MAX_FORMULA_LENGTH + 64) {
97 throw new InvalidArgumentException(
'The Plural-Forms header is too long.');
99 if (preg_match(
'/\A\s*+nplurals\s*+=\s*+(\d{1,2}+)\s*+;\s*+plural\s*+=([^;]++);?\s*+\z/', $header, $matches) !== 1) {
100 throw new InvalidArgumentException(
'The Plural-Forms header does not have the form "nplurals=<count>; plural=<expression>;".');
102 $count = (
int) $matches[1];
103 $formula = trim($matches[2]);
104 if ($count < 1 || $count > self::MAX_COUNT) {
105 throw new InvalidArgumentException(sprintf(
'nplurals must be between 1 and %d.', self::MAX_COUNT));
107 if (strlen($formula) > self::MAX_FORMULA_LENGTH) {
108 throw new InvalidArgumentException(
'The plural expression is too long.');
111 $forms =
new self($count, $formula,
self::parse($formula));
112 foreach ([...range(0, self::SAMPLED_UP_TO), ...self::EXTRA_SAMPLES] as $n) {
113 $index = $forms->evaluate($n);
114 if ($index ===
null || $index < 0 || $index >= $count) {
115 throw new InvalidArgumentException(sprintf(
116 'The plural expression yields no valid form for n = %d (nplurals=%d).',
122 if (count(self::$parsed) >= 64) {
126 return self::$parsed[$header] = $forms;
137 if ($header !==
null && trim($header) !==
'') {
140 }
catch (InvalidArgumentException
$e) {
141 $message = sprintf(
'Invalid Plural-Forms header "%s" (%s) - using "%s".', $header,
$e->getMessage(), self::GERMANIC_HEADER);
144 $message = sprintf(
'Missing Plural-Forms header - using "%s".', self::GERMANIC_HEADER);
146 if ($on_invalid !==
null) {
165 return sprintf(
'nplurals=%d; plural=%s;', $this->count, $this->formula);
174 $n = $n === PHP_INT_MIN ? PHP_INT_MAX : abs($n);
177 return $index !==
null && $index >= 0 && $index < $this->count ? $index : $this->
defaultFormIndex();
191 return max(0, $count - 1);
208 for ($form = self::defaultFormIndexForCount($count); $form >= 0; $form--) {
209 $value = (string) ($forms[$form] ??
'');
224 if ($other->count !== $this->count) {
227 foreach ([...range(0, self::SAMPLED_UP_TO), ...self::EXTRA_SAMPLES] as $n) {
228 if ($other->
formIndexFor($n) !== $this->formIndexFor($n)) {
245 $previous_forms = array_fill(0, $this->count, []);
246 foreach ([...range(0, self::SAMPLED_UP_TO), ...self::EXTRA_SAMPLES] as $n) {
250 return array_map(
static function (array $forms): array {
251 $forms = array_keys($forms);
262 if ($this->count !== 2) {
265 foreach ([...range(0, self::SAMPLED_UP_TO), ...self::EXTRA_SAMPLES] as $n) {
278 }
catch (\ArithmeticError) {
308 return match ($node[0]) {
309 '==' => (
int) ($left === $right),
310 '!=' => (
int) ($left !== $right),
311 '<' => (
int) ($left < $right),
312 '<=' => (
int) ($left <= $right),
313 '>' => (
int) ($left > $right),
314 '>=' => (
int) ($left >= $right),
316 '%' => $left % $right,
318 default =>
throw new \LogicException(sprintf(
'Unknown node "%s" of a plural expression.', (
string) $node[0])),
326 private static function parse(
string $formula): array
331 if ($position !== count($tokens)) {
332 throw new InvalidArgumentException(sprintf(
'Unexpected "%s" in the plural expression.', $tokens[$position]));
342 private static function tokenize(
string $formula): array
344 $matched = preg_match_all(
345 '/\s*(?:(\d+)|(n)|(\|\||&&|==|!=|<=|>=|[<>!?:%()]))\s*/A',
352 foreach ($matches ?: [] as $match) {
353 $consumed += strlen($match[0]);
355 if (ctype_digit(
$token) && strlen(ltrim(
$token,
'0')) > self::MAX_INTEGER_DIGITS) {
356 throw new InvalidArgumentException(
'An integer of the plural expression is too large.');
360 if ($matched ===
false || $consumed !== strlen($formula) || $tokens === []) {
361 throw new InvalidArgumentException(
'The plural expression contains something other than n, integers, ?:, ||, &&, !, comparisons, % and parentheses.');
373 private static function parseTernary(array $tokens,
int &$position,
int $depth): array
377 if (($tokens[$position] ??
null) !==
'?') {
385 return [
'?', $condition, $then, $else];
392 private static function parseBinary(array $tokens,
int &$position,
int $depth,
int $level): array
395 if ($level === count(self::BINARY_LEVELS)) {
399 while (in_array($tokens[$position] ??
null, self::BINARY_LEVELS[$level],
true)) {
400 $operator = $tokens[$position++];
401 $left = [$operator, $left,
self::parseBinary($tokens, $position, $depth, $level + 1)];
413 private static function parseUnary(array $tokens,
int &$position,
int $depth): array
416 $token = $tokens[$position] ??
null;
418 throw new InvalidArgumentException(
'The plural expression ends unexpectedly.');
427 if (ctype_digit(
$token)) {
436 throw new InvalidArgumentException(sprintf(
'Unexpected "%s" in the plural expression.',
$token));
442 private static function expect(array $tokens,
int &$position,
string $token): void
444 if (($tokens[$position] ??
null) !==
$token) {
445 throw new InvalidArgumentException(sprintf(
'"%s" expected in the plural expression.',
$token));
452 if ($depth > self::MAX_DEPTH) {
453 throw new InvalidArgumentException(
'The plural expression is nested too deeply.');