2

I want the events of the output sequence to happen as soon as possible, but not within a window of N seconds that starts at the latest event.

This is a marble diagram, assuming I want a separation of at least three dashes between events:

Input:  a-------b-cd-----e---------f-----g-h
Result: a-------b---c---d---e------f-----g---h

The signature would be:

IObservable<T> Separate<T>(this IObservable<T> source, TimeSpan separation);
Ignacio Calvo
  • 754
  • 9
  • 22

3 Answers3

4

Leaning heavily on @ibebbs answer, I have used the tests to see if there was a more simple approach.

Just before I did write the code, I saw some assumptions that had been coded into the tests. However, I dont know if these assertions are required or not. Specifically the OnCompleted time. @ibebbs is asserting that the OnCompleted should occur in the same frame as the last value. The OP made no such requirement.

If this is not a requirement then you can take a totally different approach.

When I see you marble diagram, the mental translation I made from Input to Result is as follows

Input:  a-------b-cd-----e---------f-----g-h
        a---|
                b--|
                    c--|
                        d--|
                            e--|
Result: a-------b---c---d---e------f-----g---h

i.e. each value is projected to a new single value sequence that has a long tail. That is it will not complete until the given buffer time. This then makes the code as simple as a projection from a single value to a sequence with a single value and a delayed completion. Then you just concat all these mini sequences together

public static IObservable<T> Separate<T>(this IObservable<T> source, TimeSpan separation, IScheduler scheduler)
{
    var delayedEmpty = Observable.Empty<T>().Delay(separation, scheduler);
    return source.Select(s=>
            Observable.Return(s).Concat(delayedEmpty)
        ).Concat();
}

This will solve the OP, however you will also get the same buffer on completion of the sequence that you get for each value.

Lee Campbell
  • 10,631
  • 1
  • 34
  • 29
  • Thank you, another thing learnt: you can build delays by concatenating empty ones, that's very interesting! I mark this as the answer for being much more simple and fitting my bill. If you can cope with the final delay until completion this is the best. Otherwise refer to @ibebbs one. – Ignacio Calvo Oct 26 '16 at 09:28
  • Great solution Lee. This was actually very similar to the path I went down but my implementation relied directly on the schedule rather than using existing operators and left a lot of disposables hanging around. Thanks for the pointers. – ibebbs Oct 26 '16 at 12:36
  • NP. Great job on using Tests to validate your Rx solution. Next step is to lean on marble diagrams to visually construct solutions. – Lee Campbell Oct 27 '16 at 01:27
3

Thanks for a really interesting question. I took a stab at this - flying off into scheduling future actions - and, while I managed to hit the expected output, there were significant issues with my solution.

Yours is much cleaner but... ummm... wrong. Well, slightly ;0)

I started by writing the following test fixture using Microsoft's TestScheduler:

[Fact]
public void MatchExpected()
{
    TestScheduler scheduler = new TestScheduler();

    // 0        1         2         3         4 
    // 1234567890123456789012345678901234567890
    // a-------b-cd-----e---------f-----ghX     <- Input
    IObservable<char> input = scheduler.CreateColdObservable(
        ReactiveTest.OnNext(1, 'a'),
        ReactiveTest.OnNext(9, 'b'),
        ReactiveTest.OnNext(11, 'c'),
        ReactiveTest.OnNext(12, 'd'),
        ReactiveTest.OnNext(18, 'e'),
        ReactiveTest.OnNext(28, 'f'),
        ReactiveTest.OnNext(34, 'g'),
        ReactiveTest.OnNext(35, 'h'),
        ReactiveTest.OnCompleted<char>(36)
    );

    // 0        1         2         3         4 
    // 1234567890123456789012345678901234567890
    // a-------b-cd-----e---------f-----ghX     <- Input
    // a-------b---c---d---e------f-----g---hX  <- Expected
    var expected = new []
    {
        ReactiveTest.OnNext(ReactiveTest.Subscribed + 1, 'a'),
        ReactiveTest.OnNext(ReactiveTest.Subscribed + 9, 'b'),
        ReactiveTest.OnNext(ReactiveTest.Subscribed + 13, 'c'),
        ReactiveTest.OnNext(ReactiveTest.Subscribed + 17, 'd'),
        ReactiveTest.OnNext(ReactiveTest.Subscribed + 21, 'e'),
        ReactiveTest.OnNext(ReactiveTest.Subscribed + 28, 'f'),
        ReactiveTest.OnNext(ReactiveTest.Subscribed + 34, 'g'),
        ReactiveTest.OnNext(ReactiveTest.Subscribed + 38, 'h'),
        ReactiveTest.OnCompleted<char>(ReactiveTest.Subscribed + 38)
    };            

    var actual = scheduler.Start(() => input.Separate(TimeSpan.FromTicks(4), scheduler), ReactiveTest.Subscribed + 40);

    Assert.Equal(expected, actual.Messages.ToArray());
}

In this you can see the marble diagram of input and expected output (using your original dash notation). Unfortunately, when using your implementation, you receive the following output:

// 0        1         2         3         4 
// 1234567890123456789012345678901234567890
// a-------b-cd-----e---------f-----ghX     <- Input
// a-------b---c---d---e------f-----g---hX  <- Expected
// -a-------b--c---d---e-------f-----g--hX  <- Actual

You see, the Delay overload that uses an observable to end the delay requires time on the scheduler before the observable can emit a value. Unfortunately, in instances where the value should be emitted immediately (x.delay == TimeSpan.Zero), it is actually being emitted a fraction later due to a loop through the scheduler.

As I had the test fixture and you had the workable solution, I thought I'd post back a corrected version as shown below:

public static IObservable<T> Separate<T>(this IObservable<T> source, TimeSpan separation, IScheduler scheduler)
{
    return Observable.Create<T>(
        observer =>
        {
            var timedSource = source
                .Timestamp(scheduler)
                .Scan(
                    new
                    {
                        value = default(T),
                        time = DateTimeOffset.MinValue,
                        delay = TimeSpan.Zero
                    },
                    (acc, item) =>
                    {
                        var time =
                            item.Timestamp - acc.time >= separation
                            ? item.Timestamp
                            : acc.time.Add(separation);
                        return new
                        {
                            value = item.Value,
                            time,
                            delay = time - item.Timestamp
                        };
                    })
                .Publish();

            var combinedSource = Observable.Merge(
                timedSource.Where(x => x.delay == TimeSpan.Zero),
                timedSource.Where(x => x.delay > TimeSpan.Zero).Delay(x => Observable.Timer(x.delay, scheduler))
            );

            return new CompositeDisposable(
                combinedSource.Select(x => x.value).Subscribe(observer),
                timedSource.Connect()
            );
        }
    );
}

Which provides the expected output:

// 0        1         2         3         4 
// 1234567890123456789012345678901234567890
// a-------b-cd-----e---------f-----ghX     <- Input
// a-------b---c---d---e------f-----g---hX  <- Expected
// a-------b---c---d---e------f-----g---hX  <- Actual

Note the addition of the IScheduler parameter and it's use through-out the operator code. This is good practice when implementing any operator in Rx that can potentially introduce concurrency (as this one does) and it allows you to write (extremely exacting) tests!

So there you go. Hope it helps :0)

ibebbs
  • 1,963
  • 2
  • 13
  • 20
  • Thank you for the more precise version! I was not worried about absolute precission, my only concern was to make sure that the resulting events were separated at least by the separation timespan. – Ignacio Calvo Oct 25 '16 at 18:42
1

Briefly explained:

  • Timestamp(): Adds a timestamp to each event.
  • Scan(): this function aggregates similarly to Aggregate() but generates the sequence of partially aggregated values, instead of just the final item. It is used to determine the desired timestamp of each event, taking into account the last desired timestamp, and hence the delay to be with to the original timestamp.
  • Delay(): this performs the delay itself (thanks https://twitter.com/AzazelN28, didn't know this overload!)
  • Select(): gets again the original value.
    public static IObservable<T> Separate<T>(this IObservable<T> source, TimeSpan separation)
    {
        return source
            .Timestamp()
            .Scan(
                new {
                    value = default(T),
                    time = DateTimeOffset.MinValue,
                    delay = TimeSpan.Zero },
                (acc, item) =>
                {
                    var time = 
                        item.Timestamp - acc.time >= separation
                        ? item.Timestamp
                        : acc.time.Add(separation);
                    return new
                    {
                        value = item.Value,
                        time,
                        delay = time - item.Timestamp
                    };
                })
            .Delay(x => Observable.Timer(x.delay))
            .Select(x => x.value);
    }
Ignacio Calvo
  • 754
  • 9
  • 22