Linq通用的GroupBy日期和其他字段在同一时间。[英] Linq generic GroupBy date and other fields at the same time

本文是小编为大家收集整理的关于Linq通用的GroupBy日期和其他字段在同一时间。的处理/解决方法,可以参考本文帮助大家快速定位并解决问题,中文翻译不准确的可切换到English标签页查看源文。

问题描述

我希望能够使用 Linq 同时按年/月/周/日以及其他属性对一些数据库结果进行分组.用户应该能够在运行时按年/月/周/日在分组之间切换.典型情况是我有某种 DTO,其中包含 DateTime 时间戳以及其他属性.

更新:我找到了一个可行的解决方案.请参阅下面的帖子.

对于仅按日期分组(不包括按其他属性分组),我创建了一个 DateGroupKey 类和一个扩展方法:

DateGroupKey 类

public class DateGroupKey
{
    public int Year { get; set; }
    public int Month { get; set; }
    public int Week { get; set; }
    public int Day { get; set; }
}

扩展方法

public static IEnumerable<IGrouping<DateGroupKey, TValue>> GroupByDate<TValue>(
        this IEnumerable<TValue> source,
        Func<TValue, DateTime> dateSelector,
        DateGroupType dateGroupType)
{
    Func<TValue, DateGroupKey> keySelector = null;
    switch (dateGroupType)
    {
            case DateGroupType.Year:
                keySelector = val => new DateGroupKey { Year = dateSelector(val).Year };
                break;
            case DateGroupType.Month:
                keySelector = val => new DateGroupKey { Year = dateSelector(val).Year, Month = dateSelector(val).Month };
                break;
            case DateGroupType.Week:
                keySelector = val => new DateGroupKey { Year = dateSelector(val).Year, Week = dateSelector(val).GetIso8601WeekOfYear() };
                break;
            case DateGroupType.Day:
                keySelector = val => new DateGroupKey { Year = dateSelector(val).Year, Month = dateSelector(val).Month, Day = dateSelector(val).Day };
                break;
            default:
                throw new NotSupportedException($"Group type not supported: {dateGroupType}");
    }

    return source.GroupBy(keySelector, new DateGroupKeyComparer());
}

其中DateGroupKeyComparer 是实现IEqualityComparer<DateGroupKey> 的类.我可以像这样使用扩展方法:

var grouped = results.GroupByDate<ProductDto>(x => x.TimeStamp, DateGroupType.Month)

它应该可以正常工作.现在,我想扩展这个想法,不仅可以按日期分组,还可以同时按其他一些字段分组.假设 ProductDto 类具有以下签名

public class ProductDto
{
    public DateTime TimeStamp {get; set;}
    public int ProductGroup {get; set;}
    public int Variant {get; set;}
    public double Value {get; set;}
}

我想做一些类似的事情

var grouped = results.GroupByDate<ProductDto>(x => x.TimeStamp, x => x.ProductGroup, x => x.Variant, DateGroupType.Month);

但我似乎无法解决如何解决这个问题.我想过将 DateGroupKey 抽象为一个 IDateGroupKey 接口,其中包含 Year、Month、Week 和 Day 属性,并以某种方式告诉 GroupBy 扩展如何映射到这个特定的实现,但我不确定它是否正确(或 simplay 'a') 方法.

关于如何解决这个问题有什么好的想法吗?

推荐答案

所以,我找到了一个可行的解决方案 - 我不太满意,但现在可以了.

我创建了一个通用复合键,包含一个包含日期分组信息的 DateGroupKey 对象以及一个通用对象键(用于标识不属于日期的分组字段)

public class DateGroupCompositeKey<TKey>
{
    public DateGroupKey DateKey { get; set; }
    public TKey ObjectKey { get; set; }
}

public class DateGroupKey
{
    public int Year { get; set; }
    public int Month { get; set; }
    public int Week { get; set; }
    public int Day { get; set; }
}

另外,我为这些类实现了 IEqualityComparer.

public class DateGroupKeyComparer : IEqualityComparer<DateGroupKey>
{
    public bool Equals(DateGroupKey x, DateGroupKey y)
    {
        return x.Year == y.Year &&
            x.Month == y.Month &&
            x.Week == y.Week &&
            x.Day == y.Day;
    }

    public int GetHashCode(DateGroupKey obj)
    {
        unchecked
        {
            var h = 0;
            h = (h * 31) ^ obj.Year;
            h = (h * 31) ^ obj.Month;
            h = (h * 31) ^ obj.Week;
            h = (h * 31) ^ obj.Day;
            return h;
        }
    }
}

public class DateGroupCompositeKeyComparer<TValue> : IEqualityComparer<DateGroupCompositeKey<TValue>>
{
    private readonly IEqualityComparer<TValue> _valueComparer;
    private readonly DateGroupKeyComparer _dgkComparer;

    public DateGroupCompositeKeyComparer(IEqualityComparer<TValue> valueComparer)
    {
        _valueComparer = valueComparer;
        _dgkComparer = new DateGroupKeyComparer();
    }

    public bool Equals(DateGroupCompositeKey<TValue> x, DateGroupCompositeKey<TValue> y)
    {
        return _dgkComparer.Equals(x.DateKey, y.DateKey) &&
            _valueComparer.Equals(x.ObjectKey, y.ObjectKey);
    }

    public int GetHashCode(DateGroupCompositeKey<TValue> obj)
    {
        unchecked
        {
            int h = 0;
            h = (h * 31) ^ _dgkComparer.GetHashCode(obj.DateKey);
            h = (h * 31) ^ _valueComparer.GetHashCode(obj.ObjectKey);
            return h;
        }
    }
}

现在我的扩展方法看起来像

public static class Extensions
{
    // This was the original extension
    public static IEnumerable<IGrouping<DateGroupKey, TValue>> GroupByDate<TValue>(
        this IEnumerable<TValue> source, 
        Func<TValue, DateTime> dateSelector,
        DateGroupType dateGroupType)
    {
        Func<TValue, DateGroupKey> keySelector = DateKeySelector(dateSelector, dateGroupType);
        return source.GroupBy(keySelector, new DateGroupKeyComparer());
    }

    // New extension method supporting composite grouping
    public static IEnumerable<IGrouping<DateGroupCompositeKey<TKey>, TValue>> GroupByDateComposite<TKey, TValue>(
        this IEnumerable<TValue> source,
        Func<TValue, DateTime> dateSelector,
        Func<TValue, TKey> keySelector,
        IEqualityComparer<TKey> keyComparer,
        DateGroupType dateGroupType)
    {
        var dateKeySelector = DateKeySelector(dateSelector, dateGroupType);

        DateGroupCompositeKey<TKey> func(TValue val) => 
            new DateGroupCompositeKey<TKey>
            {
                DateKey = dateKeySelector(val),
                ObjectKey = keySelector(val)
            };

        return source.GroupBy(func, new DateGroupCompositeKeyComparer<TKey>(keyComparer));
    }

    private static Func<TValue, DateGroupKey> DateKeySelector<TValue>(Func<TValue, DateTime> dateSelector, DateGroupType dateGroupType)
    {
        Func<TValue, DateGroupKey> dateKeySelector = null;
        switch (dateGroupType)
        {
            case DateGroupType.Year:
                dateKeySelector = val => new DateGroupKey { Year = dateSelector(val).Year };
                break;
            case DateGroupType.Month:
                dateKeySelector = val => new DateGroupKey { Year = dateSelector(val).Year, Month = dateSelector(val).Month };
                break;
            case DateGroupType.Week:
                dateKeySelector = val => new DateGroupKey { Year = dateSelector(val).Year, Week = dateSelector(val).GetIso8601WeekOfYear() };
                break;
            case DateGroupType.Day:
                dateKeySelector = val => new DateGroupKey { Year = dateSelector(val).Year, Month = dateSelector(val).Month, Day = dateSelector(val).Day };
                break;
            default:
                throw new NotSupportedException($"Group type not supported: {dateGroupType}");
        }
        return dateKeySelector;
    }

}

现在可以使用扩展名(如在原始帖子中,仅按日期分组)

var grouped = results.GroupBy<ProductDto>(x => x.TimeStamp, DateGroupType.Month);

或在复合版本中

var grouped = results.GroupByDateComposite<int, ProductDto>(
    x => x.TimeStamp,
    x => x.ProductGroup,
    EqualityComparer<int>.Default,
    DateGroupType.Month);

据我所知,缺点是对于更复杂的分组,比如

var grouped = results.GroupByDateComposite<CustomKey, ProductDto>(
    x => x.TimeStamp,
    x => new CustomKey{ ProductGroup = x.ProductGroup, Variant = x.Variant},
    new CustomKeyComparer(),
    DateGroupType.Month);

我需要创建单独的键类和相应的 IEqualityComparer 实现才能进行分组.

本文地址:https://www.itbaoku.cn/post/1557149.html

问题描述

I would like to be able to group some database results by year/month/week/day as well as other properties at the same time using Linq. The user should be able to switch between grouping by year/month/week/day at runtime. The typical situation is that i have some kind of DTO containing a DateTime timestamp as well as other properties.

UPDATE: I found a working solution. See post below.

For grouping only by date (excluding grouping by other properties), I have created a DateGroupKey class and an extension method:

DateGroupKey class

public class DateGroupKey
{
    public int Year { get; set; }
    public int Month { get; set; }
    public int Week { get; set; }
    public int Day { get; set; }
}

Extension method

public static IEnumerable<IGrouping<DateGroupKey, TValue>> GroupByDate<TValue>(
        this IEnumerable<TValue> source,
        Func<TValue, DateTime> dateSelector,
        DateGroupType dateGroupType)
{
    Func<TValue, DateGroupKey> keySelector = null;
    switch (dateGroupType)
    {
            case DateGroupType.Year:
                keySelector = val => new DateGroupKey { Year = dateSelector(val).Year };
                break;
            case DateGroupType.Month:
                keySelector = val => new DateGroupKey { Year = dateSelector(val).Year, Month = dateSelector(val).Month };
                break;
            case DateGroupType.Week:
                keySelector = val => new DateGroupKey { Year = dateSelector(val).Year, Week = dateSelector(val).GetIso8601WeekOfYear() };
                break;
            case DateGroupType.Day:
                keySelector = val => new DateGroupKey { Year = dateSelector(val).Year, Month = dateSelector(val).Month, Day = dateSelector(val).Day };
                break;
            default:
                throw new NotSupportedException($"Group type not supported: {dateGroupType}");
    }

    return source.GroupBy(keySelector, new DateGroupKeyComparer());
}

where the DateGroupKeyComparer is a class implementing IEqualityComparer<DateGroupKey>. I can use the extension method like this:

var grouped = results.GroupByDate<ProductDto>(x => x.TimeStamp, DateGroupType.Month)

which works as it should. Now, I would like to expand on this idea and be able to group not only by date but also some of the other fields at the same time. Say the ProductDtoclass has the following signature

public class ProductDto
{
    public DateTime TimeStamp {get; set;}
    public int ProductGroup {get; set;}
    public int Variant {get; set;}
    public double Value {get; set;}
}

I would like to do something along the lines of

var grouped = results.GroupByDate<ProductDto>(x => x.TimeStamp, x => x.ProductGroup, x => x.Variant, DateGroupType.Month);

but I can't seem to wrap my head around how to aproach this. I thought about abstracting the DateGroupKey into an IDateGroupKey interface holding the Year, Month, Week and Day properties, and somehow tell the GroupBy extension how to map to this specific implementation, but I am not sure whether it is the right (or simplay 'a') way to go.

Any good ideas of how to aproach this?

推荐答案

So, I found a working solution - I am not quite satisfied but it will do for now.

I have created a generic composite key, holding a DateGroupKey object containing information on the date-grouping as well as a generic object key (to identity grouping fields not part of the date)

public class DateGroupCompositeKey<TKey>
{
    public DateGroupKey DateKey { get; set; }
    public TKey ObjectKey { get; set; }
}

public class DateGroupKey
{
    public int Year { get; set; }
    public int Month { get; set; }
    public int Week { get; set; }
    public int Day { get; set; }
}

Also, I implemented IEqualityComparer for these classes.

public class DateGroupKeyComparer : IEqualityComparer<DateGroupKey>
{
    public bool Equals(DateGroupKey x, DateGroupKey y)
    {
        return x.Year == y.Year &&
            x.Month == y.Month &&
            x.Week == y.Week &&
            x.Day == y.Day;
    }

    public int GetHashCode(DateGroupKey obj)
    {
        unchecked
        {
            var h = 0;
            h = (h * 31) ^ obj.Year;
            h = (h * 31) ^ obj.Month;
            h = (h * 31) ^ obj.Week;
            h = (h * 31) ^ obj.Day;
            return h;
        }
    }
}

public class DateGroupCompositeKeyComparer<TValue> : IEqualityComparer<DateGroupCompositeKey<TValue>>
{
    private readonly IEqualityComparer<TValue> _valueComparer;
    private readonly DateGroupKeyComparer _dgkComparer;

    public DateGroupCompositeKeyComparer(IEqualityComparer<TValue> valueComparer)
    {
        _valueComparer = valueComparer;
        _dgkComparer = new DateGroupKeyComparer();
    }

    public bool Equals(DateGroupCompositeKey<TValue> x, DateGroupCompositeKey<TValue> y)
    {
        return _dgkComparer.Equals(x.DateKey, y.DateKey) &&
            _valueComparer.Equals(x.ObjectKey, y.ObjectKey);
    }

    public int GetHashCode(DateGroupCompositeKey<TValue> obj)
    {
        unchecked
        {
            int h = 0;
            h = (h * 31) ^ _dgkComparer.GetHashCode(obj.DateKey);
            h = (h * 31) ^ _valueComparer.GetHashCode(obj.ObjectKey);
            return h;
        }
    }
}

Now my extension method(s) look like

public static class Extensions
{
    // This was the original extension
    public static IEnumerable<IGrouping<DateGroupKey, TValue>> GroupByDate<TValue>(
        this IEnumerable<TValue> source, 
        Func<TValue, DateTime> dateSelector,
        DateGroupType dateGroupType)
    {
        Func<TValue, DateGroupKey> keySelector = DateKeySelector(dateSelector, dateGroupType);
        return source.GroupBy(keySelector, new DateGroupKeyComparer());
    }

    // New extension method supporting composite grouping
    public static IEnumerable<IGrouping<DateGroupCompositeKey<TKey>, TValue>> GroupByDateComposite<TKey, TValue>(
        this IEnumerable<TValue> source,
        Func<TValue, DateTime> dateSelector,
        Func<TValue, TKey> keySelector,
        IEqualityComparer<TKey> keyComparer,
        DateGroupType dateGroupType)
    {
        var dateKeySelector = DateKeySelector(dateSelector, dateGroupType);

        DateGroupCompositeKey<TKey> func(TValue val) => 
            new DateGroupCompositeKey<TKey>
            {
                DateKey = dateKeySelector(val),
                ObjectKey = keySelector(val)
            };

        return source.GroupBy(func, new DateGroupCompositeKeyComparer<TKey>(keyComparer));
    }

    private static Func<TValue, DateGroupKey> DateKeySelector<TValue>(Func<TValue, DateTime> dateSelector, DateGroupType dateGroupType)
    {
        Func<TValue, DateGroupKey> dateKeySelector = null;
        switch (dateGroupType)
        {
            case DateGroupType.Year:
                dateKeySelector = val => new DateGroupKey { Year = dateSelector(val).Year };
                break;
            case DateGroupType.Month:
                dateKeySelector = val => new DateGroupKey { Year = dateSelector(val).Year, Month = dateSelector(val).Month };
                break;
            case DateGroupType.Week:
                dateKeySelector = val => new DateGroupKey { Year = dateSelector(val).Year, Week = dateSelector(val).GetIso8601WeekOfYear() };
                break;
            case DateGroupType.Day:
                dateKeySelector = val => new DateGroupKey { Year = dateSelector(val).Year, Month = dateSelector(val).Month, Day = dateSelector(val).Day };
                break;
            default:
                throw new NotSupportedException($"Group type not supported: {dateGroupType}");
        }
        return dateKeySelector;
    }

}

The extension can now be used (as in the original post, by grouping only by date)

var grouped = results.GroupBy<ProductDto>(x => x.TimeStamp, DateGroupType.Month);

or in the composite version

var grouped = results.GroupByDateComposite<int, ProductDto>(
    x => x.TimeStamp,
    x => x.ProductGroup,
    EqualityComparer<int>.Default,
    DateGroupType.Month);

The downside, as far as I can tell, is that for more complicated groupings, say

var grouped = results.GroupByDateComposite<CustomKey, ProductDto>(
    x => x.TimeStamp,
    x => new CustomKey{ ProductGroup = x.ProductGroup, Variant = x.Variant},
    new CustomKeyComparer(),
    DateGroupType.Month);

i would need to create seperate key classes and corresponding IEqualityComparer implementations to be able to do the grouping.