I created a SlidingWindow operator for reactive extensions because I want to easily monitor things like rolling averages, etc. As a simple example, I want to s
Just source.Window(count, 1) - or source.Buffer(count, 1)
It be a window/buffer of "count" items, sliding by one.
Using your original test, with an argument of 3 for count, this gives the desired results:
public static IObservable<IList<T>> SlidingWindow<T>(
this IObservable<T> source, int count)
{
return source.Buffer(count, 1)
.Where(list => list.Count == count);
}
Testing like this:
var source = Observable.Range(1, 5);
var query = source.SlidingWindow(3);
using (query.Subscribe(i => Console.WriteLine(string.Join(",", i))))
{
}
Output:
1,2,3
2,3,4
3,4,5
Try this instead - I'd have to sit and have a think about it's relative performance, but it's at least likely as good, and way easier to read:
public static IObservable<IList<T>> SlidingWindow<T>(
this IObservable<T> src,
int windowSize)
{
var feed = src.Publish().RefCount();
// (skip 0) + (skip 1) + (skip 2) + ... + (skip nth) => return as list
return Observable.Zip(
Enumerable.Range(0, windowSize)
.Select(skip => feed.Skip(skip))
.ToArray());
}
Test rig:
var source = Observable.Range(0, 10);
var query = source.SlidingWindow(3);
using(query.Subscribe(Console.WriteLine))
{
Console.ReadLine();
}
Output:
ListOf(0,1,2)
ListOf(1,2,3)
ListOf(2,3,4)
ListOf(3,4,5)
ListOf(4,5,6)
...
EDIT: As an aside, I find myself compulsively .Publish().RefCount()ing ever since being burned once by not doing it...I don't think it's strictly required here, tho.
EDIT for yzorg:
If you augment the method like so, you'll see the runtime behavior more clearly:
public static IObservable<IList<T>> SlidingWindow<T>(
this IObservable<T> src,
int windowSize)
{
var feed = src.Publish().RefCount();
// (skip 0) + (skip 1) + (skip 2) + ... + (skip nth) => return as list
return Observable.Zip(
Enumerable.Range(0, windowSize)
.Select(skip =>
{
Console.WriteLine("Skipping {0} els", skip);
return feed.Skip(skip);
})
.ToArray());
}
The sliding window implementations here was not adequate for my idea of a sliding window. The closest was using Buffer(N, 1) but is a problem since it waits for the first N items before it emits the first result then slides beyond the end of the sequence. I want up to N items emitted at a time.
I ended up with this implementation:
public static IObservable<IList<T>> SlidingWindow<T>(this IObservable<T> obs, int windowSize) =>
Observable.Create<IList<T>>(observer =>
{
var buffer = new CircularBuffer<T>(windowSize);
return obs.Subscribe(
value =>
{
buffer.Add(value);
observer.OnNext(buffer.ToList());
},
ex => observer.OnError(ex),
() => observer.OnCompleted()
);
});
I was initially using a queue for the buffer but wanted to use something a bit more lightweight.
public class CircularBuffer<T> : IReadOnlyList<T>
{
private readonly T[] buffer;
private int offset;
private int count;
public CircularBuffer(int bufferSize) => this.buffer = new T[bufferSize];
public int Capacity => buffer.Length;
public int Count => count;
public T this[int index] => index < 0 || index >= count
? throw new ArgumentOutOfRangeException(nameof(index))
: buffer[(offset + index) % buffer.Length];
public void Add(T value)
{
buffer[(offset + count) % buffer.Length] = value;
if (count < buffer.Length) count++;
else offset = (offset + 1) % buffer.Length;
}
public IEnumerator<T> GetEnumerator()
{
for (var i = 0; i < count; i++)
yield return buffer[(offset + i) % buffer.Length];
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
It would yield sequences for Observable.Range(0, 10).SlidingWindow(3):
0,1,2,3,4,5,6,7,8,9
[0]
[0,1]
[0,1,2]
[1,2,3]
[2,3,4]
[3,4,5]
[4,5,6]
[5,6,7]
[6,7,8]
[7,8,9]