0

To show what I want to achieve, I prepared a simplified example:

Let's say I have an object like this:

public class SoldProduct
{
    public string Name { get; set; }
    public DateTime Date { get; set; }
}

Then, somewhere I have a method which gets different lists of SoldProduct (from different categories) like this:

List<SoldProduct> toolsSold = GetToolSales();
List<SoldProduct> materialsSold = GetMaterialSales();
List<SoldProduct> foodsSold = GetFoodSales();

Now, I need to merge them into one list of rows, where a row is something like this:

public class SoldProductRow
{
    public SoldProduct ToolSold { get; set; }
    public SoldProduct MaterialSold { get; set; }
    public SoldProduct FoodSold { get; set; }
}

Question:

So, how to convert these 3 lists into a List<SoldProductRow>? Is there some simple, efficient LINQ query? Maybe there is a really simple and efficient way to write it manually?

Let's imagine this row is something like an Excel row; we want to have 2 columns for name and date of tool sales, 2 columns for name and date of material sales, and 2 columns for name and date of food sales. Essentially list them side by side.

Difficulties:

Lists can be of different length, so if we have 3 elements in toolsSold but 5 in materialsSold and foodsSold, then the rows list should have 5 elements (longest list length) where the first 3 elements of the rows list will have all properties, but the 4th and 5th elements should have null in ToolSold and proper values in MaterialSold and FoodSold

List<SoldProduct> length is usually between 1 and 3, but actually there is List<Parent> with possibly hundreds of objects and every parent has 5 List<SoldProduct> so, I need to create hundreds of rows.

Example:

If I had lists:

toolsSold: [
  {Name: hammer, Date:24-09-2020}
]

materialsSold: [
  {Name: wood, Date:23-09-2020},
  {Name: steel, Date:17-08-2020}
]

foodsSold: [
  {Name: apple, Date:07-02-2020}
]

I would want this result (2 rows because the longest list has 2 entities):

SoldProductsRows: [
  {
     ToolSold: {Name: hammer, Date:24-09-2020}
     MaterialSold: {Name: wood, Date:23-09-2020}
     FoodSold: {Name: apple, Date:07-02-2020}
  },
  {
     ToolSold: null
     MaterialSold: {Name: steel, Date:17-08-2020}
     FoodSold: null
  },
]
3
  • What is the relationship between the different lists? The Product Name? Or Date? How do we know what belongs together on a "row" ? Commented Sep 24, 2020 at 15:32
  • they have no relation, thats why in row class there is property for each list. Commented Sep 24, 2020 at 15:34
  • I added json like example to better show what i want to achieve Commented Sep 24, 2020 at 15:44

3 Answers 3

2

Here's a simple solution without LINQ:

int count = Math.Max(Math.Max(toolsSold.Count, materialsSold.Count), foodsSold.Count);

var rowsList = new List<SoldProductRow>();
for (int i = 0; i < count; i++)
{
    rowsList.Add(new SoldProductRow
    {
        ToolSold = toolsSold.ElementAtOrDefault(i),
        MaterialSold = materialsSold.ElementAtOrDefault(i),
        FoodSold = foodsSold.ElementAtOrDefault(i)
    });
}

Steps:

  • Use Math.Max() to get the count of the longest list.
  • Use the ElementAtOrDefault() extension method to get the element of each list at a specific index if it exists; and if not, use null.
  • Use the values from the previous step to create a new SolidProductRow object and add it to the final list.

Try it online.

Sign up to request clarification or add additional context in comments.

2 Comments

Would something like i < toolsSold.Count ? toolsSold[i] : null have the same performance as using .ElementAtOrDefault, worse performance or better?
I'll answer you in two points: 1) The performance will be virtually the same because what you proposed is exactly what ElementAtOrDefault() does internally when the source is IList (e.g., List<T> or an array). 2) Whenever you find yourself asking "which is faster", you should go through the list of questions mentioned in this amazing article. Why? Because more often than not, the answer to "which is faster" doesn't really matter.
1

LINQ solutions are possible but, as demonstrated in my other answer, a for loop looks more readable in this particular case. The simplest LINQ solution I could think of is:

var rowsList2 =
    Enumerable.Range(0, count)
    .Select(i => new SoldProductRow
    {
        ToolSold = toolsSold.ElementAtOrDefault(i),
        MaterialSold = materialsSold.ElementAtOrDefault(i),
        FoodSold = foodsSold.ElementAtOrDefault(i)
    }).ToList();

..which is basically a disguised loop.

You could also achieve the same result using a modified "special" version of Zip(), but it would be unnecessarily complicated. You may create a reusable generic extension method though if you're going to be using it a lot. Just for fun, I came up with the following method1:

public static IEnumerable<TResult> ZipThreeWithDefaults<TFirst, TSecond, TThird, TResult>(
    this IEnumerable<TFirst> first,
    IEnumerable<TSecond> second,
    IEnumerable<TThird> third,
    Func<TFirst, TSecond, TThird, TResult> func)
{
    using (var e1 = first.GetEnumerator())
    using (var e2 = second.GetEnumerator())
    using (var e3 = third.GetEnumerator())
    {
        while (e1.MoveNext())
        {
            var current2 = (e2.MoveNext() ? e2.Current : default(TSecond));
            var current3 = (e3.MoveNext() ? e3.Current : default(TThird));

            yield return func(e1.Current, current2,current3);
        }
        while (e2.MoveNext())
        {
            var current3 = (e3.MoveNext() ? e3.Current : default(TThird));
            yield return func(default(TFirst), e2.Current, current3);
        }
        while (e3.MoveNext())
        {
            yield return func(default(TFirst), default(TSecond), e3.Current);
        }
    }
}

Usage:

var rowsList3 = 
    toolsSold.ZipThreeWithDefaults(materialsSold, 
                                   foodsSold, (t, m, f) => new SoldProductRow
    {
        ToolSold = t,
        MaterialSold = m,
        FoodSold = f
    }).ToList();

Here's a complete example to try online.


1 Inspired by this answer by Marc Gravell.

Comments

0

If you absolutely want to use LINQ, you could do this:

int max = new[] { toolsSold.Count, materialsSold.Count, foodsSold.Count }.Max();
List<SoldProductRow> result = 
    toolsSold.Concat(Enumerable.Repeat(null, max - toolsSold.Length))
        .Zip(materialsSold.Concat(Enumerable.Repeat(null, max - materialsSold.Length)))
        .Zip(foodsSold.Concat(Enumerable.Repeat(null, max - foodsSold.Length)))
        .Select(x => new SoldProductRow
        {
            ToolSold = x.First.First,
            MaterialSold = x.First.Second,
            FoodSold = x.Second
        })
        .ToList();

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.