records and edgs
This commit is contained in:
@@ -0,0 +1,186 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Lexorank;
|
||||
use function array_filter;
|
||||
use function chr;
|
||||
use function in_array;
|
||||
use function ord;
|
||||
use function str_split;
|
||||
use function strcmp;
|
||||
use function strlen;
|
||||
use function substr;
|
||||
final class Lexorank
|
||||
{
|
||||
public const string MIN_CHAR = '0';
|
||||
|
||||
public const string MAX_CHAR = 'z';
|
||||
|
||||
/** Usually, database like MySQL order using only the first 1024 chars */
|
||||
public const int MAX_RANK_LEN = 1024;
|
||||
|
||||
/**
|
||||
* @var non-empty-string
|
||||
* @psalm-readonly
|
||||
*/
|
||||
private string $rank;
|
||||
|
||||
/**
|
||||
* @throws LexorankException
|
||||
*/
|
||||
private function __construct(string $rank)
|
||||
{
|
||||
if(empty($rank)){
|
||||
$this->rank = "0";
|
||||
return;
|
||||
}
|
||||
self::rankValidator($rank);
|
||||
|
||||
$this->rank = $rank;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param non-empty-string $rank
|
||||
*
|
||||
* @psalm-pure
|
||||
* @throws LexorankException
|
||||
*/
|
||||
private static function rankValidator(string $rank): void
|
||||
{
|
||||
|
||||
|
||||
if (strlen($rank) > self::MAX_RANK_LEN) {
|
||||
throw new LexorankException($rank);
|
||||
}
|
||||
|
||||
$invalidChars = array_filter(
|
||||
str_split($rank),
|
||||
static function ($char) {
|
||||
return ord($char) < ord(self::MIN_CHAR) || ord($char) > ord(self::MAX_CHAR);
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
|
||||
if ($invalidChars !== []) {
|
||||
throw new LexorankException($rank);
|
||||
}
|
||||
|
||||
$lastChar = substr($rank, -1);
|
||||
if ($lastChar === self::MIN_CHAR) {
|
||||
throw new LexorankException($rank);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return non-empty-string
|
||||
*/
|
||||
public function get(): string
|
||||
{
|
||||
return $this->rank;
|
||||
}
|
||||
|
||||
public static function fromString(string $rank): self
|
||||
{
|
||||
return new self($rank);
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-pure
|
||||
*/
|
||||
public static function forEmptySequence(): self
|
||||
{
|
||||
return self::fromString(self::mid(self::MIN_CHAR, self::MAX_CHAR));
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-pure
|
||||
*/
|
||||
public static function after(self $prevRank): self
|
||||
{
|
||||
$char = substr($prevRank->get(), -1);
|
||||
|
||||
if (ord($char) + 1 >= ord(self::MAX_CHAR)) {
|
||||
return self::fromString(
|
||||
$prevRank->get() . chr(ord(self::MIN_CHAR) + 1)
|
||||
);
|
||||
}
|
||||
|
||||
$return = substr($prevRank->get(), 0, -1) . chr(ord($char) + 1);
|
||||
|
||||
|
||||
return self::fromString($return);
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-pure
|
||||
*/
|
||||
public static function before(self $nextRank): self
|
||||
{
|
||||
$char = substr($nextRank->get(), -1);
|
||||
|
||||
if (ord($char) - 1 <= ord(self::MIN_CHAR)) {
|
||||
$return = substr($nextRank->get(), 0, -1) . chr(ord($char) - 1) . chr(ord(self::MAX_CHAR) - 1);
|
||||
return self::fromString($return);
|
||||
}
|
||||
|
||||
$return = substr($nextRank->get(), 0, -1) . chr(ord($char) - 1);
|
||||
return self::fromString($return);
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-pure
|
||||
*/
|
||||
public static function betweenRanks(self $prevRank, self $nextRank): self
|
||||
{
|
||||
if (strcmp($prevRank->get(), $nextRank->get()) >= 0) {
|
||||
throw new LexorankException("problem");
|
||||
}
|
||||
|
||||
$rank = '';
|
||||
$i = 0;
|
||||
while ($i <= self::MAX_RANK_LEN) {
|
||||
$prevChar = $prevRank->getChar($i, self::MIN_CHAR);
|
||||
$nextChar = $nextRank->getChar($i, self::MAX_CHAR);
|
||||
$i++;
|
||||
|
||||
$midChar = self::mid($prevChar, $nextChar);
|
||||
if (in_array($midChar, [$prevChar, $nextChar])) {
|
||||
$rank .= $prevChar;
|
||||
continue;
|
||||
}
|
||||
|
||||
$rank .= $midChar;
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
return self::fromString($rank);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param 0|positive-int $i
|
||||
* @param non-empty-string $defaultChar
|
||||
*
|
||||
* @return non-empty-string
|
||||
*/
|
||||
private function getChar(int $i, string $defaultChar): string
|
||||
{
|
||||
return $this->rank[$i] ?? $defaultChar;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param non-empty-string $prev
|
||||
* @param non-empty-string $next
|
||||
* @return non-empty-string
|
||||
*
|
||||
* @psalm-pure
|
||||
*/
|
||||
private static function mid(string $prev, string $next): string
|
||||
{
|
||||
if (ord($prev) >= ord($next)) {
|
||||
return $prev;
|
||||
}
|
||||
|
||||
return chr((int)((ord($prev) + ord($next)) / 2));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Lexorank;
|
||||
|
||||
use Exception;
|
||||
|
||||
final class LexorankException extends Exception
|
||||
{
|
||||
// Redefine the exception so message isn't optional
|
||||
public function __construct(string $message, int $code = 0, Exception $previous = null)
|
||||
{
|
||||
// make sure everything is assigned properly
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user