LINQ를 사용하여 최소 또는 최대 속성 값을 가진 개체를 선택하는 방법


465

Nullable DateOfBirth 속성을 가진 Person 개체가 있습니다. LINQ를 사용하여 가장 짧거나 가장 작은 DateOfBirth 값을 가진 사람에 대한 Person 객체 목록을 쿼리하는 방법이 있습니까?

내가 시작한 것은 다음과 같습니다.

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

Null DateOfBirth 값은 최소 고려 사항에서 제외하기 위해 DateTime.MaxValue로 설정됩니다 (적어도 하나의 지정된 DOB가 있다고 가정).

그러나 나를 위해하는 일은 firstBornDate를 DateTime 값으로 설정하는 것입니다. 내가 얻고 싶은 것은 그와 일치하는 Person 객체입니다. 다음과 같이 두 번째 쿼리를 작성해야합니까?

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

아니면 더 적은 방법이 있습니까?


24
예제에 대한 의견 만 : 아마 여기서 Single을 사용해서는 안됩니다. 두 사람이 동일한 DateOfBirth를 가지고 있다면 예외를 던질 것입니다
Niki

1
간결한 예제가 있는 거의 중복 된 stackoverflow.com/questions/2736236/… 도 참조하십시오 .
goodeye

4
간단하고 유용한 기능입니다. MinBy는 표준 라이브러리에 있어야합니다. 우리는 풀 요청을 Microsoft github.com/dotnet/corefx에
대령 Panic

2
이것은 오늘날 존재하는 것으로 보이며, 속성을 고르는 기능을 제공하면됩니다.a.Min(x => x.foo);
jackmott

4
문제를 보여주기 위해 : 파이썬에서 max("find a word of maximal length in this sentence".split(), key=len)'문장'문자열을 반환합니다. C #에서는 "find a word of maximal length in this sentence".Split().Max(word => word.Length)8이 단어의 가장 긴 길이라고 계산하지만 가장 긴 단어 무엇인지는 알려주지 않습니다 .
대령 패닉

답변:


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

16
아마도 IComparable을 구현하고 Min (또는 for 루프)을 사용하는 것보다 약간 느립니다. 그러나 O (n) linqy 솔루션의 경우 +1입니다.
Matthew Flaschen

3
또한 <curmin.DateOfBirth이어야합니다. 그렇지 않으면 DateTime을 Person과 비교하는 것입니다.
Matthew Flaschen

2
이것을 사용하여 두 날짜 시간을 비교할 때도주의하십시오. 나는 이것을 사용하여 정렬되지 않은 컬렉션에서 마지막 변경 레코드를 찾았습니다. 내가 원하는 레코드가 같은 날짜와 시간으로 끝나기 때문에 실패했습니다.
Simon Gill

8
왜 불필요한 점검을 curMin == null합니까? 씨앗을 사용 하는 경우 curMin에만 가능 null합니다 . Aggregate()null
Good Night Nerd Pride


226

불행히도이 작업을 수행하는 기본 제공 방법은 없지만 직접 구현하기는 쉽습니다. 그것의 내장은 다음과 같습니다.

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

사용법 예 :

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

시퀀스가 비어 있으면 예외가 발생하고 첫 번째 요소가 두 개 이상인 경우 최소값으로 반환됩니다 .

또는, 우리가 가지고 구현을 사용할 수 있습니다 MoreLINQ 에, MinBy.cs을 . ( MaxBy물론 해당하는 것이 있습니다.)

패키지 관리자 콘솔을 통해 설치하십시오.

PM> 설치 패키지 morelinq


1
나는 foreach로 Ienumerator +를 대체 할 것입니다
ggf31416

5
루프 전에 MoveNext ()를 처음 호출하기 때문에 쉽게 할 수 없습니다. 대안이 있지만 더 복잡한 IMO입니다.
Jon Skeet

2
부적절하다고 생각되는 기본값 (T)을 반환 할 수는 있지만 이것은 First ()와 같은 메소드와 Dictionary 인덱서의 접근 방식과 더 일치합니다. 원하는 경우 쉽게 조정할 수 있습니다.
Jon Skeet

8
비 라이브러리 솔루션으로 Paul에게 답변을 주었지만이 코드와 MoreLINQ 라이브러리에 대한 링크에 감사드립니다.
slolife


135

참고 : OP는 데이터 소스가 무엇인지 언급하지 않았으므로 가정을 취하지 않아야 하므로이 답변을 완벽하게 포함합니다.

이 쿼리는 정답을 제공하지만 데이터 구조에 따라의 모든 항목 을 정렬해야 하므로 속도가 느릴 수 있습니다 .PeoplePeople

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

업데이트 : 실제로이 솔루션을 "순진한"것으로 호출해서는 안되지만 사용자는 자신이 쿼리하는 내용을 알아야합니다. 이 솔루션의 "느림"은 기본 데이터에 따라 다릅니다. 이것이 배열 또는 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>있었지만 내가 아는 한 그렇지 않습니다.


1
누군가 이미 그것을 게시했지만, 속도가 느리고 (공간 소비가 많은) (O (n log n) 속도가 O (n)과 min (분)에 비해) 느린 것으로 언급 한 후에 분명히 삭제했습니다. :)
Matthew Flaschen 08 년

예, 따라서 순진한 해결책에 대한 경고 :) 그러나 그것은 간단하지 않고 어떤 경우에는 사용할 수 있습니다 (소규모 컬렉션 또는 DateOfBirth가 인덱스 DB 열인 경우)
Lucas

또 하나의 특별한 경우 (아무 것도없는 경우)는 orderby에 대한 지식을 사용하여 정렬없이 가장 낮은 값을 검색 할 수 있다는 것입니다.
Rune FS

컬렉션 정렬은 선형 또는 O (n) 시간 복잡성보다 좋지 않은 Nlog (N) 연산입니다. 최소 또는 최대 시퀀스에서 하나의 요소 / 객체 만 필요하면 선형 시간 복잡성을 고수해야한다고 생각합니다.
Yawar Murtaza

@yawar 모음집은 이미 정렬되어있을 가능성이 높습니다.이 경우 O (log n)
Rune FS

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

트릭을 할 것


1
이것은 대단하다! linq projetion의 경우 OrderByDesending (...). Take (1)과 함께 사용했습니다.
Vedran Mandić

1
이것은 O (N) 시간을 초과하는 정렬과 O (N) 메모리를 사용합니다.
George Polevoy

@GeorgePolevoy는 데이터 소스에 대해 많은 것을 알고 있다고 가정합니다. 데이터 소스에 이미 제공된 필드에 정렬 된 인덱스가있는 경우 이는 상수가 낮으며 전체 목록을 순회해야하는 허용 된 응답보다 훨씬 빠릅니다. 반면에 데이터 소스가 예를 들어 어레이 인 경우 물론 옳습니다
Rune FS

@RuneFS-여전히 중요하기 때문에 답에 언급해야합니다.
rory.ap

공연은 당신을 끌어 내릴 것입니다. 나는 그것을 힘들게 배웠다. Min 또는 Max 값을 가진 개체를 원하면 전체 배열을 정렬 할 필요가 없습니다. 한 번의 스캔만으로 충분합니다. 허용되는 답변을 보거나 MoreLinq 패키지를보십시오.
Sau001 2019 년

35

그래서 당신을 요구하고 ArgMinArgMax. 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;

7.0 이전의 C # 버전의 경우 익명 유형 을 대신 사용할 수 있습니다.

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

값 튜플과 익명 유형 모두 합리적인 기본 비교기를 갖기 때문에 작동합니다. (x1, y1) 및 (x2, y2)의 경우 먼저 x1vs를 비교 x2한 다음 y1vs 를 비교 y2합니다. 이런 이유로 내장형을 .Min사용할 수 있습니다.

익명 유형과 값 튜플은 모두 값 유형이므로 매우 효율적이어야합니다.

노트

위의 ArgMin구현에서 나는 단순성과 명확성을 위해 DateOfBirth유형을 취하는 것으로 가정 했습니다 DateTime. 원래 질문은 null DateOfBirth필드가 있는 항목을 제외하도록 요청 합니다.

Null DateOfBirth 값은 최소 고려 사항에서 제외하기 위해 DateTime.MaxValue로 설정됩니다 (적어도 하나의 지정된 DOB가 있다고 가정).

사전 필터링으로 달성 할 수 있습니다

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

따라서 ArgMin또는 구현 문제에 대해서는 중요하지 않습니다 ArgMax.

노트 2

위의 접근 방식은 최소값이 동일한 두 인스턴스가있을 때 Min()구현시 인스턴스를 타이 브레이커로 비교하려고 시도 한다는 경고가 있습니다 . 그러나 인스턴스 클래스가 구현하지 않으면 IComparable런타임 오류가 발생합니다.

최소한 하나의 객체가 IComparable을 구현해야합니다.

운 좋게도 여전히 깨끗하게 고칠 수 있습니다. 아이디어는 확실한 "ID"를 명확한 타이 브레이커 역할을하는 각 항목과 연결하는 것입니다. 각 항목마다 증분 ID를 사용할 수 있습니다. 여전히 사람들 나이를 예로 사용합니다.

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

1
값 유형이 정렬 키인 경우에는 작동하지 않는 것 같습니다. "적어도 하나의 개체가 IComparable을 구현해야합니다"
liang

1
너무 좋아! 이것이 가장 좋은 대답이어야합니다.
귀도 모카

@liang 네 잘 잡았습니다. 운 좋게도 여전히 깨끗한 해결책이 있습니다. "주 2"섹션의 업데이트 된 솔루션을 참조하십시오.
KFL

선택하면 ID를 줄 수 있습니다! var youngest = people.Select ((p, i) => (p.DateOfBirth, i, p)). Min (). Item2;
Jeremy

19

추가 패키지가없는 솔루션 :

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)는 최고의 솔루션이 아닙니다. Paul Betts 는 저보다 더 뚱뚱한 해결책을주었습니다. 그러나 나는 여전히 LINQ 솔루션이며 다른 솔루션보다 더 간단하고 짧습니다.


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

3

완벽하게 간단한 집계 사용 (다른 언어로 접는 것과 동일) :

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

유일한 단점은 속성이 시퀀스 요소 당 두 번 액세스되므로 비용이 많이들 수 있다는 것입니다. 고치기 어렵다.


1

다음은보다 일반적인 해결책입니다. 그것은 본질적으로 (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

0

다시 편집하십시오.

죄송합니다. nullable을 누락시키는 것 외에도 잘못된 기능을보고있었습니다.

Min <(Of <(TSource, TResult>)>) (IEnumerable <(Of <(TSource>)>), Func <(Of <(TSource, TResult>)>)) 은 말한대로 결과 유형을 반환합니다.

가능한 해결책 중 하나는 IComparable을 구현하고 Min <(Of <(TSource>)>) (IEnumerable <(Of <(TSource>)>)) 을 사용하는 것입니다. 실제로 IEnumerable에서 요소를 반환합니다. 물론 요소를 수정할 수없는 경우에는 도움이되지 않습니다. MS의 디자인이 여기에서 약간 이상하다는 것을 알았습니다.

물론 필요한 경우 언제든지 for 루프를 수행하거나 Jon Skeet이 제공 한 MoreLINQ 구현을 사용할 수 있습니다.


0

nullable 선택기 키와 함께 작동 할 수 있고 참조 유형의 컬렉션에 적합한 다른 구현은 적절한 요소를 찾지 못하면 null을 반환합니다. 예를 들어 데이터베이스 결과를 처리하는 데 도움이 될 수 있습니다.

  public static class IEnumerableExtensions
  {
    /// <summary>
    /// Returns the element with the maximum value of a selector function.
    /// </summary>
    /// <typeparam name="TSource">The type of the elements of source.</typeparam>
    /// <typeparam name="TKey">The type of the key returned by keySelector.</typeparam>
    /// <param name="source">An IEnumerable collection values to determine the element with the maximum value of.</param>
    /// <param name="keySelector">A function to extract the key for each element.</param>
    /// <exception cref="System.ArgumentNullException">source or keySelector is null.</exception>
    /// <exception cref="System.InvalidOperationException">source contains no elements.</exception>
    /// <returns>The element in source with the maximum value of a selector function.</returns>
    public static TSource MaxBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector) => MaxOrMinBy(source, keySelector, 1);

    /// <summary>
    /// Returns the element with the minimum value of a selector function.
    /// </summary>
    /// <typeparam name="TSource">The type of the elements of source.</typeparam>
    /// <typeparam name="TKey">The type of the key returned by keySelector.</typeparam>
    /// <param name="source">An IEnumerable collection values to determine the element with the minimum value of.</param>
    /// <param name="keySelector">A function to extract the key for each element.</param>
    /// <exception cref="System.ArgumentNullException">source or keySelector is null.</exception>
    /// <exception cref="System.InvalidOperationException">source contains no elements.</exception>
    /// <returns>The element in source with the minimum value of a selector function.</returns>
    public static TSource MinBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector) => MaxOrMinBy(source, keySelector, -1);


    private static TSource MaxOrMinBy<TSource, TKey>
      (IEnumerable<TSource> source, Func<TSource, TKey> keySelector, int sign)
    {
      if (source == null) throw new ArgumentNullException(nameof(source));
      if (keySelector == null) throw new ArgumentNullException(nameof(keySelector));
      Comparer<TKey> comparer = Comparer<TKey>.Default;
      TKey value = default(TKey);
      TSource result = default(TSource);

      bool hasValue = false;

      foreach (TSource element in source)
      {
        TKey x = keySelector(element);
        if (x != null)
        {
          if (!hasValue)
          {
            value = x;
            result = element;
            hasValue = true;
          }
          else if (sign * comparer.Compare(x, value) > 0)
          {
            value = x;
            result = element;
          }
        }
      }

      if ((result != null) && !hasValue)
        throw new InvalidOperationException("The source sequence is empty");

      return result;
    }
  }

예:

public class A
{
  public int? a;
  public A(int? a) { this.a = a; }
}

var b = a.MinBy(x => x.a);
var c = a.MaxBy(x => x.a);

-2

나는 도서관을 사용하거나 전체 목록을 정렬하지 않고 자신과 비슷한 것을 찾고있었습니다. 내 솔루션은 질문 자체와 비슷하게 조금 단순화되었습니다.

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

linq 문 전에 최소값을 얻는 것이 훨씬 효율적이지 않습니까? var min = People.Min(...); var firstBorn = People.FirstOrDefault(p => p.DateOfBirth == min...그렇지 않으면 원하는 것을 찾을 때까지 반복적으로 분을 얻습니다.
Nieminen
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.