Skip to content
22 changes: 22 additions & 0 deletions MoreLinq.Test/FallbackIfEmptyTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,28 @@ public void FallbackIfEmptyWithEmptySequence()
}

[Test]
public void FallbackIfEmptyPreservesSourceCollectionIfPossible()
{
var source = new int[] { 1 };
// ReSharper disable PossibleMultipleEnumeration
Assert.AreSame(source.FallbackIfEmpty(12), source);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider turning these into test cases.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did this on ccca164 , but I'm not sure it ended up being better, as it is a bunch of extra code noise.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that ccca164 isn't any better. What I had more in mind was this:

[TestCase(new[] { 12                     })]
[TestCase(new[] { 12, 23                 })]
[TestCase(new[] { 12, 23, 34             })]
[TestCase(new[] { 12, 23, 34, 45         })]
[TestCase(new[] { 12, 23, 34, 45, 56     })]
[TestCase(new[] { 12, 23, 34, 45, 56, 67 })]
public void FallbackIfEmptyPreservesSourceCollectionIfPossible(int[] fallback)
{
    var source = new[] { 1 };
    Assert.AreSame(source.FallbackIfEmpty(fallback), source);
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But those test cases don't add much value, as they all test the same overload. The idea is to test that all overloads preserve the source collection.

Copy link
Member

@atifaziz atifaziz Aug 8, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duh! My bad. All the overloading made it very opaque at the call site. Let's revert ccca164 then and ignore the remark about breaking it down into test cases.

Assert.AreSame(source.FallbackIfEmpty(12, 23), source);
Assert.AreSame(source.FallbackIfEmpty(12, 23, 34), source);
Assert.AreSame(source.FallbackIfEmpty(12, 23, 34, 45), source);
Assert.AreSame(source.FallbackIfEmpty(12, 23, 34, 45, 56), source);
Assert.AreSame(source.FallbackIfEmpty(12, 23, 34, 45, 56, 67), source);
// ReSharper restore PossibleMultipleEnumeration
}

[Test]
public void FallbackIfEmptyPreservesFallbackCollectionIfPossible()
{
var source = new int[0];
var fallback = new int[] { 1 };
Assert.AreSame(source.FallbackIfEmpty(fallback), fallback);
Assert.AreSame(source.FallbackIfEmpty(fallback.AsEnumerable()), fallback);
}

public void FallbackIfEmptyWithEmptySequenceCollectionOptimized()
{
var source = LinqEnumerable.Empty<int>();
Expand Down
72 changes: 39 additions & 33 deletions MoreLinq/FallbackIfEmpty.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ namespace MoreLinq
{
using System;
using System.Collections.Generic;
using System.Linq;

static partial class MoreEnumerable
{
Expand Down Expand Up @@ -155,52 +154,59 @@ public static IEnumerable<T> FallbackIfEmpty<T>(this IEnumerable<T> source, IEnu
{
if (source == null) throw new ArgumentNullException(nameof(source));
if (fallback == null) throw new ArgumentNullException(nameof(fallback));
return FallbackIfEmptyImpl(source, 0, default(T), default(T), default(T), default(T), fallback);
return FallbackIfEmptyImpl(source, null, default(T), default(T), default(T), default(T), fallback);
}

static IEnumerable<T> FallbackIfEmptyImpl<T>(IEnumerable<T> source,
int? count, T fallback1, T fallback2, T fallback3, T fallback4,
IEnumerable<T> fallback)
{
var collection = source as ICollection<T>;
if (collection != null && collection.Count == 0)
if (source is ICollection<T> collection)
{
//
// Replace the empty collection with an empty sequence and
// carry on. LINQ's Enumerable.Empty is implemented
// intelligently to return the same enumerator instance and so
// does not incur an allocation. However, the same cannot be
// said for a collection like an empty array or list. This
// permits the rest of the logic while keeping the call to
// source.GetEnumerator() cheap.
//

source = Enumerable.Empty<T>();
}

using (var e = source.GetEnumerator())
{
if (e.MoveNext())
if (collection.Count == 0)
{
do { yield return e.Current; }
while (e.MoveNext());
return Fallback();
}
else
{
e.Dispose(); // eager disposal
if (count > 0 && count <= 4)
{
yield return fallback1;
if (count > 1) yield return fallback2;
if (count > 2) yield return fallback3;
if (count > 3) yield return fallback4;
}
else
return collection;
}
}

return _();

IEnumerable<T> _()
{
using (var e = source.GetEnumerator())
{
if (e.MoveNext())
{
foreach (var item in fallback)
yield return item;
do { yield return e.Current; }
while (e.MoveNext());
yield break;
}
}

foreach (var item in Fallback())
yield return item;
}

IEnumerable<T> Fallback()
{
switch (count)
{
case null: return fallback;
case int n when n >= 1 && n <= 4: return FallbackOnArgs();
default: throw new ArgumentOutOfRangeException(nameof(count), count, null);
}

IEnumerable<T> FallbackOnArgs()
{
yield return fallback1;
if (count > 1) yield return fallback2;
if (count > 2) yield return fallback3;
if (count > 3) yield return fallback4;
}
}
}
}
Expand Down