2013-10-15 2 views
3

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

Одной из задач, которые я должен был сделать недавно, является поиск элементов, которые являются нулями в наборе элементов, а затем найдите следующий непустой элемент, а затем усредняют разницу между этими нулевыми записями. То есть, скажем, у нас есть набор данных A:

A = { 0f, 1f, 2f, 5f, Null, Null, Null, 7f, Null, 8f } 

Важно отметить, что мы должны различать между 0 и Null. Разница, очевидно, в том, что 0 равно 0, а Null вообще не является данными.

Использование LINQ, есть способ, которым мы можем в принципе получить доступ к следующему подразделу A:

Subsection { Null, Null, Null, 7f } 

И иметь его в коллекции таким образом, что мы можем превратить его в (7/4f) в течение четыре запись ..

Subsection { 1.75f, 1.75f, 1.75f, 1.75f } 

Такое, что, когда снова итерация A, мы получаем следующий результат:

{ 0f, 1f, 2f, 5f, 1.75f, 1.75f, 1.75f, 1.75f, 4f, 4f } 

В настоящее время я делаю это с использованием числового значения, ищущего нулевой элемент, затем сохраняя все последовательные значения NULL в List<T> и после нахождения следующего ненулевого значения, назначая все переменные, итерируя по указанному List<T>. Он выполняет эту работу, но выглядит довольно неприятно.

Итак, ради нарциссима, есть ли способ сделать это аккуратно (= меньше беспорядка кода)?

Псевдо

a = { 0, 1, 2, 5, null, null, null, 7, null, 0 } 


nullList = new List() 
for i = 0, a.length 
    if i == null 
     nullList.add(i) 
    else 
     if nullList.length > 0 
      nullList.add(i) 
      int avg = nullList.Aggregate(x => x) 
      foreach element in nullList 
       element = avg 
      nullList.clear() 
+1

Можете ли вы опубликовать часть своего фактического кода? – JMK

+0

Будет ли такое же правило применяться к 'Null, 8f' i.e.' 4f, 4f'? – James

+0

@James - да, я исправил его –

ответ

2

Если я правильно понимаю ваш вопрос, вы хотите заменить null значения в списке со значением на основе первого не null значения. Я не понимаю, зачем вам нужен второй список null s для этого. Вот попытка просто изменить список на месте, хотя это не намного меньше, чем то, что у вас уже есть:

var A = new List<float?> { 0f, 1f, 2f, 5f, null, null, null, 7f, null, 8f }; 

for (int i = A.IndexOf(null); i != -1; i = A.IndexOf(null, i)) 
{ 
    int j = 0; 
    do { j++; } while (A[i + j] == null); 
    float f = A[i + j].Value/(j + 1); 
    do { A[i++] = f; } while (j --> 0); 
} 

// A == { 0f, 1f, 2f, 5f, 1.75f, 1.75f, 1.75f, 1.75f, 4f, 4f } 

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

Как указано в многочисленных комментариях, использование LINQ здесь не дает никаких реальных преимуществ.

+0

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

+0

Я думаю, вам нужно разделить на 'j + 1', так как j на 0 основано – Harrison

+1

@DanPantry: В вашем псевдокоде я вижу два списка:' a' и 'nullList'. Мой ответ не имеет «nullList», но, помимо этого, в основном делает то же, что и ваш псевдокод. Я не знаю, менее ли это для ваших глаз или нет :-) – dtb

2

Итак, сначала мы будем использовать вспомогательный метод, называемый GroupWhile. Он будет иметь последовательность и функцию; этой функции будет присвоен предыдущий элемент, а текущий элемент и на основе этого определит, должен ли текущий элемент быть частью новой группы или части предыдущей группы.Это позволяет нам элементы группы в то время как некоторые условия:

public static IEnumerable<IEnumerable<T>> GroupWhile<T>(
    this IEnumerable<T> source, Func<T, T, bool> predicate) 
{ 
    using (var iterator = source.GetEnumerator()) 
    { 
     if (!iterator.MoveNext()) 
      yield break; 

     List<T> list = new List<T>() { iterator.Current }; 

     T previous = iterator.Current; 

     while (iterator.MoveNext()) 
     { 
      if (predicate(previous, iterator.Current)) 
      { 
       list.Add(iterator.Current); 
      } 
      else 
      { 
       yield return list; 
       list = new List<T>() { iterator.Current }; 
      } 

      previous = iterator.Current; 
     } 
     yield return list; 
    } 
} 

С помощью этого мы можем сгруппировать элементы в то время как предыдущий элемент является нулевым. Затем мы берем каждую группу, повторите среднее значение этой группы group.Count() раз, а затем расплющить последовательность снова:

public static IEnumerable<float> ConsolodateNulls<T>(IEnumerable<float?> source) 
    where T : struct 
{ 
    return source.GroupWhile((prev, curr) => prev == null) 
     .SelectMany(group => Enumerable.Repeat(
      group.LastOrDefault(item => item != null) ?? 0/group.Count(), 
      group.Count())); 
} 
+0

Это то, что я искал, я не на 100% на самих итераторах, вероятно, поэтому я сам не мог написать его –

+0

@DanPantry Честно говоря, я бы пошел с решением, с которым вам пришлось начинать. У меня была реализация 'GroupWhile', которая уже сидит, чтобы повторно использовать, что делает это не так * плохой. Если бы вы этого не сделали, это было бы * путь * больше работы, пытаясь написать это. – Servy

+0

Решение, которое я написал, связано с большим количеством скопирования копий, вот что. По крайней мере, с LINQ он выглядит намного лучше в методе расширения и может быть загружен на 'IEnumerable ' и может использоваться в других целях за пределами этой области. Я ценю, что цикл цикла ** может использоваться ** в методе расширения, но он чувствует себя намного более niché, если это имеет смысл. Подобная ситуация происходит по всему унаследованному коду, и я являюсь основной обезьяной кода в нашей ИТ-команде :( –

0

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

public IEnumerable<float?> ProcessSequence(IEnumerable<float?> seq) 
{ 
int nullCount = 0; 
foreach(var x in seq) 
{ 
    if (x == null) 
    { 
     nullCount++; 
    } 
    else if (nullCount > 0) 
    { 
     nullCount++; 
     var mid = x/nullCount; 
     for (var i = 0; i<nullCount; i++) 
     { 
      yield return mid; 
     } 
     nullCount = 0; 
    }  
    else 
    { 
     yield return x; 
    } 
} 
} 
+0

Он имеет дело с последовательностью поплавков, а не с ints, но это не является серьезным изменением. – Servy

+0

@Servy, справа, спасибо. Может даже сделать его полностью общим с параметром Func для вычисления среднего значения. –

+0

Нет, это должно быть 'Func , T>' (или, возможно, 'Func ' , вместо этого для решения без последовательности) для вычисления среднего значения, если вы хотите поддерживать любой тип 'T'. – Servy

1

Вы можете создать метод расширения, который сделает это. Этот метод расширения может быть использован в обычном заявлении LINQ:

public static IEnumerable<float> SmoothGaps(this IEnumerable<float?> source) 
{ 
    int numberOfNulls = 0; 
    foreach(var item in source) 
    { 
     if(item == null) 
     { 
      ++numberOfNulls; 
     } 
     else 
     { 
      if(numberOfNulls != 0) 
      { 
       for(int i=0; i <= numberOfNulls; ++i) 
        yield return item.Value/(numberOfNulls + 1); 
      } 
      else 
       yield return item.Value; 
      numberOfNulls = 0; 
     } 
    } 
} 

Использование было бы просто:

var result = a.SmoothGaps(); 

null s в конце source будут просто удалены.

+0

1) Зачем вручную перебирать последовательность, а не использовать 'foreach'? 2) Вы не распоряжаетесь перечислителем 3) это, вероятно, не должно быть методом расширения; это вряд ли будет достаточно для использования; это просто загрязнение intellisense. Обычный статический метод, вероятно, прекрасен. – Servy

+0

@Servy Хорошая точка с перечислителем. Это было последствием более ранней попытки. Удалены. О методе расширения: это полностью зависит от применения OP. И оно появляется только тогда, когда включено пространство имен. –

+1

@Servy не согласен с «загрязнением intellisense». Пространства имен существуют, чтобы избежать этого. И метод расширения позволяет связывать: x.SmoothGaps(). Где (..) и т. Д. –

1

Вот как вы можете сделать это чисто в LINQ:

var data = new List<float?> { 0f, 1f, 2f, 5f, null, null, null, 7f, null, 8f }; 
var corrected = data 
    .Select((v,i) => new { 
     Index = i 
     // Find the index of the next non-null item in the list 
    , NextNonNull = i + data 
      .Skip(i) 
      .Select((vv,j) => new {j,vv}) 
      .First(p => p.vv.HasValue).j 
    , Value = v 
    }) 
    .GroupBy(p => p.NextNonNull) 
    // For each group, insert its average g.Count() times 
    .SelectMany(g => g.Select(e => data[g.Key]/g.Count())) 
    .ToList(); 
for (var i = 0 ; i != data.Count ; i++) { 
    Console.WriteLine("{0} - {1}", data[i], corrected[i]); 
} 

Отказ от ответственности: это решение предназначено только для целей развлечений. Он будет медленнее, чем ваше решение, основанное на цикле for, потенциально добавляя дополнительный порядок сложности (т. Е. Делая его O(n^2) вместо O(n)).

+0

[Демо на идеоне] (http://ideone.com/bAu3pX). – dasblinkenlight

+1

+1 для забавного меня –

1

Чистая версия LINQ с использованием Aggregate для вашего развлечения:

float?[] A = { 0f, 1f, 2f, 5f, null, null, null, 7f, null, 8f }; 
var result = A.Aggregate(Tuple.Create(new List<float>(), 0), 
(items, current) => 
{ 
    if(current.HasValue) 
    { 
     if(items.Item2 == 0) 
      items.Item1.Add(current.Value); 
     else 
     { 
      var avg = current.Value/(items.Item2 + 1); 
      for(int i = 0; i <= items.Item2; i++) 
       items.Item1.Add(avg); 
     } 
     return Tuple.Create(items.Item1, 0); 
    } 
    else 
     return Tuple.Create(items.Item1, items.Item2 + 1); 
}).Item1; 

я бы не использовать это в рабочем коде, так как руководитель среднего разработчика взорвется на Aggregate, используя Tuple в C# всегда выглядят своего рода уродливый и императивное решение работает хорошо и более понятно, чем это.

Смежные вопросы