2

This question is based on the similarly titled question here, with two differences:

  • I'm matching on multiple keys. No problem.
  • Keys may repeat. Problem.

My test code is below. I need the following behavior:

  • A CoordBundle is published as soon as at least one CoordMetrics and one CoordData is observed.
  • If a particular X/Y key recurs on either observable, a new CoordBundle is published.

What must I do to accomplish this?

public class CoordMetrics
{
    internal CoordMetrics(int x, int y, IEnumerable<IMetric> metrics)
    {
        X = x;
        Y = y;
        Metrics = metrics;
    }
    internal int X { get; private set; }
    internal int Y { get; private set; }
    internal IEnumerable<IMetric> Metrics { get; private set; }
}

public class CoordData
{
    internal CoordData(int x, int y, IEnumerable<IDatum> data)
    {
        X = x;
        Y = y;
        Data = data;
    }

    internal int X { get; private set; }
    internal int Y { get; private set; }
    internal IEnumerable<IDatum> Data { get; private set; }
}

public class CoordBundle
{
    internal CoordBundle(int x, int y, IEnumerable<IMetric> metrics, IEnumerable<IDatum> data)
    {
        X = x;
        Y = y;
        Metrics = metrics;
        Data = data;
    }

    internal int X { get; private set; }
    internal int Y { get; private set; }
    internal IEnumerable<IMetric> Metrics { get; private set; }
    internal IEnumerable<IDatum> Data { get; private set; }
}

[TestClass]
public class PairingTest
{
    [TestMethod, TestCategory("Temp")]
    public void PairedObservableTest()
    {
        Trace.Listeners.Add(new TextWriterTraceListener(Console.Out));
        var aSource = new Subject<CoordMetrics>();
        var bSource = new Subject<CoordData>();

        var paired = Observable.Merge(aSource.Select(a => new Pair(a, null)), bSource.Select(b => new Pair(null, b)))
                                .GroupBy(p => p.Item1 != null ? new { p.Item1.X, p.Item1.Y } : new { p.Item2.X, p.Item2.Y })
                                .SelectMany(g => g.Buffer(2).Take(1))
                                .Select(g => new Pair(
                                  g.ElementAt(0).Item1 ?? g.ElementAt(1).Item1,
                                  g.ElementAt(0).Item2 ?? g.ElementAt(1).Item2))
                                 .Select(t => new CoordBundle(t.Item1.X, t.Item1.Y, t.Item1.Metrics, t.Item2.Data));

        paired.Subscribe(g => Trace.WriteLine(String.Format("{0},{1}", g.X, g.Y)));

        bSource.OnNext(new CoordData(2, 1, Enumerable.Empty<IDatum>()));
        aSource.OnNext(new CoordMetrics(2, 2, Enumerable.Empty<IMetric>()));
        aSource.OnNext(new CoordMetrics(1, 1, Enumerable.Empty<IMetric>()));
        bSource.OnNext(new CoordData(1, 2, Enumerable.Empty<IDatum>()));
        bSource.OnNext(new CoordData(2, 2, Enumerable.Empty<IDatum>()));
        bSource.OnNext(new CoordData(1, 1, Enumerable.Empty<IDatum>()));
        aSource.OnNext(new CoordMetrics(1, 2, Enumerable.Empty<IMetric>()));
        aSource.OnNext(new CoordMetrics(2, 1, Enumerable.Empty<IMetric>()));
        aSource.OnNext(new CoordMetrics(2, 2, Enumerable.Empty<IMetric>()));
        bSource.OnNext(new CoordData(2,2,Enumerable.Empty<IDatum>()));
    }
}

Desired output - the code above outputs only the first 4 lines:

2,2
1,1 
1,2
2,1
2,2
2,2
Community
  • 1
  • 1
ket
  • 728
  • 7
  • 22
  • It would be helpfull if you created a *minimal* complete verifiable example http://stackoverflow.com/help/mcve. The types `Pair`, `IMetric` and `IData` are not defined. It also appears that they are not actually required for your example either. Lastly, there appear to be no asserts in your test, it would be great if instead of your comment, you moved your expectation to a set of asserts. – Lee Campbell Jun 02 '16 at 03:01
  • Great feedback, I'll try to make my questions a little cleaner next time around. – ket Jun 03 '16 at 15:49

1 Answers1

1

I think I have what you want. To be fair this isn't an easy problem. It is fairly common that one sequence is the seed for the other, but here your complication is that either sequence can be the seed of the other.

The first thing I did to get a working solution was to break it down in to a verifiable unit test. I suggest using the TestScheduler and its associated types to do this (instead of subjects etc).

I created the marble diagram from what I think your requirements were. Then I could map that into the two test input sequences and the expected output sequence.

The last part was to actually create the query.

The approach I ended up with* was to create two sequences that would try to match from a master sequence and a child sequence -> SelectMany + Where. However as both inputs could play the role of the master sequence I needed to do this twice. As I would subscribe twice, I needed to share the sequences -> Publish(). Also as each sequence could produce multiple values, I needed to cancel matching from previous matches when the duplicate arrives -> TakeUntil. Finally I just merged the two results sets together -> Merge.

*I considered GroupJoin & CombineLatest but they didn't seems to work for me.

[TestClass]
public class PairingTest
{
    [TestMethod, TestCategory("Temp")]
    public void PairedObservableTest()
    {
        var scheduer = new TestScheduler();

        /*
        Legend 
            a = aSource (CoordMetrics)
            b = bSource (CoordData)
            r = expected result


        a   ----2--1-----------1--2--2-----
                2  1           2  1  2

        b   -2--------1--2--1-----------2--
             1        2  2  1           2

        r   -------------2--1--1--2--2--2--
                         2  1  2  1  2  2
        */
        var aSource = scheduer.CreateColdObservable<CoordMetrics>(
            ReactiveTest.OnNext(5, new CoordMetrics(2, 2)),
            ReactiveTest.OnNext(8, new CoordMetrics(1, 1)),
            ReactiveTest.OnNext(20, new CoordMetrics(1, 2)),
            ReactiveTest.OnNext(23, new CoordMetrics(2, 1)),
            ReactiveTest.OnNext(26, new CoordMetrics(2, 2))
        );
        var bSource = scheduer.CreateColdObservable<CoordData>(
            ReactiveTest.OnNext(2, new CoordData(2, 1)),
            ReactiveTest.OnNext(11, new CoordData(1, 2)),
            ReactiveTest.OnNext(14, new CoordData(2, 2)),
            ReactiveTest.OnNext(17, new CoordData(1, 1)),
            ReactiveTest.OnNext(29, new CoordData(2, 2))
        );

        var testObserver = scheduer.CreateObserver<string>();
        Implementation(aSource, bSource)
            .Subscribe(testObserver);



        scheduer.Start();

        ReactiveAssert.AreElementsEqual(
            new[] {
                    ReactiveTest.OnNext(14, "2,2"),
                    ReactiveTest.OnNext(17, "1,1"),
                    ReactiveTest.OnNext(20, "1,2"),
                    ReactiveTest.OnNext(23, "2,1"),
                    ReactiveTest.OnNext(26, "2,2"),
                    ReactiveTest.OnNext(29, "2,2")
                },
            testObserver.Messages
        );
    }

    private static IObservable<string> Implementation(IObservable<CoordMetrics> aSource, IObservable<CoordData> bSource)
    {
        return Observable.Create<string>(observer =>
        {
            var aShared = aSource.Publish();
            var bShared = bSource.Publish();

            var fromA = aShared.SelectMany(a => bShared
                    //Find matching values from B's
                    .Where(b => a.X == b.X && a.Y == b.Y)
                    //Only run until another matching A is produced
                    .TakeUntil(aShared.Where(a2 => a2.X == a.X && a2.Y == a.Y))
                    //Project/Map to required type.
                    .Select(b => new CoordBundle(a.X, a.Y /*,  a.Metrics, b.Data*/ ))
                );

            var fromB = bShared.SelectMany(b => aShared
                    //Find matching values from A's
                    .Where(a => a.X == b.X && a.Y == b.Y)
                    //Only run until another matching B is produced
                    .TakeUntil(bShared.Where(b2 => b2.X == b.X && b2.Y == b.Y))
                    //Project/Map to required type.
                    .Select(a => new CoordBundle(a.X, a.Y /*,  a.Metrics, b.Data*/ ))
                );

            var paired = Observable.Merge(fromA, fromB);

            paired
                .Select(g => String.Format("{0},{1}", g.X, g.Y))
                .Subscribe(observer);

            return new CompositeDisposable(aShared.Connect(), bShared.Connect());
        });
    }
}

// Define other methods and classes here
public class CoordMetrics
{
    internal CoordMetrics(int x, int y)
    {
        X = x;
        Y = y;
    }
    internal int X { get; private set; }
    internal int Y { get; private set; }
}

public class CoordData
{
    internal CoordData(int x, int y)
    {
        X = x;
        Y = y;
    }

    internal int X { get; private set; }
    internal int Y { get; private set; }
}

public class CoordBundle
{
    internal CoordBundle(int x, int y)
    {
        X = x;
        Y = y;
    }

    internal int X { get; private set; }
    internal int Y { get; private set; }
}
public class Pair
{
    public Pair(CoordMetrics x, CoordData y)
    {
        Item1 = x;
        Item2 = y;
    }
    public CoordMetrics Item1 { get; set; }
    public CoordData Item2 { get; set; }
}
Lee Campbell
  • 10,631
  • 1
  • 34
  • 29
  • This works perfect, and was also really informative about how to actually test observables. Thanks! – ket Jun 03 '16 at 15:50