<commit_before><?php

declare(strict_types=1);

namespace Keiko\Uuid\Shortener;

use Brick\Math\BigInteger;
use Brick\Math\RoundingMode;
use Keiko\Uuid\Shortener\Number\BigInt\Converter;
use Keiko\Uuid\Shortener\Number\BigInt\ConverterInterface;
use function extension_loaded;

/** @psalm-immutable */
class Shortener
{
    /**
     * @var Dictionary
     */
    private $dictionary;

    /**
     * @var ConverterInterface
     */
    private $converter;

    /**
     * @param Dictionary         $dictionary
     * @param ConverterInterface $converter
     */
    public function __construct(Dictionary $dictionary, ConverterInterface $converter)
    {
        $this->dictionary = $dictionary;
        $this->converter = $converter;
    }

    public static function make(Dictionary $dictionary) : self
    {
        if (extension_loaded('gmp')) {
            return new GMPShortener($dictionary);
        }

        return new self($dictionary, new Converter());
    }

    public function reduce(string $uuid): string
    {
        $dictionaryLength = $this->dictionary->getLength();
        $uuidInt = $this->converter->fromHex($uuid);
        $output = '';

        while ($uuidInt->isPositive()) {
            $previousNumber = clone $uuidInt;
            $uuidInt = $uuidInt->dividedBy($dictionaryLength, RoundingMode::DOWN);
            $digit = $previousNumber->mod($dictionaryLength);
            $output .= $this->dictionary->getCharAt($digit->toInt());
        }

        return $output;
    }

    public function expand(string $shortUuid): string
    {
        $number = BigInteger::zero();
        foreach (str_split(strrev($shortUuid)) as $char) {
            $number = $number
                ->multipliedBy($this->dictionary->getLength())
                ->plus($this->dictionary->getCharIndex($char));
        }

        return $this->formatHex($this->converter->toHex($number));
    }

    private function formatHex(string $hex): string
    {
        $hex = str_pad($hex, 32, '0', \STR_PAD_LEFT);
        preg_match('/([a-f0-9]{8})([a-f0-9]{4})([a-f0-9]{4})([a-f0-9]{4})([a-f0-9]{12})/', $hex, $matches);
        array_shift($matches);

        return implode('-', $matches);
    }
}<commit_msg>Improved performance of shortening/expanding with the default shortener by applying micro-optimizations<commit_after><?php

declare(strict_types=1);

namespace Keiko\Uuid\Shortener;

use Brick\Math\BigInteger;
use Brick\Math\RoundingMode;
use Keiko\Uuid\Shortener\Number\BigInt\Converter;
use Keiko\Uuid\Shortener\Number\BigInt\ConverterInterface;
use function extension_loaded;
use function str_pad;
use function str_split;
use function strrev;
use function substr;

/** @psalm-immutable */
class Shortener
{
    /**
     * @var Dictionary
     */
    private $dictionary;

    /**
     * @var ConverterInterface
     */
    private $converter;

    /**
     * @param Dictionary         $dictionary
     * @param ConverterInterface $converter
     */
    public function __construct(Dictionary $dictionary, ConverterInterface $converter)
    {
        $this->dictionary = $dictionary;
        $this->converter = $converter;
    }

    public static function make(Dictionary $dictionary) : self
    {
        if (extension_loaded('gmp')) {
            return new GMPShortener($dictionary);
        }

        return new self($dictionary, new Converter());
    }

    public function reduce(string $uuid): string
    {
        $uuidInt = $this->converter->fromHex($uuid);
        $output = '';

        while ($uuidInt->isPositive()) {
            $output .= $this->dictionary->charsSet[
                $uuidInt->mod($this->dictionary->length)
                    ->toInt()
            ];

            $uuidInt = $uuidInt->dividedBy($this->dictionary->length, RoundingMode::DOWN);
        }

        return $output;
    }

    public function expand(string $shortUuid): string
    {
        $number = BigInteger::zero();
        foreach (str_split(strrev($shortUuid)) as $char) {
            $number = $number
                ->multipliedBy($this->dictionary->length)
                ->plus($this->dictionary->charIndexes[$char]);
        }

        $base16Uuid = str_pad($this->converter->toHex($number), 32, '0', STR_PAD_LEFT);

        return substr($base16Uuid,0, 8)
            . '-'
            . substr($base16Uuid, 8, 4)
            . '-'
            . substr($base16Uuid, 12, 4)
            . '-'
            . substr($base16Uuid, 16, 4)
            . '-'
            . substr($base16Uuid, 20, 12);
    }
}
{'fraction_non_alphanumeric': 0.14276257722859664, 'fraction_numerical': 0.011032656663724626, 'mean_word_length': 2.4031531531531534, 'pattern_counts': {'":': 0, '<': 5, '<?xml version=': 0, '>': 46, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '761196', 'n_tokens_mistral': 1406, 'n_tokens_neox': 1320, 'n_words': 250}