Skip to content

Commit

Permalink
Performance optimizations
Browse files Browse the repository at this point in the history
  • Loading branch information
GromNaN committed Nov 28, 2024
1 parent 417e365 commit 6331ebb
Showing 1 changed file with 52 additions and 51 deletions.
103 changes: 52 additions & 51 deletions src/Sqids.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
);
Expand All @@ -635,7 +638,6 @@ public function __construct(
}

$this->alphabet = $this->shuffle($alphabet);
$this->minLength = $minLength;
$this->blocklist = $filteredBlocklist;
}

Expand All @@ -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);
Expand All @@ -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));
}
}

Expand Down Expand Up @@ -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];
Expand All @@ -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);
}
}
Expand All @@ -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
Expand Down

0 comments on commit 6331ebb

Please sign in to comment.