Collapse a List by Sum (against a Property) using Lambda

Answered Collapse a List by Sum (against a Property) using Lambda

  • 2012년 4월 12일 목요일 오후 7:09
     
     

    Hello,

    I'm new to Lamda expressions and I have the following scenario.  Say I have a list with 2 properties (I'd like to avoid using the Key thingy), and there will be unique values in the first property (a Decimal), but potentially equal values in the second property (a Date). 

    What I'd like to do is to take the list, Sum all the values of Decimal in the first property, where their Dates from the second property are equal.  I'd like to end up with the same type of List as I started with (i.e., not a "var", etc.).

    For Example - starting List:

    Decimal     Date

    5               1/1/2012

    10             2/1/2012

    7               2/1/2012

    3               3/1/2012

    The ending List should be:

    5               1/1/2012

    17             2/1/2012

    3               3/1/2012

    Thanks!

모든 응답

  • 2012년 4월 12일 목요일 오후 7:13
     
     제안된 답변 코드 있음
    list = list.GroupBy(item => item.Date)
    .Select(group => new MyItem(group.Sum(item => item.Number), group.Key))
    .ToList();


    I just made up variable/Type names since you didn't define any.

    For the record, there is no such thing as a 'var' type (unless you do something like "class var {...}").  'var', as a keyword, just means, "I was too lazy to type in what this is actually supposed to be, you figure it out for me".  If the compiler can't figure it out it's a compile time error.

    Most of the Linq methods, such as the select/group by used here, return IEnumerable<T>.  If you're really sure you want a list, the conversion is trivial, but an IEnumerable is sufficient more often than you might think.


  • 2012년 4월 12일 목요일 오후 7:22
     
     

    Thanks.

    "MyItem" is the base object that the list is made up of, correct?

    What does the "group" key word stand for?

    I've tried this and I cannot get "Number" to show up as valid for "group.Number.Sum()".

    • 편집됨 sdfsda 2012년 4월 12일 목요일 오후 7:24
    •  
  • 2012년 4월 12일 목요일 오후 7:38
     
     

    It just occurs to me the base object I have does not have a "new" constructor that would seem to support this.

    Is there a way to use the actual list and just "collapse" it without having to create a new one?

  • 2012년 4월 12일 목요일 오후 7:38
     
     

    I think its supposed to be group.Sum(item=>item.Decimal)

    Continuing the var discussion, IEnumerable<T> is also lazily calculated, which can be less of a resource hog.

    • 편집됨 Idea Hat 2012년 4월 12일 목요일 오후 7:39
    •  
  • 2012년 4월 12일 목요일 오후 7:43
     
     답변됨 코드 있음

    Idea Hat is right. Try this code. 

    public partial class _Default : System.Web.UI.Page
        {
            protected void Page_Load(object sender, EventArgs e)
            {
                List<Temp> temp = new List<Temp>();
                temp.Add(new Temp(5,"1/1/2012"));
                temp.Add(new Temp(10, "2/1/2012"));
                temp.Add(new Temp(7, "2/1/2012"));
                temp.Add(new Temp(3, "3/1/2012"));
    
                var temp1 = temp.GroupBy(p => p.Date).Select(group => new Temp(group.Sum(item => item.Decimal),group.Key)).ToList();
                Grid1.DataSource = temp1;
                Grid1.DataBind();   
            }
        }
    
        public class Temp
        {
            private int _decimal;
            private string _date;
            public Temp(int _decimal,string _date)
            {
                Decimal = _decimal;
                Date = _date;
            }
    
            public int Decimal
            {
                get;
                set;
            }
            public string Date
            {
                get;
                set;
            }
        }

    Output:

    Decimal Date
    5 1/1/2012
    17 2/1/2012
    3 3/1/2012

    Thanks,


    Parth



    • 편집됨 ParthPatel 2012년 4월 12일 목요일 오후 7:51 changed variables
    • 편집됨 ParthPatel 2012년 4월 12일 목요일 오후 7:52
    • 답변으로 제안됨 ParthPatel 2012년 4월 12일 목요일 오후 7:55
    • 답변으로 표시됨 sdfsda 2012년 4월 13일 금요일 오후 12:33
    •  
  • 2012년 4월 12일 목요일 오후 7:47
     
     

    Can you expand on this:  "group.Sum(item=>item.Decimal"?

    Where is this supposed to be at?

  • 2012년 4월 12일 목요일 오후 7:48
     
     

    ""MyItem" is the base object that the list is made up of, correct?"

    Correct.  Again, if you told us what it was I wouldn't need to make stuff up.  The definition of the class would be helpful too (what are the properties, constructors, etc.).

    "What does the "group" key word stand for?"

    It's not a keyword, it's just the name that I chose for the parameter of that lambda function.  You could make up whatever you want, but it's a variable representing a group, so I called it "group".  The coloring function on this site seems to like to color it differently, but just ignore that.  In VS it looks normal.

    "I've tried this and I cannot get "Number" to show up as valid for "group.Number.Sum()"."

    That's because you didn't give me the definition of the unnamed class that you're using.  You say that you have a class with a date and number.  Not knowing what they might actually be called, I just called them "Date" and "Number".  I figured you could figure out the rest.  If you can't, you'll need to provide the definition of the class.

    I also was slightly off in my syntax, which is what Idea Hat was saying. I've updated the code accordingly.

    "It just occurs to me the base object I have does not have a "new" constructor that would seem to support this."

    So then how does one create a new object of that type?  Is there a relevant method?  Is there a parameter-less constructor?  If so, use that and then just set the properties on the new item.

    "Is there a way to use the actual list and just "collapse" it without having to create a new one?"

    You can but it would be quite a bit more work.  It would be easy enough to just remove all of the items and then add a bunch of new ones, but it wouldn't perform as well.  If lots of other places are holding onto references to this List you could do that.  If you just feel bad about recreating it, don't.  It's logically a new list, it makes sense to make a new one.  If you're concerned about performance, again, you'll likely end up doing more work to try to re-use the list, so only do that if there is a real compelling need.


  • 2012년 4월 12일 목요일 오후 7:54
     
     
    @Parth the private fields in your class are entirely unused; you can just delete them since you're using auto-generated properties.
  • 2012년 4월 12일 목요일 오후 7:55
     
     

    Of course there's a way, but not with LINQ.

    And even that's not true.  You could do it with LINQ but the solution would be awful and would violate every principle of good programming and specifically good usage of LINQ.  LINQ queries must not cause side effects.  A side effect is changing the source from which the query is being generated. 

    If your object has mutable properties - meaning you can change Decimal and DateTime - but you can't create a new object - then the object is designed poorly.

  • 2012년 4월 12일 목요일 오후 7:57
     
      코드 있음

    servy42:

    To answer the question of the constructor, the basic list I'm using is an inheritance from the CSLA framework:

    public class PaymentViews : ReadOnlyListBase<PaymentViews, PaymentView>

    None of the "constructors" follow the "new(var 1, Key)" format.

    As I said, I'm new to Lamda and find it quite confusing and obscure.


    • 편집됨 sdfsda 2012년 4월 12일 목요일 오후 7:59
    •  
  • 2012년 4월 12일 목요일 오후 7:59
     
     

    Evan,

    I agree, but I did not create these classes. 

    I'm not familiar enough with how Lamda works to figure out if any type of expression will work with these classes.

  • 2012년 4월 12일 목요일 오후 8:01
     
     
    I see that Idea Hat's fix should work, I just need to get the correct constructor arguments for my particular class into it.  Not sure if the class' definition can handle that.
  • 2012년 4월 12일 목요일 오후 8:06
     
     

    "Is there a way to use the actual list and just "collapse" it without having to create a new one?"

    To expand Servy's comment it would also be slowwwwwwww. Removing an object from a middle of a List<T> is not a fast operation (you have to move all the rest of the objects around).

    You could, instead, use a Dictionary<Date,Decimal>, but you'll have to use Keys for that.

  • 2012년 4월 12일 목요일 오후 8:07
     
     
    Since I'm working with an implementation of the CSLA framework, I notice the book has a section on LINQ to CSLA - guess I'll have to read up on that to make sure I can do this at all.....
  • 2012년 4월 12일 목요일 오후 8:08
     
     

    you can use the following syntax to create the new object on multiple lines. (This is not .Linq per se, but more Anon. Methods)

    (group=>

    {

    Temp t = new Temp();

    //fill your data in here

    return t;

    })


    • 편집됨 Idea Hat 2012년 4월 12일 목요일 오후 8:09
    •  
  • 2012년 4월 12일 목요일 오후 8:12
     
     

    @Evan:

    "Of course there's a way, but not with LINQ.

    And even that's not true.  You could do it with LINQ but the solution would be awful and would violate every principle of good programming and specifically good usage of LINQ.  LINQ queries must not cause side effects.  A side effect is changing the source from which the query is being generated. "

    It wouldn't actually be that bad.  If it had to be done I'd just call list.Clear and then pass the query used above into the AddRange() function.  It would perform worse than just making a new list, but it wouldn't really violate any major prinicpals other than re-using the list for no good reason.  If there was a good reason (the list was referenced elsewhere) then it would not be a bad idea at all.

    "If your object has mutable properties - meaning you can change Decimal and DateTime - but you can't create a new object - then the object is designed poorly."

    Agreed.  My guess is this isn't quite the case here though.  I'd say the OP just doesn't realize how to use the class, not that it's poorly designed.



  • 2012년 4월 12일 목요일 오후 8:16
     
     

    "To answer the question of the constructor, the basic list I'm using is an inheritance from the CSLA framework:

    public class PaymentViews : ReadOnlyListBase<PaymentViews, PaymentView>

    None of the "constructors" follow the "new(var 1, Key)" format.

    As I said, I'm new to Lamda and find it quite confusing and obscure."

    If you don't actually tell us what the details of the class are you make it much harder for us to help you.  You're continuing to force us to guess and guess while you tell us what works and what doesn't.  If you answered the requests I've made you would eliminate these steps.

    As for Lambdas, Google is an okay research.  A Lambda is a mathematical concept, and it's basically a function.  In C#, it's essentially a shorter syntax that lets you define an anonymous function.  Adding in C# to the search terms will give you more details in a C# context.

  • 2012년 4월 12일 목요일 오후 8:21
     
      코드 있음

    "I see that Idea Hat's fix should work, I just need to get the correct constructor arguments for my particular class into it.  Not sure if the class' definition can handle that."

    If it's not your class but some other class then you don't want to try 'injecting' a constructor.  That's way overcomplicating things.  You can create your own helper function outside of the class to accomplish a comparable goal:

    public static MyClass createMyClass(decimal number, DateTime date)
    {
      MyClass item = new MyClass();
      item.Number = number;
      item.Date = date;
      return item;
    }

    Then just use that method in the LINQ 'Select' statement rather than the constructor.  You could also use a multi-line select, or an object initializer block, but this is simpler and more readable in your case.

  • 2012년 4월 12일 목요일 오후 8:23
     
     

    "Since I'm working with an implementation of the CSLA framework, I notice the book has a section on LINQ to CSLA - guess I'll have to read up on that to make sure I can do this at all....."

    Assuming CSLA is some sort of database or external service, that would be using LINQ to interact with it directly.  This code was written to be Linq to object, and just manipulate things in memory.  Without knowing anything about what CSLA is (since you haven't even told us) I couldn't comment about it's Linq provider.

  • 2012년 4월 12일 목요일 오후 9:25
     
     답변됨 코드 있음

    Here is just a bit different example, where the custom class does not take any parameters in the constructor:

        class Program
        {
            static void Main(string[] args)
            {
                List<MyClass> list = new List<MyClass>();
                list.Add(new MyClass { MyNum = 5, MyDate = new DateTime(2012, 1, 1) });
                list.Add(new MyClass { MyNum = 10, MyDate = new DateTime(2012, 1, 2) });
                list.Add(new MyClass { MyNum = 7, MyDate = new DateTime(2012, 1, 2) });
                list.Add(new MyClass { MyNum = 3, MyDate = new DateTime(2012, 1, 3) });
    
                List<MyClass> query = list.GroupBy(g => g.MyDate).Select(s => new MyClass { MyDate = s.Key, MyNum = s.Sum(i => i.MyNum) }).ToList();
            }
    
        }
    
        public class MyClass
        {
            public int MyNum { get; set; }
            public DateTime MyDate { get; set; }
        }



    Mitja

    • 답변으로 표시됨 sdfsda 2012년 4월 13일 금요일 오후 12:31
    •  
  • 2012년 4월 13일 금요일 오후 3:56
     
     답변됨 코드 있음

    Okay, so I did figure out how to get what I wanted, despite the class having no sufficient constructor (I could have added one, but not really what I needed).

    Basically, all I need is a properly formatted list with a handful (4) of values to use for a Recursion function.  So I made a simple Struct to use, and after looking up more blogs on how to use Linq/Lambda, I figured out that the thing I wasn't understanding was how to setup a "return" object from the "from" portion of the query.  I thought at first that "Key" was a single-value only, but figured out I could add to it whatever I wanted to pull any data value from the original list into my new resultant list.  So this is what I ended up with:

    List<InterestBalancePortion> _list = 
    (from p in Payments
     group p by new {p.ProcessDate, p.PaymentTypeID } 
     into l
     select new InterestBalancePortion 
        {
            AccrualBalance = l.Sum(p => p.Amount), 
            PaymentType = (TPS_Enum.PaymentType)l.Key.PaymentTypeID,
            StartDate = l.Key.ProcessDate,
            EndDate = this.AsOfDate
         }
    ).ToList();

    So, starting with my above example values, I did get my correctly "collapsed" list result.

    Finally, a point on forum etiquette:

    Mitja Bonca, Idea Hat, Evan and ParthPatel - Thanks very much for your constructive comments.  I know in a forum it is hard to convey exactly all that is going on, short of a code-dump, which is not practical.  You all did the best you could with what I was able to provide, and it was enough to help me along.

    However, some forum users can express frustrated and snide remarks that do not add to the discussion nor do they invite "newbies" to bring forward their questions. I am an experienced programmer (but far from expert), but (perhaps sadly) this experience does not cover Linq/Lamda and so I am quite ignorant here.  Yes, the class I had to work with in this example was poorly designed for use in this case, and although I could have added the necessary corrections, the least-intrusive refactoring, I felt, was to use a localized struct for my needs.  It is not the case that I simply don't realize how to use the class.

    I don't care how many forum points or gold medals one has, a basic politeness and patience is called for in these forums.  Many thanks to all of you who did so.


    • 답변으로 표시됨 sdfsda 2012년 4월 13일 금요일 오후 3:56
    • 편집됨 sdfsda 2012년 4월 13일 금요일 오후 3:57
    •  
  • 2012년 4월 16일 월요일 오전 5:27
     
     

    Hi,

    Thanks everyone I got the solution from this post.

  • 2012년 4월 16일 월요일 오후 12:02
     
     

    I'd just call list.Clear and then pass the query used above into the AddRange() function.

    You'd need to store the result of the query into a temporary list. Running the query after the list is cleared would return nothing.