Как разделить список со встроенными маркерами перехода, используя LINQ или Reactive Extensions? - PullRequest
2 голосов
/ 26 февраля 2011

У меня есть следующий вход, поступающий из внешнего источника:

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

И я хотел бы извлечь список, который содержит только «хорошие» значения, так что в этом случае это будет 1,5,7. Я не могу предположить, сколько будет переходов и будет ли первый переход с «Хорошего» на «Плохой» или наоборот.

Мое текущее решение - использовать Enumerable.First, чтобы найти первый переход (чтобы проверить, являются ли первые значения хорошими или плохими), а затем выполнить цикл foreach над входом, поддерживая логическое значение isValueGood.

Есть ли более красноречивый подход для этого, используя LINQ или Reactive Extensions?

РЕДАКТИРОВАНИЕ : расширенный вопрос, чтобы задать также возможные решения в Rx.

Ответы [ 7 ]

2 голосов
/ 26 февраля 2011

LINQ действительно сияет, когда работает элемент за элементом, поэтому я подозреваю, что любой подход, использующий LINQ, на самом деле не будет элегантным (потому что, следует ли передавать конкретное значение в поток, зависит от значений, расположенных дальше в последовательности).

Я бы сделал то, что ты делаешь, но немного по-другому.Вместо того, чтобы использовать Enumerable.First, я бы сохранял текущее состояние как значение

enum State { Good, Bad, Unknown };

. Установите для начального состояния значение Неизвестно.Буферизуйте значения, пока не узнаете состояние.Если состояние указывает, что первый набор значений находился в состоянии Good, выведите эти буферизованные значения, а затем продолжайте, как вы описываете.Он предотвращает возможный O(n) обход, чтобы найти начальное состояние, и предотвращает обход последовательности дважды.

1 голос
/ 27 февраля 2011

Вот несколько вариантов, выберите один.Давайте назовем наш перечислимый e:

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

Если у вас есть перечислимое значение, а сигнал включения / выключения смешан, то использование .Scan () наиболее очевидно.По сути, это функциональная версия цикла foreach с изменяемым флагом:

        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);

Если у вас есть перечислимый объект и вы не возражаете написать свою собственную функцию расширения специально для этого случая, то, вероятно, наиболее уместно использовать yield return:

    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);

Если у вас есть наблюдаемые, и особенно если маркеры существуют как отдельные наблюдаемые, лучший вариант для создания расширения AndOn на основе .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);
    }

Вы можете использоватьAndOn с перечисленным выше, как это:

        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();
        }

И, наконец, способ RX geek, с помощью оператора Join, доступного в выпадающем списке RX в декабре 2010 года:

        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();
        }
1 голос
/ 26 февраля 2011

Вот как вы это делаете в Rx:

var allGoodStates = Observable.Concat(
    states.TakeUntil(x => stateTransitionsToBad(x)).Where(x => isNotTransition()),
    states.TakeUntil(x => stateTransitionsToGood(x).Where(_ => false)
).Repeat();
1 голос
/ 26 февраля 2011

Учитывая seq, определенное как в вашем посте, linq однострочный будет:

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);

Мне пришлось заставить ToArray() перезапустить синтаксический анализ последовательности, иначе он линейный. g в первой последовательности позволяет мне делать вычисления на каждом шаге, все еще выбирая все.

Я почти чувствую, как Эрик Липперт дрожит от моего поста ... снова.

1 голос
/ 26 февраля 2011
    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;
    }

Использование:

    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();
    }
0 голосов
/ 26 февраля 2011

Если ваш IEnumrable называется вводом, вы можете сделать что-то вроде этого:

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;
 });

ПРИМЕЧАНИЕ. Это похоже на то, как вы в настоящее время используете метод foreach.

0 голосов
/ 26 февраля 2011

Я думаю, что такие методы, как GoodValues ​​и BadValues, имеют смысл, но я бы не стал реализовывать их как методы расширения для IEnumerable<string> типов, потому что они не имеют смысла для всех IEnumerable<string> типов. Поэтому я бы сделал небольшой небольшой класс (возможно, под названием GoodBadList), который наследуется от IEnumerable<string> и предлагает два метода, которые я упомянул. Эти методы для каждого значения в списке будут смотреть вперед и назад до тех пор, пока не будет найден переход, определить направление, в котором происходит переход, и использовать его для возврата или отклонения этого значения в результирующем списке. Я знаю, что интересно писать элегантные операторы LINQ, но веселее иметь полное элегантное решение, которое включает в себя написание класса, когда это имеет смысл. Этот GoodBadList - это концепция, которая, кажется, существует в вашем домене, поэтому я бы изложил ее по частям. Что ты думаешь?

...