Rotate - Transposing a List<List<string>> using LINQ C#
Asked Answered
I

7

16

I'm having a List<List<string>>, which is return from the remote data source (i.e., WCF). So, I need to modify the following data into a user-friendly list using LINQ

The C# Code is

List<List<string>> PersonInfo = new List<List<string>>()
{
    new List<string>() {"John", "Peter", "Watson"},
    new List<string>() {"1000", "1001", "1002"}
}

Appropriate Screen Shot: Existing

enter image description here

I need to rotate the data as like the below Screenshot: Proposed

enter image description here

Kindly assist me how to rotate the data using LINQ C#

Intermingle answered 14/9, 2016 at 7:41 Comment(6)
Are there always going to be two lists inside the main one?Geometer
@Geometer Yes. List<List<string>>Intermingle
The question is: does PersonInfo always contain 2 lists, or can there be more than 2?Evidentiary
I am not sure you understood me. I meant, are there always going to be one list with the names and one list with the numbers?Geometer
Just googled fo c# rotate 2d list linq: https://mcmap.net/q/747712/-linq-swap-columns-into-rowsCulver
This is called transposing rather than pivoting by the way.Journeywork
R
32

This is a simple and flexible solution, it will handle multiple inner lists with any number of dimensions.

List<List<string>> PersonInfo = new List<List<string>>()
{
    new List<string>() {"John", "Peter", "Watson"},
    new List<string>() {"1000", "1001", "1002"}
};


var result = PersonInfo
    .SelectMany(inner => inner.Select((item, index) => new { item, index }))
    .GroupBy(i => i.index, i => i.item)
    .Select(g => g.ToList())
    .ToList();
Remediable answered 14/9, 2016 at 8:6 Comment(4)
I realize this might revive this thread, but being not wholly familiar with this segment of VB.net, I was wondering if it would be possible to interject a Nothing or similar placeholder value if the input lists are uneven?Remittee
@Remittee That's entirely possible. I'd say it's worth a separate question though - how you can convert a jagged array to a rectangular array, filling gaps with a specific value.Remediable
Surprised that this works (it does)! It depends heavily on the implementation of GroupBy, in the sense that it guarantees to preserve the order of the groups and also the order of the elements within the groups! That said, I think you can omit the to .ToList() calls, as the IEnumerables are arrays under the hood.Barometer
@Barometer The .ToList() calls are to satisfy the types specified in the question. They're not required for transposing, for that only, you can lose the last two lines altogether.Remediable
C
10

Here is a generic extension method

public static IEnumerable<IEnumerable<T>> Pivot<T>(this IEnumerable<IEnumerable<T>> source)
{
    var enumerators = source.Select(e => e.GetEnumerator()).ToArray();
    try
    {
        while (enumerators.All(e => e.MoveNext()))
        {
            yield return enumerators.Select(e => e.Current).ToArray();
        }
    }
    finally
    {
        Array.ForEach(enumerators, e => e.Dispose());
    }
}

so you can

var result = PersonInfo.Pivot();
Caddish answered 14/9, 2016 at 8:4 Comment(0)
E
2

Assuming there are only ever 2 lists inside PersonInfo:

var rotated = PersonInfo[0]
    .Zip(PersonInfo[1], (a, b) => new List<string> { a, b }).ToList();

If there can be any number of Lists inside of PersonInfo:

Enumerable.Range(0, PersonInfo[0].Count)
    .Select(i => PersonInfo.Select(lst => lst[i]).ToList()).ToList();
Evidentiary answered 14/9, 2016 at 8:1 Comment(0)
K
2

You can use Enumerable.Range and Enumerable.ElementAtOrDefault:

List<List<string>> rotated = Enumerable.Range(0, PersonInfo.Max(list => list.Count))
 .Select(i => PersonInfo.Select(list => list.ElementAtOrDefault(i)).ToList())
 .ToList();

PersonInfo.Max(list => list.Count) returns the max-size of the lists. This will be the new size of the main list, in this case 3. Enumerable.Range is like a for-loop. For every list it will now select all strings at these indexes. If the sizes are different you'll get null(because of ElementAtOrDefault).

If the lists had the same size you can apply the same query to get the original list back:

PersonInfo = Enumerable.Range(0, rotated.Max(list => list.Count))
 .Select(i => rotated.Select(list => list.ElementAtOrDefault(i)).ToList())
 .ToList();

As extension:

public static IEnumerable<IList<T>> Rotate<T>(this IEnumerable<IList<T>> sequences)
{
    var list = sequences as IList<IList<T>> ?? sequences.ToList();
    int maxCount = list.Max(l => l.Count);
    return Enumerable.Range(0, maxCount)
        .Select(i => list.Select(l => l.ElementAtOrDefault(i)).ToList());
}

Usage:

IEnumerable<IList<string>> rotated = PersonInfo.Rotate();
IEnumerable<IList<string>> rotatedPersonInfo = rotated.Rotate(); // append ToList to get the original list
Kalman answered 14/9, 2016 at 8:14 Comment(2)
@Oliver: why delete? Of course the lists must have the same size, but that is the case here. But thanks for noting, I've fixed my answerKalman
I read it as Min not Max (duh) so I was referring to data loss rather than additional values. Different none the less!Remediable
V
0

This extends the Zip idea above to any number of lists. Zip will truncate the row lists to the smallest rank.

List<List<string>> PersonInfo = new List<List<string>>()
{
    new List<string>() {"John", "Peter", "Watson"},
    new List<string>() {"1000", "1001", "1002"},
    new List<string>() {"2000", "2001", "2002"},
    new List<string>() {"3000", "3001", "3002"}
};

var seed = Enumerable.Empty<List<string>>();
var transformed = PersonInfo.Aggregate(seed, (acc, r) =>
   acc.Any()
 ? acc.Zip(r, (row, nextElement) => { row.Add(nextElement); return row; })
 : r.Select(e => new List<string> { e }) //initialize target list using first row
); 
Vraisemblance answered 22/7, 2018 at 18:23 Comment(0)
Z
0

Just do something like:

var persons = Enumerable.Range(0, PersonInfo.First().Count()).Select(i => PersonInfo.Select(e => e[i]).ToList()).ToList();

or

var persons = Enumerable.Range(0, PersonInfo[0].Count()).Select(i => {
    return PersonInfo.Select(e => {
        return e[i];
    }).ToList();
}).ToList();

and check the result like below:

persons.ForEach(p => Console.WriteLine("{0} {1}", p[0], p[1]));
Zebrawood answered 6/6, 2021 at 7:51 Comment(0)
P
-1

Try this:

List<List<string>> PersonInfo = new List<List<string>>(){
new List<string>() {"John", "Peter", "Watson"},
new List<string>() {"1000", "1001", "1002"}};

List<List<string>> PivitedPersonInfo = new List<List<string>>();
for (int i = 0; i < PersonInfo.First().Count; i++)
{
    PivitedPersonInfo.Add(PersonInfo.Select(x => x.ElementAt(i)).ToList());
}
Postbellum answered 14/9, 2016 at 7:59 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.