Skip to content

Commit 5cf0a1f

Browse files
authored
Merge pull request #57 from xp-forge/feature/window-chunked
2 parents c6ac43c + b02c99a commit 5cf0a1f

File tree

2 files changed

+188
-12
lines changed

2 files changed

+188
-12
lines changed

src/main/php/util/data/Sequence.class.php

Lines changed: 86 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,19 @@
77
/**
88
* Sequences API for PHP
99
*
10-
* @test xp://util.data.unittest.SequenceTest
11-
* @test xp://util.data.unittest.SequenceCreationTest
12-
* @test xp://util.data.unittest.SequenceSortingTest
13-
* @test xp://util.data.unittest.SequenceCollectionTest
14-
* @test xp://util.data.unittest.SequenceConcatTest
15-
* @test xp://util.data.unittest.SequenceFilteringTest
16-
* @test xp://util.data.unittest.SequenceFlatteningTest
17-
* @test xp://util.data.unittest.SequenceIteratorTest
18-
* @test xp://util.data.unittest.SequenceMappingTest
19-
* @test xp://util.data.unittest.SequenceReductionTest
20-
* @test xp://util.data.unittest.SequenceResultSetTest
21-
* @test xp://util.data.unittest.SequenceSkipTest
10+
* @test util.data.unittest.SequenceTest
11+
* @test util.data.unittest.SequenceCreationTest
12+
* @test util.data.unittest.SequenceSortingTest
13+
* @test util.data.unittest.SequenceCollectionTest
14+
* @test util.data.unittest.SequenceConcatTest
15+
* @test util.data.unittest.SequenceFilteringTest
16+
* @test util.data.unittest.SequenceFlatteningTest
17+
* @test util.data.unittest.SequenceIteratorTest
18+
* @test util.data.unittest.SequenceMappingTest
19+
* @test util.data.unittest.SequenceReductionTest
20+
* @test util.data.unittest.SequenceResultSetTest
21+
* @test util.data.unittest.SequenceSkipTest
22+
* @test util.data.unittest.SequenceWindowTest
2223
*/
2324
class Sequence implements Value, IteratorAggregate {
2425
public static $EMPTY;
@@ -642,6 +643,79 @@ public function sorted($comparator= null) {
642643
return new self($sort);
643644
}
644645

646+
/**
647+
* Returns a chunked stream with chunks not exceeding the given size.
648+
* The last chunk may have a smaller size.
649+
*
650+
* Calling `chunked($n)` is the same as `windowed($n, $n, true)` but
651+
* uses a much simpler algorithm internally.
652+
*
653+
* @param int $size
654+
* @return self
655+
* @throws lang.IllegalArgumentException
656+
*/
657+
public function chunked($size) {
658+
if ($size <= 0) {
659+
throw new IllegalArgumentException('Size must be greater than zero');
660+
}
661+
662+
$f= function() use($size) {
663+
$chunk= [];
664+
foreach ($this->elements as $element) {
665+
$chunk[]= $element;
666+
if (sizeof($chunk) < $size) continue;
667+
668+
yield $chunk;
669+
$chunk= [];
670+
}
671+
if ($chunk) yield $chunk;
672+
};
673+
return new self($f());
674+
}
675+
676+
/**
677+
* Returns a sliding window stream - a list of element ranges that you
678+
* would see if you were looking at the collection through a sliding
679+
* window of the given size.
680+
*
681+
* @param int $size
682+
* @param int $step
683+
* @param bool $partial
684+
* @return self
685+
* @throws lang.IllegalArgumentException
686+
*/
687+
public function windowed($size, $step= 1, $partial= false) {
688+
if ($size <= 0 || $step <= 0) {
689+
throw new IllegalArgumentException('Both size and step must be greater than zero');
690+
}
691+
692+
$f= function() use($size, $step, $partial) {
693+
$it= $this->getIterator();
694+
$chunk= [];
695+
do {
696+
697+
// Fetch $size elements and yield them as a chunk
698+
while (sizeof($chunk) < $size && $it->valid()) {
699+
$chunk[]= $it->current();
700+
$it->next();
701+
}
702+
if ($chunk && $partial || $size === sizeof($chunk)) yield $chunk;
703+
704+
// Step forward, potentially skipping some elements
705+
$s= $step - sizeof($chunk);
706+
if ($s > 0) {
707+
while ($s-- > 0 && $it->valid()) $it->next();
708+
$chunk= [];
709+
} else {
710+
$chunk= array_slice($chunk, $step);
711+
}
712+
} while ($it->valid());
713+
714+
if ($chunk && $partial) yield $chunk;
715+
};
716+
return new self($f());
717+
}
718+
645719
/** @return string */
646720
public function hashCode() {
647721
return 'S'.Objects::hashOf($this->elements);
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
<?php namespace util\data\unittest;
2+
3+
use lang\IllegalArgumentException;
4+
use test\{Assert, Expect, Test, Values};
5+
use util\data\Sequence;
6+
7+
class SequenceWindowTest extends AbstractSequenceTest {
8+
9+
#[Test]
10+
public function chunked() {
11+
$this->assertSequence(
12+
[[1, 2], [3, 4]],
13+
Sequence::of([1, 2, 3, 4])->chunked(2)
14+
);
15+
}
16+
17+
#[Test]
18+
public function chunked_with_partial() {
19+
$this->assertSequence(
20+
[[1, 2], [3, 4], [5]],
21+
Sequence::of([1, 2, 3, 4, 5])->chunked(2)
22+
);
23+
}
24+
25+
#[Test]
26+
public function sliding_window() {
27+
$this->assertSequence(
28+
[[1, 2], [2, 3], [3, 4]],
29+
Sequence::of([1, 2, 3, 4])->windowed(2)
30+
);
31+
}
32+
33+
#[Test]
34+
public function window_size_equal_to_step() {
35+
$this->assertSequence(
36+
[[1, 2], [3, 4]],
37+
Sequence::of([1, 2, 3, 4])->windowed(2, 2)
38+
);
39+
}
40+
41+
#[Test]
42+
public function skip_elements() {
43+
$this->assertSequence(
44+
[[1, 2], [4, 5]],
45+
Sequence::of([1, 2, 3, 4, 5])->windowed(2, 3)
46+
);
47+
}
48+
49+
#[Test, Values([[true, [[1, 2], [3, 4], [5]]], [false, [[1, 2], [3, 4]]]])]
50+
public function partial_window($flag, $expected) {
51+
$this->assertSequence(
52+
$expected,
53+
Sequence::of([1, 2, 3, 4, 5])->windowed(2, 2, $flag)
54+
);
55+
}
56+
57+
#[Test]
58+
public function partial_window_multiple_partials_at_end() {
59+
$this->assertSequence(
60+
[[1, 2, 3, 4, 5], [4, 5, 6, 7, 8], [7, 8, 9, 10], [10]],
61+
Sequence::of([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])->windowed(5, 3, true)
62+
);
63+
}
64+
65+
#[Test]
66+
public function skip_elements_and_return_partial() {
67+
$this->assertSequence(
68+
[[1, 2], [5]],
69+
Sequence::of([1, 2, 3, 4, 5])->windowed(2, 4, true)
70+
);
71+
}
72+
73+
#[Test, Values([[[], []], [[1], [[1]]]])]
74+
public function chunked_edge_cases($elements, $expected) {
75+
$this->assertSequence($expected, Sequence::of($elements)->chunked(2));
76+
}
77+
78+
#[Test, Values([[[], []], [[1], []]])]
79+
public function windowed_edge_cases_without_partial($elements, $expected) {
80+
$this->assertSequence($expected, Sequence::of($elements)->windowed(2, 1, false));
81+
}
82+
83+
#[Test, Values([[[], []], [[1], [[1]]]])]
84+
public function windowed_edge_cases_with_partial($elements, $expected) {
85+
$this->assertSequence($expected, Sequence::of($elements)->windowed(2, 1, true));
86+
}
87+
88+
#[Test, Expect(IllegalArgumentException::class), Values([-1, 0])]
89+
public function chunk_size_must_greater_than_zero($size) {
90+
Sequence::of([1])->chunked($size);
91+
}
92+
93+
#[Test, Expect(IllegalArgumentException::class), Values([-1, 0])]
94+
public function window_size_must_greater_than_zero($size) {
95+
Sequence::of([1])->windowed($size);
96+
}
97+
98+
#[Test, Expect(IllegalArgumentException::class), Values([-1, 0])]
99+
public function window_step_must_greater_than_zero($step) {
100+
Sequence::of([1])->windowed(1, $step);
101+
}
102+
}

0 commit comments

Comments
 (0)