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