Как использовать LINQ для выбора объекта с минимальным или максимальным значением свойства

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

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

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

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

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

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

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

Ответ 1

People.Aggregate((curMin, x) => (curMin == null || (x.DateOfBirth ?? DateTime.MaxValue) <
    curMin.DateOfBirth ? x : curMin))

Ответ 2

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

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

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

Ответ 3

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

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

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

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

Ответ 4

People.OrderBy(p => p.DateOfBirth.GetValueOrDefault(DateTime.MaxValue)).First()

Сделал бы трюк

Ответ 5

Поэтому вы просите ArgMin или ArgMax. У С# нет встроенного API для них.

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

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

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

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

Для С# 7.0 и выше, поддерживающих кортеж value :

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

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

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

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

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

НОТА

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

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

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

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

Поэтому это несущественно для вопроса об использовании ArgMin или ArgMax.

ЗАМЕТКА 2

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

По крайней мере, один объект должен реализовать IComparable

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

var youngest = Enumerable.Range(0, int.MaxValue)
               .Zip(people, (idx, ppl) => (ppl.DateOfBirth, idx, ppl)).Min().Item3;

Ответ 6

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

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, и оно здесь более простое и короткое, чем другие решения.

Ответ 7

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

Ответ 8

ИЗМЕНИТЬ еще раз:

К сожалению. Кроме того, что отсутствует значение nullable, я искал неправильную функцию,

Min < (Of < (TSource, TResult > ) > ) (IEnumerable < (Of < (TSource > ) > ), Func < ( Of < (TSource, TResult > ) > )) возвращает тип результата, как вы сказали.

Я бы сказал, что одним из возможных решений является реализация IComparable и использование Min < (Of < (TSource > ) > ) (IEnumerable < (Of < (TSource > ) > )), который действительно возвращает элемент из IEnumerable. Конечно, это не поможет вам, если вы не можете изменить элемент. Я считаю, что дизайн MS немного странный.

Конечно, вы всегда можете сделать цикл for, если вам нужно, или использовать реализацию MoreLINQ, которую дал Jon Skeet.

Ответ 9

Отметили это только для Entity framework 6.0.0 > : Это можно сделать:

var MaxValue = dbContext.YourDataClass.Select(x => x.ColumnToFindMaxValueFrom).Max();
var MinValue = dbContext.YourDataClass.Select(x => x.ColumnToFindMinValueFrom).Min();

Ответ 10

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

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

Ответ 11

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

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

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

Ответ 12

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

var firstBorn = People.FirstOrDefault(p => p.DateOfBirth == People.Min(p2 => p2.DateOfBirth));

Ответ 13

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

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

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