From e4a0120c04cc64c686e8fd6defc5f4795b31ac3f Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 31 Oct 2017 11:06:27 +0100 Subject: [PATCH 1/3] Initial implementation of fluent interface to catch exceptions --- src/main/php/util/data/Sequence.class.php | 303 +++++++++++++----- .../util/data/unittest/CatchTest.class.php | 50 +++ .../php/util/data/unittest/PeekTest.class.php | 2 +- .../unittest/SequenceCreationTest.class.php | 2 +- .../unittest/SequenceIteratorTest.class.php | 4 +- .../util/data/unittest/SequenceTest.class.php | 8 +- 6 files changed, 277 insertions(+), 92 deletions(-) create mode 100755 src/test/php/util/data/unittest/CatchTest.class.php diff --git a/src/main/php/util/data/Sequence.class.php b/src/main/php/util/data/Sequence.class.php index 9ab7161..66c95ff 100755 --- a/src/main/php/util/data/Sequence.class.php +++ b/src/main/php/util/data/Sequence.class.php @@ -26,7 +26,8 @@ class Sequence implements \lang\Value, \IteratorAggregate { public static $EMPTY; - protected $elements; + private $elements; + private $error= null; static function __static() { self::$EMPTY= new self([]); @@ -43,10 +44,14 @@ public function iterator() { return new SequenceIterator($this); } /** @return iterable */ public function getIterator() { foreach ($this->elements as $key => $element) { + if ($this->error) throw $this->error; yield $key => $element; } } + /** @return self */ + public static function empty() { return new self([]); } + /** * Creates a new stream with an enumeration of elements * @@ -111,8 +116,8 @@ public static function generate($supplier) { * @throws util.data.CannotReset if streamed and invoked more than once */ public function first($filter= null) { - $instance= $filter ? $this->filter($filter) : $this; - foreach ($instance->elements as $element) { + $filter && $this->filter($filter); + foreach ($this->elements as $element) { return new Optional($element); } return Optional::$EMPTY; @@ -126,9 +131,10 @@ public function first($filter= null) { * @throws lang.IllegalArgumentException if streamed and invoked more than once */ public function toArray($map= null) { - $instance= $map ? $this->map($map) : $this; + $map && $this->map($map); $return= []; - foreach ($instance->elements as $element) { + foreach ($this->elements as $element) { + if ($this->error) throw $this->error; $return[]= $element; } return $return; @@ -142,9 +148,10 @@ public function toArray($map= null) { * @throws lang.IllegalArgumentException if streamed and invoked more than once */ public function toMap($map= null) { - $instance= $map ? $this->map($map) : $this; + $map && $this->map($map); $return= []; - foreach ($instance->elements as $key => $element) { + foreach ($this->elements as $key => $element) { + if ($this->error) throw $this->error; $return[$key]= $element; } return $return; @@ -159,6 +166,7 @@ public function toMap($map= null) { public function count() { $return= 0; foreach ($this->elements as $element) { + if ($this->error) throw $this->error; $return++; } return $return; @@ -199,6 +207,7 @@ public function reduce($identity, $accumulator) { $closure= Functions::$BINARYOP->newInstance($accumulator); $return= $identity; foreach ($this->elements as $element) { + if ($this->error) throw $this->error; $return= $closure($return, $element); } return $return; @@ -215,13 +224,16 @@ public function collect(ICollector $collector) { $accumulator= $collector->accumulator(); $finisher= $collector->finisher(); + $elements= $this->elements; $return= $collector->supplier()->__invoke(); if (Functions::$CONSUME_WITH_KEY->isInstance($accumulator)) { foreach ($this->elements as $key => $element) { + if ($this->error) throw $this->error; $accumulator($return, $element, $key); } } else { foreach ($this->elements as $element) { + if ($this->error) throw $this->error; $accumulator($return, $element); } } @@ -240,15 +252,30 @@ public function each($consumer= null, $args= null) { $i= 0; if (null !== $args) { $inv= Functions::$RECV->newInstance($consumer); - foreach ($this->elements as $element) { $inv($element, ...$args); $i++; } + foreach ($this->elements as $element) { + if ($this->error) throw $this->error; + $inv($element, ...$args); + $i++; + } } else if (Functions::$RECV_WITH_KEY->isInstance($consumer)) { $inv= Functions::$RECV_WITH_KEY->cast($consumer); - foreach ($this->elements as $key => $element) { $inv($element, $key); $i++; } + foreach ($this->elements as $key => $element) { + if ($this->error) throw $this->error; + $inv($element, $key); + $i++; + } } else if (null !== $consumer) { $inv= Functions::$RECV->newInstance($consumer); - foreach ($this->elements as $element) { $inv($element); $i++; } + foreach ($this->elements as $element) { + if ($this->error) throw $this->error; + $inv($element); + $i++; + } } else { - foreach ($this->elements as $element) { $i++; } + foreach ($this->elements as $element) { + if ($this->error) throw $this->error; + $i++; + } } return $i; } @@ -261,34 +288,36 @@ public function each($consumer= null, $args= null) { * @throws lang.IllegalArgumentException */ public function limit($arg) { + $elements= $this->elements; + if (is_numeric($arg)) { $max= (int)$arg; - $f= function() use($max) { + $f= function() use($elements, $max) { $i= 0; - foreach ($this->elements as $key => $element) { + foreach ($elements as $key => $element) { if (++$i > $max) break; yield $key => $element; } }; } else if (Functions::$APPLY_WITH_KEY->isInstance($arg)) { $limit= Functions::$APPLY_WITH_KEY->cast($arg); - $f= function() use($limit) { - foreach ($this->elements as $key => $element) { + $f= function() use($elements, $limit) { + foreach ($elements as $key => $element) { if ($limit($element, $key)) break; yield $key => $element; } }; } else { $limit= Functions::$APPLY->newInstance($arg); - $f= function() use($limit) { - foreach ($this->elements as $key => $element) { + $f= function() use($elements, $limit) { + foreach ($elements as $key => $element) { if ($limit($element)) break; yield $key => $element; } }; } - - return new self($f()); + $this->elements= $f(); + return $this; } /** @@ -299,19 +328,21 @@ public function limit($arg) { * @throws lang.IllegalArgumentException */ public function skip($arg) { + $elements= $this->elements; + if (is_numeric($arg)) { $max= (int)$arg; - $f= function() use($max) { + $f= function() use($elements, $max) { $i= 0; - foreach ($this->elements as $key => $element) { + foreach ($elements as $key => $element) { if (++$i > $max) yield $key => $element; } }; } else if (Functions::$APPLY_WITH_KEY->isInstance($arg)) { $skip= Functions::$APPLY_WITH_KEY->cast($arg); - $f= function() use($skip) { + $f= function() use($elements, $skip) { $skipping= true; - foreach ($this->elements as $key => $element) { + foreach ($elements as $key => $element) { if ($skipping) { if ($skip($element, $key)) continue; $skipping= false; @@ -321,9 +352,9 @@ public function skip($arg) { }; } else { $skip= Functions::$APPLY->newInstance($arg); - $f= function() use($skip) { + $f= function() use($elements, $skip) { $skipping= true; - foreach ($this->elements as $key => $element) { + foreach ($elements as $key => $element) { if ($skipping) { if ($skip($element)) continue; $skipping= false; @@ -332,8 +363,8 @@ public function skip($arg) { } }; } - - return new self($f()); + $this->elements= $f(); + return $this; } /** @@ -344,29 +375,53 @@ public function skip($arg) { * @throws lang.IllegalArgumentException */ public function filter($predicate) { + $elements= $this->elements; + if ($predicate instanceof Filter || is('util.Filter', $predicate)) { - $f= function() use($predicate) { - foreach ($this->elements as $key => $element) { - if ($predicate->accept($element)) yield $key => $element; + $f= function() use($elements, $predicate) { + foreach ($elements as $key => $element) { + try { + if ($predicate->accept($element)) yield $key => $element; + } catch (\Throwable $t) { + $this->error= $t; + } catch (\Exception $e) { + $this->error= $e; + } } }; } else if (Functions::$APPLY_WITH_KEY->isInstance($predicate)) { $filter= Functions::$APPLY_WITH_KEY->cast($predicate); - $f= function() use($filter) { - foreach ($this->elements as $key => $element) { - if ($filter($element, $key)) yield $key => $element; + $f= function() use($elements, $filter) { + foreach ($elements as $key => $element) { + $keep= true; + try { + $keep= $filter($element, $key); + } catch (\Throwable $t) { + $this->error= $t; + } catch (\Exception $e) { + $this->error= $e; + } + $keep && yield $key => $element; } }; } else { $filter= Functions::$APPLY->newInstance($predicate); - $f= function() use($filter) { - foreach ($this->elements as $key => $element) { - if ($filter($element)) yield $key => $element; + $f= function() use($elements, $filter) { + foreach ($elements as $key => $element) { + $keep= true; + try { + $keep= $filter($element); + } catch (\Throwable $t) { + $this->error= $t; + } catch (\Exception $e) { + $this->error= $e; + } + $keep && yield $key => $element; } }; } - - return new self($f()); + $this->elements= $f(); + return $this; } /** @@ -377,33 +432,49 @@ public function filter($predicate) { * @throws lang.IllegalArgumentException */ public function map($function) { + $elements= $this->elements; + if (Functions::$APPLY_WITH_KEY->isInstance($function)) { - $mapper= Functions::$APPLY_WITH_KEY->cast($function); - $f= function() use($mapper) { - foreach ($this->elements as $key => $element) { - $mapped= $mapper($element, $key); + $m= Functions::$APPLY_WITH_KEY->cast($function); + $f= function() use($elements, $m) { + foreach ($elements as $key => $element) { + $mapped= null; + try { + $mapped= $m($element, $key); + } catch (\Throwable $t) { + $this->error= $t; + } catch (\Exception $e) { + $this->error= $e; + } if ($mapped instanceof \Generator) { - foreach ($mapped as $key => $value) { yield $key => $value; } + foreach ($mapped as $k => $v) { yield $k => $v; } } else { yield $key => $mapped; } } }; } else { - $mapper= Functions::$APPLY->newInstance($function); - $f= function() use($mapper) { - foreach ($this->elements as $key => $element) { - $mapped= $mapper($element); + $m= Functions::$APPLY->newInstance($function); + $f= function() use($elements, $m) { + foreach ($elements as $key => $element) { + $mapped= null; + try { + $mapped= $m($element); + } catch (\Throwable $t) { + $this->error= $t; + } catch (\Exception $e) { + $this->error= $e; + } if ($mapped instanceof \Generator) { - foreach ($mapped as $key => $value) { yield $key => $value; } + foreach ($mapped as $k => $v) { yield $k => $v; } } else { yield $key => $mapped; } } }; } - - return new self($f()); + $this->elements= $f(); + return $this; } /** @@ -415,29 +486,31 @@ public function map($function) { * @throws lang.IllegalArgumentException */ public function flatten($function= null) { + $elements= $this->elements; + if (null === $function) { - $f= function() { - foreach ($this->elements as $element) { + $f= function() use($elements) { + foreach ($elements as $element) { foreach ($element as $k => $v) { yield $k => $v; } } }; } else if (Functions::$APPLY_WITH_KEY->isInstance($function)) { $mapper= Functions::$APPLY_WITH_KEY->cast($function); - $f= function() use($mapper) { - foreach ($this->elements as $key => $element) { + $f= function() use($elements, $mapper) { + foreach ($elements as $key => $element) { foreach ($mapper($element, $key) as $k => $v) { yield $k => $v; } } }; } else { $mapper= Functions::$APPLY->newInstance($function); - $f= function() use($mapper) { - foreach ($this->elements as $key => $element) { + $f= function() use($elements, $mapper) { + foreach ($elements as $key => $element) { foreach ($mapper($element) as $k => $v) { yield $k => $v; } } }; } - - return new self($f()); + $this->elements= $f(); + return $this; } /** @@ -450,33 +523,53 @@ public function flatten($function= null) { * @throws lang.IllegalArgumentException */ public function peek($action, $args= null) { + $elements= $this->elements; + if (null !== $args) { $peek= Functions::$RECV->newInstance($action); - $f= function() use($peek, $args) { - foreach ($this->elements as $key => $element) { - $peek($element, ...$args); + $f= function() use($elements, $peek, $args) { + foreach ($elements as $key => $element) { + try { + $peek($element, ...$args); + } catch (\Throwable $t) { + $this->error= $t; + } catch (\Exception $e) { + $this->error= $e; + } yield $key => $element; } }; } else if (Functions::$RECV_WITH_KEY->isInstance($action)) { $peek= Functions::$RECV_WITH_KEY->cast($action); - $f= function() use($peek) { - foreach ($this->elements as $key => $element) { - $peek($element, $key); + $f= function() use($elements, $peek) { + foreach ($elements as $key => $element) { + try { + $peek($element, $key); + } catch (\Throwable $t) { + $this->error= $t; + } catch (\Exception $e) { + $this->error= $e; + } yield $key => $element; } }; } else { $peek= Functions::$RECV->newInstance($action); - $f= function() use($peek) { - foreach ($this->elements as $key => $element) { - $peek($element); + $f= function() use($elements, $peek) { + foreach ($elements as $key => $element) { + try { + $peek($element); + } catch (\Throwable $t) { + $this->error= $t; + } catch (\Exception $e) { + $this->error= $e; + } yield $key => $element; } }; } - - return new self($f()); + $this->elements= $f(); + return $this; } /** @@ -492,26 +585,39 @@ public function collecting(&$return, ICollector $collector) { $accumulator= $collector->accumulator(); $finisher= $collector->finisher(); + $elements= $this->elements; $return= $collector->supplier()->__invoke(); if (Functions::$CONSUME_WITH_KEY->isInstance($accumulator)) { - $f= function() use(&$return, $accumulator, $finisher) { - foreach ($this->elements as $key => $element) { - $accumulator($return, $element, $key); + $f= function() use($elements, &$return, $accumulator, $finisher) { + foreach ($elements as $key => $element) { + try { + $accumulator($return, $element, $key); + } catch (\Throwable $t) { + $this->error= $t; + } catch (\Exception $e) { + $this->error= $e; + } yield $key => $element; } $finisher && $return= $finisher($return); }; } else { - $f= function() use(&$return, $accumulator, $finisher) { - foreach ($this->elements as $key => $element) { - $accumulator($return, $element); + $f= function() use($elements, &$return, $accumulator, $finisher) { + foreach ($elements as $key => $element) { + try { + $accumulator($return, $element); + } catch (\Throwable $t) { + $this->error= $t; + } catch (\Exception $e) { + $this->error= $e; + } yield $key => $element; } $finisher && $return= $finisher($return); }; } - - return new self($f()); + $this->elements= $f(); + return $this; } /** @@ -523,14 +629,15 @@ public function collecting(&$return, ICollector $collector) { * @return self */ public function counting(&$count) { - $f= function() use(&$count) { - foreach ($this->elements as $key => $element) { + $elements= $this->elements; + $f= function() use($elements, &$count) { + foreach ($elements as $key => $element) { $count++; yield $key => $element; } }; - - return new self($f()); + $this->elements= $f(); + return $this; } /** @@ -541,16 +648,44 @@ public function counting(&$count) { */ public function distinct($function= null) { $hash= Functions::$APPLY->newInstance($function ?: 'util.Objects::hashOf'); - return self::of(function() use($hash) { + $elements= $this->elements; + $f= function() use($elements, $hash) { $set= []; - foreach ($this->elements as $e) { + foreach ($elements as $e) { $h= $hash($e); if (!isset($set[$h])) { $set[$h]= true; yield $e; } } - }); + }; + $this->elements= $f(); + return $this; + } + + /** + * Catches an exception + * + * @param function(lang.Throwable): var $handler + * @return self + */ + public function catch($handler) { + $elements= $this->elements; + $f= function() use($elements, $handler) { + foreach ($elements as $key => $element) { + if ($this->error) { + $result= $handler($this->error); + $this->error= null; + if ($result instanceof \Generator) { + foreach ($result as $k => $v) { yield $k => $v; } + } + } else { + yield $key => $element; + } + } + }; + $this->elements= $f(); + return $this; } /** diff --git a/src/test/php/util/data/unittest/CatchTest.class.php b/src/test/php/util/data/unittest/CatchTest.class.php new file mode 100755 index 0000000..9f592cd --- /dev/null +++ b/src/test/php/util/data/unittest/CatchTest.class.php @@ -0,0 +1,50 @@ +peek(function($i) { if (0 === $i % 2) throw new IllegalStateException('Only odd numbers expected'); }) + ; + $fixture->toArray(); + } + + #[@test] + public function catch_nothing() { + $fixture= Sequence::of([1, 2, 3]) + ->catch(function($e) { throw new IllegalStateException('Should not be reached'); }) + ; + $this->assertEquals([1, 2, 3], $fixture->toArray()); + } + + #[@test] + public function catch_exception_from_peek() { + $fixture= Sequence::of([1, 2, 3]) + ->peek(function($i) { if (0 === $i % 2) throw new IllegalStateException('Only odd numbers expected'); }) + ->catch(function($e) { yield -1; }) + ; + $this->assertEquals([1, -1, 3], $fixture->toArray()); + } + + #[@test] + public function catch_exception_from_map() { + $fixture= Sequence::of([1, 2, 3]) + ->map(function($i) { if (0 === $i % 2) throw new IllegalStateException('Only odd numbers expected'); return $i; }) + ->catch(function($e) { yield -1; }) + ; + $this->assertEquals([1, -1, 3], $fixture->toArray()); + } + + #[@test] + public function catch_exception_from_filter() { + $fixture= Sequence::of([1, 2, 3]) + ->filter(function($i) { if (0 === $i % 2) throw new IllegalStateException('Only odd numbers expected'); return true; }) + ->catch(function($e) { yield -1; }) + ; + $this->assertEquals([1, -1, 3], $fixture->toArray()); + } +} \ No newline at end of file diff --git a/src/test/php/util/data/unittest/PeekTest.class.php b/src/test/php/util/data/unittest/PeekTest.class.php index fd875a7..024c898 100755 --- a/src/test/php/util/data/unittest/PeekTest.class.php +++ b/src/test/php/util/data/unittest/PeekTest.class.php @@ -10,7 +10,7 @@ class PeekTest extends AbstractSequenceTest { #[@test, @expect(IllegalArgumentException::class), @values(['@non-existant-func@'])] public function invalid($arg) { - Sequence::$EMPTY->peek($arg); + Sequence::empty()->peek($arg); } #[@test] diff --git a/src/test/php/util/data/unittest/SequenceCreationTest.class.php b/src/test/php/util/data/unittest/SequenceCreationTest.class.php index 0d74001..37a8d6c 100755 --- a/src/test/php/util/data/unittest/SequenceCreationTest.class.php +++ b/src/test/php/util/data/unittest/SequenceCreationTest.class.php @@ -48,7 +48,7 @@ public function invalid_type_for_generate($input) { #[@test] public function passing_null_to_of_yields_an_empty_sequence() { - $this->assertEquals(Sequence::$EMPTY, Sequence::of(null)); + $this->assertEquals(Sequence::empty(), Sequence::of(null)); } #[@test] diff --git a/src/test/php/util/data/unittest/SequenceIteratorTest.class.php b/src/test/php/util/data/unittest/SequenceIteratorTest.class.php index 5774dd2..0575514 100755 --- a/src/test/php/util/data/unittest/SequenceIteratorTest.class.php +++ b/src/test/php/util/data/unittest/SequenceIteratorTest.class.php @@ -33,12 +33,12 @@ public function next() { #[@test] public function hasNext_returns_false_when_at_end_of_sequence() { - $this->assertFalse(Sequence::$EMPTY->iterator()->hasNext()); + $this->assertFalse(Sequence::empty()->iterator()->hasNext()); } #[@test, @expect(NoSuchElementException::class)] public function next_throws_exception_when_at_end_of_sequence() { - Sequence::$EMPTY->iterator()->next(); + Sequence::empty()->iterator()->next(); } #[@test, @values('util.data.unittest.Enumerables::validArrays')] diff --git a/src/test/php/util/data/unittest/SequenceTest.class.php b/src/test/php/util/data/unittest/SequenceTest.class.php index 579f83d..1607cdd 100755 --- a/src/test/php/util/data/unittest/SequenceTest.class.php +++ b/src/test/php/util/data/unittest/SequenceTest.class.php @@ -28,12 +28,12 @@ protected function assertNotTwice($seq, $func) { #[@test] public function empty_sequence() { - $this->assertSequence([], Sequence::$EMPTY); + $this->assertSequence([], Sequence::empty()); } #[@test] public function toArray_for_empty_sequence() { - $this->assertEquals([], Sequence::$EMPTY->toArray()); + $this->assertEquals([], Sequence::empty()->toArray()); } #[@test, @values('util.data.unittest.Enumerables::validArrays')] @@ -51,7 +51,7 @@ public function toArray_optionally_accepts_mapper() { #[@test] public function toMap_for_empty_sequence() { - $this->assertEquals([], Sequence::$EMPTY->toMap()); + $this->assertEquals([], Sequence::empty()->toMap()); } #[@test, @values('util.data.unittest.Enumerables::validMaps')] @@ -186,7 +186,7 @@ function($r, $e) { /* Intentionally empty */ }); #[@test] public function toString_for_empty_sequence() { - $this->assertEquals('util.data.Sequence', Sequence::$EMPTY->toString()); + $this->assertEquals('util.data.Sequence', Sequence::empty()->toString()); } #[@test] From 6f81a7c4e08a1b0b7678715ab8f15a26d93d2950 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 31 Oct 2017 11:10:26 +0100 Subject: [PATCH 2/3] Remove unused lang.Object import --- src/test/php/util/data/unittest/AbstractSequenceTest.class.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/php/util/data/unittest/AbstractSequenceTest.class.php b/src/test/php/util/data/unittest/AbstractSequenceTest.class.php index 92deabf..3ac802e 100755 --- a/src/test/php/util/data/unittest/AbstractSequenceTest.class.php +++ b/src/test/php/util/data/unittest/AbstractSequenceTest.class.php @@ -1,6 +1,5 @@ Date: Tue, 31 Oct 2017 11:11:02 +0100 Subject: [PATCH 3/3] Remove PHP 5.6 and HHVM for the moment --- .travis.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 6765634..93163e1 100755 --- a/.travis.yml +++ b/.travis.yml @@ -5,10 +5,9 @@ language: php dist: trusty php: - - 5.6 - 7.0 - 7.1 - - hhvm + - 7.2 - nightly matrix: