C# Enumerable.Take with default value
Asked Answered
A

7

21

What is the best way to get exactly x values from an Enumerable in C#. If i use Enumerable .Take() like this:

var myList = Enumerable.Range(0,10);
var result = myList.Take(20);

The result will only have 10 elements.

I want to fill the missing entries with a default value. Something like this:

var myList = Enumerable.Range(0,10);
var result = myList.TakeOrDefault(20, default(int));  //Is there anything like this?

Is there such a function in C# and if not, what would be the best way to achieve this?

Anya answered 27/7, 2015 at 14:56 Comment(9)
Why not just write an extension method that checks the count and returns the default value for remaining entries?Coss
@Jamiec Is it wrong to answer if some else has already answered? I commented that it can be done via extension methods and sat down to frame the answer. It took me time but heck I will post if I wrote somethingCoss
@GaneshR. - no, not at all. But you might want to come back and delete your comment after, otherwise it looks a bit weirdErvinervine
since you have hardcoded values (the 10 and 20) in your example,i dont know if it suits your example but you could create an array with lenght 20 then use Array.Copy() to copy your values to the new array,then call AsEnumerable() on the new array, the rest of the values will be the default for that type(for primitive value types 0),this is only according to your example where you use int,where you also can use double for example or other primitive value types.Souterrain
default(int) is just a convoluted way of writing 0...Revise
@MattiVirkkunen - I know, it was just a bad example.Anya
@MattiVirkkunen Technically, yes. But semantically, it's very different. And of course, it's a lot more useful with types that don't have simple literals, like default(string) or default(DateTime). Just as importantly, you can use default(T) with generics.Masry
@Luaan: string has plenty of simple literals, and default(DateTime) is yet again just a less readable way of writing DateTime.MinValue. default only exists for generics, and there's no point in using it with a concrete type.Revise
@MattiVirkkunen You and I disagree on what is more readable then - DateTime.MinValue has an explicit meaning to me saying "this is the lowest possible value a datetime can have". default(DateTime) says "this is the default value". There's no string literal for null. string.Empty is an empty string (do you use string.Empty or ""? @""?). default(string) is a null of type string - very important for type inference. What a feature was originally designed doesn't matter all that much - the important thing is what it's actually good at doing :)Masry
C
19

You could do something like:

var result = myList.Concat(Enumerable.Repeat(default(int), 20)).Take(20); 

And it would be easy to turn this into an extension method:

public static IEnumerable<T> TakeOrDefault<T>(this IEnumerable<T> list, int count, T defaultValue)
{
    return  list.Concat(Enumerable.Repeat(defaultValue, count)).Take(count);
}

But there is a subtle gotcha here. This would work perfectly fine for value types, for a reference type, if your defaultValue isn't null, you are adding the same object multiple times. Which probably isn't want you want. For example, if you had this:

var result = myList.TakeOrDefault(20, new Foo());

You are going to add the same instance of Foo to pad your collection. To solve that problem, you'd need something like this:

public static IEnumerable<T> TakeOrDefault<T>(this IEnumerable<T> list, int count, Func<T> defaultFactory)
{
    return  list.Concat(Enumerable.Range(0, count).Select(i => defaultFactory())).Take(count);
}

Which you'd call like this:

var result = myList.TakeOrDefault(20, () => new Foo())

Of course, both methods can co-exist, so you could easily have:

// pad a list of ints with zeroes
var intResult = myIntList.TakeOrDefault(20, default(int));
// pad a list of objects with null
var objNullResult = myObjList.TakeOrDefault(20, (object)null);
// pad a list of Foo with new (separate) instances of Foo
var objPadNewResult = myFooList.TakeOrDefault(20, () => new Foo());
Costar answered 27/7, 2015 at 15:0 Comment(3)
No real point subtracting the count though (if you're just going to follow with Take, which might be neater.)Hexavalent
I like your second solution. I would drop the first solution - it could throw an exception (as you noted) and it enumerates myList twice.Sweeney
Thanks the Concat/Repeat version worked perfectly for my needs.Anya
E
11

Its not there by default, but it's easy enough to write as an extension method

public static IEnumerable<T> TakeOrDefault<T>(this IEnumerable<T> items, int count, T defaultValue)
{
    var i = 0;
    foreach(var item in items)
    {
        i++;
        yield return item;
        if(i == count)
             yield break;
    }
    while(i++<count)
    {
        yield return defaultValue;
    }
}

Live example: http://rextester.com/XANF91263

Ervinervine answered 27/7, 2015 at 15:0 Comment(3)
You don't need the T defaultValue. You can return default(T).Angelenaangeleno
@YuvalItzchakov Useful if they want to supply their own default value, but meeting in the middle could be made an optional parameter.Warison
@YuvalItzchakov - I only added that as the OP requested it. Of course it means you can pass default(int) or you could pass 17 (when enumerating over a IEnumerable<int>..)Ervinervine
C
5

What you're looking for is a general-purpose PadTo method, which extends the collection's length if needed using a given value.

public static IEnumerable<T> PadTo<T>(this IEnumerable<T> source, int len)
{
    return source.PadTo(len, default(T));
}

public static IEnumerable<T> PadTo<T>(this IEnumerable<T> source, int len, T elem)
{
    return source.PadTo(len, () => elem);
}

public static IEnumerable<T> PadTo<T>(this IEnumerable<T> source, int len, Func<T> elem)
{
    int i = 0;
    foreach(var t in source)
    {
        i++;
        yield return t;
    }

    while(i++ < len)
        yield return elem();
}

You can now express:

myList.Take(20).PadTo(20);

This is analogous to Scala's List[A].padTo

Chose answered 27/7, 2015 at 15:9 Comment(0)
M
1

You could use Concat for this purpose. You can use a simple helper method to join this all together:

public IEnumerable<T> TakeSpawn(this IEnumerable<T> @this, int take, T defaultElement)
{
  return @this.Concat(Enumerable.Repeat(defaultElement, take)).Take(take);
}

The idea is that you always append another enumerable on the end of the original enumerable, so if the input doesn't have enough elements, it will start enumerating from the Repeat.

Masry answered 27/7, 2015 at 14:59 Comment(3)
@Ervinervine No, it'll work for anything, as far as I can see.Fridell
@Fridell - I read that as Enujmerable.Range, its not it's Enumerable.Repeat. I get 0/10 for reading comprehension.Ervinervine
@Ervinervine No, you were right. I noticed a split second after I posted the question, and fixed it right away :)Masry
B
0

There isn't anything in the .NET Framework, not that I'm aware of. This can be achieved easily using an extension method though and it works for all types if you supply a default value yourself:

public static class ListExtensions
{
    public static IEnumerable<T> TakeOrDefault<T>(this List<T> list, int count, T defaultValue)
    {
        int missingItems = count - list.Count;
        List<T> extra = new List<T>(missingItems);

        for (int i = 0; i < missingItems; i++)
            extra.Add(defaultValue);

        return list.Take(count).Concat(extra);
    }
}
Batavia answered 27/7, 2015 at 15:8 Comment(1)
He is using an IEnumerable. You are using a List. IEumerable is looked up at run time. List allocates memory at declarationCoss
C
0

Why not just write an extension method that checks the count and returns the default value for remaining entries:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace ConsoleApplication3
{
    class Program
    {
        static void Main(string[] args)
        {
            List<int> values = new List<int>{1, 2, 3, 4};

            IEnumerable<int> moreValues = values.TakeOrDefault(3, 100);
            Console.WriteLine(moreValues.Count());

            moreValues = values.TakeOrDefault(4, 100);
            Console.WriteLine(moreValues.Count());

            moreValues = values.TakeOrDefault(10, 100);
            Console.WriteLine(moreValues.Count());

        }
    }

    public static class ExtensionMethods
    {
        public static IEnumerable<T> TakeOrDefault<T>(this IEnumerable<T> enumerable, int count, T defaultValue)
        {
            int returnedCount = 0;
            foreach (T variable in enumerable)
            {
                returnedCount++;
                yield return variable;
                if (returnedCount == count)
                {
                    yield break;
                }
            }

            if (returnedCount < count)
            {
                for (int i = returnedCount; i < count; i++)
                {
                    yield return defaultValue;
                }
            }
        }
    }
}
Coss answered 27/7, 2015 at 15:14 Comment(1)
Please do comment when down votingCoss
B
0

I wrote a quick extension for this which depends on T being a value type.

  public static class Extensions
    {
        public static IEnumerable<T> TakeOrDefault<T>(this IEnumerable<T> list, int totalElements)
        {
            List<T> finalList = list.ToList();

            if (list.Count() < totalElements)
            {
                for (int i = list.Count(); i < totalElements; i++)
                {
                    finalList.Add(Activator.CreateInstance<T>());
                }
            }

            return finalList;
        }
    }
Baur answered 27/7, 2015 at 15:18 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.