Skip to content

Commit 7ecb542

Browse files
committed
Arrays as selectors support added.
1 parent 5d4ac50 commit 7ecb542

File tree

12 files changed

+415
-116
lines changed

12 files changed

+415
-116
lines changed

src/Interfaces/ArrayViewInterface.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,9 @@ public function is(callable $predicate): MaskSelectorInterface;
8686
/**
8787
* Returns a subview of this view based on a selector or string slice.
8888
*
89-
* @param ArraySelectorInterface|string $selector The selector or string to filter the subview.
89+
* @template S of string|array<mixed>|ArrayViewInterface<mixed>|ArraySelectorInterface
90+
*
91+
* @param S $selector The selector or string to filter the subview.
9092
* @param bool|null $readonly Flag indicating if the subview should be read-only.
9193
*
9294
* @return ArrayViewInterface<T> A new view representing the subview of this view.

src/Traits/ArrayViewAccessTrait.php

Lines changed: 52 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,26 @@
1010
use Smoren\ArrayView\Exceptions\ReadonlyError;
1111
use Smoren\ArrayView\Interfaces\ArraySelectorInterface;
1212
use Smoren\ArrayView\Interfaces\ArrayViewInterface;
13+
use Smoren\ArrayView\Selectors\IndexListSelector;
14+
use Smoren\ArrayView\Selectors\MaskSelector;
1315
use Smoren\ArrayView\Selectors\SliceSelector;
1416
use Smoren\ArrayView\Structs\Slice;
17+
use Smoren\ArrayView\Util;
1518

1619
/**
1720
* Trait providing methods for accessing elements in ArrayView object.
1821
* The trait implements methods for accessing, retrieving, setting,
1922
* and unsetting elements in the ArrayView object.
2023
*
2124
* @template T Type of ArrayView values.
25+
* @template S of string|array<mixed>|ArrayViewInterface<mixed>|ArraySelectorInterface Type of selectors.
2226
*/
2327
trait ArrayViewAccessTrait
2428
{
2529
/**
2630
* Check if the specified offset exists in the ArrayView object.
2731
*
28-
* @param numeric|string|ArraySelectorInterface $offset The offset to check.
32+
* @param numeric|S $offset The offset to check.
2933
*
3034
* @return bool
3135
*
@@ -37,21 +41,17 @@ public function offsetExists($offset): bool
3741
return $this->numericOffsetExists($offset);
3842
}
3943

40-
if (\is_string($offset) && Slice::isSlice($offset)) {
41-
return true;
44+
try {
45+
return $this->toSelector($offset)->compatibleWith($this);
46+
} catch (KeyError $e) {
47+
return false;
4248
}
43-
44-
if ($offset instanceof ArraySelectorInterface) {
45-
return $offset->compatibleWith($this);
46-
}
47-
48-
return false;
4949
}
5050

5151
/**
5252
* Get the value at the specified offset in the ArrayView object.
5353
*
54-
* @param numeric|string|ArraySelectorInterface $offset The offset to get the value from.
54+
* @param numeric|S $offset The offset to get the value from.
5555
*
5656
* @return T|array<T> The value at the specified offset.
5757
*
@@ -63,30 +63,20 @@ public function offsetExists($offset): bool
6363
#[\ReturnTypeWillChange]
6464
public function offsetGet($offset)
6565
{
66-
/** @var mixed $offset */
6766
if (\is_numeric($offset)) {
6867
if (!$this->numericOffsetExists($offset)) {
6968
throw new IndexError("Index {$offset} is out of range.");
7069
}
7170
return $this->source[$this->convertIndex(\intval($offset))];
7271
}
7372

74-
if (\is_string($offset) && Slice::isSlice($offset)) {
75-
return $this->subview(new SliceSelector($offset))->toArray();
76-
}
77-
78-
if ($offset instanceof ArraySelectorInterface) {
79-
return $this->subview($offset)->toArray();
80-
}
81-
82-
$strOffset = \is_scalar($offset) ? \strval($offset) : \gettype($offset);
83-
throw new KeyError("Invalid key: \"{$strOffset}\".");
73+
return $this->subview($this->toSelector($offset))->toArray();
8474
}
8575

8676
/**
8777
* Set the value at the specified offset in the ArrayView object.
8878
*
89-
* @param numeric|string|ArraySelectorInterface $offset The offset to set the value at.
79+
* @param numeric|S $offset The offset to set the value at.
9080
* @param T|array<T>|ArrayViewInterface<T> $value The value to set.
9181
*
9282
* @return void
@@ -99,40 +89,27 @@ public function offsetGet($offset)
9989
*/
10090
public function offsetSet($offset, $value): void
10191
{
102-
/** @var mixed $offset */
10392
if ($this->isReadonly()) {
10493
throw new ReadonlyError("Cannot modify a readonly view.");
10594
}
10695

107-
if (\is_numeric($offset)) {
108-
if (!$this->numericOffsetExists($offset)) {
109-
throw new IndexError("Index {$offset} is out of range.");
110-
}
111-
112-
// @phpstan-ignore-next-line
113-
$this->source[$this->convertIndex(\intval($offset))] = $value;
114-
return;
115-
}
116-
117-
if (\is_string($offset) && Slice::isSlice($offset)) {
118-
/** @var array<T>|ArrayViewInterface<T> $value */
119-
$this->subview(new SliceSelector($offset))->set($value);
96+
if (!\is_numeric($offset)) {
97+
$this->subview($this->toSelector($offset))->set($value);
12098
return;
12199
}
122100

123-
if ($offset instanceof ArraySelectorInterface) {
124-
$this->subview($offset)->set($value);
125-
return;
101+
if (!$this->numericOffsetExists($offset)) {
102+
throw new IndexError("Index {$offset} is out of range.");
126103
}
127104

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

132109
/**
133110
* Unset the value at the specified offset in the array-like object.
134111
*
135-
* @param numeric|string|ArraySelectorInterface $offset The offset to unset the value at.
112+
* @param numeric|S $offset The offset to unset the value at.
136113
*
137114
* @return void
138115
*
@@ -144,4 +121,37 @@ public function offsetUnset($offset): void
144121
{
145122
throw new NotSupportedError();
146123
}
124+
125+
/**
126+
* Converts array to selector.
127+
*
128+
* @param S $input value to convert.
129+
*
130+
* @return ArraySelectorInterface
131+
*/
132+
protected function toSelector($input): ArraySelectorInterface
133+
{
134+
if ($input instanceof ArraySelectorInterface) {
135+
return $input;
136+
}
137+
138+
if (\is_string($input) && Slice::isSlice($input)) {
139+
return new SliceSelector($input);
140+
}
141+
142+
if ($input instanceof ArrayViewInterface) {
143+
$input = $input->toArray();
144+
}
145+
146+
if (!\is_array($input) || !Util::isArraySequential($input)) {
147+
$strOffset = \is_scalar($input) ? \strval($input) : \gettype($input);
148+
throw new KeyError("Invalid key: \"{$strOffset}\".");
149+
}
150+
151+
if (\count($input) > 0 && \is_bool($input[0])) {
152+
return new MaskSelector($input);
153+
}
154+
155+
return new IndexListSelector($input);
156+
}
147157
}

src/Views/ArrayView.php

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@
55
namespace Smoren\ArrayView\Views;
66

77
use Smoren\ArrayView\Exceptions\IndexError;
8+
use Smoren\ArrayView\Exceptions\KeyError;
89
use Smoren\ArrayView\Exceptions\SizeError;
910
use Smoren\ArrayView\Exceptions\ReadonlyError;
1011
use Smoren\ArrayView\Exceptions\ValueError;
1112
use Smoren\ArrayView\Interfaces\ArraySelectorInterface;
1213
use Smoren\ArrayView\Interfaces\ArrayViewInterface;
1314
use Smoren\ArrayView\Interfaces\MaskSelectorInterface;
1415
use Smoren\ArrayView\Selectors\MaskSelector;
15-
use Smoren\ArrayView\Selectors\SliceSelector;
1616
use Smoren\ArrayView\Traits\ArrayViewAccessTrait;
1717
use Smoren\ArrayView\Util;
1818

@@ -32,7 +32,9 @@
3232
class ArrayView implements ArrayViewInterface
3333
{
3434
/**
35-
* @use ArrayViewAccessTrait<T> for array access methods.
35+
* @use ArrayViewAccessTrait<T, string|array<mixed>|ArrayViewInterface<mixed>|ArraySelectorInterface>
36+
*
37+
* for array access methods.
3638
*/
3739
use ArrayViewAccessTrait;
3840

@@ -258,19 +260,20 @@ public function is(callable $predicate): MaskSelectorInterface
258260
* $subview[0] = [11]; // throws ReadonlyError
259261
* ```
260262
*
261-
* @param ArraySelectorInterface|string $selector The selector or string to filter the subview.
263+
* @template S of string|array<mixed>|ArrayViewInterface<mixed>|ArraySelectorInterface
264+
*
265+
* @param S $selector The selector or string to filter the subview.
262266
* @param bool|null $readonly Flag indicating if the subview should be read-only.
263267
*
264268
* @return ArrayViewInterface<T> A new view representing the subview of this view.
265269
*
266270
* @throws IndexError if the selector is IndexListSelector and some indexes are out of range.
267271
* @throws SizeError if the selector is MaskSelector and size of the mask not equals to size of the view.
272+
* @throws KeyError if the selector is not valid (e.g. non-sequential array).
268273
*/
269274
public function subview($selector, bool $readonly = null): ArrayViewInterface
270275
{
271-
return is_string($selector)
272-
? (new SliceSelector($selector))->select($this, $readonly)
273-
: $selector->select($this, $readonly);
276+
return $this->toSelector($selector)->select($this, $readonly);
274277
}
275278

276279
/**
@@ -530,12 +533,8 @@ private function numericOffsetExists($offset): bool
530533
return false;
531534
}
532535

533-
// Numeric string must be 'integer'
534-
if (\is_string($offset) && \preg_match('/^-?\d+$/', $offset) !== 1) {
535-
return false;
536-
}
537-
538-
if (\is_numeric($offset) && !\is_integer($offset + 0)) {
536+
// Numeric string must be integer
537+
if (!\is_integer($offset + 0)) {
539538
return false;
540539
}
541540

@@ -544,6 +543,7 @@ private function numericOffsetExists($offset): bool
544543
} catch (IndexError $e) {
545544
return false;
546545
}
546+
547547
return \is_array($this->source)
548548
? \array_key_exists($index, $this->source)
549549
: $this->source->offsetExists($index);

tests/unit/ArrayIndexListView/IssetTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ public function testIssetSelectorTrue(array $source, array $indexes)
4747
$view = ArrayView::toView($source);
4848

4949
$this->assertTrue(isset($view[new IndexListSelector($indexes)]));
50+
$this->assertTrue(isset($view[$indexes]));
5051

5152
$subview = $view->subview(new IndexListSelector($indexes));
5253
$this->assertSame(\count($indexes), \count($subview));

tests/unit/ArrayIndexListView/ReadTest.php

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,58 @@ public function testReadByMethod(array $source, array $indexes, array $expected)
4343
/**
4444
* @dataProvider dataProviderForRead
4545
*/
46-
public function testReadByIndex(array $source, array $mask, array $expected)
46+
public function testReadByIndex(array $source, array $indexes, array $expected)
4747
{
4848
$view = ArrayView::toView($source);
49-
$subArray = $view[new IndexListSelector($mask)];
49+
$subArray = $view[new IndexListSelector($indexes)];
50+
51+
$this->assertSame($expected, $subArray);
52+
$this->assertSame(\count($expected), \count($subArray));
53+
54+
for ($i = 0; $i < \count($subArray); ++$i) {
55+
$this->assertSame($expected[$i], $subArray[$i]);
56+
}
57+
58+
for ($i = 0; $i < \count($view); ++$i) {
59+
$this->assertSame($source[$i], $view[$i]);
60+
}
61+
62+
$this->assertSame($source, $view->toArray());
63+
$this->assertSame($source, [...$view]);
64+
$this->assertSame($expected, $subArray);
65+
}
66+
67+
/**
68+
* @dataProvider dataProviderForRead
69+
*/
70+
public function testReadByArrayIndex(array $source, array $indexes, array $expected)
71+
{
72+
$view = ArrayView::toView($source);
73+
$subArray = $view[$indexes];
74+
75+
$this->assertSame($expected, $subArray);
76+
$this->assertSame(\count($expected), \count($subArray));
77+
78+
for ($i = 0; $i < \count($subArray); ++$i) {
79+
$this->assertSame($expected[$i], $subArray[$i]);
80+
}
81+
82+
for ($i = 0; $i < \count($view); ++$i) {
83+
$this->assertSame($source[$i], $view[$i]);
84+
}
85+
86+
$this->assertSame($source, $view->toArray());
87+
$this->assertSame($source, [...$view]);
88+
$this->assertSame($expected, $subArray);
89+
}
90+
91+
/**
92+
* @dataProvider dataProviderForRead
93+
*/
94+
public function testReadByArrayViewIndex(array $source, array $indexes, array $expected)
95+
{
96+
$view = ArrayView::toView($source);
97+
$subArray = $view[ArrayView::toView($indexes)];
5098

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

tests/unit/ArrayIndexListView/WriteTest.php

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,37 @@ class WriteTest extends \Codeception\Test\Unit
1010
/**
1111
* @dataProvider dataProviderForMaskSubviewWrite
1212
*/
13-
public function testWriteByIndex(array $source, array $config, array $toWrite, array $expected)
13+
public function testWriteByIndex(array $source, array $indexes, array $toWrite, array $expected)
1414
{
1515
$view = ArrayView::toView($source);
1616

17-
$view[new IndexListSelector($config)] = $toWrite;
17+
$view[new IndexListSelector($indexes)] = $toWrite;
18+
19+
$this->assertSame($expected, [...$view]);
20+
$this->assertSame($expected, $source);
21+
}
22+
23+
/**
24+
* @dataProvider dataProviderForMaskSubviewWrite
25+
*/
26+
public function testWriteByArrayIndex(array $source, array $indexes, array $toWrite, array $expected)
27+
{
28+
$view = ArrayView::toView($source);
29+
30+
$view[$indexes] = $toWrite;
31+
32+
$this->assertSame($expected, [...$view]);
33+
$this->assertSame($expected, $source);
34+
}
35+
36+
/**
37+
* @dataProvider dataProviderForMaskSubviewWrite
38+
*/
39+
public function testWriteByArrayViewIndex(array $source, array $indexes, array $toWrite, array $expected)
40+
{
41+
$view = ArrayView::toView($source);
42+
43+
$view[ArrayView::toView($indexes)] = $toWrite;
1844

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

0 commit comments

Comments
 (0)