开发者

How can I split a list with embedded transition markers using LINQ or Reactive Extensions?

I have the following input, arriving from an external source:

IEnumerable<string> = new [] { "1", "5", "Transition Good->Bad", "3", "2",    
                              "Transition Bad->Good", "7", "Transition Good->Bad", 
                              "9", "12" };

And I'd like to extract a list that contains only "Good" values, so in this case, that would be 1,5,7. I cannot make any assumptions about how many transitions there will be, and whether the first transition will be from "Good" to "Bad" or the other way around.

My current solution is use Enumerable.First to find the first transition (to check whether the first values are Good or Bad), and then do a foreac开发者_开发问答h loop over the input, maintaining a "isValueGood" boolean throughout.

Is there a more eloquent approach of doing this, using LINQ or Reactive Extensions?

EDITED: Expanded question to ask for possible solutions in Rx as well.


LINQ really shines when working element by element, so I would suspect any approach using LINQ is not really going to be elegant (because whether or not to stream a particular value depends on values farther along in the sequence).

I would do what you're doing, but slightly different. Rather than using Enumerable.First, I would store the current state as a value of

enum State { Good, Bad, Unknown };

Set the initial state to Unknown. Buffer the values until you know the state. If the state indicates that the first set of values was in a Good state, output those buffered values, and then proceed as you describe. The prevents a possibly O(n) walk to find the initial state, and it prevents walking the sequence twice.


Given seq defined as in your post, a linq one-liner would be:

bool? good=null, first=null;
var result=seq
  .Select(w=>new 
  { 
    g=(w.Contains("Good->")?(good=(first.HasValue?false:!(first=true)))
      :(w.Contains("Bad->")?(good=(first.HasValue?true:!(first=false))):good)), 
    n=w.Contains("->")?null:w
  })
  .Where(w=>w.n!=null && ((!good.HasValue)||(good.HasValue&&good.Value))).ToArray()
  .Where(w=>(w.g.HasValue&&w.g.Value)||(!w.g.HasValue&&first.Value))
  .Select(w=>w.n);

I had to force the ToArray() to restart the parsing of the sequence, otherwise it's linear. The g in the first sequence is to let me do calculations at each step while still selecting everything.

I can almost feel Eric Lippert shuddering at my post... again.


    static IEnumerable<string> GetGoodItems(IEnumerable<string> items)
    {
        var first = items.FirstOrDefault(i => i == "bad->good" || i == "good->bad");
        return first != null
            ? first == "bad->good"
                ? GetGoodItemsImpl(items.SkipWhile(i => i != "bad->good").Skip(1)) 
                : GetGoodItemsImpl(items) 
            : GetGoodItemsImpl(items);
    }

    static IEnumerable<string> GetGoodItemsImpl(IEnumerable<string> items)
    {
        var goodItems = items.TakeWhile(i => i != "good->bad");
        var remaining = items.SkipWhile(i => i != "bad->good").Skip(1);
        return remaining.Any() ? goodItems.Concat(GetGoodItems(remaining)) : goodItems;
    }

Usage:

    static void Main()
    {
        var values = new[] { "1", "2", "3", "4", "good->bad", "13", "14", "15", "bad->good", "2", "1", "good->bad", "15", "12", "11", "bad->good", "3" };
        Console.WriteLine(string.Join(", ", GetGoodItems(values)));

        var values2 = new[] { "1", "2", "3", "4", "bad->good", "13", "14", "15", "good->bad", "2", "1", "bad->good", "15", "12", "11", "good->bad", "3" };
        Console.WriteLine(string.Join(", ", GetGoodItems(values2)));
        Console.ReadKey();
    }


Here's how you do this in Rx:

var allGoodStates = Observable.Concat(
    states.TakeUntil(x => stateTransitionsToBad(x)).Where(x => isNotTransition()),
    states.TakeUntil(x => stateTransitionsToGood(x).Where(_ => false)
).Repeat();


Here are few choices, pick one. Let's name our enumerable e:

        var e = new[]
                     {
                         "1", "5",
                         "Transition Good->Bad",
                         "3", "2",
                         "Transition Bad->Good",
                         "7",
                         "Transition Good->Bad",
                         "9", "12"
                     };

If you have enumerable and the on/off signal is mixed in, then using .Scan() is most evident. It is basically a functional version of a foreach loop with a mutable flag:

        var goods = e
            .Scan(Tuple.Create("", 1),
                  (x, y) => Tuple.Create(y,
                      y.StartsWith("Transition") 
                      ? y.EndsWith("Good") ? 1 : -1 
                      : x.Item2))
            .Where(x => !x.Item1.StartsWith("Transition") && x.Item2 > 0)
            .Select(x => x.Item1);

If you have enumerable and don't mind writing your own extension function specifically for this case, using yield return is probably most elegant:

    public static IEnumerable<TSource> SplitByMarkers<TSource>(
        this IEnumerable<TSource> source, Func<TSource, int> fMarker)
    {
        var isOn = true;
        foreach (var value in source)
        {
            var m = fMarker(value);
            if (m == 0)
                if (isOn)
                    yield return value;
                else
                    continue;
            else
                isOn = m > 0;
        }
    }
        var goods = e.SplitByMarkers(x => 
            x.StartsWith("Transition") 
            ? x.EndsWith("Good") ? 1 : -1 
            : 0);

If you have observable, and especially if the markers exist as a separate observable, the best option to create AndOn extension based on .CombineLatest:

    public static IObservable<TSource> AndOn<TSource>(
        this IObservable<TSource> source, IObservable<bool> onOff)
    {
        return source
            .CombineLatest(onOff, (v, on) => new { v, on })
            .Where(x => x.on)
            .Select(x => x.v);
    }

You can use AndOn with the above enumerable like this:

        var o = e.ToObservable().Publish();
        var onOff = o
            .Where(x => x.StartsWith("Transition"))
            .Select(x => x.EndsWith("Good"))
            .StartWith(true);
        var goods = o
            .AndOn(onOff)
            .Where(x => !x.StartsWith("Transition"));

        using (goods.Subscribe(Console.WriteLine))
        using (o.Connect())
        {
            Console.ReadKey();
        }

And finally, RX geek way, using the Join operator available in the Dec 2010 drop of RX:

        var o = e.ToObservable().Publish();
        var gb = o.Where(x => x == "Transition Good->Bad");
        var bg = o.Where(x => x == "Transition Bad->Good").Publish("");
        var goods =
            from s in o
            join g in bg on Observable.Empty<string>() equals gb
            where !s.StartsWith("Transition")
            select s;

        using (goods.Subscribe(Console.WriteLine))
        using (bg.Connect())
        using (o.Connect())
        {
            Console.ReadKey();
        }


I think methods such as GoodValues and BadValues make good sense, but I wouldn't implement them as extension methods to IEnumerable<string> types because they don't make sense for all IEnumerable<string> types. So I would make a quick little class (maybe called GoodBadList) that inherits from IEnumerable<string> and offers the two methods I mentioned. Those methods would, for each value in the list, look both forward and backward until a transition is found, determine which direction the transition is in and use that to either return or reject that value in the resulting list. I know it's fun to write elegant LINQ statements, but it's more fun to have an entire elegant solution which includes writing a class when it makes sense. This GoodBadList is a concept that seems to exist in your domain, so I would put it in bits. What do you think?


Provided your IEnumrable<string> is called input, you could do something like this:

bool addItem = input.First(item => item.StartsWith("Transition")).EndsWith("->Bad");
var good = input.Where(item =>
{
     if (item.StartsWith("Transition"))
     {
         addItem = item.EndsWith("->Good");
         return false;
     }
     return addItem;
 });

NOTE: This is similar to the way you are currently doing it with your foreach method.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜