У меня есть объект 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);
Или существует более простой способ сделать это?
People.Aggregate((curMin, x) => (curMin == null || (x.DateOfBirth ?? DateTime.MaxValue) <
curMin.DateOfBirth ? x : curMin))
К сожалению, для этого не существует встроенного метода.
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;
}
}
Обратите внимание, что это вызовет исключение, если последовательность пуста и вернет первый элемент с минимальным значением, если его больше.
ПРИМЕЧАНИЕ. Я включаю этот ответ для полноты, поскольку ОП не упоминает, что такое источник данных, и мы не должны делать никаких предположений.
Этот запрос дает правильный ответ, но может быть медленнее, поскольку может потребоваться отсортировать все элементы в 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>
, но, насколько мне известно, это не так.
People.OrderBy(p => p.DateOfBirth.GetValueOrDefault(DateTime.MaxValue)).First()
Сделал бы трюк
Поэтому вы просите 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;
Решение без дополнительных пакетов:
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, и оно здесь более простое и короткое, чем другие решения.
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();
}
Ниже приводится более общее решение. Он по сути делает то же самое (в порядке 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
Совершенно простое использование агрегата (эквивалентно сложению на других языках):
var firstBorn = People.Aggregate((min, x) => x.DateOfBirth < min.DateOfBirth ? x : min);
Единственным недостатком является то, что свойство доступно дважды для каждого элемента последовательности, что может быть дорого. Это трудно исправить.
Отметили это только для Entity framework 6.0.0 > : Это можно сделать:
var MaxValue = dbContext.YourDataClass.Select(x => x.ColumnToFindMaxValueFrom).Max();
var MinValue = dbContext.YourDataClass.Select(x => x.ColumnToFindMinValueFrom).Min();
Я искал нечто подобное себе, желательно без использования библиотеки или сортировки всего списка. Мое решение оказалось аналогичным самому вопросу, немного упрощенному.
var firstBorn = People.FirstOrDefault(p => p.DateOfBirth == People.Min(p2 => p2.DateOfBirth));
var min = People.Min(...); var firstBorn = People.FirstOrDefault(p => p.DateOfBirth == min...
В противном случае он получает минимальное количество раз, пока не найдет var min = People.Min(...); var firstBorn = People.FirstOrDefault(p => p.DateOfBirth == min...
.
ИЗМЕНИТЬ еще раз:
К сожалению. Кроме того, что отсутствует значение 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.
Чтобы получить максимальное или минимальное значение свойства из массива объектов:
Создайте список, в котором хранится каждое значение свойства:
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}
}