Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Performance optimizations #18

Merged
merged 1 commit into from
Dec 29, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 39 additions & 46 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 @@ -606,16 +612,12 @@ public function __construct(
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) ||
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Type is already validated in the arg type.

$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 +637,6 @@ public function __construct(
}

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

Expand All @@ -655,11 +656,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(),
);
Comment on lines -658 to -662
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of creating a new array, the exception is thrown directly when there is an invalid number.

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 Down Expand Up @@ -688,26 +690,24 @@ protected function encodeNumbers(array $numbers, int $increment = 0): string
$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), strlen($this->alphabet)));
}
}

Expand Down Expand Up @@ -736,11 +736,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;
}
Comment on lines -739 to -743
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This split operation is replaced by a more efficient regex.

if (!preg_match('/^[' . preg_quote($this->alphabet, '/') . ']+$/', $id)) {
return $ret;
}

$prefix = $id[0];
Expand All @@ -753,13 +750,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 +769,36 @@ public function decode(string $id): array

protected function shuffle(string $alphabet): string
{
$chars = str_split($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 = strlen($alphabet) - 1; $j > 0; $i++, $j--) {
$r = ($i * $j + ord($alphabet[$i]) + ord($alphabet[$j])) % strlen($alphabet);
[$alphabet[$i], $alphabet[$r]] = [$alphabet[$r], $alphabet[$i]];
Comment on lines +772 to +774
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Manipulation of individual characters of the string, instead of using an array of chars.

}

return implode('', $chars);
return $alphabet;
}

protected function toId(int $num, string $alphabet): string
{
$id = [];
$chars = str_split($alphabet);

$result = $num;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reuse and modify the variable $num instead of creating a new one.


$id = '';
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, strlen($alphabet)))] . $id;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Appending to the end of the string is the same as using array_unshift on an array.

$num = $this->math->divide($num, strlen($alphabet));
} 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));
$number = 0;
for ($i = 0; $i < strlen($id); $i++) {
$number = $this->math->add(
$this->math->multiply($number, strlen($alphabet)),
strpos($alphabet, $id[$i]),
);
}

return $number;
}, 0));
return $this->math->intval($number);
}

protected function isBlockedId(string $id): bool
Expand Down
Loading