From b62cb773c701ab441351d7ec4cfc8643455efef8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Thu, 28 Nov 2024 23:56:41 +0100 Subject: [PATCH] Performance optimizations --- src/Sqids.php | 103 +++++++++++++++++++++++++------------------------- 1 file changed, 52 insertions(+), 51 deletions(-) diff --git a/src/Sqids.php b/src/Sqids.php index 3ad16ab..d3c5a71 100644 --- a/src/Sqids.php +++ b/src/Sqids.php @@ -17,6 +17,12 @@ use InvalidArgumentException; use RuntimeException; +use function strlen; +use function ord; +use function in_array; +use function count; +use function array_key_exists; + class Sqids implements SqidsInterface { final public const DEFAULT_ALPHABET = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; @@ -598,24 +604,21 @@ public function __construct( $alphabet = self::DEFAULT_ALPHABET; } - if (mb_strlen($alphabet) != strlen($alphabet)) { + $alphabetLength = strlen($alphabet); + if (mb_strlen($alphabet) != $alphabetLength) { throw new InvalidArgumentException('Alphabet cannot contain multibyte characters'); } - if (strlen($alphabet) < 3) { + if ($alphabetLength < 3) { throw new InvalidArgumentException('Alphabet length must be at least 3'); } - if (count(array_unique(str_split($alphabet))) !== strlen($alphabet)) { + if (preg_match('/(.).*\1/', $alphabet)) { throw new InvalidArgumentException('Alphabet must contain unique characters'); } $minLengthLimit = 255; - if ( - !is_int($minLength) || - $minLength < 0 || - $minLength > $minLengthLimit - ) { + if ($minLength < 0 || $minLength > $minLengthLimit) { throw new InvalidArgumentException( 'Minimum length has to be between 0 and ' . $minLengthLimit, ); @@ -635,7 +638,6 @@ public function __construct( } $this->alphabet = $this->shuffle($alphabet); - $this->minLength = $minLength; $this->blocklist = $filteredBlocklist; } @@ -655,11 +657,12 @@ public function encode(array $numbers): string return ''; } - $inRangeNumbers = array_filter($numbers, fn($n) => $n >= 0 && $n <= self::maxValue()); - if (count($inRangeNumbers) != count($numbers)) { - throw new InvalidArgumentException( - 'Encoding supports numbers between 0 and ' . self::maxValue(), - ); + foreach ($numbers as $n) { + if ($n < 0 || $n > self::maxValue()) { + throw new InvalidArgumentException( + 'Encoding supports numbers between 0 and ' . self::maxValue(), + ); + } } return $this->encodeNumbers($numbers); @@ -674,40 +677,39 @@ public function encode(array $numbers): string */ protected function encodeNumbers(array $numbers, int $increment = 0): string { - if ($increment > strlen($this->alphabet)) { + $length = strlen($this->alphabet); + if ($increment > $length) { throw new InvalidArgumentException('Reached max attempts to re-generate the ID'); } $offset = count($numbers); foreach ($numbers as $i => $v) { - $offset += ord($this->alphabet[$v % strlen($this->alphabet)]) + $i; + $offset += ord($this->alphabet[$v % $length]) + $i; } - $offset %= strlen($this->alphabet); - $offset = ($offset + $increment) % strlen($this->alphabet); + $offset %= $length; + $offset = ($offset + $increment) % $length; $alphabet = substr($this->alphabet, $offset) . substr($this->alphabet, 0, $offset); $prefix = $alphabet[0]; $alphabet = strrev($alphabet); - $ret = [$prefix]; + $id = $prefix; for ($i = 0; $i != count($numbers); $i++) { $num = $numbers[$i]; - $ret[] = $this->toId($num, substr($alphabet, 1)); + $id .= $this->toId($num, substr($alphabet, 1)); if ($i < count($numbers) - 1) { - $ret[] = $alphabet[0]; + $id .= $alphabet[0]; $alphabet = $this->shuffle($alphabet); } } - $id = implode('', $ret); - if ($this->minLength > strlen($id)) { $id .= $alphabet[0]; while ($this->minLength - strlen($id) > 0) { $alphabet = $this->shuffle($alphabet); - $id .= substr($alphabet, 0, min($this->minLength - strlen($id), strlen($alphabet))); + $id .= substr($alphabet, 0, min($this->minLength - strlen($id), $length)); } } @@ -736,11 +738,8 @@ public function decode(string $id): array return $ret; } - $alphabetChars = str_split($this->alphabet); - foreach (str_split($id) as $c) { - if (!in_array($c, $alphabetChars)) { - return $ret; - } + if (!preg_match('/^[' . preg_quote($this->alphabet, '/') . ']+$/', $id)) { + return $ret; } $prefix = $id[0]; @@ -753,13 +752,13 @@ public function decode(string $id): array $separator = $alphabet[0]; $chunks = explode($separator, $id, 2); - if (!empty($chunks)) { + if (array_key_exists(0, $chunks)) { if ($chunks[0] == '') { return $ret; } $ret[] = $this->toNumber($chunks[0], substr($alphabet, 1)); - if (count($chunks) > 1) { + if (array_key_exists(1, $chunks)) { $alphabet = $this->shuffle($alphabet); } } @@ -772,40 +771,42 @@ public function decode(string $id): array protected function shuffle(string $alphabet): string { - $chars = str_split($alphabet); + $length = strlen($alphabet); - for ($i = 0, $j = count($chars) - 1; $j > 0; $i++, $j--) { - $r = ($i * $j + ord($chars[$i]) + ord($chars[$j])) % count($chars); - [$chars[$i], $chars[$r]] = [$chars[$r], $chars[$i]]; + for ($i = 0, $j = $length - 1; $j > 0; $i++, $j--) { + $r = ($i * $j + ord($alphabet[$i]) + ord($alphabet[$j])) % $length; + [$alphabet[$i], $alphabet[$r]] = [$alphabet[$r], $alphabet[$i]]; } - return implode('', $chars); + return $alphabet; } protected function toId(int $num, string $alphabet): string { - $id = []; - $chars = str_split($alphabet); - - $result = $num; - + $id = ''; + $length = strlen($alphabet); do { - array_unshift($id, $chars[$this->math->intval($this->math->mod($result, count($chars)))]); - $result = $this->math->divide($result, count($chars)); - } while ($this->math->greaterThan($result, 0)); + $id = $alphabet[$this->math->intval($this->math->mod($num, $length))] . $id; + $num = $this->math->divide($num, $length); + } while ($this->math->greaterThan($num, 0)); - return implode('', $id); + return $id; } protected function toNumber(string $id, string $alphabet): int { - $chars = str_split($alphabet); - return $this->math->intval(array_reduce(str_split($id), function ($a, $v) use ($chars) { - $number = $this->math->multiply($a, count($chars)); - $number = $this->math->add($number, array_search($v, $chars)); + $length = strlen($alphabet); + $max = strlen($id); + + $number = 0; + for ($i = 0; $i < $max; $i++) { + $number = $this->math->add( + $this->math->multiply($number, $length), + strpos($alphabet, $id[$i]), + ); + } - return $number; - }, 0)); + return $this->math->intval($number); } protected function isBlockedId(string $id): bool