Skip to content

Commit 2194542

Browse files
github-actions[bot]Copilotdsyme
authored
Add AsyncSeq.windowed for sliding window over async sequences (#241)
Adds AsyncSeq.windowed : int -> AsyncSeq<'T> -> AsyncSeq<'T[]> This is a sliding-window combinator analogous to Seq.windowed and Seq.pairwise (which is a special case with windowSize=2). Each window is yielded as an immutable array snapshot; the window slides one element at a time. Implementation uses a Queue<'T> of capacity windowSize to maintain the current window with O(1) enqueue/dequeue per element. Includes 7 tests covering: empty source, source shorter than window, exact-size source, sliding windows, size-1, size-2, and ArgumentException for windowSize < 1. All 201 tests pass. Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Copilot <[email protected]> Co-authored-by: Don Syme <[email protected]>
1 parent 29d1c0c commit 2194542

3 files changed

Lines changed: 63 additions & 0 deletions

File tree

src/FSharp.Control.AsyncSeq/AsyncSeq.fs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1087,6 +1087,21 @@ module AsyncSeq =
10871087
let! moven = ie.MoveNext()
10881088
b := moven }
10891089

1090+
let windowed (windowSize:int) (source:AsyncSeq<'T>) : AsyncSeq<'T[]> =
1091+
if windowSize < 1 then invalidArg (nameof windowSize) "must be positive"
1092+
asyncSeq {
1093+
let window = System.Collections.Generic.Queue<'T>(windowSize)
1094+
use ie = source.GetEnumerator()
1095+
let! move = ie.MoveNext()
1096+
let b = ref move
1097+
while b.Value.IsSome do
1098+
window.Enqueue(b.Value.Value)
1099+
if window.Count = windowSize then
1100+
yield window.ToArray()
1101+
window.Dequeue() |> ignore
1102+
let! moven = ie.MoveNext()
1103+
b := moven }
1104+
10901105
let pickAsync (f:'T -> Async<'U option>) (source:AsyncSeq<'T>) = async {
10911106
use ie = source.GetEnumerator()
10921107
let! v = ie.MoveNext()

src/FSharp.Control.AsyncSeq/AsyncSeq.fsi

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,14 @@ module AsyncSeq =
198198
/// singleton input sequence.
199199
val pairwise : source:AsyncSeq<'T> -> AsyncSeq<'T * 'T>
200200

201+
/// Returns an asynchronous sequence that yields sliding windows of the given size
202+
/// over the source sequence, each yielded as an array. The first window is emitted
203+
/// once <c>windowSize</c> elements have been consumed; subsequent windows slide one
204+
/// element at a time. The sequence is empty when the source has fewer than
205+
/// <c>windowSize</c> elements. Raises <c>System.ArgumentException</c> if
206+
/// <c>windowSize</c> is less than 1.
207+
val windowed : windowSize:int -> source:AsyncSeq<'T> -> AsyncSeq<'T []>
208+
201209
/// Asynchronously aggregate the elements of the input asynchronous sequence using the
202210
/// specified asynchronous 'aggregation' function.
203211
val foldAsync : folder:('State -> 'T -> Async<'State>) -> state:'State -> source:AsyncSeq<'T> -> Async<'State>

tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2599,6 +2599,46 @@ let ``AsyncSeq.pairwise with three elements should produce two pairs`` () =
25992599
let result = AsyncSeq.pairwise source |> AsyncSeq.toListSynchronously
26002600
Assert.AreEqual([(1, 2); (2, 3)], result)
26012601

2602+
[<Test>]
2603+
let ``AsyncSeq.windowed empty sequence returns empty`` () =
2604+
let result = AsyncSeq.windowed 3 AsyncSeq.empty<int> |> AsyncSeq.toListSynchronously
2605+
Assert.AreEqual([], result)
2606+
2607+
[<Test>]
2608+
let ``AsyncSeq.windowed fewer elements than window returns empty`` () =
2609+
let source = asyncSeq { yield 1; yield 2 }
2610+
let result = AsyncSeq.windowed 3 source |> AsyncSeq.toListSynchronously
2611+
Assert.AreEqual([], result)
2612+
2613+
[<Test>]
2614+
let ``AsyncSeq.windowed exact window size returns single window`` () =
2615+
let source = asyncSeq { yield 1; yield 2; yield 3 }
2616+
let result = AsyncSeq.windowed 3 source |> AsyncSeq.toListSynchronously
2617+
Assert.AreEqual([[|1; 2; 3|]], result)
2618+
2619+
[<Test>]
2620+
let ``AsyncSeq.windowed sliding window produces correct windows`` () =
2621+
let source = asyncSeq { yield 1; yield 2; yield 3; yield 4; yield 5 }
2622+
let result = AsyncSeq.windowed 3 source |> AsyncSeq.toListSynchronously
2623+
Assert.AreEqual([[|1;2;3|]; [|2;3;4|]; [|3;4;5|]], result)
2624+
2625+
[<Test>]
2626+
let ``AsyncSeq.windowed size 1 returns each element as singleton array`` () =
2627+
let source = asyncSeq { yield 10; yield 20; yield 30 }
2628+
let result = AsyncSeq.windowed 1 source |> AsyncSeq.toListSynchronously
2629+
Assert.AreEqual([[|10|]; [|20|]; [|30|]], result)
2630+
2631+
[<Test>]
2632+
let ``AsyncSeq.windowed size 2 is equivalent to pairwise as arrays`` () =
2633+
let source = asyncSeq { yield 1; yield 2; yield 3; yield 4 }
2634+
let result = AsyncSeq.windowed 2 source |> AsyncSeq.toListSynchronously
2635+
Assert.AreEqual([[|1;2|]; [|2;3|]; [|3;4|]], result)
2636+
2637+
[<Test>]
2638+
let ``AsyncSeq.windowed with size 0 raises ArgumentException`` () =
2639+
Assert.Throws<System.ArgumentException>(fun () ->
2640+
AsyncSeq.windowed 0 (asyncSeq { yield 1 }) |> AsyncSeq.toListSynchronously |> ignore) |> ignore
2641+
26022642
[<Test>]
26032643
let ``AsyncSeq.distinctUntilChangedWith should work with custom equality`` () =
26042644
let source = asyncSeq { yield "a"; yield "A"; yield "B"; yield "b"; yield "c" }

0 commit comments

Comments
 (0)