source

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

nicesource 2023. 5. 29. 11:00
반응형

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

Nullable DateOfBirth 속성을 가진 Person 개체가 있습니다.LINQ를 사용하여 DateOfBirth 값이 가장 빠르거나 가장 작은 개체의 사용자 개체 목록을 쿼리할 수 있는 방법이 있습니까?

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

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

Null DateOfBirth 값이 DateTime으로 설정되어 있습니다.최소값에서 제외하기 위한 최대값(최소한 하나 이상의 DOB가 지정된 것으로 가정).

하지만 firstBornDate를 DateTime 값으로 설정하는 것이 전부입니다.제가 원하는 것은 그것과 일치하는 사용자 객체입니다.다음과 같은 두 번째 쿼리를 작성해야 합니까?

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

아니면 좀 더 희박한 방법이 있을까요?

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

안타깝게도 이를 위한 기본 제공 방법은 없지만, 사용자가 직접 구현하기에 충분히 쉽습니다.그 핵심은 다음과 같습니다.

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

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

또는 MinBy.cs 의 MoreLINQ에 있는 구현을 사용할 수 있습니다.MaxBy물론입니다.)

패키지 관리자 콘솔을 통해 설치:

PM> 설치 - 패키지 추가 linq

참고: OP가 데이터 소스가 무엇인지 언급하지 않았기 때문에 완전성을 위해 이 답변을 포함하며, 우리는 어떠한 가정도 해서는 안 됩니다.

이 쿼리는 정답을 제공하지만 모든 항목을 정렬해야 하므로 속도가 더 느릴 수 있습니다.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: 인덱스 LINQ 또는 객체 데이터베이스 db4o 참조)를 사용하여 이 솔루션을 보다 빠르게 만들 수 있습니다.Aggregate()또는MaxBy()/MinBy()전체 컬렉션을 한 번 더 반복해야 합니다.사실, LINQ to Objects이 (LINQ to Objects)에서 특별한 경우를 수 .OrderBy() 정렬컬경우와 같은 정렬된 SortedList<T>하지만 제가 알기로는 그렇지 않습니다.

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

할 수 있을 것 같습니다.

그래서 당신은 요구하는 것입니다.ArgMin또는ArgMaxC#에는 이러한 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 {age = p.DateOfBirth, ppl = p}).Min().ppl;

값튜과익유모유기작있다니동합때및에. (x1, y1) 경 (x2, y2) 의비다니합교먼우를 비교하기 합니다.x1x2,그리고나서y1y2그래서 빌트인이.Min이러한 유형에 사용할 수 있습니다.

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

메모

의 의에위에서.ArgMin가 가정한 DateOfBirth활자를 DateTime간결하고 명확하게 하기 위해.원래 질문은 null인 항목을 제외하도록 요청합니다.DateOfBirth선택사항:

Null DateOfBirth 값이 DateTime으로 설정되어 있습니다.최소값에서 제외하기 위한 최대값(최소한 하나 이상의 DOB가 지정된 것으로 가정).

사전 필터링을 통해 달성할 수 있습니다.

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

그래서 그것을 구현하는 것은 중요하지 않습니다.ArgMin또는ArgMax.

참고 2

값이 가 두 개는 min 값이 같은 에 주의해야 합니다.Min()구현은 인스턴스를 타이 브레이커로 비교하려고 시도합니다.가 구현되지 IComparable그러면 런타임 오류가 발생합니다.

하나 이상의 개체가 IComparable을 구현해야 합니다.

다행히도, 이것은 여전히 꽤 깨끗하게 고쳐질 수 있습니다.이 개념은 명확한 타이 브레이커 역할을 하는 각 항목에 거리가 있는 "ID"를 연결하는 것입니다.각 항목에 대해 증분 ID를 사용할 수 있습니다.여전히 사람들의 나이를 예로 들 수 있습니다.

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

.NET 6은 MaxBy/MinBy를 기본적으로 지원합니다.그래서 당신은 이것을 간단한 것으로 할 수 있을 것입니다.

People.MinBy(p => p.DateOfBirth)

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

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 솔루션이며 다른 솔루션보다 더 간단하고 짧습니다.

.Net 6(Preview 7) 이상부터는 새로운 내장 메서드 Enumerable이 있습니다.MaxByEnumerable입니다.MinBy가 이 목표를 달성했습니다.

var lastBorn = people.MaxBy(p => p.DateOfBirth);

var firstBorn = people.MinBy(p => p.DateOfBirth);
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();
}

Aggregate의 완벽하게 단순한 사용(다른 언어에서 접기와 동일):

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

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

당신은 SQL에서만 trick을 제한하고 가져오는 것처럼 그냥 order by와 같이 할 수 있습니다.따라서 DateOfBirth 오름차순으로 주문한 다음 첫 번째 행을 가져옵니다.

var query = from person in People
            where person.DateOfBirth!=null
            orderby person.DateOfBirth
            select person;
var firstBorn = query.Take(1).toList();

다음은 보다 일반적인 솔루션입니다.기본적으로 동일한 작업(O(N) 순서)을 수행하지만 모든 IE 번호 유형에 대해 수행되며 속성 선택기가 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 firstBornDate = People.GroupBy(p => p.DateOfBirth).Min(g => g.Key).FirstOrDefault();

저는 도서관을 이용하거나 전체 목록을 분류하지 않고 저 자신과 비슷한 것을 찾고 있었습니다.저의 해결책은 질문 자체와 유사하게 끝났습니다. 단지 조금 단순화되었을 뿐입니다.

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

다시 편집:

죄송합니다. 제가 잘못된 기능을 보고 있던 nullable을 놓친 것 외에도,

최소<(<(TSource, Tresult> 중)>)IEnumberable<(Of <(TSOURce>)>), Func<(Of <(TSOURce,Tresult>))는 말씀하신 대로 결과 유형을 반환합니다.

가능한 해결책 중 하나는 I을 비교 가능한 것으로 구현하고 Min<(Of <(TSOURce>의)>)을 사용하는 것입니다.IEnumberable<(Of <(TSource>의)>), 실제로 IEnumberable에서 요소를 반환합니다.물론 요소를 수정할 수 없다면 도움이 되지 않습니다.저는 MS의 디자인이 좀 이상하다고 생각합니다.

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

null 가능한 선택기 키와 참조 유형 컬렉션에서 작동할 수 있는 또 다른 구현은 적절한 요소를 찾을 수 없는 경우 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);

최소 또는 최대 속성 값을 가진 개체를 선택하려는 경우.다른 방법은 비교 가능한 I 구현을 사용하는 것입니다.

public struct Money : IComparable<Money>
{
   public Money(decimal value) : this() { Value = value; }
   public decimal Value { get; private set; }
   public int CompareTo(Money other) { return Value.CompareTo(other.Value); }
}

최대 구현은 다음과 같습니다.

var amounts = new List<Money> { new Money(20), new Money(10) };
Money maxAmount = amounts.Max();

최소 구현이 됩니다.

var amounts = new List<Money> { new Money(20), new Money(10) };
Money maxAmount = amounts.Min();

이러한 방식으로 모든 개체를 비교하여 개체 유형을 반환하는 동안 최대 및 최소 값을 얻을 수 있습니다.최소)을 얻을 수 있습니다.

이것이 누군가에게 도움이 되기를 바랍니다.

개체와 발견된 최소값을 모두 반환하는 IEnumberable의 확장 함수를 통한 방법입니다.컬렉션의 개체에 대해 모든 작업을 수행할 수 있는 Func가 필요합니다.

public static (double min, T obj) tMin<T>(this IEnumerable<T> ienum, 
            Func<T, double> aFunc)
        {
            var okNull = default(T);
            if (okNull != null)
                throw new ApplicationException("object passed to Min not nullable");

            (double aMin, T okObj) best = (double.MaxValue, okNull);
            foreach (T obj in ienum)
            {
                double q = aFunc(obj);
                if (q < best.aMin)
                    best = (q, obj);
            }
            return (best);
        }

예를 들어, 객체가 공항이고 주어진 공항(위도, 경도)에서 가장 가까운 공항을 찾으려고 합니다.공항에는 dist(lat, lon) 기능이 있습니다.

(double okDist, Airport best) greatestPort = airPorts.tMin(x => x.dist(okLat, okLon));

MoreLinq처럼 기존 linq 확장자를 사용할 수 있습니다.그러나 이러한 방법만 필요한 경우 다음과 같은 간단한 코드를 사용할 수 있습니다.

public static IEnumerable<T> MinBys<T>(this IEnumerable<T> collection, Func<T, IComparable> selector)
{
    var dict = collection.GroupBy(selector).ToDictionary(g => g.Key);
    return dict[dict.Keys.Min()];
}
public static IEnumerable<T> MaxBys<T>(this IEnumerable<T> collection, Func<T, IComparable> selector)
{
    var dict = collection.GroupBy(selector).ToDictionary(g => g.Key);
    return dict[dict.Keys.Max()];
}

최소값과 최대값을 가져오는 간단한 방법은 다음과 같습니다.

    `dbcontext.tableName.Select(x=>x.Feild1).Min()`
    

언급URL : https://stackoverflow.com/questions/914109/how-to-use-linq-to-select-object-with-minimum-or-maximum-property-value

반응형