1

Is it possible to replace the Key by which an IGrouping is grouped?

I'm currently grouping by an anonymous type like this:

var groups = orders.GroupBy(o => new { o.Date.Year, o.Date.Month });

But now my grouping key is an anonymous type. I would like to replace this grouping key by a defined Type, "YearMonth", with an overridden ToString Method.

public class YearMonth
{
    public int Year { get; set; }
    public int Month { get; set; }

    public override string ToString()
    {
        return Year + "-" + Month;
    }
}

Is there any way to replace the grouping key? Or to create a new IGrouping out of the existing IGrouping with a new grouping key?

Protector one
  • 6,926
  • 5
  • 62
  • 86
  • 4
    Why do you want to group on one type and then change the key instead of just grouping on the key you *actually* want to have? – Servy Oct 10 '16 at 13:55
  • 5
    Just use `o => new YearMonth(o.Date.Year, o.Date.Month)` instead... so long as `YearMonth` overrides `Equals` and `GetHashCode` appropriately, that should be fine... at least for LINQ to Objects. – Jon Skeet Oct 10 '16 at 13:55
  • 2
    It is `readonly` for a good reason, you should not modify later what was used to identify equal objects. – Tim Schmelter Oct 10 '16 at 13:56
  • @Protectorone pick your poison then - either use an anonymous type that has equality checks built-in or define your own. – D Stanley Oct 10 '16 at 13:59
  • @Jon: Isn't it silly to implement a hashing function when using an anonymous type _just works_? It feels like I'm writing too much code here. – Protector one Oct 10 '16 at 14:02
  • @DStanley: Does it have to be poison? I came here looking for sugar. – Protector one Oct 10 '16 at 14:03
  • 2
    Well you're the one with a type that you want to be a key for the grouping... if you're going to use a type for a key, you need it to handle key-related operations... – Jon Skeet Oct 10 '16 at 14:03
  • @Jon: I could just group by string, like: `o.Date.ToString("yyyy-MM")`, but that feels wrong too! – Protector one Oct 10 '16 at 14:04
  • What does your overridden `ToString` do? Can you make it dynamic and pass the key value into it? – D Stanley Oct 10 '16 at 14:04
  • There are also several "dynamic" equality comparers in the wild that just look at the value of all public properties (just like anonymous types). – D Stanley Oct 10 '16 at 14:06
  • @DStanley: "In the wild"? What about "inside the box"? I want to write less code here, not more. A Taco Bell programming thing. (http://widgetsandshit.com/teddziuba/2010/10/taco-bell-programming.html) Principle of Least Astonishment and all that. (https://en.wikipedia.org/wiki/Principle_of_least_astonishment) – Protector one Oct 10 '16 at 14:09
  • Yes, grouping by a string feels pretty wrong to me. But you haven't explained why you want the new type, or why you're unwilling to implement equality on it. If you want the grouping to be strongly typed outside the method creating it, just create the type and implement equality. If you don't, then stick with the anonymous type and format it when you need a string representation. – Jon Skeet Oct 10 '16 at 14:37
  • @Jon: I want the new type to keep things strongly typed. (I didn't think of using a struct.) Implementing equality is not the issue; it's the hashing function. I simply do not know how, and don't want to botch it. – Protector one Oct 12 '16 at 06:17
  • 1
    @Protectorone: Well that's easy - there are *lots* of questions on SO about implementing GetHashCode correctly. http://stackoverflow.com/questions/263400 is a good start. – Jon Skeet Oct 12 '16 at 06:17

1 Answers1

2

Well personally I would just group on the string value, since that seems to be what you really care about from the key.

Another simple option would be to create a date representing the month by using a constant day:

orders.GroupBy(o => new DateTime (o.Date.Year, o.Date.Month, 1))

Then you have built-in value equality and string formatting.

You could make YearMoth an immutable struct, which will also give you value-equality semantics:

public struct YearMonth
{
    public readonly int Year;
    public readonly int Month;

    public YearMonth(int year, int month)
    {
        Year = year;
        Month = month;
    }

    public override string ToString()
    {
        return Year + "-" + Month;
    }
}
D Stanley
  • 149,601
  • 11
  • 178
  • 240
  • This looks pretty good! Why would you want to group by the string value instead? This way you keep all the data intact, strongly typed and all. – Protector one Oct 10 '16 at 14:15
  • Do you care about anything other than the string value? do you need the month and year separately? Strings are just as valid a grouping key as any other type, provided you don't need to worry about case sensitivity, whitespace, etc. Since _you_ control the string format those aren't a problem. Like I said, that's what _I_ would do until I had a compelling reason to do something _different_. – D Stanley Oct 10 '16 at 14:18
  • I think the string is all I need _right now_, but somewhere down the line having the separate data might be useful. I don't want to burn any bridges just for the sake of, "I don't need those right now". – Protector one Oct 10 '16 at 14:21
  • This would need overriding Equals and GetHashCode, in the `struct YearMonth`, for it to be completely useful – Mrinal Kamboj Oct 10 '16 at 18:37
  • @MrinalKamboj value types get built-in value equality semantics, which is what the OP wanted. It uses reflection and can _benefit_ from an explicit overload, but it's not _required_. – D Stanley Oct 10 '16 at 19:59
  • If I `GroupBy` this struct, I get this ArgumentException: "At least one object must implement IComparable". Any ideas? – Protector one Dec 14 '16 at 14:58
  • Do you have an `OrderBy` somewhere? `GroupBy` does not use `IComparable`. If you do, then your struct needs to implement `IComparable` to "compare" two instances. – D Stanley Dec 14 '16 at 15:33