Как использовать LINQ для выбора объекта с минимальным или максимальным значением свойства - PullRequest
411 голосов
/ 27 мая 2009

У меня есть объект Person со свойством Nullable DateOfBirth. Есть ли способ использовать LINQ для запроса списка объектов Person для объекта с самым ранним / наименьшим значением DateOfBirth.

Вот с чего я начал:

var firstBornDate = People.Min(p => p.DateOfBirth.GetValueOrDefault(DateTime.MaxValue));

Нулевым значениям DateOfBirth присвоено значение DateTime.MaxValue, чтобы исключить их из минимального рассмотрения (при условии, что хотя бы у одного указан указанный DOB).

Но все, что мне нужно, это установить firstBornDate в значение DateTime. То, что я хотел бы получить, это объект Person, который соответствует этому. Нужно ли мне написать второй запрос так:

var firstBorn = People.Single(p=> (p.DateOfBirth ?? DateTime.MaxValue) == firstBornDate);

Или есть более простой способ сделать это?

Ответы [ 12 ]

269 голосов
/ 27 мая 2009
People.Aggregate((curMin, x) => (curMin == null || (x.DateOfBirth ?? DateTime.MaxValue) <
    curMin.DateOfBirth ? x : curMin))
205 голосов
/ 27 мая 2009

К сожалению, для этого нет встроенного метода.

PM> Install-Package morelinq

var firstBorn = People.MinBy(p => p.DateOfBirth ?? DateTime.MaxValue);

В качестве альтернативы, вы можете использовать реализацию, которую мы получили в MoreLINQ , в MinBy.cs . (Существует соответствующий MaxBy, конечно.) Вот его смелость:

public static TSource MinBy<TSource, TKey>(this IEnumerable<TSource> source,
    Func<TSource, TKey> selector)
{
    return source.MinBy(selector, null);
}

public static TSource MinBy<TSource, TKey>(this IEnumerable<TSource> source,
    Func<TSource, TKey> selector, IComparer<TKey> comparer)
{
    if (source == null) throw new ArgumentNullException("source");
    if (selector == null) throw new ArgumentNullException("selector");
    comparer = comparer ?? Comparer<TKey>.Default;

    using (var sourceIterator = source.GetEnumerator())
    {
        if (!sourceIterator.MoveNext())
        {
            throw new InvalidOperationException("Sequence contains no elements");
        }
        var min = sourceIterator.Current;
        var minKey = selector(min);
        while (sourceIterator.MoveNext())
        {
            var candidate = sourceIterator.Current;
            var candidateProjected = selector(candidate);
            if (comparer.Compare(candidateProjected, minKey) < 0)
            {
                min = candidate;
                minKey = candidateProjected;
            }
        }
        return min;
    }
}

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

116 голосов
/ 27 мая 2009

ПРИМЕЧАНИЕ. Я включил этот ответ для полноты, поскольку в ОП не упоминается источник данных, и мы не должны делать никаких предположений.

Этот запрос дает правильный ответ, но может быть медленнее , поскольку может потребоваться отсортировать все элементов в People, в зависимости от структуры данных People:

var oldest = People.OrderBy(p => p.DateOfBirth ?? DateTime.MaxValue).First();

ОБНОВЛЕНИЕ: На самом деле я не должен называть это решение «наивным», но пользователю действительно нужно знать, к чему он обращается. «Медлительность» этого решения зависит от базовых данных. Если это массив или List<T>, то у LINQ to Objects нет другого выбора, кроме как сначала отсортировать всю коллекцию перед выбором первого элемента. В этом случае это будет медленнее, чем предложенное другое решение. Однако если это таблица LINQ to SQL и DateOfBirth является индексированным столбцом, то SQL Server будет использовать индекс вместо сортировки всех строк. Другие пользовательские реализации IEnumerable<T> могут также использовать индексы (см. i4o: Indexed LINQ или объектную базу данных db4o ) и сделать это решение быстрее, чем Aggregate() или MaxBy() / MinBy(), который должен выполнить итерацию всей коллекции один раз. На самом деле, LINQ to Objects мог (теоретически) создавать специальные случаи в OrderBy() для отсортированных коллекций, таких как SortedList<T>, но, насколько я знаю, этого не происходит.

60 голосов
/ 27 мая 2009
People.OrderBy(p => p.DateOfBirth.GetValueOrDefault(DateTime.MaxValue)).First()

сделает трюк

20 голосов
/ 04 января 2018

Итак, вы запрашиваете ArgMin или ArgMax. C # не имеет встроенного API для них.

Я искал чистый и эффективный (O (n) вовремя) способ сделать это. И я думаю, что нашел один:

Общая форма этого шаблона:

var min = data.Select(x => (key(x), x)).Min().Item2;
                            ^           ^       ^
              the sorting key           |       take the associated original item
                                Min by key(.)

Специально, используя пример из оригинального вопроса:

Для C # 7.0 и выше, который поддерживает значение кортежа :

var youngest = people.Select(p => (p.DateOfBirth, p)).Min().Item2;

Для версии C # до 7.0 вместо нее можно использовать анонимный тип :

var youngest = people.Select(p => new { ppl = p; age = p.DateOfBirth }).Min().ppl;

Они работают, потому что и кортеж значений, и анонимный тип имеют разумные сравнения по умолчанию: для (x1, y1) и (x2, y2) сначала сравниваются x1 против x2, затем y1 против y2. Вот почему встроенный .Min может использоваться на этих типах.

И так как анонимный тип и кортеж значений являются типами значений, они оба должны быть очень эффективными.

ПРИМЕЧАНИЕ

В моих приведенных выше реализациях ArgMin я предполагал, что DateOfBirth принимает тип DateTime для простоты и ясности. Исходный вопрос просит исключить эти записи с нулевым DateOfBirth полем:

Нулевым значениям DateOfBirth присвоено значение DateTime.MaxValue, чтобы исключить их из минимального рассмотрения (при условии, что хотя бы у одного указан указанный DOB).

Это может быть достигнуто с предварительной фильтрацией

people.Where(p => p.DateOfBirth.HasValue)

Так что вопрос внедрения ArgMin или ArgMax.

несущественен.

ПРИМЕЧАНИЕ 2

Приведенный выше подход имеет одну оговорку: когда два экземпляра имеют одинаковое минимальное значение, реализация Min() попытается сравнить экземпляры как прерыватели связей. Однако, если класс экземпляров не реализует IComparable, будет выдана ошибка времени выполнения:

Как минимум один объект должен реализовывать IComparable

К счастью, это можно исправить довольно чисто. Идея состоит в том, чтобы связать отдаленный «идентификатор» с каждой записью, которая служит однозначным нарушителем связей. Мы можем использовать инкрементный идентификатор для каждой записи. Все еще используя возраст людей в качестве примера:

var youngest = Enumerable.Range(0, int.MaxValue)
               .Zip(people, (idx, ppl) => (ppl.DateOfBirth, idx, ppl)).Min().Item3;
9 голосов
/ 02 января 2017

Решение без дополнительных пакетов:

var min = lst.OrderBy(i => i.StartDate).FirstOrDefault();
var max = lst.OrderBy(i => i.StartDate).LastOrDefault();

также вы можете заключить его в расширение:

public static class LinqExtensions
{
    public static T MinBy<T, TProp>(this IEnumerable<T> source, Func<T, TProp> propSelector)
    {
        return source.OrderBy(propSelector).FirstOrDefault();
    }

    public static T MaxBy<T, TProp>(this IEnumerable<T> source, Func<T, TProp> propSelector)
    {
        return source.OrderBy(propSelector).LastOrDefault();
    }
}

и в этом случае:

var min = lst.MinBy(i => i.StartDate);
var max = lst.MaxBy(i => i.StartDate);

Кстати ... O (n ^ 2) - не лучшее решение. Пол Беттс дал более толстое решение, чем мое. Но мое решение по-прежнему LINQ, и оно здесь более простое и короткое, чем другие решения.

3 голосов
/ 23 июня 2015
public class Foo {
    public int bar;
    public int stuff;
};

void Main()
{
    List<Foo> fooList = new List<Foo>(){
    new Foo(){bar=1,stuff=2},
    new Foo(){bar=3,stuff=4},
    new Foo(){bar=2,stuff=3}};

    Foo result = fooList.Aggregate((u,v) => u.bar < v.bar ? u: v);
    result.Dump();
}
0 голосов
/ 28 марта 2019

Совершенно простое использование агрегата (эквивалентно складыванию на других языках):

var firstBorn = People.Aggregate((min, x) => x.DateOfBirth < min.DateOfBirth ? x : min);

Единственный недостаток - доступ к свойству дважды для каждого элемента последовательности, что может быть дорого. Это трудно исправить.

0 голосов
/ 13 мая 2018

Чтобы получить максимум или минимум свойства из массива объектов:

Составьте список, в котором хранится каждое значение свойства:

list<int> values = new list<int>;

Добавить все значения свойств в список:

foreach (int i in obj.desiredProperty)
{    values.add(i);  }

Получить максимальное или минимальное значение из списка:

int Max = values.Max;
int Min = values.Min;

Теперь вы можете перебирать свой массив объектов и сравнивать значения свойств, которые вы хотите проверить, с max или min int:

foreach (obj o in yourArray)
{
    if (o.desiredProperty == Max)
       {return o}

    else if (o.desiredProperty == Min)
        {return o}
}
0 голосов
/ 16 июня 2017

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

public static class LinqExtensions
{
    public static T MinBy<T>(this IEnumerable<T> source, Func<T, IComparable> selector)
    {
        if (source == null)
        {
            throw new ArgumentNullException(nameof(source));
        }
        if (selector == null)
        {
            throw new ArgumentNullException(nameof(selector));
        }
        return source.Aggregate((min, cur) =>
        {
            if (min == null)
            {
                return cur;
            }
            var minComparer = selector(min);
            if (minComparer == null)
            {
                return cur;
            }
            var curComparer = selector(cur);
            if (curComparer == null)
            {
                return min;
            }
            return minComparer.CompareTo(curComparer) > 0 ? cur : min;
        });
    }
}

Тесты:

var nullableInts = new int?[] {5, null, 1, 4, 0, 3, null, 1};
Assert.AreEqual(0, nullableInts.MinBy(i => i));//should pass
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...