From a8137c6ee418eaf3ffbd1f5b3068d45adc8cf48f Mon Sep 17 00:00:00 2001 From: kageru Date: Mon, 7 Oct 2019 23:19:48 +0200 Subject: [PATCH] Add Sequence.coalesce() --- .gitignore | 2 + .../kotlin/moe/kageru/sekwences/Sekwences.kt | 47 ++++++++++++++++++ .../moe/kageru/sekwences/SekwencesTest.kt | 48 +++++++++++++++++++ 3 files changed, 97 insertions(+) create mode 100644 src/main/kotlin/moe/kageru/sekwences/Sekwences.kt create mode 100644 src/test/kotlin/moe/kageru/sekwences/SekwencesTest.kt diff --git a/.gitignore b/.gitignore index 3731e5c..1a5e63f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ gradle* .gradle/ +build/ +out/ diff --git a/src/main/kotlin/moe/kageru/sekwences/Sekwences.kt b/src/main/kotlin/moe/kageru/sekwences/Sekwences.kt new file mode 100644 index 0000000..0e675fc --- /dev/null +++ b/src/main/kotlin/moe/kageru/sekwences/Sekwences.kt @@ -0,0 +1,47 @@ +package moe.kageru.sekwences + +/** + * Returns a sequences that optionally merges adjacent elements. + * Heavily inspired by Itertools.coalesce() from the Rust crate. + * + * https://docs.rs/itertools/0.8.0/itertools/trait.Itertools.html#method.coalesce + */ +fun Sequence.coalesce(merger: (T, T) -> T?): Sequence { + return if (this.iterator().hasNext()) { + CoalescingSequence(this, merger) + } else { + emptySequence() + } +} + +class CoalescingSequence(private val source: Sequence, private inline val merger: (T, T) -> T?) : Sequence { + override fun iterator(): Iterator { + return CoalescingIterator(source.iterator(), merger) + } +} + +class CoalescingIterator(private val source: Iterator, private inline val merger: (T, T) -> T?) : Iterator { + private var previous: T = if (source.hasNext()) source.next() else error("Please don’t pass empty iterators in here") + private var hasNext = true + + override fun hasNext() = hasNext + + override tailrec fun next(): T { + if (!source.hasNext()) { + hasNext = false + return previous + } + + val current = source.next() + val merged = merger(previous, current) + // If the elements can’t be merged, return the first, then save the second for next time. + return if (merged == null) { + val ret = previous + previous = current + ret + } else { + previous = merged + next() + } + } +} diff --git a/src/test/kotlin/moe/kageru/sekwences/SekwencesTest.kt b/src/test/kotlin/moe/kageru/sekwences/SekwencesTest.kt new file mode 100644 index 0000000..a5440e1 --- /dev/null +++ b/src/test/kotlin/moe/kageru/sekwences/SekwencesTest.kt @@ -0,0 +1,48 @@ +package moe.kageru.sekwences + +import io.kotlintest.shouldBe +import io.kotlintest.specs.ShouldSpec + +class SekwencesTest : ShouldSpec({ + "should merge numbers" { + val input = sequenceOf(1, 2, 3, 4, 5, 6, 4) + val output = input.coalesce { first, second -> + val sum = first + second + if (sum <= 10) sum else null + }.toList() + output shouldBe listOf(10, 5, 10) + } + + "should merge collections" { + val input = sequenceOf("apple", "apricot", "banana", "peach", "pear", "plum") + val output = input.map { listOf(it) }.coalesce { first, second -> + if (first.first()[0] == second.first()[0]) { + first + second + } else { + null + } + }.toList() + output shouldBe listOf(listOf("apple", "apricot"), listOf("banana"), listOf("peach", "pear", "plum")) + } + + "should handle empty sequences" { + val input = emptySequence() + val output = input.coalesce { _, _ -> 0 } + output shouldBe emptySequence() + } + + "should handle single elements" { + val input = sequenceOf(1) + val output = input.coalesce { a, b -> if (a > 5) a else b }.toList() + output shouldBe listOf(1) + } + + "should not modify sequences that can’t be merged" { + val input = listOf(6, 7, 34, 8) + val output = input.asSequence().coalesce { first, second -> + val sum = first + second + if (sum <= 10) sum else null + }.toList() + output shouldBe input + } +}) \ No newline at end of file