[
  {
    "path": ".coveralls.yml",
    "content": "coverage_clover: clover.xml\njson_path: coveralls-upload.json\n"
  },
  {
    "path": ".gitignore",
    "content": "/vendor/\n"
  },
  {
    "path": ".travis.yml",
    "content": "language: php\n\nenv:\n  global:\n    - COVERALLS=0\n    - PHPCS=0\n\nmatrix:\n    include:\n        - php: 7.2\n        - php: 7.3\n        - php: 7.4\n          env: COVERALLS=1 PHPCS=1\n        - php: hhvm\n        - php: nightly\n\n    allow_failures:\n        - php: hhvm\n        - php: nightly\n    fast_finish: true\n\nbefore_script:\n  - composer install --no-interaction --prefer-source --dev\n\nscript:\n  - sh -c \"if [ '$COVERALLS' = '1' ]; then ./vendor/bin/phpunit --coverage-clover clover.xml ; else ./vendor/bin/phpunit ; fi\"\n  - sh -c \"if [ '$PHPCS' = '1' ]; then vendor/bin/phpcs -p --extensions=php --standard=./phpcs.xml ./src ./tests ; fi\"\n\nafter_script:\n  - sh -c \"if [ '$COVERALLS' = '1' ]; then vendor/bin/coveralls ; fi\"\n\nnotifications:\n    on_success: never\n    on_failure: always\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Change Log\nAll notable changes to this project will be documented in this file\nwhich adheres to the guidelines at http://keepachangelog.com/.\n\nThis project adheres to [Semantic Versioning](http://semver.org/).\n\n## [Unreleased]\n## [2.0.2] - 2016-04-22\n### Changed\n- Pinned symfony component version to pass tests on 5.4.x.\n- Updated coveralls config for new version.\n- Tweaked README.md to recommend install version 2.\n- Sorted phpcs errors.\n\n## [2.0.1] - 2016-04-22\n### Changed\n- Make MD5 hasher return an integer to prevent incorrect remapping\ndue to PHP treating numeric string array keys as integers.\n\n## [2.0.0] - 2015-10-08\n### Added\n- This CHANGELOG.md file.\n- A ROADMAP.md file.\n- PSR-4 autoloading.\n- Introduce namespacing.\n- Full PSR-2 support.\n\n### Changed\n- Reorganisation of files.\n- Updated readme to reflect composer installation recommendation.\n\n### Removed\n- PHP<5.4 support\n\n## [1.0.0] - 2015-10-16\n### Added\n- Setup automatic testing with Travis.\n- Monitor code coverage with Coveralls.\n- Get as close to PSR-2 as possible without changing class names.\n\n### Changed\n- Migrate tests to PHPUnit.\n\n### Removed\n- Legacy autoloader.\n\n## [0.1.0] - 2012-04-04\nPosterity release\n\n\n[Unreleased]: https://github.com/pda/flexihash/compare/v2.0.2...master\n[2.0.2]: https://github.com/pda/flexihash/compare/v2.0.1...v2.0.2\n[2.0.1]: https://github.com/pda/flexihash/compare/v2.0.0...v2.0.1\n[2.0.0]: https://github.com/pda/flexihash/compare/v1.0.0...v2.0.0\n[1.0.0]: https://github.com/pda/flexihash/compare/v0.1.0...v1.0.0\n[0.1.0]: https://github.com/pda/flexihash/tree/v0.1.0\n"
  },
  {
    "path": "LICENCE",
    "content": "The MIT License\n\nCopyright (c) 2008 Paul Annesley\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n\n"
  },
  {
    "path": "README.md",
    "content": "# Flexihash\n\n[![Build Status](https://travis-ci.org/pda/flexihash.svg?branch=master)](https://travis-ci.org/pda/flexihash) [![Coverage Status](https://coveralls.io/repos/github/pda/flexihash/badge.svg?branch=master)](https://coveralls.io/github/pda/flexihash?branch=master)\n\nFlexihash is a small PHP library which implements [consistent hashing](http://en.wikipedia.org/wiki/Consistent_hashing), which is most useful in distributed caching. It requires PHP5 and uses [PHPUnit](http://simpletest.org/) for unit testing.\n\n## Installation\n\n[Composer](https://getcomposer.org/) is the recommended installation technique. You can find flexihash on [Packagist](https://packagist.org/packages/flexihash/flexihash) so installation is as easy as\n```\ncomposer require flexihash/flexihash\n```\nor in your `composer.json`\n```json\n{\n    \"require\": {\n        \"flexihash/flexihash\": \"^3.0.0\"\n    }\n}\n```\n\n## Usage\n\n```php\n$hash = new Flexihash();\n\n// bulk add\n$hash->addTargets(['cache-1', 'cache-2', 'cache-3']);\n\n// simple lookup\n$hash->lookup('object-a'); // \"cache-1\"\n$hash->lookup('object-b'); // \"cache-2\"\n\n// add and remove\n$hash\n  ->addTarget('cache-4')\n  ->removeTarget('cache-1');\n\n// lookup with next-best fallback (for redundant writes)\n$hash->lookupList('object', 2); // [\"cache-2\", \"cache-4\"]\n\n// remove cache-2, expect object to hash to cache-4\n$hash->removeTarget('cache-2');\n$hash->lookup('object'); // \"cache-4\"\n```\n\n## Tests\n\n### Unit Test\n\n```\n% vendor/bin/phpunit\n```\n\n### Benchmark Test\n\n```\n% vendor/bin/phpunit tests/BenchmarkTest.php\n```\n\n## Further Reading\n\n  * http://www.spiteful.com/2008/03/17/programmers-toolbox-part-3-consistent-hashing/\n  * http://weblogs.java.net/blog/tomwhite/archive/2007/11/consistent_hash.html\n"
  },
  {
    "path": "ROADMAP.md",
    "content": "#Roadmap\n\n## v1.0.0\n\nThis maintains the historical API but allows for composer autoloading.\n\n- [x] Composer support.\n- [x] PSR2 bar class names.\n- [x] Migrate tests to PHPUnit.\n\n\n## v2.0.0\n\nThe historical API will be broken by classname changes.\n\n- [x] Introduce namespacing.\n- [x] PSR4 autoloading.\n- [x] Automated testing.\n- [x] PHP 5.4 minimum.\n\n## v3.0.0\n\n- [x] PHP 7.2 minimum.\n- [x] PHPUnit 8.\n- [x] Enable strict typing mode for all PHP files.\n\n## v4.0.0\n\n- [ ] PHP 7.3 minimum.\n- [ ] PHPUnit 9.\n"
  },
  {
    "path": "composer.json",
    "content": "{\n    \"name\": \"flexihash/flexihash\",\n    \"type\": \"library\",\n    \"description\": \"Flexihash is a small PHP library which implements consistent hashing\",\n    \"homepage\": \"https://github.com/pda/flexihash\",\n    \"license\": \"MIT\",\n    \"authors\": [\n        {\n            \"name\": \"Paul Annesley\",\n            \"email\": \"paul@annesley.cc\",\n            \"homepage\": \"http://paul.annesley.cc\"\n        },\n        {\n            \"name\": \"Dom Morgan\",\n            \"email\": \"dom@d3r.com\",\n            \"homepage\": \"https://d3r.com\"\n        }\n    ],\n    \"require\": {\n        \"php\": \">=7.2.0\"\n    },\n    \"require-dev\": {\n        \"phpunit/phpunit\": \"^8\",\n        \"squizlabs/php_codesniffer\": \"3.*\",\n        \"php-coveralls/php-coveralls\": \"^2.2\",\n        \"symfony/config\": \"^5.1.3\",\n        \"symfony/console\": \"^5.1.3\",\n        \"symfony/filesystem\": \"^5.1.3\",\n        \"symfony/stopwatch\": \"^5.1.3\",\n        \"symfony/yaml\": \"^5.1.3\"\n    },\n    \"autoload\": {\n        \"psr-4\": {\n            \"Flexihash\\\\\": \"src/\"\n        }\n    },\n    \"autoload-dev\": {\n        \"psr-4\": {\n            \"Flexihash\\\\Tests\\\\\": \"tests/\"\n        }\n    }\n}\n"
  },
  {
    "path": "phpcs.xml",
    "content": "<?xml version=\"1.0\"?>\n<ruleset name=\"Flexihash\">\n <description>Flexihash Coding Standard</description>\n <rule ref=\"PSR2\">\n </rule>\n</ruleset>\n"
  },
  {
    "path": "phpunit.xml.dist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n<phpunit xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:noNamespaceSchemaLocation=\"http://schema.phpunit.de/4.2/phpunit.xsd\"\n         backupGlobals=\"false\"\n         convertErrorsToExceptions=\"true\"\n         convertNoticesToExceptions=\"true\"\n         convertWarningsToExceptions=\"true\"\n         bootstrap=\"vendor/autoload.php\"\n         stopOnFailure=\"false\">\n\n    <testsuites>\n        <testsuite name=\"Flexihash Test Suite\">\n            <directory>tests</directory>\n            <exclude>tests/BenchmarkTest.php</exclude>\n        </testsuite>\n    </testsuites>\n\n</phpunit>\n"
  },
  {
    "path": "src/Exception.php",
    "content": "<?php\ndeclare(strict_types=1);\n\nnamespace Flexihash;\n\n/**\n * An exception thrown by Flexihash.\n *\n * @author Paul Annesley\n * @license http://www.opensource.org/licenses/mit-license.php\n */\nclass Exception extends \\Exception\n{\n}\n"
  },
  {
    "path": "src/Flexihash.php",
    "content": "<?php\ndeclare(strict_types=1);\n\nnamespace Flexihash;\n\nuse Flexihash\\Hasher\\HasherInterface;\nuse Flexihash\\Hasher\\Crc32Hasher;\n\n/**\n * A simple consistent hashing implementation with pluggable hash algorithms.\n *\n * @author Paul Annesley\n * @license http://www.opensource.org/licenses/mit-license.php\n */\nclass Flexihash\n{\n    /**\n     * The number of positions to hash each target to.\n     *\n     * @var int\n     */\n    private $replicas = 64;\n\n    /**\n     * The hash algorithm, encapsulated in a Flexihash_Hasher implementation.\n     * @var object Flexihash_Hasher\n     */\n    private $hasher;\n\n    /**\n     * Internal counter for current number of targets.\n     * @var int\n     */\n    private $targetCount = 0;\n\n    /**\n     * Internal map of positions (hash outputs) to targets.\n     * @var array { position => target, ... }\n     */\n    private $positionToTarget = [];\n\n    /**\n     * Internal map of targets to lists of positions that target is hashed to.\n     * @var array { target => [ position, position, ... ], ... }\n     */\n    private $targetToPositions = [];\n\n    /**\n     * Whether the internal map of positions to targets is already sorted.\n     * @var bool\n     */\n    private $positionToTargetSorted = false;\n\n    /**\n     * Sorted positions.\n     *\n     * @var array\n     */\n    private $sortedPositions = [];\n\n    /**\n     * Internal counter for current number of positions.\n     *\n     * @var integer\n     */\n    private $positionCount = 0;\n\n    /**\n     * Constructor.\n     * @param \\Flexihash\\Hasher\\HasherInterface $hasher\n     * @param int $replicas Amount of positions to hash each target to.\n     */\n    public function __construct(HasherInterface $hasher = null, $replicas = null)\n    {\n        $this->hasher = $hasher ? $hasher : new Crc32Hasher();\n        if (!empty($replicas)) {\n            $this->replicas = $replicas;\n        }\n    }\n\n    /**\n     * Add a target.\n     * @param string $target\n     * @param float $weight\n     * @chainable\n     */\n    public function addTarget($target, $weight = 1)\n    {\n        if (isset($this->targetToPositions[$target])) {\n            throw new Exception(\"Target '$target' already exists.\");\n        }\n\n        $this->targetToPositions[$target] = [];\n\n        // hash the target into multiple positions\n        for ($i = 0; $i < round($this->replicas * $weight); ++$i) {\n            $position = $this->hasher->hash($target.$i);\n            $this->positionToTarget[$position] = $target; // lookup\n            $this->targetToPositions[$target] [] = $position; // target removal\n        }\n\n        $this->positionToTargetSorted = false;\n        ++$this->targetCount;\n\n        return $this;\n    }\n\n    /**\n     * Add a list of targets.\n     *\n     * @param array $targets\n     * @param float $weight\n     * @return self fluent\n     */\n    public function addTargets($targets, $weight = 1)\n    {\n        foreach ($targets as $target) {\n            $this->addTarget($target, $weight);\n        }\n\n        return $this;\n    }\n\n    /**\n     * Remove a target.\n     *\n     * @param string $target\n     * @return self fluent\n     * @throws \\Flexihash\\Exception when target does not exist\n     */\n    public function removeTarget($target)\n    {\n        if (!isset($this->targetToPositions[$target])) {\n            throw new Exception(\"Target '$target' does not exist.\");\n        }\n\n        foreach ($this->targetToPositions[$target] as $position) {\n            unset($this->positionToTarget[$position]);\n        }\n\n        unset($this->targetToPositions[$target]);\n\n        $this->positionToTargetSorted = false;\n        --$this->targetCount;\n\n        return $this;\n    }\n\n    /**\n     * A list of all potential targets.\n     * @return array\n     */\n    public function getAllTargets(): array\n    {\n        return array_keys($this->targetToPositions);\n    }\n\n    /**\n     * Looks up the target for the given resource.\n     * @param string $resource\n     * @return string\n     * @throws \\Flexihash\\Exception when no targets defined\n     */\n    public function lookup($resource): string\n    {\n        $targets = $this->lookupList($resource, 1);\n        if (empty($targets)) {\n            throw new Exception('No targets exist');\n        }\n\n        return $targets[0];\n    }\n\n    /**\n     * Get a list of targets for the resource, in order of precedence.\n     * Up to $requestedCount targets are returned, less if there are fewer in total.\n     *\n     * @param string $resource\n     * @param int $requestedCount The length of the list to return\n     * @return array List of targets\n     * @throws \\Flexihash\\Exception when count is invalid\n     */\n    public function lookupList($resource, $requestedCount): array\n    {\n        if (!$requestedCount) {\n            throw new Exception('Invalid count requested');\n        }\n\n        // handle no targets\n        if (empty($this->positionToTarget)) {\n            return [];\n        }\n\n        // optimize single target\n        if ($this->targetCount == 1) {\n            return array_unique(array_values($this->positionToTarget));\n        }\n\n        // hash resource to a position\n        $resourcePosition = $this->hasher->hash($resource);\n\n        $results = [];\n\n        $this->sortPositionTargets();\n\n        $positions = $this->sortedPositions;\n        $low = 0;\n        $high = $this->positionCount - 1;\n        $notfound = false;\n\n        // binary search of the first position greater than resource position\n        while ($high >= $low || $notfound = true) {\n            $probe = (int) floor(($high + $low) / 2);\n\n            if ($notfound === false && $positions[$probe] <= $resourcePosition) {\n                $low = $probe + 1;\n            } elseif ($probe === 0 || $resourcePosition > $positions[$probe - 1] || $notfound === true) {\n                if ($notfound) {\n                    // if not found is true, it means binary search failed to find any position greater\n                    // than ressource position, in this case, the last position is the bigest lower\n                    // position and first position is the next one after cycle\n                    $probe = 0;\n                }\n\n                $results[] = $this->positionToTarget[$positions[$probe]];\n\n                if ($requestedCount > 1) {\n                    for ($i = $requestedCount - 1; $i > 0; --$i) {\n                        if (++$probe > $this->positionCount - 1) {\n                            $probe = 0; // cycle\n                        }\n                        $results[] = $this->positionToTarget[$positions[$probe]];\n                    }\n                }\n\n                break;\n            } else {\n                $high = $probe - 1;\n            }\n        }\n\n        return array_unique($results);\n    }\n\n    public function __toString(): string\n    {\n        return sprintf(\n            '%s{targets:[%s]}',\n            get_class($this),\n            implode(',', $this->getAllTargets())\n        );\n    }\n\n    // ----------------------------------------\n    // private methods\n\n    /**\n     * Sorts the internal mapping (positions to targets) by position.\n     */\n    private function sortPositionTargets()\n    {\n        // sort by key (position) if not already\n        if (!$this->positionToTargetSorted) {\n            ksort($this->positionToTarget, SORT_REGULAR);\n            $this->positionToTargetSorted = true;\n            $this->sortedPositions = array_keys($this->positionToTarget);\n            $this->positionCount = count($this->sortedPositions);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Hasher/Crc32Hasher.php",
    "content": "<?php\ndeclare(strict_types=1);\n\nnamespace Flexihash\\Hasher;\n\n/**\n * Uses CRC32 to hash a value into a signed 32bit int address space.\n * Under 32bit PHP this (safely) overflows into negatives ints.\n *\n * @author Paul Annesley\n * @license http://www.opensource.org/licenses/mit-license.php\n */\nclass Crc32Hasher implements HasherInterface\n{\n    public function hash($string): int\n    {\n        return crc32($string);\n    }\n}\n"
  },
  {
    "path": "src/Hasher/HasherInterface.php",
    "content": "<?php\ndeclare(strict_types=1);\n\nnamespace Flexihash\\Hasher;\n\n/**\n * Hashes given values into a sortable fixed size address space.\n *\n * @author Paul Annesley\n * @license http://www.opensource.org/licenses/mit-license.php\n */\ninterface HasherInterface\n{\n    /**\n     * Hashes the given string into a 32bit address space.\n     *\n     * The data must have 0xFFFFFFFF possible values, and be sortable by\n     * PHP sort functions using SORT_REGULAR.\n     *\n     * @param string\n     * @return mixed A sortable format with 0xFFFFFFFF possible values\n     */\n    public function hash($string);\n}\n"
  },
  {
    "path": "src/Hasher/Md5Hasher.php",
    "content": "<?php\ndeclare(strict_types=1);\n\nnamespace Flexihash\\Hasher;\n\n/**\n * Uses MD5 to hash a value into a 32bit int.\n *\n * @author Paul Annesley\n * @license http://www.opensource.org/licenses/mit-license.php\n */\nclass Md5Hasher implements HasherInterface\n{\n    /**\n     * {@inheritDoc}\n     *\n     * 8 hexits = 32bit, which also allows us to forego having to check whether\n     * it's over PHP_INT_MAX.\n     *\n     * The substring is converted to an int since hex strings sometimes get\n     * treated as ints if all digits are ints and this results in unexpected\n     * sorting order.\n     *\n     * @param  string $string\n     * @return int | float\n     * @author Dom Morgan <dom@d3r.com>\n     */\n    public function hash($string)\n    {\n        return hexdec(substr(md5($string), 0, 8));\n    }\n}\n"
  },
  {
    "path": "tests/BenchmarkTest.php",
    "content": "<?php\ndeclare(strict_types=1);\n\nnamespace Flexihash\\Tests;\n\nuse Flexihash\\Flexihash;\nuse Flexihash\\Hasher\\Crc32Hasher;\nuse Flexihash\\Hasher\\Md5Hasher;\n\n/**\n * Benchmarks, not really tests.\n *\n * @author Paul Annesley\n * @group benchmark\n * @license http://www.opensource.org/licenses/mit-license.php\n */\nclass BenchmarkTest extends \\PHPUnit\\Framework\\TestCase\n{\n    private $targets = 10;\n    private $lookups = 1000;\n\n    public function dump($message): void\n    {\n        echo $message.\"\\n\";\n    }\n\n    public function testAddTargetWithNonConsistentHash(): void\n    {\n        $results1 = [];\n        foreach (range(1, $this->lookups) as $i) {\n            $results1[$i] = $this->basicHash(\"t$i\", 10);\n        }\n\n        $results2 = [];\n        foreach (range(1, $this->lookups) as $i) {\n            $results2[$i] = $this->basicHash(\"t$i\", 11);\n        }\n\n        $differences = 0;\n        foreach (range(1, $this->lookups) as $i) {\n            if ($results1[$i] !== $results2[$i]) {\n                ++$differences;\n            }\n        }\n\n        $percent = round($differences / $this->lookups * 100);\n\n        $this->dump(\"NonConsistentHash: {$percent}% of lookups changed \".\n            \"after adding a target to the existing {$this->targets}\");\n    }\n\n    public function testRemoveTargetWithNonConsistentHash(): void\n    {\n        $results1 = [];\n        foreach (range(1, $this->lookups) as $i) {\n            $results1[$i] = $this->basicHash(\"t$i\", 10);\n        }\n\n        $results2 = [];\n        foreach (range(1, $this->lookups) as $i) {\n            $results2[$i] = $this->basicHash(\"t$i\", 9);\n        }\n\n        $differences = 0;\n        foreach (range(1, $this->lookups) as $i) {\n            if ($results1[$i] !== $results2[$i]) {\n                ++$differences;\n            }\n        }\n\n        $percent = round($differences / $this->lookups * 100);\n\n        $this->dump(\"NonConsistentHash: {$percent}% of lookups changed \".\n            \"after removing 1 of {$this->targets} targets\");\n    }\n\n    public function testHopeAddingTargetDoesNotChangeMuchWithCrc32Hasher(): void\n    {\n        $hashSpace = new Flexihash(\n            new Crc32Hasher()\n        );\n        foreach (range(1, $this->targets) as $i) {\n            $hashSpace->addTarget(\"target$i\");\n        }\n\n        $results1 = [];\n        foreach (range(1, $this->lookups) as $i) {\n            $results1[$i] = $hashSpace->lookup(\"t$i\");\n        }\n\n        $hashSpace->addTarget('target-new');\n\n        $results2 = [];\n        foreach (range(1, $this->lookups) as $i) {\n            $results2[$i] = $hashSpace->lookup(\"t$i\");\n        }\n\n        $differences = 0;\n        foreach (range(1, $this->lookups) as $i) {\n            if ($results1[$i] !== $results2[$i]) {\n                ++$differences;\n            }\n        }\n\n        $percent = round($differences / $this->lookups * 100);\n\n        $this->dump(\"ConsistentHash: {$percent}% of lookups changed \".\n            \"after adding a target to the existing {$this->targets}\");\n    }\n\n    public function testHopeRemovingTargetDoesNotChangeMuchWithCrc32Hasher(): void\n    {\n        $hashSpace = new Flexihash(\n            new Crc32Hasher()\n        );\n        foreach (range(1, $this->targets) as $i) {\n            $hashSpace->addTarget(\"target$i\");\n        }\n\n        $results1 = [];\n        foreach (range(1, $this->lookups) as $i) {\n            $results1[$i] = $hashSpace->lookup(\"t$i\");\n        }\n\n        $hashSpace->removeTarget('target1');\n\n        $results2 = [];\n        foreach (range(1, $this->lookups) as $i) {\n            $results2[$i] = $hashSpace->lookup(\"t$i\");\n        }\n\n        $differences = 0;\n        foreach (range(1, $this->lookups) as $i) {\n            if ($results1[$i] !== $results2[$i]) {\n                ++$differences;\n            }\n        }\n\n        $percent = round($differences / $this->lookups * 100);\n\n        $this->dump(\"ConsistentHash: {$percent}% of lookups changed \".\n            \"after removing 1 of {$this->targets} targets\");\n    }\n\n    public function testHashDistributionWithCrc32Hasher(): void\n    {\n        $hashSpace = new Flexihash(\n            new Crc32Hasher()\n        );\n\n        foreach (range(1, $this->targets) as $i) {\n            $hashSpace->addTarget(\"target$i\");\n        }\n\n        $results = [];\n        foreach (range(1, $this->lookups) as $i) {\n            $results[$i] = $hashSpace->lookup(\"t$i\");\n        }\n\n        $distribution = [];\n        foreach ($hashSpace->getAllTargets() as $target) {\n            $distribution[$target] = count(array_keys($results, $target));\n        }\n\n        $this->dump(sprintf(\n            'Distribution of %d lookups per target (min/max/median/avg): %d/%d/%d/%d',\n            $this->lookups / $this->targets,\n            min($distribution),\n            max($distribution),\n            round($this->median($distribution)),\n            round(array_sum($distribution) / count($distribution))\n        ));\n    }\n\n    public function testHasherSpeed(): void\n    {\n        $hashCount = 100000;\n\n        $md5Hasher = new Md5Hasher();\n        $crc32Hasher = new Crc32Hasher();\n\n        $start = microtime(true);\n        for ($i = 0; $i < $hashCount; ++$i) {\n            $md5Hasher->hash(\"test$i\");\n        }\n        $timeMd5 = microtime(true) - $start;\n\n        $start = microtime(true);\n        for ($i = 0; $i < $hashCount; ++$i) {\n            $crc32Hasher->hash(\"test$i\");\n        }\n        $timeCrc32 = microtime(true) - $start;\n\n        $this->dump(sprintf(\n            'Hashers timed over %d hashes (MD5 / CRC32): %f / %f',\n            $hashCount,\n            $timeMd5,\n            $timeCrc32\n        ));\n    }\n\n    // ----------------------------------------\n\n    private function basicHash($value, $targets):int\n    {\n        return abs(crc32($value) % $targets);\n    }\n\n    /**\n     * @param array $array list of numeric values\n     * @return numeric\n     */\n    private function median($values):int\n    {\n        $values = array_values($values);\n        sort($values);\n\n        $count = count($values);\n        $middleFloor = floor($count / 2);\n\n        if ($count % 2 == 1) {\n            return $values[$middleFloor];\n        } else {\n            return ($values[$middleFloor] + $values[$middleFloor + 1]) / 2;\n        }\n    }\n}\n"
  },
  {
    "path": "tests/FlexihashTest.php",
    "content": "<?php\ndeclare(strict_types=1);\n\nnamespace Flexihash\\Tests;\n\nuse Flexihash\\Exception;\nuse Flexihash\\Flexihash;\nuse Flexihash\\Tests\\Hasher\\MockHasher;\n\n/**\n * @author Paul Annesley\n * @license http://www.opensource.org/licenses/mit-license.php\n */\nclass FlexihashTest extends \\PHPUnit\\Framework\\TestCase\n{\n    public function testGetAllTargetsEmpty(): void\n    {\n        $hashSpace = new Flexihash();\n        $this->assertEquals($hashSpace->getAllTargets(), []);\n    }\n\n    public function testAddTargetThrowsExceptionOnDuplicateTarget(): void\n    {\n        $hashSpace = new Flexihash();\n        $hashSpace->addTarget('t-a');\n        $this->expectException('Flexihash\\Exception');\n        $hashSpace->addTarget('t-a');\n    }\n\n    public function testAddTargetAndGetAllTargets(): void\n    {\n        $hashSpace = new Flexihash();\n        $hashSpace\n            ->addTarget('t-a')\n            ->addTarget('t-b')\n            ->addTarget('t-c')\n            ;\n\n        $this->assertEquals($hashSpace->getAllTargets(), ['t-a', 't-b', 't-c']);\n    }\n\n    public function testAddTargetsAndGetAllTargets(): void\n    {\n        $targets = ['t-a', 't-b', 't-c'];\n\n        $hashSpace = new Flexihash();\n        $hashSpace->addTargets($targets);\n        $this->assertEquals($hashSpace->getAllTargets(), $targets);\n    }\n\n    public function testRemoveTarget(): void\n    {\n        $hashSpace = new Flexihash();\n        $hashSpace\n            ->addTarget('t-a')\n            ->addTarget('t-b')\n            ->addTarget('t-c')\n            ->removeTarget('t-b')\n            ;\n        $this->assertEquals($hashSpace->getAllTargets(), ['t-a', 't-c']);\n    }\n\n    public function testRemoveTargetFailsOnMissingTarget(): void\n    {\n        $hashSpace = new Flexihash();\n        $this->expectException('Flexihash\\Exception');\n        $hashSpace->removeTarget('not-there');\n    }\n\n    public function testHashSpaceRepeatableLookups(): void\n    {\n        $hashSpace = new Flexihash();\n        foreach (range(1, 10) as $i) {\n            $hashSpace->addTarget(\"target$i\");\n        }\n\n        $this->assertEquals($hashSpace->lookup('t1'), $hashSpace->lookup('t1'));\n        $this->assertEquals($hashSpace->lookup('t2'), $hashSpace->lookup('t2'));\n    }\n\n    public function testHashSpaceLookupListEmpty(): void\n    {\n        $hashSpace = new Flexihash();\n        $this->assertEmpty($hashSpace->lookupList('t1', 2));\n    }\n\n    public function testHashSpaceLookupListNoTargets(): void\n    {\n        $this->expectException('Flexihash\\Exception');\n        $this->expectExceptionMessage('No targets exist');\n        $hashSpace = new Flexihash();\n        $hashSpace->lookup('t1');\n    }\n\n    public function testHashSpaceLookupListNo(): void\n    {\n        $this->expectException('Flexihash\\Exception');\n        $this->expectExceptionMessage('Invalid count requested');\n        $hashSpace = new Flexihash();\n        $hashSpace->lookupList('t1', 0);\n    }\n\n    public function testHashSpaceLookupsAreValidTargets(): void\n    {\n        $targets = [];\n        foreach (range(1, 10) as $i) {\n            $targets [] = \"target$i\";\n        }\n\n        $hashSpace = new Flexihash();\n        $hashSpace->addTargets($targets);\n\n        foreach (range(1, 10) as $i) {\n            $this->assertTrue(\n                in_array($hashSpace->lookup(\"r$i\"), $targets),\n                'target must be in list of targets'\n            );\n        }\n    }\n\n    public function testHashSpaceConsistentLookupsAfterAddingAndRemoving(): void\n    {\n        $hashSpace = new Flexihash();\n        foreach (range(1, 10) as $i) {\n            $hashSpace->addTarget(\"target$i\");\n        }\n\n        $results1 = [];\n        foreach (range(1, 100) as $i) {\n            $results1 [] = $hashSpace->lookup(\"t$i\");\n        }\n\n        $hashSpace\n            ->addTarget('new-target')\n            ->removeTarget('new-target')\n            ->addTarget('new-target')\n            ->removeTarget('new-target')\n            ;\n\n        $results2 = [];\n        foreach (range(1, 100) as $i) {\n            $results2 [] = $hashSpace->lookup(\"t$i\");\n        }\n\n        // This is probably optimistic, as adding/removing a target may\n        // clobber existing targets and is not expected to restore them.\n        $this->assertEquals($results1, $results2);\n    }\n\n    public function testHashSpaceConsistentLookupsWithNewInstance(): void\n    {\n        $hashSpace1 = new Flexihash();\n        foreach (range(1, 10) as $i) {\n            $hashSpace1->addTarget(\"target$i\");\n        }\n        $results1 = [];\n        foreach (range(1, 100) as $i) {\n            $results1 [] = $hashSpace1->lookup(\"t$i\");\n        }\n\n        $hashSpace2 = new Flexihash();\n        foreach (range(1, 10) as $i) {\n            $hashSpace2->addTarget(\"target$i\");\n        }\n        $results2 = [];\n        foreach (range(1, 100) as $i) {\n            $results2 [] = $hashSpace2->lookup(\"t$i\");\n        }\n\n        $this->assertEquals($results1, $results2);\n    }\n\n    public function testGetMultipleTargets(): void\n    {\n        $hashSpace = new Flexihash();\n        foreach (range(1, 10) as $i) {\n            $hashSpace->addTarget(\"target$i\");\n        }\n\n        $targets = $hashSpace->lookupList('resource', 2);\n\n        $this->assertIsArray($targets);\n        $this->assertEquals(count($targets), 2);\n        $this->assertNotEquals($targets[0], $targets[1]);\n    }\n\n    public function testGetMultipleTargetsWithOnlyOneTarget(): void\n    {\n        $hashSpace = new Flexihash();\n        $hashSpace->addTarget('single-target');\n\n        $targets = $hashSpace->lookupList('resource', 2);\n\n        $this->assertIsArray($targets);\n        $this->assertEquals(count($targets), 1);\n        $this->assertEquals($targets[0], 'single-target');\n    }\n\n    public function testGetMoreTargetsThanExist(): void\n    {\n        $hashSpace = new Flexihash();\n        $hashSpace->addTarget('target1');\n        $hashSpace->addTarget('target2');\n\n        $targets = $hashSpace->lookupList('resource', 4);\n\n        $this->assertIsArray($targets);\n        $this->assertEquals(count($targets), 2);\n        $this->assertNotEquals($targets[0], $targets[1]);\n    }\n\n    public function testGetMultipleTargetsNeedingToLoopToStart(): void\n    {\n        $mockHasher = new MockHasher();\n        $hashSpace = new Flexihash($mockHasher, 1);\n\n        $mockHasher->setHashValue(10);\n        $hashSpace->addTarget('t1');\n\n        $mockHasher->setHashValue(20);\n        $hashSpace->addTarget('t2');\n\n        $mockHasher->setHashValue(30);\n        $hashSpace->addTarget('t3');\n\n        $mockHasher->setHashValue(40);\n        $hashSpace->addTarget('t4');\n\n        $mockHasher->setHashValue(50);\n        $hashSpace->addTarget('t5');\n\n        $mockHasher->setHashValue(35);\n        $targets = $hashSpace->lookupList('resource', 4);\n\n        $this->assertEquals($targets, ['t4', 't5', 't1', 't2']);\n    }\n\n    public function testGetMultipleTargetsWithoutGettingAnyBeforeLoopToStart(): void\n    {\n        $mockHasher = new MockHasher();\n        $hashSpace = new Flexihash($mockHasher, 1);\n\n        $mockHasher->setHashValue(10);\n        $hashSpace->addTarget('t1');\n\n        $mockHasher->setHashValue(20);\n        $hashSpace->addTarget('t2');\n\n        $mockHasher->setHashValue(30);\n        $hashSpace->addTarget('t3');\n\n        $mockHasher->setHashValue(100);\n        $targets = $hashSpace->lookupList('resource', 2);\n\n        $this->assertEquals($targets, ['t1', 't2']);\n    }\n\n    public function testGetMultipleTargetsWithoutNeedingToLoopToStart(): void\n    {\n        $mockHasher = new MockHasher();\n        $hashSpace = new Flexihash($mockHasher, 1);\n\n        $mockHasher->setHashValue(10);\n        $hashSpace->addTarget('t1');\n\n        $mockHasher->setHashValue(20);\n        $hashSpace->addTarget('t2');\n\n        $mockHasher->setHashValue(30);\n        $hashSpace->addTarget('t3');\n\n        $mockHasher->setHashValue(15);\n        $targets = $hashSpace->lookupList('resource', 2);\n\n        $this->assertEquals($targets, ['t2', 't3']);\n    }\n\n    public function testFallbackPrecedenceWhenServerRemoved(): void\n    {\n        $mockHasher = new MockHasher();\n        $hashSpace = new Flexihash($mockHasher, 1);\n\n        $mockHasher->setHashValue(10);\n        $hashSpace->addTarget('t1');\n\n        $mockHasher->setHashValue(20);\n        $hashSpace->addTarget('t2');\n\n        $mockHasher->setHashValue(30);\n        $hashSpace->addTarget('t3');\n\n        $mockHasher->setHashValue(15);\n\n        $this->assertEquals($hashSpace->lookup('resource'), 't2');\n        $this->assertEquals(\n            $hashSpace->lookupList('resource', 3),\n            ['t2', 't3', 't1']\n        );\n\n        $hashSpace->removeTarget('t2');\n\n        $this->assertEquals($hashSpace->lookup('resource'), 't3');\n        $this->assertEquals(\n            $hashSpace->lookupList('resource', 3),\n            ['t3', 't1']\n        );\n\n        $hashSpace->removeTarget('t3');\n\n        $this->assertEquals($hashSpace->lookup('resource'), 't1');\n        $this->assertEquals(\n            $hashSpace->lookupList('resource', 3),\n            ['t1']\n        );\n    }\n\n    /**\n     * Does the __toString method behave as we expect.\n     *\n     * @author Dom Morgan <dom@d3r.com>\n     */\n    public function testHashSpaceToString(): void\n    {\n        $mockHasher = new MockHasher();\n        $hashSpace = new Flexihash($mockHasher, 1);\n        $hashSpace->addTarget('t1');\n        $hashSpace->addTarget('t2');\n\n        $this->assertSame(\n            $hashSpace->__toString(),\n            'Flexihash\\Flexihash{targets:[t1,t2]}'\n        );\n    }\n}\n"
  },
  {
    "path": "tests/Hasher/HasherTest.php",
    "content": "<?php\ndeclare(strict_types=1);\n\nnamespace Flexihash\\Tests\\Hasher;\n\nuse Flexihash\\Hasher\\Crc32Hasher;\nuse Flexihash\\Hasher\\Md5Hasher;\n\n/**\n * @author Paul Annesley\n * @license http://www.opensource.org/licenses/mit-license.php\n */\nclass HasherTest extends \\PHPUnit\\Framework\\TestCase\n{\n    public function testCrc32Hash(): void\n    {\n        $hasher = new Crc32Hasher();\n        $result1 = $hasher->hash('test');\n        $result2 = $hasher->hash('test');\n        $result3 = $hasher->hash('different');\n\n        $this->assertEquals($result1, $result2);\n        $this->assertNotEquals($result1, $result3); // fragile but worthwhile\n    }\n\n    public function testMd5Hash(): void\n    {\n        $hasher = new Md5Hasher();\n        $result1 = $hasher->hash('test');\n        $result2 = $hasher->hash('test');\n        $result3 = $hasher->hash('different');\n\n        $this->assertEquals($result1, $result2);\n        $this->assertNotEquals($result1, $result3); // fragile but worthwhile\n    }\n}\n"
  },
  {
    "path": "tests/Hasher/MockHasher.php",
    "content": "<?php\ndeclare(strict_types=1);\n\nnamespace Flexihash\\Tests\\Hasher;\n\nuse Flexihash\\Hasher\\HasherInterface;\n\n/**\n * @author Paul Annesley\n * @license http://www.opensource.org/licenses/mit-license.php\n */\nclass MockHasher implements HasherInterface\n{\n    private $hashValue;\n\n    public function setHashValue($hash): void\n    {\n        $this->hashValue = $hash;\n    }\n\n    public function hash($value)\n    {\n        return $this->hashValue;\n    }\n}\n"
  }
]