From 34cd894b05860b5b9733054b172510b269b157bf Mon Sep 17 00:00:00 2001 From: Erik van Oosten Date: Sat, 26 Oct 2019 09:28:31 +0200 Subject: [PATCH 1/8] Iterator.takeUntilException --- .../decorators/IteratorDecorator.scala | 56 +++++++++++++++++ .../decorators/IteratorDecoratorTest.scala | 63 +++++++++++++++++++ 2 files changed, 119 insertions(+) diff --git a/src/main/scala/scala/collection/decorators/IteratorDecorator.scala b/src/main/scala/scala/collection/decorators/IteratorDecorator.scala index 632e6de..b9657a4 100644 --- a/src/main/scala/scala/collection/decorators/IteratorDecorator.scala +++ b/src/main/scala/scala/collection/decorators/IteratorDecorator.scala @@ -2,6 +2,7 @@ package scala.collection package decorators import scala.annotation.tailrec +import scala.util.control.NonFatal /** Enriches Iterator with additional methods. * @@ -225,4 +226,59 @@ class IteratorDecorator[A](val `this`: Iterator[A]) extends AnyVal { } } } + + /** Gives elements from the source iterator until the source iterator ends or throws an exception. + * + * @return an iterator that takes items until the source iterator ends or throws an exception + * @note Reuse: $consumesAndProducesIterator + */ + def takeUntilException: Iterator[A] = { + new AbstractIterator[A] { + private val wrapped = `this`.buffered + + override def hasNext: Boolean = { + try { + val n = wrapped.hasNext + // By already invoking `head` (and therefore also `next` on `this`), + // we are sure that `wrapped.next` will not throw when it is used from + // `next`. + if (n) wrapped.head + n + } catch { + case NonFatal(t) => false + } + } + + override def next(): A = wrapped.next + } + } + + /** Gives elements from the source iterator until the source iterator ends or throws an exception. + * + * @param exceptionCaught a callback invoked from `hasNext` when the source iterator throws an exception + * @return an iterator that takes items until the wrapped iterator ends or throws an exception + * @note Reuse: $consumesAndProducesIterator + */ + def takeUntilException(exceptionCaught: Throwable => Unit): Iterator[A] = { + new AbstractIterator[A] { + private val wrapped = `this`.buffered + + override def hasNext: Boolean = { + try { + val n = wrapped.hasNext + // By already invoking `head` (and therefore also `next` on `this`), + // we are sure that `wrapped.next` will not throw when it is used from + // `next`. + if (n) wrapped.head + n + } catch { + case NonFatal(t) => + exceptionCaught(t) + false + } + } + + override def next(): A = wrapped.next + } + } } diff --git a/src/test/scala/scala/collection/decorators/IteratorDecoratorTest.scala b/src/test/scala/scala/collection/decorators/IteratorDecoratorTest.scala index b435004..aa408dc 100644 --- a/src/test/scala/scala/collection/decorators/IteratorDecoratorTest.scala +++ b/src/test/scala/scala/collection/decorators/IteratorDecoratorTest.scala @@ -117,4 +117,67 @@ class IteratorDecoratorTest { Iterator((1,1), (1,2), (2,3), (1,4)).splitBy(_._1).toSeq ) } + + @Test + def takeUntilExceptionShouldWrapAnyNonThrowingIterator(): Unit = { + Assert.assertEquals(Seq(1, 2, 3, 4, 5), Iterator(1, 2, 3, 4, 5).takeUntilException.toSeq) + Assert.assertEquals(Seq(1, 2, 3, 4, 5), Iterator(1, 2, 3, 4, 5).takeUntilException(_ => ()).toSeq) + Assert.assertEquals(Seq.empty, Iterator.empty.takeUntilException.toSeq) + Assert.assertEquals(Seq.empty, Iterator.empty.takeUntilException(_ => ()).toSeq) + // Works with infinite iterators: + Assert.assertEquals(Seq(1, 2, 3, 4, 5), Iterator.from(1).takeUntilException.take(5).toSeq) + Assert.assertEquals(Seq(1, 2, 3, 4, 5), Iterator.from(1).takeUntilException(_ => ()).take(5).toSeq) + } + + @Test + def takeUntilExceptionShouldTakeTillAnExceptionFromHasNext(): Unit = { + val toThrow = new RuntimeException("~expected exception~") + def brokenIterator: Iterator[Int] = new AbstractIterator[Int] { + private var previousPosition = 0 + + override def hasNext: Boolean = { + if (previousPosition == 3) { + throw toThrow + } else { + true + } + } + + override def next(): Int = { + previousPosition += 1 + previousPosition + } + } + + Assert.assertEquals(Seq(1, 2, 3), brokenIterator.takeUntilException.toSeq) + + var caught: Throwable = null + Assert.assertEquals(Seq(1, 2, 3), brokenIterator.takeUntilException(caught = _).toSeq) + Assert.assertSame(toThrow, caught) + } + + @Test + def takeUntilExceptionShouldTakeTillAnExceptionFromNext(): Unit = { + val toThrow = new RuntimeException("~expected exception~") + def brokenIterator: Iterator[Int] = new AbstractIterator[Int] { + private var previousPosition = 0 + + override def hasNext: Boolean = true + + override def next(): Int = { + if (previousPosition == 3) { + throw toThrow + } else { + previousPosition += 1 + previousPosition + } + } + } + + Assert.assertEquals(Seq(1, 2, 3), brokenIterator.takeUntilException.toSeq) + + var caught: Throwable = null + Assert.assertEquals(Seq(1, 2, 3), brokenIterator.takeUntilException(caught = _).toSeq) + Assert.assertSame(toThrow, caught) + } } From af5e5b7b7aea7fb0e074b5eed563f7e63b3f0a3f Mon Sep 17 00:00:00 2001 From: Erik van Oosten Date: Sat, 26 Oct 2019 12:10:08 +0200 Subject: [PATCH 2/8] Iterator.takeUntilException removed duplication --- .../decorators/IteratorDecorator.scala | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/src/main/scala/scala/collection/decorators/IteratorDecorator.scala b/src/main/scala/scala/collection/decorators/IteratorDecorator.scala index aa25bea..a2a2fb7 100644 --- a/src/main/scala/scala/collection/decorators/IteratorDecorator.scala +++ b/src/main/scala/scala/collection/decorators/IteratorDecorator.scala @@ -235,24 +235,7 @@ class IteratorDecorator[A](val `this`: Iterator[A]) extends AnyVal { * @note Reuse: $consumesAndProducesIterator */ def takeUntilException: Iterator[A] = { - new AbstractIterator[A] { - private val wrapped = `this`.buffered - - override def hasNext: Boolean = { - try { - val n = wrapped.hasNext - // By already invoking `head` (and therefore also `next` on `this`), - // we are sure that `wrapped.next` will not throw when it is used from - // `next`. - if (n) wrapped.head - n - } catch { - case NonFatal(t) => false - } - } - - override def next(): A = wrapped.next - } + takeUntilException(_ => ()) } /** Gives elements from the source iterator until the source iterator ends or throws an exception. From 7980efabc738d11d6012784bd76eaf161b777728 Mon Sep 17 00:00:00 2001 From: Erik van Oosten Date: Thu, 31 Oct 2019 08:11:20 +0100 Subject: [PATCH 3/8] documented that it only stops at non fatal exceptions --- .../collection/decorators/IteratorDecorator.scala | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/main/scala/scala/collection/decorators/IteratorDecorator.scala b/src/main/scala/scala/collection/decorators/IteratorDecorator.scala index a2a2fb7..4d85007 100644 --- a/src/main/scala/scala/collection/decorators/IteratorDecorator.scala +++ b/src/main/scala/scala/collection/decorators/IteratorDecorator.scala @@ -229,19 +229,21 @@ class IteratorDecorator[A](val `this`: Iterator[A]) extends AnyVal { } } - /** Gives elements from the source iterator until the source iterator ends or throws an exception. + /** Gives elements from the source iterator until the source iterator ends or throws a NonFatal exception. * - * @return an iterator that takes items until the source iterator ends or throws an exception + * @return an iterator that takes items until the source iterator ends or throws a NonFatal exception + * @see scala.util.control.NonFatal * @note Reuse: $consumesAndProducesIterator */ def takeUntilException: Iterator[A] = { takeUntilException(_ => ()) } - /** Gives elements from the source iterator until the source iterator ends or throws an exception. + /** Gives elements from the source iterator until the source iterator ends or throws a NonFatal exception. * - * @param exceptionCaught a callback invoked from `hasNext` when the source iterator throws an exception - * @return an iterator that takes items until the wrapped iterator ends or throws an exception + * @param exceptionCaught a callback invoked from `hasNext` when the source iterator throws aNonFatal exception + * @return an iterator that takes items until the wrapped iterator ends or throws aNonFatal exception + * @see scala.util.control.NonFatal * @note Reuse: $consumesAndProducesIterator */ def takeUntilException(exceptionCaught: Throwable => Unit): Iterator[A] = { From 649ea97b61736144dda7a2b79b8d8f3016903e99 Mon Sep 17 00:00:00 2001 From: Erik van Oosten Date: Tue, 5 Nov 2019 11:10:04 +0100 Subject: [PATCH 4/8] typo in scaladocs --- .../scala/scala/collection/decorators/IteratorDecorator.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/scala/scala/collection/decorators/IteratorDecorator.scala b/src/main/scala/scala/collection/decorators/IteratorDecorator.scala index 4d85007..52b0192 100644 --- a/src/main/scala/scala/collection/decorators/IteratorDecorator.scala +++ b/src/main/scala/scala/collection/decorators/IteratorDecorator.scala @@ -241,8 +241,8 @@ class IteratorDecorator[A](val `this`: Iterator[A]) extends AnyVal { /** Gives elements from the source iterator until the source iterator ends or throws a NonFatal exception. * - * @param exceptionCaught a callback invoked from `hasNext` when the source iterator throws aNonFatal exception - * @return an iterator that takes items until the wrapped iterator ends or throws aNonFatal exception + * @param exceptionCaught a callback invoked from `hasNext` when the source iterator throws a NonFatal exception + * @return an iterator that takes items until the wrapped iterator ends or throws a NonFatal exception * @see scala.util.control.NonFatal * @note Reuse: $consumesAndProducesIterator */ From 8319c9c2a91d385a87ec0ae0ef6985ef3fea31e1 Mon Sep 17 00:00:00 2001 From: Erik van Oosten Date: Sat, 9 Nov 2019 11:22:11 +0100 Subject: [PATCH 5/8] Travis already has (an old version) of sdkman installed. Force an update. --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 6b8b848..00103f6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,6 +13,7 @@ before_install: # adding $HOME/.sdkman to cache would create an empty directory, which interferes with the initial installation - "[[ -d $HOME/.sdkman/bin ]] || rm -rf $HOME/.sdkman/" - curl -sL https://get.sdkman.io | bash + - sdk selfupdate force - echo sdkman_auto_answer=true > $HOME/.sdkman/etc/config - source "$HOME/.sdkman/bin/sdkman-init.sh" From ce5f617504a255d229defd61e212ccc6a24197e6 Mon Sep 17 00:00:00 2001 From: Erik van Oosten Date: Sat, 9 Nov 2019 11:24:28 +0100 Subject: [PATCH 6/8] Travis already has (an old version) of sdkman installed. Force an update. Attempt 2. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 00103f6..26452f5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,9 +13,9 @@ before_install: # adding $HOME/.sdkman to cache would create an empty directory, which interferes with the initial installation - "[[ -d $HOME/.sdkman/bin ]] || rm -rf $HOME/.sdkman/" - curl -sL https://get.sdkman.io | bash - - sdk selfupdate force - echo sdkman_auto_answer=true > $HOME/.sdkman/etc/config - source "$HOME/.sdkman/bin/sdkman-init.sh" + - sdk selfupdate force install: - sdk install java $(sdk list java | grep -o "$ADOPTOPENJDK\.[0-9\.]*hs-adpt" | head -1) From 9a31fca8716a3cbc0b432e9e86f480b8b9af7a45 Mon Sep 17 00:00:00 2001 From: Erik van Oosten Date: Sat, 9 Nov 2019 11:26:17 +0100 Subject: [PATCH 7/8] Travis already has (an old version) of sdkman installed. Force an update. Attempt 3. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 26452f5..ceda960 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,7 +15,7 @@ before_install: - curl -sL https://get.sdkman.io | bash - echo sdkman_auto_answer=true > $HOME/.sdkman/etc/config - source "$HOME/.sdkman/bin/sdkman-init.sh" - - sdk selfupdate force + - sdk update install: - sdk install java $(sdk list java | grep -o "$ADOPTOPENJDK\.[0-9\.]*hs-adpt" | head -1) From 4f7afdc9d5841b2ef8e9fad1027091c3e8972fb7 Mon Sep 17 00:00:00 2001 From: Erik van Oosten Date: Sat, 9 Nov 2019 11:31:30 +0100 Subject: [PATCH 8/8] Travis already has (an old version) of sdkman installed. Force an update. Attempt 4. --- .travis.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index ceda960..a3fc75a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,9 +13,8 @@ before_install: # adding $HOME/.sdkman to cache would create an empty directory, which interferes with the initial installation - "[[ -d $HOME/.sdkman/bin ]] || rm -rf $HOME/.sdkman/" - curl -sL https://get.sdkman.io | bash - - echo sdkman_auto_answer=true > $HOME/.sdkman/etc/config + - (echo sdkman_auto_answer=true; echo sdkman_auto_selfupdate=true) > $HOME/.sdkman/etc/config - source "$HOME/.sdkman/bin/sdkman-init.sh" - - sdk update install: - sdk install java $(sdk list java | grep -o "$ADOPTOPENJDK\.[0-9\.]*hs-adpt" | head -1)