Как я могу обнаружить недостающие элементы в IEnumerable‹T›?

У меня есть IEnumerable<T>, содержащий список элементов данных с согласованными интервалами в одном из свойств:

List<Interval> list = new List<Interval>
            { 
                new Interval{ TIME_KEY = 600},
                new Interval{ TIME_KEY = 605},
                new Interval{ TIME_KEY = 615},
                new Interval{ TIME_KEY = 620},
                new Interval{ TIME_KEY = 630}
            };

Как я могу запросить этот список (предпочтительно с помощью Linq), чтобы получить список, который выглядит следующим образом:

 List<Interval> list = new List<Interval>
                { 
                    new Interval{ TIME_KEY = 610},
                    new Interval{ TIME_KEY = 625}
                };

?

РЕДАКТИРОВАТЬ: я, вероятно, буду знать, каким должно быть интервальное расстояние, но если есть способ определить его, изучив данные, это было бы огромным бонусом!

РЕДАКТИРОВАТЬ: изменено на числовые значения


person Chris McCall    schedule 14.09.2010    source источник
comment
Вы знаете, каким будет интервал, или программа должна это выяснить?   -  person NullUserException    schedule 14.09.2010
comment
Есть ли числовое представление этих чисел в классе Interval?   -  person Steve Danner    schedule 14.09.2010
comment
Я могу быть в меньшинстве по этому вопросу, но это, кажется, хороший пример того, как не использовать linq. Простой краткий цикл foreach будет легко понятен с первого взгляда, но решения linq, похоже, не проходят проверку на беглый взгляд. Конечно, вы можете сделать это в одной строке, но должны ли вы?   -  person Brian Gideon    schedule 15.09.2010


Ответы (4)


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

// I'd probably rename SelectBetween to SelectConsecutive
list.SelectConsecutive((x, y) => new { Original = x, Interval = y - x})
    .Where(pair => pair.Interval != 5)
    .Select(pair => new Interval(pair.Original + 5));

(Несколько псевдокод, но я надеюсь, вы понимаете, к чему я клоню.)

Однако это сгенерирует только один элемент, когда он отсутствует... если вы перейдете от 0 к 20, он не сгенерирует 5, 10, 15.

Чтобы добавить немного мяса ко второму предложению Хенка:

var missing = Enumerable.Range(0, expectedElementCount)
                        .Select(x => new Interval(baseInterval + 5 * x)
                        .Except(list);
person Jon Skeet    schedule 14.09.2010
comment
Разве SelectConsecutive не очень похож на сканирование из Haskell? Если это так, нельзя ли это реализовать с помощью Zip и Skip: xs.Zip(xs.Skip(1), f)? - person R. Martinho Fernandes; 14.09.2010
comment
Во втором фрагменте кода мне трудно понять, что должна представлять база (это также зарезервированное ключевое слово). Не могли бы вы немного пояснить? - person Chris McCall; 14.09.2010
comment
Я предполагаю, что base является первым элементом в списке, он же var @base = list.First(). - person R. Martinho Fernandes; 14.09.2010
comment
@Martinho: Да, разница в том, что SelectConsecutive будет сканировать список только один раз. Мне не нравится повторять это дважды, что делает Zip/Skip. - person Jon Skeet; 14.09.2010
comment
@Chris: Да, базовый интервал был базовым интервалом, из которого строится все остальное. Переименовал его, чтобы избежать конфликта ключевых слов :) - person Jon Skeet; 14.09.2010
comment
Это должен быть первый раз, когда мне не нравится ваш ответ, может быть, это мой скудный ум, но я думаю, что это можно сделать более читабельно/просто. Это как смотреть, как пьяный грабитель избивает твоего любимого супергероя, грустный день.. - person Jimmy Hoffa; 14.09.2010
comment
@ Джимми: я думаю, что второй подход достаточно прост, не так ли? Должен признаться, я не совсем понял ваш ответ :( - person Jon Skeet; 14.09.2010
comment
@Jon Skeet: Да, мой ответ, возможно, тоже не самый простой для понимания, это просто то, что пришло на ум сразу после того, как я понял, что Хенк уже упоминал о выполнении foreach без linq, что я считаю самым простым/наиболее читаемым способом определения отсутствующего интервала. элементы. Что касается второй части вашего ответа, я действительно не понимаю ни одну из частей вашего ответа. - person Jimmy Hoffa; 14.09.2010
comment
@Jimmy: Я считаю, что вторая часть моего ответа в целом похожа на вашу - создайте полный список, а затем удалите существующие. Первая часть предназначена для обнаружения отсутствующих элементов путем простого сравнения последовательных элементов. - person Jon Skeet; 15.09.2010

Эффективным и простым способом было бы просто просмотреть этот список с помощью foreach и обнаружить пробелы.
Я полагаю, что 5-минутный такт установлен?

Чтобы использовать LINQ, вы можете создать полный список и найти разницу, но это кажется излишним.


Учитывая 2-ю часть, определяя интервал:

Из вашего примера, вероятно, подойдет образец из 3 или 4 значений. Но вы не можете быть абсолютно уверены даже после проверки всех значений. Данные вашего примера не исключают 1-минутную частоту с большим количеством пропущенных значений.

Так что вам нужны очень хорошие спецификации в отношении этой части.

person Henk Holterman    schedule 14.09.2010
comment
Я собирался предложить второй подход... отредактирую свой ответ с помощью кода. - person Jon Skeet; 14.09.2010
comment
+1 за то, что не предлагает перейти непосредственно к linq для чего-то сложного и сложного для понимания в linq, но простого и легкого для понимания при обычной итерации. - person Jimmy Hoffa; 14.09.2010

Это будет работать, если интервал известен, если у вас есть доступ к Zip (поставляется с .NET 4):

list.Zip(list.Skip(1), (x,y) => new { x, delta = y - x })
    .SelectMany(a => Enumerable.Range(1, a.delta/interval - 1)
                               .Select(i => a.x + i*interval));

Обратите внимание, что это повторяет список дважды, поэтому, если источник является ленивым перечислением, вам нужно сначала его буферизовать. Эта конструкция с Zip и Skip — это быстрый и грязный способ спроецировать последовательные элементы вместе. В библиотеке System.Interactive Reactive Extensions есть метод Scan для этого, и Джон показал возможный реализация в другом ответе. Ни один из них не повторяет список дважды, поэтому они были бы гораздо лучшим выбором.

Если интервал должен быть определен, вы можете получить минимальную дельту:

var deltas = list.Zip(list.Skip(1), (x,y) => y - x );
var interval = deltas.Min();
list.Zip(deltas, (x, delta) => new { x, delta })
    .SelectMany(a => Enumerable.Range(1, a.delta/interval - 1)
                               .Select(i => a.x + i*interval));

Однако есть некоторые предположения, которые я сделал:

  • все разности между элементами кратны интервалу;
  • ввод отсортирован.

Как это работает:

  1. Сначала он строит последовательность пар с каждым элементом, кроме последнего, и интервалом до следующего за ним элемента;
  2. Затем для каждой из этих пар он создает все недостающие значения в пределах дельты: в каждой дельте есть ровно a.delta/interval - 1 значений, и каждое из них находится на определенном количестве интервалов от хранилища элементов в паре, следовательно, a.x + i*interval.
  3. SelectMany заботится о том, чтобы объединить все эти последовательности отсутствующих значений в одно.
person R. Martinho Fernandes    schedule 14.09.2010
comment
Ваше решение хорошее, но оно будет работать только в том случае, если список отсортирован (как в примере). - person Gabe; 14.09.2010

Попробуй это:

private static IEnumerable<Interval> CalculateMissingIntervals(IEnumerable<Interval> list, int step) {
  return list.Zip(list.Skip(1), 
    (i1, i2) => IntervalRange(i1.TIME_KEY + step, i2.TIME_KEY, step)).
    SelectMany(x => x);
}
private static IEnumerable<Interval> IntervalRange(int start, int end, int step) {
  for (var i = start; i < end; i += step) {
    yield return new Interval { TIME_KEY = i };
  }
}

Предполагается, что исходный список отсортирован.

person Jordão    schedule 14.09.2010