Skip to content

Commit 875a605

Browse files
authored
FlowOps: sample operator (#254)
1 parent 23d12d5 commit 875a605

File tree

5 files changed

+153
-0
lines changed

5 files changed

+153
-0
lines changed

core/src/main/scala/ox/flow/FlowOps.scala

+41
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,19 @@ class FlowOps[+T]:
7373
def filter(f: T => Boolean): Flow[T] = Flow.usingEmitInline: emit =>
7474
last.run(FlowEmit.fromInline(t => if f(t) then emit.apply(t)))
7575

76+
/** Emits only every nth element emitted by this flow.
77+
*
78+
* @param n
79+
* The interval between two emitted elements.
80+
*/
81+
def sample(n: Int): Flow[T] = Flow.usingEmitInline: emit =>
82+
var sampleCounter = 0
83+
last.run(
84+
FlowEmit.fromInline: t =>
85+
sampleCounter += 1
86+
if n != 0 && sampleCounter % n == 0 then emit(t)
87+
)
88+
7689
/** Applies the given mapping function `f` to each element emitted by this flow, for which the function is defined, and emits the result.
7790
* If `f` is not defined at an element, the element will be skipped.
7891
*
@@ -82,6 +95,23 @@ class FlowOps[+T]:
8295
def collect[U](f: PartialFunction[T, U]): Flow[U] = Flow.usingEmitInline: emit =>
8396
last.run(FlowEmit.fromInline(t => if f.isDefinedAt(t) then emit.apply(f(t))))
8497

98+
/** Transforms the elements of the flow by applying an accumulation function to each element, producing a new value at each step. The
99+
* resulting flow contains the accumulated values at each point in the original flow.
100+
*
101+
* @param initial
102+
* The initial value to start the accumulation.
103+
* @param f
104+
* The accumulation function that is applied to each element of the flow.
105+
*/
106+
def scan[V](initial: V)(f: (V, T) => V): Flow[V] = Flow.usingEmitInline: emit =>
107+
emit(initial)
108+
var accumulator = initial
109+
last.run(
110+
FlowEmit.fromInline: t =>
111+
accumulator = f(accumulator, t)
112+
emit(accumulator)
113+
)
114+
85115
/** Applies the given effectful function `f` to each element emitted by this flow. The returned flow emits the elements unchanged. If `f`
86116
* throws an exceptions, the flow fails and propagates the exception.
87117
*/
@@ -441,6 +471,17 @@ class FlowOps[+T]:
441471
emit((t, otherDefault)); true
442472
)
443473

474+
/** Combines each element from this and the index of the element (starting at 0).
475+
*/
476+
def zipWithIndex: Flow[(T, Long)] = Flow.usingEmitInline: emit =>
477+
var index = 0L
478+
last.run(
479+
FlowEmit.fromInline: t =>
480+
val zipped = (t, index)
481+
index += 1
482+
emit(zipped)
483+
)
484+
444485
/** Emits a given number of elements (determined byc `segmentSize`) from this flow to the returned flow, then emits the same number of
445486
* elements from the `other` flow and repeats. The order of elements in both flows is preserved.
446487
*
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package ox.flow
2+
3+
import org.scalatest.flatspec.AnyFlatSpec
4+
import org.scalatest.matchers.should.Matchers
5+
import ox.*
6+
7+
class FlowOpsFilterTest extends AnyFlatSpec with Matchers:
8+
behavior of "filter"
9+
10+
it should "not filter anything from the empty flow" in:
11+
val c = Flow.empty[Int]
12+
val s = c.filter(_ % 2 == 0)
13+
s.runToList() shouldBe List.empty
14+
15+
it should "filter out everything if no element meets 'f'" in:
16+
val c = Flow.fromValues(1 to 10: _*)
17+
val s = c.filter(_ => false)
18+
s.runToList() shouldBe List.empty
19+
20+
it should "not filter anything if all the elements meet 'f'" in:
21+
val c = Flow.fromValues(1 to 10: _*)
22+
val s = c.filter(_ => true)
23+
s.runToList() shouldBe (1 to 10)
24+
25+
it should "filter out elements that don't meet 'f'" in:
26+
val c = Flow.fromValues(1 to 10: _*)
27+
val s = c.filter(_ % 2 == 0)
28+
s.runToList() shouldBe (2 to 10 by 2)
29+
30+
end FlowOpsFilterTest
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package ox.flow
2+
3+
import org.scalatest.flatspec.AnyFlatSpec
4+
import org.scalatest.matchers.should.Matchers
5+
import ox.*
6+
7+
class FlowOpsSampleTest extends AnyFlatSpec with Matchers:
8+
behavior of "sample"
9+
10+
it should "not sample anything from an empty flow" in:
11+
val c = Flow.empty[Int]
12+
val s = c.sample(5)
13+
s.runToList() shouldBe List.empty
14+
15+
it should "not sample anything when 'n == 0'" in:
16+
val c = Flow.fromValues(1 to 10: _*)
17+
val s = c.sample(0)
18+
s.runToList() shouldBe List.empty
19+
20+
it should "sample every element of the flow when 'n == 1'" in:
21+
val c = Flow.fromValues(1 to 10: _*)
22+
val n = 1
23+
val s = c.sample(n)
24+
s.runToList() shouldBe (n to 10 by n)
25+
26+
it should "sample every nth element of the flow" in:
27+
val c = Flow.fromValues(1 to 10: _*)
28+
val n = 3
29+
val s = c.sample(n)
30+
s.runToList() shouldBe (n to 10 by n)
31+
32+
end FlowOpsSampleTest
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package ox.flow
2+
3+
import org.scalatest.flatspec.AnyFlatSpec
4+
import org.scalatest.matchers.should.Matchers
5+
import ox.*
6+
7+
class FlowOpsScanTest extends AnyFlatSpec with Matchers:
8+
behavior of "scan"
9+
10+
it should "scan the empty flow" in:
11+
val flow: Flow[Int] = Flow.empty
12+
val scannedFlow = flow.scan(0)((acc, el) => acc + el)
13+
scannedFlow.runToList() shouldBe List(0)
14+
15+
it should "scan a flow of summed Int" in:
16+
val flow = Flow.fromValues(1 to 10: _*)
17+
val scannedFlow = flow.scan(0)((acc, el) => acc + el)
18+
scannedFlow.runToList() shouldBe List(0, 1, 3, 6, 10, 15, 21, 28, 36, 45, 55)
19+
20+
it should "scan a flow of multiplied Int" in:
21+
val flow = Flow.fromValues(1 to 10: _*)
22+
val scannedFlow = flow.scan(1)((acc, el) => acc * el)
23+
scannedFlow.runToList() shouldBe List(1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880, 3628800)
24+
25+
it should "scan a flow of concatenated String" in:
26+
val flow = Flow.fromValues("f", "l", "o", "w")
27+
val scannedFlow = flow.scan("my")((acc, el) => acc + el)
28+
scannedFlow.runToList() shouldBe List("my", "myf", "myfl", "myflo", "myflow")
29+
30+
end FlowOpsScanTest
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package ox.flow
2+
3+
import org.scalatest.concurrent.Eventually
4+
import org.scalatest.flatspec.AnyFlatSpec
5+
import org.scalatest.matchers.should.Matchers
6+
7+
class FlowOpsZipWithIndexTest extends AnyFlatSpec with Matchers with Eventually:
8+
behavior of "zipWithIndex"
9+
10+
it should "not zip anything from an empty flow" in:
11+
val c = Flow.empty[Int]
12+
val s = c.zipWithIndex
13+
s.runToList() shouldBe List.empty
14+
15+
it should "zip flow with index" in:
16+
val c = Flow.fromValues(1 to 5: _*)
17+
val s = c.zipWithIndex
18+
s.runToList() shouldBe List((1, 0), (2, 1), (3, 2), (4, 3), (5, 4))
19+
20+
end FlowOpsZipWithIndexTest

0 commit comments

Comments
 (0)