Skip to content

Commit

Permalink
Arrays as selectors support added.
Browse files Browse the repository at this point in the history
  • Loading branch information
Smoren committed Mar 15, 2024
1 parent 5d4ac50 commit 7ecb542
Show file tree
Hide file tree
Showing 12 changed files with 415 additions and 116 deletions.
4 changes: 3 additions & 1 deletion src/Interfaces/ArrayViewInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,9 @@ public function is(callable $predicate): MaskSelectorInterface;
/**
* Returns a subview of this view based on a selector or string slice.
*
* @param ArraySelectorInterface|string $selector The selector or string to filter the subview.
* @template S of string|array<mixed>|ArrayViewInterface<mixed>|ArraySelectorInterface
*
* @param S $selector The selector or string to filter the subview.
* @param bool|null $readonly Flag indicating if the subview should be read-only.
*
* @return ArrayViewInterface<T> A new view representing the subview of this view.
Expand Down
94 changes: 52 additions & 42 deletions src/Traits/ArrayViewAccessTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,26 @@
use Smoren\ArrayView\Exceptions\ReadonlyError;
use Smoren\ArrayView\Interfaces\ArraySelectorInterface;
use Smoren\ArrayView\Interfaces\ArrayViewInterface;
use Smoren\ArrayView\Selectors\IndexListSelector;
use Smoren\ArrayView\Selectors\MaskSelector;
use Smoren\ArrayView\Selectors\SliceSelector;
use Smoren\ArrayView\Structs\Slice;
use Smoren\ArrayView\Util;

/**
* Trait providing methods for accessing elements in ArrayView object.
* The trait implements methods for accessing, retrieving, setting,
* and unsetting elements in the ArrayView object.
*
* @template T Type of ArrayView values.
* @template S of string|array<mixed>|ArrayViewInterface<mixed>|ArraySelectorInterface Type of selectors.
*/
trait ArrayViewAccessTrait
{
/**
* Check if the specified offset exists in the ArrayView object.
*
* @param numeric|string|ArraySelectorInterface $offset The offset to check.
* @param numeric|S $offset The offset to check.
*
* @return bool
*
Expand All @@ -37,21 +41,17 @@ public function offsetExists($offset): bool
return $this->numericOffsetExists($offset);
}

if (\is_string($offset) && Slice::isSlice($offset)) {
return true;
try {
return $this->toSelector($offset)->compatibleWith($this);
} catch (KeyError $e) {
return false;
}

if ($offset instanceof ArraySelectorInterface) {
return $offset->compatibleWith($this);
}

return false;
}

/**
* Get the value at the specified offset in the ArrayView object.
*
* @param numeric|string|ArraySelectorInterface $offset The offset to get the value from.
* @param numeric|S $offset The offset to get the value from.
*
* @return T|array<T> The value at the specified offset.
*
Expand All @@ -63,30 +63,20 @@ public function offsetExists($offset): bool
#[\ReturnTypeWillChange]
public function offsetGet($offset)
{
/** @var mixed $offset */
if (\is_numeric($offset)) {
if (!$this->numericOffsetExists($offset)) {
throw new IndexError("Index {$offset} is out of range.");
}
return $this->source[$this->convertIndex(\intval($offset))];
}

if (\is_string($offset) && Slice::isSlice($offset)) {
return $this->subview(new SliceSelector($offset))->toArray();
}

if ($offset instanceof ArraySelectorInterface) {
return $this->subview($offset)->toArray();
}

$strOffset = \is_scalar($offset) ? \strval($offset) : \gettype($offset);
throw new KeyError("Invalid key: \"{$strOffset}\".");
return $this->subview($this->toSelector($offset))->toArray();
}

/**
* Set the value at the specified offset in the ArrayView object.
*
* @param numeric|string|ArraySelectorInterface $offset The offset to set the value at.
* @param numeric|S $offset The offset to set the value at.
* @param T|array<T>|ArrayViewInterface<T> $value The value to set.
*
* @return void
Expand All @@ -99,40 +89,27 @@ public function offsetGet($offset)
*/
public function offsetSet($offset, $value): void
{
/** @var mixed $offset */
if ($this->isReadonly()) {
throw new ReadonlyError("Cannot modify a readonly view.");
}

if (\is_numeric($offset)) {
if (!$this->numericOffsetExists($offset)) {
throw new IndexError("Index {$offset} is out of range.");
}

// @phpstan-ignore-next-line
$this->source[$this->convertIndex(\intval($offset))] = $value;
return;
}

if (\is_string($offset) && Slice::isSlice($offset)) {
/** @var array<T>|ArrayViewInterface<T> $value */
$this->subview(new SliceSelector($offset))->set($value);
if (!\is_numeric($offset)) {
$this->subview($this->toSelector($offset))->set($value);
return;
}

if ($offset instanceof ArraySelectorInterface) {
$this->subview($offset)->set($value);
return;
if (!$this->numericOffsetExists($offset)) {
throw new IndexError("Index {$offset} is out of range.");
}

$strOffset = \is_scalar($offset) ? \strval($offset) : \gettype($offset);
throw new KeyError("Invalid key: \"{$strOffset}\".");
// @phpstan-ignore-next-line
$this->source[$this->convertIndex(\intval($offset))] = $value;
}

/**
* Unset the value at the specified offset in the array-like object.
*
* @param numeric|string|ArraySelectorInterface $offset The offset to unset the value at.
* @param numeric|S $offset The offset to unset the value at.
*
* @return void
*
Expand All @@ -144,4 +121,37 @@ public function offsetUnset($offset): void
{
throw new NotSupportedError();
}

/**
* Converts array to selector.
*
* @param S $input value to convert.
*
* @return ArraySelectorInterface
*/
protected function toSelector($input): ArraySelectorInterface
{
if ($input instanceof ArraySelectorInterface) {
return $input;
}

if (\is_string($input) && Slice::isSlice($input)) {
return new SliceSelector($input);
}

if ($input instanceof ArrayViewInterface) {
$input = $input->toArray();
}

if (!\is_array($input) || !Util::isArraySequential($input)) {
$strOffset = \is_scalar($input) ? \strval($input) : \gettype($input);
throw new KeyError("Invalid key: \"{$strOffset}\".");
}

if (\count($input) > 0 && \is_bool($input[0])) {
return new MaskSelector($input);
}

return new IndexListSelector($input);
}
}
24 changes: 12 additions & 12 deletions src/Views/ArrayView.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@
namespace Smoren\ArrayView\Views;

use Smoren\ArrayView\Exceptions\IndexError;
use Smoren\ArrayView\Exceptions\KeyError;
use Smoren\ArrayView\Exceptions\SizeError;
use Smoren\ArrayView\Exceptions\ReadonlyError;
use Smoren\ArrayView\Exceptions\ValueError;
use Smoren\ArrayView\Interfaces\ArraySelectorInterface;
use Smoren\ArrayView\Interfaces\ArrayViewInterface;
use Smoren\ArrayView\Interfaces\MaskSelectorInterface;
use Smoren\ArrayView\Selectors\MaskSelector;
use Smoren\ArrayView\Selectors\SliceSelector;
use Smoren\ArrayView\Traits\ArrayViewAccessTrait;
use Smoren\ArrayView\Util;

Expand All @@ -32,7 +32,9 @@
class ArrayView implements ArrayViewInterface
{
/**
* @use ArrayViewAccessTrait<T> for array access methods.
* @use ArrayViewAccessTrait<T, string|array<mixed>|ArrayViewInterface<mixed>|ArraySelectorInterface>
*
* for array access methods.
*/
use ArrayViewAccessTrait;

Expand Down Expand Up @@ -258,19 +260,20 @@ public function is(callable $predicate): MaskSelectorInterface
* $subview[0] = [11]; // throws ReadonlyError
* ```
*
* @param ArraySelectorInterface|string $selector The selector or string to filter the subview.
* @template S of string|array<mixed>|ArrayViewInterface<mixed>|ArraySelectorInterface
*
* @param S $selector The selector or string to filter the subview.
* @param bool|null $readonly Flag indicating if the subview should be read-only.
*
* @return ArrayViewInterface<T> A new view representing the subview of this view.
*
* @throws IndexError if the selector is IndexListSelector and some indexes are out of range.
* @throws SizeError if the selector is MaskSelector and size of the mask not equals to size of the view.
* @throws KeyError if the selector is not valid (e.g. non-sequential array).
*/
public function subview($selector, bool $readonly = null): ArrayViewInterface
{
return is_string($selector)
? (new SliceSelector($selector))->select($this, $readonly)
: $selector->select($this, $readonly);
return $this->toSelector($selector)->select($this, $readonly);
}

/**
Expand Down Expand Up @@ -530,12 +533,8 @@ private function numericOffsetExists($offset): bool
return false;
}

// Numeric string must be 'integer'
if (\is_string($offset) && \preg_match('/^-?\d+$/', $offset) !== 1) {
return false;
}

if (\is_numeric($offset) && !\is_integer($offset + 0)) {
// Numeric string must be integer
if (!\is_integer($offset + 0)) {
return false;
}

Expand All @@ -544,6 +543,7 @@ private function numericOffsetExists($offset): bool
} catch (IndexError $e) {
return false;
}

return \is_array($this->source)
? \array_key_exists($index, $this->source)
: $this->source->offsetExists($index);
Expand Down
1 change: 1 addition & 0 deletions tests/unit/ArrayIndexListView/IssetTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ public function testIssetSelectorTrue(array $source, array $indexes)
$view = ArrayView::toView($source);

$this->assertTrue(isset($view[new IndexListSelector($indexes)]));
$this->assertTrue(isset($view[$indexes]));

$subview = $view->subview(new IndexListSelector($indexes));
$this->assertSame(\count($indexes), \count($subview));
Expand Down
52 changes: 50 additions & 2 deletions tests/unit/ArrayIndexListView/ReadTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,58 @@ public function testReadByMethod(array $source, array $indexes, array $expected)
/**
* @dataProvider dataProviderForRead
*/
public function testReadByIndex(array $source, array $mask, array $expected)
public function testReadByIndex(array $source, array $indexes, array $expected)
{
$view = ArrayView::toView($source);
$subArray = $view[new IndexListSelector($mask)];
$subArray = $view[new IndexListSelector($indexes)];

$this->assertSame($expected, $subArray);
$this->assertSame(\count($expected), \count($subArray));

for ($i = 0; $i < \count($subArray); ++$i) {
$this->assertSame($expected[$i], $subArray[$i]);
}

for ($i = 0; $i < \count($view); ++$i) {
$this->assertSame($source[$i], $view[$i]);
}

$this->assertSame($source, $view->toArray());
$this->assertSame($source, [...$view]);
$this->assertSame($expected, $subArray);
}

/**
* @dataProvider dataProviderForRead
*/
public function testReadByArrayIndex(array $source, array $indexes, array $expected)
{
$view = ArrayView::toView($source);
$subArray = $view[$indexes];

$this->assertSame($expected, $subArray);
$this->assertSame(\count($expected), \count($subArray));

for ($i = 0; $i < \count($subArray); ++$i) {
$this->assertSame($expected[$i], $subArray[$i]);
}

for ($i = 0; $i < \count($view); ++$i) {
$this->assertSame($source[$i], $view[$i]);
}

$this->assertSame($source, $view->toArray());
$this->assertSame($source, [...$view]);
$this->assertSame($expected, $subArray);
}

/**
* @dataProvider dataProviderForRead
*/
public function testReadByArrayViewIndex(array $source, array $indexes, array $expected)
{
$view = ArrayView::toView($source);
$subArray = $view[ArrayView::toView($indexes)];

$this->assertSame($expected, $subArray);
$this->assertSame(\count($expected), \count($subArray));
Expand Down
30 changes: 28 additions & 2 deletions tests/unit/ArrayIndexListView/WriteTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,37 @@ class WriteTest extends \Codeception\Test\Unit
/**
* @dataProvider dataProviderForMaskSubviewWrite
*/
public function testWriteByIndex(array $source, array $config, array $toWrite, array $expected)
public function testWriteByIndex(array $source, array $indexes, array $toWrite, array $expected)
{
$view = ArrayView::toView($source);

$view[new IndexListSelector($config)] = $toWrite;
$view[new IndexListSelector($indexes)] = $toWrite;

$this->assertSame($expected, [...$view]);
$this->assertSame($expected, $source);
}

/**
* @dataProvider dataProviderForMaskSubviewWrite
*/
public function testWriteByArrayIndex(array $source, array $indexes, array $toWrite, array $expected)
{
$view = ArrayView::toView($source);

$view[$indexes] = $toWrite;

$this->assertSame($expected, [...$view]);
$this->assertSame($expected, $source);
}

/**
* @dataProvider dataProviderForMaskSubviewWrite
*/
public function testWriteByArrayViewIndex(array $source, array $indexes, array $toWrite, array $expected)
{
$view = ArrayView::toView($source);

$view[ArrayView::toView($indexes)] = $toWrite;

$this->assertSame($expected, [...$view]);
$this->assertSame($expected, $source);
Expand Down
Loading

0 comments on commit 7ecb542

Please sign in to comment.