Skip to content

Commit 5ed575d

Browse files
authored
loneElement and loneElementOption: extension methods for Iterable (#339)
1 parent c522906 commit 5ed575d

File tree

3 files changed

+103
-0
lines changed

3 files changed

+103
-0
lines changed
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package flatgraph.traversal
2+
3+
class IterableOnceExtension[A](val iterable: IterableOnce[A]) extends AnyVal {
4+
5+
/** @see {{{loneElement(hint)}}} */
6+
def loneElement: A =
7+
loneElement(hint = "")
8+
9+
/** @return
10+
* the one and only element from an Iterable
11+
* @throws NoSuchElementException
12+
* if the Iterable is empty
13+
* @throws AssertionError
14+
* if the Iterable has more than one element
15+
*/
16+
def loneElement(hint: String): A = {
17+
lazy val hintMaybe =
18+
if (hint.isEmpty) ""
19+
else s" Hint: $hint"
20+
21+
val iter = iterable.iterator
22+
if (iter.isEmpty) {
23+
throw new NoSuchElementException(s"Iterable was expected to have exactly one element, but it is empty.$hintMaybe")
24+
} else {
25+
val res = iter.next()
26+
if (iter.hasNext) {
27+
val collectionSizeHint = iterable.knownSize match {
28+
case -1 => "it has more than one" // cannot be computed cheaply, i.e. without traversing the collection
29+
case knownSize => s"it has $knownSize"
30+
}
31+
throw new AssertionError(s"Iterable was expected to have exactly one element, but $collectionSizeHint.$hintMaybe")
32+
}
33+
res
34+
}
35+
}
36+
37+
/** @return
38+
* {{{Some(element)}}} if the Iterable has exactly one element, or {{{None}}} if the Iterable has zero or more than 1 element. Note: if
39+
* the lone element is {{{null}}}, this will return {{{Some(null)}}}, which is in accordance with how {{{headOption}}} works.
40+
*/
41+
def loneElementOption: Option[A] = {
42+
val iter = iterable.iterator
43+
if (iter.isEmpty) {
44+
None
45+
} else {
46+
val result = iter.next()
47+
if (iter.hasNext) {
48+
None
49+
} else {
50+
Some(result)
51+
}
52+
}
53+
}
54+
}

core/src/main/scala/flatgraph/traversal/Language.scala

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ trait language {
2828

2929
implicit def iteratorToNumericSteps[A: Numeric](iter: IterableOnce[A]): NumericSteps[A] =
3030
new NumericSteps[A](iter)
31+
32+
implicit def iterableOnceExtension[A](iter: IterableOnce[A]): IterableOnceExtension[A] =
33+
new IterableOnceExtension[A](iter)
3134
}
3235

3336
@Traversal(elementType = classOf[AnyRef])
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package flatgraph.traversal
2+
3+
import flatgraph.traversal.language.*
4+
import org.scalatest.matchers.should.Matchers
5+
import org.scalatest.wordspec.AnyWordSpec
6+
7+
import scala.collection.mutable.ArrayBuffer
8+
9+
class IterableOnceExtensionTests extends AnyWordSpec with Matchers {
10+
11+
"loneElement returns the one and only element from an Iterable, and throws an exception otherwise" in {
12+
Seq(1).loneElement shouldBe 1
13+
Seq(1).loneElement("some context") shouldBe 1
14+
Seq(null).loneElement shouldBe null
15+
16+
intercept[NoSuchElementException] {
17+
Seq.empty.loneElement
18+
}.getMessage should include("it is empty")
19+
20+
intercept[NoSuchElementException] {
21+
Seq.empty.loneElement("some context")
22+
}.getMessage should include("it is empty. Hint: some context")
23+
24+
intercept[AssertionError] {
25+
Seq(1, 2).loneElement
26+
}.getMessage should include("it has more than one")
27+
28+
intercept[AssertionError] {
29+
ArrayBuffer(1, 2).loneElement
30+
}.getMessage should include("it has 2") // ArrayBuffer can 'cheaply' compute their size, so we can have it in the exception message
31+
32+
intercept[AssertionError] {
33+
Seq(1, 2).loneElement("some context")
34+
}.getMessage should include("it has more than one. Hint: some context")
35+
}
36+
37+
"loneElementOption returns an Option of the one and only element from an Iterable, or else None" in {
38+
Seq(1).loneElementOption shouldBe Some(1)
39+
Seq(null).loneElementOption shouldBe Some(null)
40+
41+
Seq.empty.loneElementOption shouldBe None
42+
43+
Seq(1, 2).loneElementOption shouldBe None
44+
}
45+
46+
}

0 commit comments

Comments
 (0)