Skip to content

Commit 166384d

Browse files
Merge pull request #18 from thomaslevesque/fix-min-max-null
Fix Min/Max(By) to correctly handle null values
2 parents d65fd7f + a49e1b0 commit 166384d

File tree

3 files changed

+242
-32
lines changed

3 files changed

+242
-32
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
namespace System.Diagnostics.CodeAnalysis
2+
{
3+
/// <summary>Specifies that an output may be null even if the corresponding type disallows it.</summary>
4+
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)]
5+
internal sealed class MaybeNullAttribute : Attribute
6+
{
7+
}
8+
}

src/Linq.Extras/MinMax.cs

Lines changed: 146 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Diagnostics.CodeAnalysis;
34
using JetBrains.Annotations;
45
using Linq.Extras.Internal;
56
using Linq.Extras.Properties;
@@ -15,7 +16,14 @@ partial class XEnumerable
1516
/// <param name="source">The sequence to return the maximum element from.</param>
1617
/// <param name="comparer">The comparer used to compare elements.</param>
1718
/// <returns>The maximum element according to the specified comparer.</returns>
19+
/// <remarks>
20+
/// If <c>TSource</c> is a reference type or nullable value type, null values are ignored, unless the sequence consists
21+
/// entirely of null values (in which case the method will return null).
22+
/// If <c>TSource</c> is a reference type or nullable value type, and the sequence is empty, the method will return <c>null</c>.
23+
/// If <c>TSource</c> is a value type, and the sequence is empty, the method will throw an <see cref="InvalidOperationException"/>.
24+
/// </remarks>
1825
[Pure]
26+
[return: MaybeNull]
1927
public static TSource Max<TSource>(
2028
[NotNull] this IEnumerable<TSource> source,
2129
[NotNull] IComparer<TSource> comparer)
@@ -32,7 +40,14 @@ public static TSource Max<TSource>(
3240
/// <param name="source">The sequence to return the minimum element from.</param>
3341
/// <param name="comparer">The comparer used to compare elements.</param>
3442
/// <returns>The minimum element according to the specified comparer.</returns>
43+
/// <remarks>
44+
/// If <c>TSource</c> is a reference type or nullable value type, null values are ignored, unless the sequence consists
45+
/// entirely of null values (in which case the method will return null).
46+
/// If <c>TSource</c> is a reference type or nullable value type, and the sequence is empty, the method will return <c>null</c>.
47+
/// If <c>TSource</c> is a value type, and the sequence is empty, the method will throw an <see cref="InvalidOperationException"/>.
48+
/// </remarks>
3549
[Pure]
50+
[return: MaybeNull]
3651
public static TSource Min<TSource>(
3752
[NotNull] this IEnumerable<TSource> source,
3853
[NotNull] IComparer<TSource> comparer)
@@ -47,22 +62,56 @@ private static TSource Extreme<TSource>(this IEnumerable<TSource> source, ICompa
4762
{
4863
comparer = comparer ?? Comparer<TSource>.Default;
4964
TSource extreme = default!;
50-
bool first = true;
51-
foreach (var item in source)
65+
66+
using var e = source.GetEnumerator();
67+
if (extreme is null)
5268
{
53-
int compare = 0;
54-
if (!first)
55-
compare = comparer.Compare(item, extreme);
69+
// For nullable types, return null if the sequence is empty
70+
// or contains only null values.
5671

57-
if (Math.Sign(compare) == sign || first)
72+
// First, skip until the first non-null value, if any
73+
do
5874
{
59-
extreme = item;
75+
if (!e.MoveNext())
76+
{
77+
return extreme;
78+
}
79+
80+
extreme = e.Current;
81+
} while (extreme is null);
82+
83+
while (e.MoveNext())
84+
{
85+
if (e.Current is null)
86+
{
87+
continue;
88+
}
89+
90+
if (Math.Sign(comparer.Compare(e.Current, extreme)) == sign)
91+
{
92+
extreme = e.Current;
93+
}
6094
}
61-
first = false;
6295
}
96+
else
97+
{
98+
// For non-nullable types, throw an exception if the sequence is empty
99+
100+
if (!e.MoveNext())
101+
{
102+
throw EmptySequenceException();
103+
}
104+
105+
extreme = e.Current;
63106

64-
if (first)
65-
throw EmptySequenceException();
107+
while (e.MoveNext())
108+
{
109+
if (Math.Sign(comparer.Compare(e.Current, extreme)) == sign)
110+
{
111+
extreme = e.Current;
112+
}
113+
}
114+
}
66115

67116
return extreme;
68117
}
@@ -81,16 +130,22 @@ private static InvalidOperationException EmptySequenceException()
81130
/// <param name="keySelector">A delegate that returns the key used to compare elements.</param>
82131
/// <param name="keyComparer">A comparer to compare the keys.</param>
83132
/// <returns>The element of <c>source</c> that has the maximum value for the specified key.</returns>
133+
/// <remarks>
134+
/// If <c>TKey</c> is a reference type or nullable value type, null keys are ignored, unless the sequence consists
135+
/// entirely of items with null keys (in which case the method will return null).
136+
/// If <c>TKey</c> is a reference type or nullable value type, and the sequence is empty, the method will return <c>null</c>.
137+
/// If <c>TKey</c> is a value type, and the sequence is empty, the method will throw an <see cref="InvalidOperationException"/>.
138+
/// </remarks>
84139
[Pure]
140+
[return: MaybeNull]
85141
public static TSource MaxBy<TSource, TKey>(
86142
[NotNull] this IEnumerable<TSource> source,
87143
[NotNull] Func<TSource, TKey> keySelector,
88144
IComparer<TKey>? keyComparer = null)
89145
{
90146
source.CheckArgumentNull(nameof(source));
91147
keySelector.CheckArgumentNull(nameof(keySelector));
92-
var comparer = XComparer.By(keySelector, keyComparer);
93-
return source.Max(comparer);
148+
return source.ExtremeBy(keySelector, keyComparer, 1);
94149
}
95150

96151
/// <summary>
@@ -102,16 +157,93 @@ public static TSource MaxBy<TSource, TKey>(
102157
/// <param name="keySelector">A delegate that returns the key used to compare elements.</param>
103158
/// <param name="keyComparer">A comparer to compare the keys.</param>
104159
/// <returns>The element of <c>source</c> that has the minimum value for the specified key.</returns>
160+
/// <remarks>
161+
/// If <c>TKey</c> is a reference type or nullable value type, null keys are ignored, unless the sequence consists
162+
/// entirely of items with null keys (in which case the method will return null).
163+
/// If <c>TKey</c> is a reference type or nullable value type, and the sequence is empty, the method will return <c>null</c>.
164+
/// If <c>TKey</c> is a value type, and the sequence is empty, the method will throw an <see cref="InvalidOperationException"/>.
165+
/// </remarks>
105166
[Pure]
167+
[return: MaybeNull]
106168
public static TSource MinBy<TSource, TKey>(
107169
[NotNull] this IEnumerable<TSource> source,
108170
[NotNull] Func<TSource, TKey> keySelector,
109171
IComparer<TKey>? keyComparer = null)
110172
{
111173
source.CheckArgumentNull(nameof(source));
112174
keySelector.CheckArgumentNull(nameof(keySelector));
113-
var comparer = XComparer.By(keySelector, keyComparer);
114-
return source.Min(comparer);
175+
return source.ExtremeBy(keySelector, keyComparer, -1);
176+
}
177+
178+
[Pure]
179+
private static TSource ExtremeBy<TSource, TKey>(
180+
this IEnumerable<TSource> source,
181+
Func<TSource, TKey> keySelector,
182+
IComparer<TKey>? keyComparer,
183+
int sign)
184+
{
185+
keyComparer = keyComparer ?? Comparer<TKey>.Default;
186+
TSource extreme = default!;
187+
TKey extremeKey = default!;
188+
189+
using var e = source.GetEnumerator();
190+
191+
if (extremeKey is null)
192+
{
193+
// For nullable types, return null if the sequence is empty
194+
// or contains only values with null keys.
195+
196+
// First, skip until the first non-null key value, if any
197+
do
198+
{
199+
if (!e.MoveNext())
200+
{
201+
return extreme;
202+
}
203+
204+
extreme = e.Current;
205+
extremeKey = keySelector(extreme);
206+
} while (extremeKey is null);
207+
208+
while (e.MoveNext())
209+
{
210+
var currentKey = keySelector(e.Current);
211+
if (currentKey is null)
212+
{
213+
continue;
214+
}
215+
216+
if (Math.Sign(keyComparer.Compare(currentKey, extremeKey)) == sign)
217+
{
218+
extreme = e.Current;
219+
extremeKey = currentKey;
220+
}
221+
}
222+
}
223+
else
224+
{
225+
// For non-nullable types, throw an exception if the sequence is empty
226+
227+
if (!e.MoveNext())
228+
{
229+
throw EmptySequenceException();
230+
}
231+
232+
extreme = e.Current;
233+
extremeKey = keySelector(e.Current);
234+
235+
while (e.MoveNext())
236+
{
237+
var currentKey = keySelector(e.Current);
238+
if (Math.Sign(keyComparer.Compare(currentKey, extremeKey)) == sign)
239+
{
240+
extreme = e.Current;
241+
extremeKey = currentKey;
242+
}
243+
}
244+
}
245+
246+
return extreme;
115247
}
116248
}
117249
}

0 commit comments

Comments
 (0)