I got an Observable, I want to group them modulo 10. I want to make the result to be an hot observable and that when a new subscriber subscribes he got all of the GroupedObservable being played before but instead of getting the Key, I want the latest value. I want the updates as well, skipping the latest value.
In this example, to make it simpler, we will only work with modulo result equals 5. But I want my solution working for everything.
Example:
--------15-----25---------...
Let's add on the previous example some points in time where we will subscribe:
---S1-----15--S2---25-----S3----...
The expected results are:
S1 to receive Latest: 15, Updates: an observable starting with 25 and updates %10 ==5 that will arrive later.
Explanation: S1 will be notified after the 15 arrives, 15 is the latest element so I want it straight away. The second argument will be an observable that will produce later 25 and the %10 == 5 elements in the future.
S2 to receive Latest: 15, Updates: an observable starting with 25 and updates %10 ==5 that will arrive later.
Explanation: S2 will be notified on subscription, 15 is the latest element so I want it straight away. The second argument will be an observable that will produce later 25 and the %10 == 5 elements in the future.
S3 to receive Latest: 25, Updates: an observable with updates %10 ==5 that will arrive later.
Explanation: S3 will be notified on subscription, 25 is the latest element so I want it straight away. The second argument will be an observable that will produce the %10 == 5 elements in the future.
Here are some resolution tries:
The code below use Tuple and NUnit.
First attempt
[Test]
public void WhenWeGroupByReplaying1()
{
var subject = new Subject<uint>();
var observable = subject.GroupBy(t => t%10)
.Select(t =>
{
var connectableObservable = t.Replay(1);
connectableObservable.Connect();
return (key: t.Key, updates: connectableObservable);
}).Replay();
observable.Connect();
// I will block on the First of the lambda below
var getLastAndUpdates = observable
.Select(t => (first: t.updates.First(),updates: t.updates.Skip(1)));
getLastAndUpdates.Subscribe(t =>
{
Console.WriteLine($"[1] - FIRST: {t.first}");
t.updates.Subscribe(t2 => Console.WriteLine($"[1] - UPDATE: {t2}"));
});
subject.OnNext(15);
getLastAndUpdates.Subscribe(t =>
{
Console.WriteLine($"[2] - FIRST: {t.first}");
t.updates.Subscribe(t2 => Console.WriteLine($"[2] - UPDATE: {t2}"));
});
subject.OnNext(25);
getLastAndUpdates.Subscribe(t =>
{
Console.WriteLine($"[3] - FIRST: {t.first}");
t.updates.Subscribe(t2 => Console.WriteLine($"[3] - UPDATE: {t2}"));
});
}
This solution will be blocking as shown in comment.
Second attempt
[Test]
public void WhenWeGroupByReplaying2()
{
var subject = new Subject<uint>();
var observable = subject.GroupBy(t => t, t => t, new ModuloEqualityComparer())
.Select(t =>
{
var connectableObservable = t.Publish(t.Key);
connectableObservable.Connect();
return (key: t.Key, updates: connectableObservable);
}).Replay();
observable.Connect();
var getLastAndUpdates = observable
.Select(t => (first: t.updates.First(),updates: t.updates.Skip(1)));
getLastAndUpdates.Subscribe(t =>
{
Console.WriteLine($"[1] - FIRST: {t.first}");
t.updates.Subscribe(t2 => Console.WriteLine($"[1] - UPDATE: {t2}"));
});
subject.OnNext(15);
getLastAndUpdates.Subscribe(t =>
{
Console.WriteLine($"[2] - FIRST: {t.first}");
t.updates.Subscribe(t2 => Console.WriteLine($"[2] - UPDATE: {t2}"));
});
subject.OnNext(25);
getLastAndUpdates.Subscribe(t =>
{
Console.WriteLine($"[3] - FIRST: {t.first}");
t.updates.Subscribe(t2 => Console.WriteLine($"[3] - UPDATE: {t2}"));
});
}
private class ModuloEqualityComparer : IEqualityComparer<uint>
{
public bool Equals(uint x, uint y)
{
return x % 10 == y % 10;
}
public int GetHashCode(uint obj)
{
return (obj % 10).GetHashCode();
}
}
Result:
[1] - FIRST: 15
[1] - UPDATE: 15
[2] - FIRST: 15
[1] - UPDATE: 25
[2] - UPDATE: 25
[3] - FIRST: 25
Expected result: (exact order doesn't mater)
[1] - FIRST: 15
[2] - FIRST: 15
[1] - UPDATE: 25
[2] - UPDATE: 25
[3] - FIRST: 25
Third attempt
[Test]
public void WhenWeGroupByReplaying3()
{
var subject = new Subject<uint>();
var observable = subject.GroupBy(t => (key: t%10, value:t), t => t, new ModuloEqualityComparer2())
.Select(t =>
{
var connectableObservable = t.Publish(t.Key.Item2);
connectableObservable.Connect();
return (key: t.Key, updates: connectableObservable);
}).Replay();
observable.Connect();
var getLastAndUpdates = observable
.Select(t => (first: t.updates.First(),updates: t.updates.Skip(1)));
getLastAndUpdates.Subscribe(t =>
{
Console.WriteLine($"[1] - FIRST: {t.first}");
t.updates.Subscribe(t2 => Console.WriteLine($"[1] - UPDATE: {t2}"));
});
subject.OnNext(15);
getLastAndUpdates.Subscribe(t =>
{
Console.WriteLine($"[2] - FIRST: {t.first}");
t.updates.Subscribe(t2 => Console.WriteLine($"[2] - UPDATE: {t2}"));
});
subject.OnNext(25);
getLastAndUpdates.Subscribe(t =>
{
Console.WriteLine($"[3] - FIRST: {t.first}");
t.updates.Subscribe(t2 => Console.WriteLine($"[3] - UPDATE: {t2}"));
});
}
private class ModuloEqualityComparer2 : IEqualityComparer<(uint,uint)>
{
private readonly ModuloEqualityComparer _moduloEqualityComparer = new ModuloEqualityComparer();
public bool Equals((uint, uint) x, (uint, uint) y)
{
return _moduloEqualityComparer.Equals(x.Item1, y.Item1);
}
public int GetHashCode((uint, uint) obj)
{
return _moduloEqualityComparer.GetHashCode(obj.Item1);
}
}
Result:
[1] - FIRST: 15
[1] - UPDATE: 15
[2] - FIRST: 15
[1] - UPDATE: 25
[2] - UPDATE: 25
[3] - FIRST: 25
Expected result: (exact order doesn't mater)
[1] - FIRST: 15
[2] - FIRST: 15
[1] - UPDATE: 25
[2] - UPDATE: 25
[3] - FIRST: 25
Thanks for reading.
I'm not entirely sure what you're trying to achieve, but hopefully this will help you:
There's a couple things wrong with your code:
.First() is obsolete for a reason. You shouldn't be using blocking code with Rx
.Replay() requires a dummy subscription to work correctly. I'm not sure if that's what was plaguing your code or not, but to achieve your aims, you want that.
Nested subscriptions are generally a bad idea. I have replaced the nested subscriptions with a .Merge().
If this doesn't solve your problem, I suggest amending your question to describe what you're trying to accomplish by using Rx. This smells a bit like an XY situation.
Here's the code:
var subject = new Subject<uint>();
var observable = subject.GroupBy(t => t % 10)
.Select(t => t.Replay(1).RefCount()).Replay().RefCount();
// dummy subscriptions required for Replay to work correctly.
var dummySub = observable.Merge().Subscribe();
observable
.Select(o => o.Select((t, index) => (t.Key, t.num, index)))
.Merge()
.Subscribe(t =>
{
if (t.index == 0)
Console.WriteLine($"[1] - FIRST: {t.num}");
else
Console.WriteLine($"[1] - UPDATE: {t.num}");
});
subject.OnNext(15);
observable
.Select(o => o.Select((t, index) => (t.Key, t.num, index)))
.Merge()
.Subscribe(t =>
{
if (t.index == 0)
Console.WriteLine($"[1] - FIRST: {t.num}");
else
Console.WriteLine($"[1] - UPDATE: {t.num}");
});
subject.OnNext(25);
observable
.Select(o => o.Select((t, index) => (t.Key, t.num, index)))
.Merge()
.Subscribe(t =>
{
if (t.index == 0)
Console.WriteLine($"[1] - FIRST: {t.num}");
else
Console.WriteLine($"[1] - UPDATE: {t.num}");
});
Related
I'm trying to group my list using linq by an interval of 30 minutes.
Let’s say we have this list:
X called at 10:00 AM
Y called at 10:10 AM
Y called at 10:20 AM
Y called at 10:35 AM
X called at 10:40 AM
Y called at 10:45 AM
What i need is to group these items in a 30 minutes frame and by user, like so:
X called at 10:00 AM
Y called 3 times between 10:10 AM and 10:35 AM
X called at 10:40 AM
Y called at 10:45 AM
Here's what i'm using with Linq:
myList
.GroupBy(i => i.caller, (k, g) => g
.GroupBy(i => (long)new TimeSpan(Convert.ToDateTime(i.date).Ticks - g.Min(e => Convert.ToDateTime(e.date)).Ticks).TotalMinutes / 30)
.Select(g => new
{
count = g.Count(),
obj = g
}));
I need the result in one list, but instead im getting the result in nested lists, which needs multiple foreach to extract.
Any help is much appreciated!
I think you are looking for SelectMany which will unwind one level of grouping:
var ans = myList
.GroupBy(c => c.caller, (caller, cg) => new { Key = caller, MinDateTime = cg.Min(c => c.date), Calls = cg })
.SelectMany(cg => cg.Calls.GroupBy(c => (int)(c.date - cg.MinDateTime).TotalMinutes / 30))
.OrderBy(cg => cg.Min(c => c.date))
.ToList();
Note: The GroupBy return selects the Min as a minor efficiency improvement so you don't constantly re-find the minimum DateTime for each group per call.
Note 2: The (int) conversion creates the buckets - otherwise, .TotalMinutes returns a double and the division by 30 just gives you a (unique) fractional answer and you get no grouping into buckets.
By modifying the initial code (again for minor efficiency), you can reformat the answer to match your textual result:
var ans = myList
.GroupBy(c => c.caller, (caller, cg) => new { Key = caller, MinDateTime = cg.Min(c => c.date), Calls = cg })
.SelectMany(cg => cg.Calls.GroupBy(c => (int)(c.date - cg.MinDateTime).TotalMinutes / 30), (bucket, cg) => new { FirstCall = cg.MinBy(c => c.date), Calls = cg })
.OrderBy(fcc => fcc.FirstCall.date)
.ToList();
var ans2 = ans.Select(fcc => new { Caller = fcc.FirstCall.caller, FirstCallDateTime = fcc.FirstCall.date, LastCallDateTime = fcc.Calls.Max(c => c.date), Count = fcc.Calls.Count() })
.ToList();
Instead of grouping by a DateTime, try grouping by a key derived from the date.
string GetTimeBucketId(DateTime time) {
return $"${time.Year}-{time.Month}-{time.Day}T{time.Hour}-{time.Minute % 30}";
}
myList
.GroupBy(i => GetTimeBucketId(i.caller.date))
.Select(g => { Count = g.Count(), Key = g.Key });
I have one observable (mainSequence). If a condition is meet it should invoke an async method once until the condition changes. The methods return value will indicate success.
On failure I have a subscription which will inform the user.
Other observable are likely to subscribe to the mainSequence and have a similar error handling pattern.
But the consecutive observers to mainSequence will cause to invoke the mainSequence again. I only would like to have it invoked once hence my DistinctUntilChanged.
The example below outputs:
Working on 6
Working on 6
Working on 100
Working on 6
Working on 101
The output I want is:
Working on 6
Working on 100
Working on 101
I'm missing an reactive operator on my mainSequence, I just don't know which one.
public static void Main()
{
bool IsNumberOk(int n) => n > 5;
Task<bool> DoSomethingAsync(int n)
{
Console.WriteLine($"Working on {n}");
return Task.FromResult(true);
}
var mainSequence = Observable.Range(0, 10)
.Where(IsNumberOk)
.DistinctUntilChanged(IsNumberOk)
.SelectMany(DoSomethingAsync);
// sequence one error handling
mainSequence.Where(x => !x).Subscribe(_ => Console.WriteLine($"Something went wrong with {nameof(mainSequence)}"));
for (var i = 0; i < 2; i++)
{
var iTemp = 100 + i;
var consecutive = mainSequence
.Where(x => x) // if no error on mainSequence
.Select(_ => iTemp)
.DistinctUntilChanged()
.SelectMany(DoSomethingAsync);
consecutive.Where(x => !x).Subscribe(_ => Console.WriteLine($"Something went wrong with {iTemp}"));
}
}
You have misunderstanding with regard to the distinction between an observable and a subscription. They are two distinct things.
The best parallel, in my mind, is that an observable is like a class and a subscription is like an instance of a class. Like a class, the observable is defined once. Each subscription is a new instance of the observable.
Let's take this code - somewhat cut-down from your code in the question.
Task<int> DoSomethingAsync(int n)
{
Console.WriteLine($"Working on {n}");
return Task.FromResult(-n);
}
IObservable<int> mainSequence =
Observable
.Range(0, 3)
.SelectMany(DoSomethingAsync);
That's a single observable.
Now let's do this:
IDisposable mainSubscription1 =
mainSequence
.Subscribe(x => Console.WriteLine($"(1){nameof(mainSequence)}OnNext({x})"));
IDisposable mainSubscription2 =
mainSequence
.Subscribe(x => Console.WriteLine($"(2){nameof(mainSequence)}OnNext({x})"));
I have created two subscriptions, so I get two completely distinct instances of the observable. They run entirely separate of each other. In fact, Observable.Range outputs its values immediately, so each subscription blocks until it is complete. You get this output:
Working on 0
(1)mainSequenceOnNext(0)
Working on 1
(1)mainSequenceOnNext(-1)
Working on 2
(1)mainSequenceOnNext(-2)
Working on 0
(2)mainSequenceOnNext(0)
Working on 1
(2)mainSequenceOnNext(-1)
Working on 2
(2)mainSequenceOnNext(-2)
You can get Observable.Range to not block like this:
IObservable<int> mainSequence =
Observable
.Range(0, 3, Scheduler.Default)
.SelectMany(DoSomethingAsync);
But you still have two completely independent instances of the observable running. You get something like this:
Working on 0
Working on 0
(1)mainSequenceOnNext(0)
Working on 1
(2)mainSequenceOnNext(0)
Working on 1
(1)mainSequenceOnNext(-1)
Working on 2
(2)mainSequenceOnNext(-1)
Working on 2
(1)mainSequenceOnNext(-2)
(2)mainSequenceOnNext(-2)
Now, if you want to share a single observable then you need to Publish it and Connect to the published observable to get the values flowing.
Here's the full code:
IConnectableObservable<int> mainSequence =
Observable
.Range(0, 3, Scheduler.Default)
.SelectMany(DoSomethingAsync)
.Publish();
IDisposable mainSubscription1 =
mainSequence
.Subscribe(x => Console.WriteLine($"(1){nameof(mainSequence)}OnNext({x})"));
IDisposable mainSubscription2 =
mainSequence
.Subscribe(x => Console.WriteLine($"(2){nameof(mainSequence)}OnNext({x})"));
IDisposable mainConnection =
mainSequence
.Connect();
Now when I run that, the two subscriptions don't start producing values until the .Connect() is called.
You get this:
Working on 0
(1)mainSequenceOnNext(0)
(2)mainSequenceOnNext(0)
Working on 1
(1)mainSequenceOnNext(-1)
(2)mainSequenceOnNext(-1)
Working on 2
(1)mainSequenceOnNext(-2)
(2)mainSequenceOnNext(-2)
Now if I had to get your code working, here's what it would look like:
public static void Main()
{
bool IsNumberOk(int n) => n > 5;
Task<bool> DoSomethingAsync(int n)
{
Console.WriteLine($"Working on {n}");
return Task.FromResult(true);
}
var mainSequence =
Observable
.Range(0, 10)
.Where(IsNumberOk)
.DistinctUntilChanged(IsNumberOk)
.SelectMany(DoSomethingAsync)
.Publish();
mainSequence
.Where(x => !x)
.Subscribe(_ => Console.WriteLine($"Something went wrong with {nameof(mainSequence)}"));
for (var i = 0; i < 2; i++)
{
var iTemp = 100 + i;
var consecutive =
mainSequence
.Where(x => x)
.Select(_ => iTemp)
.DistinctUntilChanged()
.SelectMany(DoSomethingAsync);
consecutive
.Where(x => !x)
.Subscribe(_ => Console.WriteLine($"Something went wrong with {iTemp}"));
}
IDisposable mainConnection =
mainSequence
.Connect();
}
It now produces this:
Working on 6
Working on 100
Working on 101
I have an observable that emits unique values e.g.
var source=Observable.Range(1,100).Publish();
source.Connect();
I want to observe its values from e.g. two observers but each observer to get notified only for values not seen in other observers.
So if first observer contains the value 10 the second observer should never get notified for the 10 value.
Update
I chose #Asti`s answer cause it was first and although buggy it pointed to the right direction and up-voted #Shlomo's answer. Too bad I cannot accept both answers as #Shlomo answer was more correct and I really appreciate all his help we get on this tag.
Observables aren't supposed to behave differently for different observers; a better approach would be to give each observer its own filtered observable.
That being said, if your constraints require that you need this behavior in a single observable - we can use a Round-Robin method.
public static IEnumerable<T> Repeat<T>(this IEnumerable<T> source)
{
for (; ; )
foreach (var item in source.ToArray())
yield return item;
}
public static IObservable<T> RoundRobin<T>(this IObservable<T> source)
{
var subscribers = new List<IObserver<T>>();
var shared = source
.Zip(subscribers.Repeat(), (value, observer) => (value, observer))
.Publish()
.RefCount();
return Observable.Create<T>(observer =>
{
subscribers.Add(observer);
var subscription =
shared
.Where(pair => pair.observer == observer)
.Select(pair => pair.value)
.Subscribe(observer);
var dispose = Disposable.Create(() => subscribers.Remove(observer));
return new CompositeDisposable(subscription, dispose);
});
}
Usage:
var source = Observable.Range(1, 100).Publish();
var dist = source.RoundRobin();
dist.Subscribe(i => Console.WriteLine($"One sees {i}"));
dist.Subscribe(i => Console.WriteLine($"Two sees {i}"));
source.Connect();
Result:
One sees 1
Two sees 2
One sees 3
Two sees 4
One sees 5
Two sees 6
One sees 7
Two sees 8
One sees 9
Two sees 10
If you already have a list of observers, the code becomes much simpler.
EDIT: #Asti fixed his bug, and I fixed mine based on his answer. Our answers are now largely similar. I have an idea how to do a purely reactive one, if I have time I'll post that later.
Fixed code:
public static IObservable<T> RoundRobin2<T>(this IObservable<T> source)
{
var subscribers = new BehaviorSubject<ImmutableList<IObserver<T>>>(ImmutableList<IObserver<T>>.Empty);
ImmutableList<IObserver<T>> latest = ImmutableList<IObserver<T>>.Empty;
subscribers.Subscribe(l => latest = l);
var shared = source
.Select((v, i) => (v, i))
.WithLatestFrom(subscribers, (t, s) => (t.v, t.i, s))
.Publish()
.RefCount();
return Observable.Create<T>(observer =>
{
subscribers.OnNext(latest.Add(observer));
var dispose = Disposable.Create(() => subscribers.OnNext(latest.Remove(observer)));
var sub = shared
.Where(t => t.i % t.s.Count == t.s.FindIndex(o => o == observer))
.Select(t => t.v)
.Subscribe(observer);
return new CompositeDisposable(dispose, sub);
});
}
Original answer:
I upvoted #Asti's answer, because he's largely correct: Just because you can, doesn't mean you should. And his answer largely works, but it's subject to a bug:
This works fine:
var source = Observable.Range(1, 20).Publish();
var dist = source.RoundRobin();
dist.Subscribe(i => Console.WriteLine($"One sees {i}"));
dist.Take(1).Subscribe(i => Console.WriteLine($"Two sees {i}"));
This doesn't:
var source = Observable.Range(1, 20).Publish();
var dist = source.RoundRobin();
dist.Take(1).Subscribe(i => Console.WriteLine($"One sees {i}"));
dist.Subscribe(i => Console.WriteLine($"Two sees {i}"));
Output is:
One sees 1
Two sees 1
Two sees 2
Two sees 3
Two sees 4
...
I first thought the bug is Halloween related, but now I'm not sure. The .ToArray() in Repeat should take care of that. I also wrote a pure-ish observable implementation which has the same bug. This implementation doesn't guarantee a perfect Round Robin, but that wasn't in the question:
public static IObservable<T> RoundRobin2<T>(this IObservable<T> source)
{
var subscribers = new BehaviorSubject<ImmutableList<IObserver<T>>>(ImmutableList<IObserver<T>>.Empty);
ImmutableList<IObserver<T>> latest = ImmutableList<IObserver<T>>.Empty;
subscribers.Subscribe(l => latest = l);
var shared = source
.Select((v, i) => (v, i))
.WithLatestFrom(subscribers, (t, s) => (t.v, t.i, s))
.Publish()
.RefCount();
return Observable.Create<T>(observer =>
{
subscribers.OnNext(latest.Add(observer));
var dispose = Disposable.Create(() => subscribers.OnNext(latest.Remove(observer)));
var sub = shared
.Where(t => t.i % t.s.Count == t.s.FindIndex(o => o == observer))
.Select(t => t.v)
.Subscribe(observer);
return new CompositeDisposable(dispose, sub);
});
}
This is a simple distributed queue implementation using TPL Dataflow. But with respect to different observers not seeing the same value, there's little chance of it behaving incorrectly. It's not round-robin, but actually has back-pressure semantics.
public static IObservable<T> Distribute<T>(this IObservable<T> source)
{
var buffer = new BufferBlock<T>();
source.Subscribe(buffer.AsObserver());
return Observable.Create<T>(observer =>
buffer.LinkTo(new ActionBlock<T>(observer.OnNext, new ExecutionDataflowBlockOptions { BoundedCapacity = 1 })
);
}
Output
One sees 1
Two sees 2
One sees 3
Two sees 4
One sees 5
One sees 6
One sees 7
One sees 8
One sees 9
One sees 10
I might prefer skipping Rx entirely and just using TPL Dataflow.
I have object list (name is list) with e.g. next values:
[0] {Id='04239',Color='03',MatPre='145698',Stz=210,Spp=1}
[1] {Id='04239',Color='03',MatPre='145698',Stz=210,Spp=3}
[2] {Id='04239',Color='KB',MatPre='145698',Stz=210,Spp=3}
[3] {Id='04239',Color='KB',MatPre='145698',Stz=210,Spp=2}
[4] {Id='04239',Color='03',MatPre='145698',Stz=210,Spp=4}
Result or new list that I whant to get is list with v items:
[0] {Id='04239',Color='03',MatPre='145698',Stz=210,Spp=3}
[1] {Id='04239',Color='03',MatPre='145698',Stz=210,Spp=4}
[2] {Id='04239',Color='KB',MatPre='145698',Stz=210,Spp=3}
I would need something like this
var test = list.GroupBy(x => new { x.Color })
.Where(x => x.Spp is greater than first
smaller x.Spp in same
group of Color)
.SelectMany(x => x).ToList();
I don't know how to do this.
This should do it, although calling Min every time might not be efficient, you might consider query syntax to make it better:
list.GroupBy(x => x.Color)
.SelectMany(g => g.Where(x => x.Spp > g.Min(_ => _.Spp));
I have a stream with letters (A-Z) and numbers (1-9). I do want to join letters that arrive within a timeout (this can change) and always emit numbers immediately. Can you suggest me which functions are best to do this?
Sample working code (not sure this is correct and/or a good solution):
private BehaviorSubject<TimeSpan> sTimeouts = new BehaviorSubject<TimeSpan>(0.ms());
private IObservable<string> lettersJoined(IObservable<char> ob)
{
return Observable.Create<string>(observer =>
{
var letters = new List<char>();
var lettersFlush = new SerialDisposable();
return ob.Subscribe(c =>
{
if (char.IsUpper(c))
{
if ((await sTimeouts.FirstAsync()).Ticks > 0)
{
letters.Add(c);
lettersFlush.Disposable =
VariableTimeout(sTimeouts)
.Subscribe(x => {
observer.OnNext(String.Concat(letters));
letters.Clear();
});
}
else
observer.OnNext(letters.ToString());
}
else if (char.IsDigit(c))
observer.OnNext(c.ToString());
}
}
}
private IObservable<long> VariableTimeout(IObservable<TimeSpan> timeouts)
{
return Observable.Create<long>(obs =>
{
var sd = new SerialDisposable();
var first = DateTime.Now;
return timeouts
.Subscribe(timeout =>
{
if (timeout.Ticks == 0 || first + timeout < DateTime.Now)
{
sd.Disposable = null;
obs.OnNext(timeout.Ticks);
obs.OnCompleted();
}
else
{
timeout -= DateTime.Now - first;
sd.Disposable =
Observable
.Timer(timeout)
.Subscribe(t => {
obs.OnNext(t);
obs.OnCompleted();
});
}
});
});
}
private void ChangeTimeout(int timeout)
{
sTimeouts.OnNext(timeout.ms())
}
// I use the following extension method
public static class TickExtensions
{
public static TimeSpan ms(this int ms)
{
return TimeSpan.FromMilliseconds(ms);
}
}
To modify the timeout, I can simply change the private timeout variable, but probably a Subject for it would be OK if needed/better.
UPDATE
var scheduler = new TestScheduler();
var timeout = scheduler.CreateColdObservable<int>(
ReactiveTest.OnNext(0000.Ms(), 2000),
ReactiveTest.OnNext(4300.Ms(), 1000));
var input = scheduler.CreateColdObservable<char>(
ReactiveTest.OnNext(0100.Ms(), '1'),
ReactiveTest.OnNext(1600.Ms(), '2'),
ReactiveTest.OnNext(1900.Ms(), 'A'),
ReactiveTest.OnNext(2100.Ms(), 'B'),
ReactiveTest.OnNext(4500.Ms(), 'C'),
ReactiveTest.OnNext(5100.Ms(), 'A'),
ReactiveTest.OnNext(5500.Ms(), '5'),
ReactiveTest.OnNext(6000.Ms(), 'B'),
ReactiveTest.OnNext(7200.Ms(), '1'),
ReactiveTest.OnNext(7500.Ms(), 'B'),
ReactiveTest.OnNext(7700.Ms(), 'A'),
ReactiveTest.OnNext(8400.Ms(), 'A'));
var expected = scheduler.CreateColdObservable<string>(
ReactiveTest.OnNext(0100.Ms(), "1"),
ReactiveTest.OnNext(1600.Ms(), "2"),
ReactiveTest.OnNext(4100.Ms(), "AB"),
ReactiveTest.OnNext(5500.Ms(), "5"),
ReactiveTest.OnNext(7000.Ms(), "CAB"),
ReactiveTest.OnNext(7200.Ms(), "1"),
ReactiveTest.OnNext(9400.Ms(), "BAA"));
// if ReactiveTest.OnNext(3800.Ms(), 1000)
// then expected is ReactiveTest.OnNext(3800.Ms(), "AB")
UPDATE #2
Refined solution correctly supporting timeout change during buffering
Several things that may help out here.
First marble diagrams are nice for helping visualize the problem, but when proving if something works or not, lets be prescriptive and unit test with ITestableObservable<T> instances.
Second, I am not sure what your solution should be. If I look at your marble diagrams I see some discrepancies. Here I have added a Timeline to help visualize.
111111111122222222223
Time: 123456789012345678901234567890
Input: 1---2--A-B----C--A-B-1--B-A--A
Output: 1---2----AB-------CAB-1-----BAA
Here I see the "AB" output published at unit 10.
Then I see the "CAB" output published at unit 19.
Further I see the "BAA" output published at unit 29.
But you suggest these should occur at constant timeouts apart.
So then I think it is maybe the gap between values that is important, but this doesn't seem to add up either. This just leads me back to my point above, please provide a unit test that could pass or fail.
Thirdly, regarding your implementation you could make it slightly better by using the SerialDisposable type for the lettersFlush type.
To help me set up the unit test I create the following block of code
var scheduler = new TestScheduler();
var input = scheduler.CreateColdObservable<char>(
ReactiveTest.OnNext(0100.Ms(), '1'),
ReactiveTest.OnNext(0500.Ms(), '2'),
ReactiveTest.OnNext(0800.Ms(), 'A'),
ReactiveTest.OnNext(1000.Ms(), 'B'),
ReactiveTest.OnNext(1500.Ms(), 'C'),
ReactiveTest.OnNext(1800.Ms(), 'A'),
ReactiveTest.OnNext(2000.Ms(), 'B'),
ReactiveTest.OnNext(2200.Ms(), '1'),
ReactiveTest.OnNext(2500.Ms(), 'B'),
ReactiveTest.OnNext(2700.Ms(), 'A'),
ReactiveTest.OnNext(3000.Ms(), 'A'));
var expected = scheduler.CreateColdObservable<string>(
ReactiveTest.OnNext(0100.Ms(), "1"),
ReactiveTest.OnNext(0500.Ms(), "2"),
ReactiveTest.OnNext(1000.Ms(), "AB"),
ReactiveTest.OnNext(2000.Ms(), "CAB"),
ReactiveTest.OnNext(2200.Ms(), "1"),
ReactiveTest.OnNext(3000.Ms(), "BAA"));
I have taken some liberties to change some values to what I think you meant by your marble diagrams.
If I then use the very good answer provided above by #Shlomo, I can see further issues with just using fuzzy marble diagrams. As the buffer boundary would have to happen after the last value to be included occurs, these windows need to close on an off-by-one time.
void Main()
{
var scheduler = new TestScheduler();
var input = scheduler.CreateColdObservable<char>(
ReactiveTest.OnNext(0100.Ms(), '1'),
ReactiveTest.OnNext(0500.Ms(), '2'),
ReactiveTest.OnNext(0800.Ms(), 'A'),
ReactiveTest.OnNext(1000.Ms(), 'B'),
ReactiveTest.OnNext(1500.Ms(), 'C'),
ReactiveTest.OnNext(1800.Ms(), 'A'),
ReactiveTest.OnNext(2000.Ms(), 'B'),
ReactiveTest.OnNext(2200.Ms(), '1'),
ReactiveTest.OnNext(2500.Ms(), 'B'),
ReactiveTest.OnNext(2700.Ms(), 'A'),
ReactiveTest.OnNext(3000.Ms(), 'A'));
var expected = scheduler.CreateColdObservable<string>(
ReactiveTest.OnNext(0100.Ms(), "1"),
ReactiveTest.OnNext(0500.Ms(), "2"),
ReactiveTest.OnNext(1000.Ms()+1, "AB"),
ReactiveTest.OnNext(2000.Ms()+1, "CAB"),
ReactiveTest.OnNext(2200.Ms(), "1"),
ReactiveTest.OnNext(3000.Ms()+1, "BAA"));
/*
111111111122222222223
Time: 123456789012345678901234567890
Input: 1---2--A-B----C--A-B-1--B-A--A
Output: 1---2----AB-------CAB-1-----BAA
*/
var bufferBoundaries = //Observable.Timer(TimeSpan.FromSeconds(1), scheduler);
//Move to a hot test sequence to force the windows to close just after the values are produced
scheduler.CreateHotObservable<Unit>(
ReactiveTest.OnNext(1000.Ms()+1, Unit.Default),
ReactiveTest.OnNext(2000.Ms()+1, Unit.Default),
ReactiveTest.OnNext(3000.Ms()+1, Unit.Default),
ReactiveTest.OnNext(4000.Ms()+1, Unit.Default));
var publishedFinal = input
.Publish(i => i
.Where(c => char.IsLetter(c))
.Buffer(bufferBoundaries)
.Where(l => l.Any())
.Select(lc => new string(lc.ToArray()))
.Merge(i
.Where(c => char.IsNumber(c))
.Select(c => c.ToString())
)
);
var observer = scheduler.CreateObserver<string>();
publishedFinal.Subscribe(observer);
scheduler.Start();
//This test passes with the "+1" values hacked in.
ReactiveAssert.AreElementsEqual(
expected.Messages,
observer.Messages);
}
// Define other methods and classes here
public static class TickExtensions
{
public static long Ms(this int ms)
{
return TimeSpan.FromMilliseconds(ms).Ticks;
}
}
I suppose my point is that Rx is deterministic, therefore we can create tests that are deterministic. So while your question is a very good one, and I do believe that #Shlomo provides a solid final answer, we can do better than just fuzzy marble diagrams and using Random in our examples/tests.
Being precise here should help prevent silly race conditions in production, and help reader better understand these solutions.
Assuming sampleInput as your sample Input:
var charStream = "12ABCAB1BAA".ToObservable();
var random = new Random();
var randomMilliTimings = Enumerable.Range(0, 12)
.Select(i => random.Next(2000))
.ToList();
var sampleInput = charStream
.Zip(randomMilliTimings, (c, ts) => Tuple.Create(c, TimeSpan.FromMilliseconds(ts)))
.Select(t => Observable.Return(t.Item1).Delay(t.Item2))
.Concat();
First, instead of changing a mutable variable, it would be best to instead generate some stream to represent your buffer windows:
Input: 1---2--A-B----C--A-B-1--B-A--A
Window: ---------*--------*---------*--
Output: 1---2----AB-------CAB-1-----BAA
I generated a stream of incrementing TimeSpans and called it bufferBoundaries like so to demonstrate:
var bufferBoundaries = Observable.Range(1, 20)
.Select(t => Observable.Return(t).Delay(TimeSpan.FromSeconds(t)))
.Concat();
This would look like this:
Seconds: 0--1--2--3--4--5--6--7--8--9--10
BB : ---1-----2--------3-----------4-
... next you want to split that sampleInput up into separate streams for letters and numbers, and handle them accordingly:
var letters = sampleInput
.Where(c => char.IsLetter(c))
.Buffer(bufferBoundaries)
.Where(l => l.Any())
.Select(lc => new string(lc.ToArray()));
var numbers = sampleInput
.Where(c => char.IsNumber(c))
.Select(c => c.ToString());
Next, merge the two streams together:
var finalOutput = letters.Merge(numbers);
Lastly, it's generally not a good idea to subscribe twice to the same input (in our case, sampleInput) if you can help it. So in our case, we should replace letters, numbers, and finalOutput with the following:
var publishedFinal = sampleInput
.Publish(_si => _si
.Where(c => char.IsLetter(c))
.Buffer(bufferBoundaries)
.Where(l => l.Any())
.Select(lc => new string(lc.ToArray()))
.Merge( _si
.Where(c => char.IsNumber(c))
.Select(c => c.ToString())
)
);