Last active
September 2, 2018 20:56
-
-
Save bagart/474b7c26fa2749187e78bd1f24d24f2c to your computer and use it in GitHub Desktop.
DoubleLinked.php with not numeric keys
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php declare(strict_types=1); | |
class DoubleLinkedException extends \Exception | |
{ | |
} | |
class WrongNameDoubleLinkedException extends DoubleLinkedException | |
{ | |
} | |
class NotFoundDoubleLinkedException extends DoubleLinkedException | |
{ | |
} | |
class DoubleLinked implements Iterator, ArrayAccess, Countable | |
{ | |
protected $name_first; | |
protected $name_last; | |
protected $count = 0; | |
protected $random_len = 10; | |
protected $name_current; | |
public function count() | |
{ | |
return $this->count; | |
} | |
public function next(string $name = null) | |
{ | |
$name_current_next = $this->keyNext($name); | |
if ($name_current_next == null) { | |
$this->name_current = null; | |
return null; | |
} | |
$this->rewind($name_current_next);//set current | |
return $this->getByKey($name_current_next);//current | |
} | |
public function prev(string $name = null) | |
{ | |
$name_current_prev = $this->keyPrev($name); | |
if ($name_current_prev == null) { | |
return null; | |
} | |
$this->rewind($name_current_prev);//set current | |
return $this->getByKey($name_current_prev);//current | |
} | |
public function keyNext(string $name = null): ?string | |
{ | |
if ($name === null) { | |
$name = $this->key(); | |
if ($name === null) { | |
return null; | |
} | |
} else { | |
$this->checkValidKey($name); | |
} | |
$name_current_next = "next_{$name}"; | |
return $this->$name_current_next ?? null; | |
} | |
public function keyPrev($name = null): ?string | |
{ | |
if ($name === null) { | |
$name = $this->key(); | |
if ($name === null) { | |
return null; | |
} | |
} else { | |
$this->checkValidKey($name); | |
} | |
$name_current_prev = "prev_{$name}"; | |
return $this->$name_current_prev ?? null; | |
} | |
public function rewind(string $name = null): void | |
{ | |
if ($name === null) { | |
$this->name_current = $this->name_first; | |
} else { | |
$this->valid($name); | |
$this->name_current = $name; | |
} | |
} | |
public function toArray(): array | |
{ | |
$name = $this->keyFirst(); | |
$result = []; | |
while ($name !== null) { | |
$result[$name] = $this->getByKey($name); | |
$name = $this->keyNext($name); | |
} | |
return $result; | |
} | |
protected function getNewKey(): string | |
{ | |
$i = 0; | |
while (true) { | |
$name = bin2hex(random_bytes($this->random_len)); | |
$key = "value_{$name}"; | |
if (!isset($this->$key)) { | |
return $name; | |
} | |
if (++$i > 100) { | |
$this->random_len += 1; | |
$i = 0; | |
if ($this->random_len > 120) { | |
throw new \Exception('iterable error'); | |
} | |
} | |
} | |
} | |
public function pushAfter($name_after, $value): string | |
{ | |
$this->checkValidKey($name_after); | |
$name = $this->getNewKey(); | |
$name_cur_value = "value_{$name}"; | |
$name_cur_prev = "prev_{$name}"; | |
$name_cur_next = "next_{$name}"; | |
$name_prev_next = "next_{$name_after}"; | |
$name_next_prev = "prev_{$this->$name_prev_next}"; | |
$this->$name_cur_value = $value; | |
$this->$name_cur_prev = $name_after; | |
$this->$name_cur_next = $this->$name_prev_next; | |
$this->$name_next_prev = $name; | |
$this->$name_prev_next = $name; | |
if ($this->name_last === $name_after) { | |
$this->name_last = $name; | |
} | |
$this->count += 1; | |
return $name; | |
} | |
function push(... $values): void | |
{ | |
$pushed = []; | |
foreach ($values as $value) { | |
$name = $this->getNewKey(); | |
$name_cur_value = "value_{$name}"; | |
$name_cur_prev = "prev_{$name}"; | |
$name_prev = $this->name_last; | |
$this->$name_cur_value = $value; | |
if ($name_prev === null) { | |
assert($this->name_first === null); | |
assert($this->count === 0); | |
$this->name_first = $name; | |
$this->name_current = $this->name_current ?? $name; | |
} else { | |
$name_prev_next = "next_{$name_prev}"; | |
$this->$name_prev_next = $name; | |
$this->$name_cur_prev = $name_prev; | |
} | |
$this->name_last = $name; | |
$this->count += 1; | |
//$pushed[] = $name; | |
} | |
//todo revert on exception | |
} | |
function unshift(... $values): void | |
{ | |
$pushed = []; | |
foreach ($values as $value) { | |
$name = $this->getNewKey(); | |
$name_cur_value = "value_{$name}"; | |
$name_cur_next = "next_{$name}"; | |
$name_next = $this->name_first; | |
$this->$name_cur_value = $value; | |
if ($name_next === null) { | |
assert($this->name_last === null); | |
assert($this->count === 0); | |
$this->name_last = $name; | |
$this->name_current = $this->name_current ?? $name; | |
} else { | |
$name_next_prev = "prev_{$name_next}"; | |
$this->$name_next_prev = $name; | |
$this->$name_cur_next = $name_next; | |
} | |
$this->name_first = $name; | |
$this->count += 1; | |
//$pushed[] = $name; | |
} | |
//todo revert on exception | |
} | |
public function current() | |
{ | |
$current_name = $this->key(); | |
return $current_name ? $this->getByKey($current_name) : null; | |
} | |
public function key(): ?string | |
{ | |
return $this->name_current; | |
} | |
public function offsetGet($offset) | |
{ | |
return $this->getByKey($this->getKeyByOffset($offset)); | |
} | |
public function getByKey($name) | |
{ | |
$this->checkValidKey($name); | |
$current_value_key = "value_{$name}"; | |
$result = $this->$current_value_key ?? null; | |
if ($result === null && !$this->isKeyExist($name)) { | |
throw new NotFoundDoubleLinkedException("!{$name}"); | |
} | |
return $result; | |
} | |
public function first() | |
{ | |
if (!$this->name_first) { | |
return null; | |
} | |
return $this->getByKey($this->name_first); | |
} | |
public function last() | |
{ | |
if (!$this->name_last) { | |
return null; | |
} | |
return $this->getByKey($this->name_last); | |
} | |
public function end() | |
{ | |
$this->name_current = $this->name_last; | |
return $this->current(); | |
} | |
public function keyFirst(): ?string | |
{ | |
return $this->name_first; | |
} | |
public function keyLast(): ?string | |
{ | |
return $this->name_last; | |
} | |
public function valid(string $name = null): bool | |
{ | |
if ($name === null) { | |
return $this->name_current !== null; | |
} | |
return $this->isKeyExist($name); | |
} | |
protected function checkValidKey($name) | |
{ | |
$result = ( | |
( | |
is_string($name) | |
|| ( | |
is_object($name) | |
&& $name instanceof StringableClass | |
) | |
) | |
&& strlen($name) > 0 | |
); | |
if (!$result) { | |
throw new WrongNameDoubleLinkedException('wrong name: ' . var_export($name, true)); | |
} | |
return $this; | |
} | |
public function isKeyExist($name) | |
{ | |
$this->checkValidKey($name); | |
return property_exists( | |
$this, | |
"value_{$name}" | |
); | |
} | |
public function offsetExists($offset) | |
{ | |
return $this->isKeyExist( | |
$this->getKeyByOffset($offset) | |
); | |
} | |
public function getKeyByOffset($offset) | |
{ | |
if (is_numeric($offset) && $offset < 0) { | |
throw new DoubleLinkedException('wrong offset'); | |
} | |
$cur = $this->keyFirst(); | |
while ($cur !== null && --$offset > 0) { | |
$cur = $this->keyNext($cur); | |
} | |
if ($cur === null) { | |
throw new NotFoundDoubleLinkedException(); | |
} | |
return $cur; | |
} | |
/** | |
* too slow. USE pushAfter | |
* @param mixed $offset | |
* @param mixed $value | |
* @throws DoubleLinkedException | |
* @throws NotFoundDoubleLinkedException | |
*/ | |
public function offsetSet($offset, $value) | |
{ | |
return $this->pushAfter( | |
$this->getKeyByOffset($offset), | |
$value | |
); | |
} | |
public function offsetUnset($offset): void | |
{ | |
$this->unsetKey( | |
$this->getKeyByOffset($offset) | |
); | |
} | |
public function unsetKey($name): void | |
{ | |
if (!$this->isKeyExist($name)) { | |
return; | |
} | |
$name_cur_value = "value_{$name}"; | |
$name_cur_next = "next_{$name}"; | |
$name_cur_prev = "prev_{$name}"; | |
$name_prev_next = "next_{$this->$name_cur_prev}"; | |
$name_next_prev = "prev_{$this->$name_cur_next}"; | |
$this->$name_next_prev = $this->$name_cur_prev ?? null; | |
$this->$name_prev_next = $this->$name_cur_next ?? null; | |
if ($this->name_first === $name) { | |
$this->name_first = $this->$name_cur_next ?? null; | |
} | |
if ($this->name_last === $name) { | |
$this->name_last = $this->$name_cur_prev ?? null; | |
} | |
unset( | |
$this->$name_cur_value, | |
$this->$name_cur_prev, | |
$this->$name_cur_next | |
); | |
} | |
} |
Author
bagart
commented
Sep 2, 2018
•
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment