views:

335

answers:

6

I have a generic List (of Foo) which contains n objects of Type Foo. One of the properties of Foo is PropertyA. PropertyA can be one of ValueA, ValueB or ValueC. Is there an easy way of splitting this into three seperate Lists, one for ValueA, one for ValueB and one for ValueC?

I can write some code which loops the original list and adds each item to a new list based on the property value but this does not seem to be very maintainable (what if I suddenly get a ValueD for example?)

**EDIT. I should have mentioned that I'm using version 2.0 of the framework.

+3  A: 

You can use Enumerable.GroupBy:

var groupings = list.GroupBy(x => x.PropertyA);

foreach(var grouping in groupings)
{
    // grouping.Key is the grouped value

    foreach(var entry in grouping)
    {
        // process
    }
}
Richard Szalay
+5  A: 

In C# I would write:

  List<List<foo>> result = fooList
    .GroupBy(foo => foo.PropertyA)
    .Select(g => g.ToList())
    .ToList();
David B
+1. Telerik converter says the VB equivalent is `Dim result As List(Of List(Of foo)) = fooList.GroupBy(Function(foo As ) foo.PropertyA).[Select](Function(g As ) g.ToList()).ToList()`
MarkJ
+5  A: 

If you want exactly 3 lists for valueA, valueB and valueC (even if one of them is empty):

Dim listA = (From x in myList Where x.PropertyA = ValueA).ToList()
Dim listB = (From x in myList Where x.PropertyA = ValueB).ToList()
...

Otherwise, use the GroupBy operator as suggested by others.


EDIT: Since you are using Framework 2.0, I guess you'll have to resort to your loop idea. A generic algorithm implementing GroupBy shouldn't be too difficult, though. Something along the lines of

Dim dic as New Dictionary(Of TypeOfYourValues, List(Of Foo))
For Each e As Foo In myList
    If Not dic.ContainsKey(e.PropertyA) Then
        dic(e.PropertyA) = New List(Of Foo)
    End if
    dic(e.PropertyA).Add(e)
Next

Then loop through the values of the dictionary.

Heinzi
+1  A: 
var query = from foo in list
            group foo by foo.PropertyA;

List<Foo> valueAGroup = query.First(g => g.Key == ValueA).ToList();
List<Foo> valueBGroup = query.First(g => g.Key == ValueB).ToList();
List<Foo> valueCGroup = query.First(g => g.Key == ValueC).ToList();

Or you could leave out the ToList() calls if an IEnumerable<Foo> is good enough.

If it is possible that for a ValueX there are no items for which PropertyA equals ValueX, then First will throw an exception. In that case, it's a good idea to do this:

List<Foo> valueXGroup = (query.FirstOrDefault(g => g.Key == ValueX) ??
    Enumerable.Empty<Foo>()).ToList();

This will give you an empty list instead of throwing an exception.

Joren
Won't this throw if there are no items with ValueA?
David B
Indeed. I updated to give a suggestion for that case.
Joren
+3  A: 

See below the C# for the VB.Net version - note that there's one additional class (FooFinder) since there are no anonymous methods in VB.NET, so I needed something to be able to store the match state.

Here's a more "functional" way to accomplish the same thing, but still using C# 2.0 syntax. Note the important difference from other solutions (looping/dictionaries) is the use of the FindAll method on List, which will iterate over your collection and return all items for which the delegate returns true. C#:

using System;
using System.Collections.Generic;

namespace SplitList
{
    class Program
    {
        class Foo
        {
            public Foo(string propertyA, int number)
            {
                _propertyA = propertyA;
                _number = number;
            }

            private int _number;

            private string _propertyA;

            public string PropertyA
            {
                get { return _propertyA; }
            }

            public int Number
            {
                get { return _number; }
            }
        }
        static void Main(string[] args)
        {
            List<Foo> foos = new List<Foo>();
            foos.Add(new Foo("ValueA", 1));
            foos.Add(new Foo("ValueA", 2));
            foos.Add(new Foo("ValueA", 3));
            foos.Add(new Foo("ValueA", 4));
            foos.Add(new Foo("ValueB", 5));
            foos.Add(new Foo("ValueB", 6));
            foos.Add(new Foo("ValueC", 7));
            foos.Add(new Foo("ValueC", 8));
            foos.Add(new Foo("ValueC", 9));

            List<Foo> aFoos = foos.FindAll(delegate(Foo f) { return f.PropertyA == "ValueA"; });
            List<Foo> bFoos = foos.FindAll(delegate(Foo f) { return f.PropertyA == "ValueB"; });
            List<Foo> cFoos = foos.FindAll(delegate(Foo f) { return f.PropertyA == "ValueC"; });
            WriteFoos("ValueA", aFoos);
            WriteFoos("ValueB", bFoos);
            WriteFoos("ValueC", cFoos);
            Console.ReadLine();
        }

        private static void WriteFoos(string propertyAValue, List<Foo> list)
        {
            Console.WriteLine("Group {0}:", propertyAValue);
            list.ForEach(delegate(Foo f)
                             {
                             Console.WriteLine("Number:{0}, PropertyA:{1}", f.Number, f.PropertyA);
                             });

        }
    }
}

VB.NET:

Module Module1

 Class FooFinder
  Public Sub New(ByVal propertyAValue As String)
   Me.PropertyAValue = propertyAValue
  End Sub
  Public ReadOnly PropertyAValue As String
  Function Matches(ByVal f As Foo) As Boolean
   Return (f.PropertyAValue = Me.PropertyAValue)
  End Function
 End Class
 Class Foo

  Public Sub New(ByVal propertyAValue As String, ByVal number As Integer)
   _propertyAValue = propertyAValue
   _number = number
  End Sub

  Private _propertyAValue As String
  Private _number As Integer

  Public Property PropertyAValue() As String
   Get
    Return _propertyAValue
   End Get
   Set(ByVal value As String)
    _propertyAValue = value
   End Set
  End Property

  Public Property Number() As Integer
   Get
    Return _number
   End Get
   Set(ByVal value As Integer)
    _number = value
   End Set
  End Property
 End Class
 Sub Main()

  Dim foos As New List(Of Foo)
  foos.Add(New Foo("ValueA", 1))
  foos.Add(New Foo("ValueA", 2))
  foos.Add(New Foo("ValueA", 3))
  foos.Add(New Foo("ValueB", 4))
  foos.Add(New Foo("ValueB", 5))
  foos.Add(New Foo("ValueC", 6))
  foos.Add(New Foo("ValueC", 7))
  foos.Add(New Foo("ValueC", 8))
  foos.Add(New Foo("ValueC", 9))

  Dim aFoos As List(Of Foo) = foos.FindAll(AddressOf New FooFinder("ValueA").Matches)
  Dim bFoos As List(Of Foo) = foos.FindAll(AddressOf New FooFinder("ValueB").Matches)
  Dim cFoos As List(Of Foo) = foos.FindAll(AddressOf New FooFinder("ValueC").Matches)

  WriteFoos("ValueA", aFoos)
  WriteFoos("ValueB", bFoos)
  WriteFoos("ValueC", cFoos)
  Console.ReadLine()


 End Sub

 Private Sub WriteFoos(ByVal propertyAValue As String, ByVal list As List(Of Foo))
  Console.WriteLine("PropertyAValue:{0}", propertyAValue)
  For Each f As Foo In list
   Console.WriteLine("Number:{0}, PropertyAValue:{1}", f.Number, f.PropertyAValue)
  Next
 End Sub
End Module
Doug Rohrer
I like the look of this a lot, converting to VB via Telerik give a compilation error on the word 'Function', any idea how this would translate? (Dim aFoos As List(Of Foo) = foos.FindAll(Function(f As Foo) Do Return f.PropertyA = "ValueA" End Function)
Simon
I've updated my response with a VB.NET version - slightly different because of the lack of anonymous methods/delegates in VB.NET 2.0, but the same basic idea.
Doug Rohrer
Perfect. Thanks.
Simon
+2  A: 

In C# with .Net 2.0, I have written (too many times):

 //if PropertyA is not int, change int to whatever that type is
Dictionary<int, List<foo>> myCollections =
  new Dictionary<int, List<foo>>();
//
foreach(Foo myFoo in fooList)
{
  //if I haven't seen this key before, make a new entry
  if (!myCollections.ContainsKey(myFoo.PropertyA))
  {
    myCollections.Add(myFoo.PropertyA, new List<foo>());
  }
  //now add the value to the entry.
  myCollections[myFoo.PropertyA].Add(myFoo);
}
//
// now recollect these lists into the result.
List<List<Foo>> result = new List<List<Foo>>();
foreach(List<Foo> someFoos in myCollections.Values)
{
  result.Add(someFoos);
}

Nowadays, I just write:

List<List<foo>> result = fooList
  .GroupBy(foo => foo.PropertyA)
  .Select(g => g.ToList())
  .ToList();

Or

 ILookup<TypeOfPropertyA, foo>> result = fooList.ToLookup(foo => foo.PropertyA);
David B
Works perfectly.
Simon