Linq通用GroupBy同时显示日期和其他字段

时间:2018-07-05 13:03:05

标签: c# linq

我希望能够使用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);

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

有什么好的方法可以解决这个问题吗?

2 个答案:

答案 0 :(得分:0)

我会说你需要做这样的事情:

我的模型班

public class MyClass
{
    public MyClass(DateTime date, string name, int partNumber)
    {
        Date = date;
        Name = name;
        PartNumber = partNumber;
    }
    public DateTime Date { get;}

    public string Name { get;  }

    public int PartNumber {get;}
}

扩展方法以获取星期数:

public static class DateTimeExtensions
{
    public static int WeekNumber(this DateTime date)
    {
        var cal = new GregorianCalendar();
        var week = cal.GetWeekOfYear(date, CalendarWeekRule.FirstDay, DayOfWeek.Sunday);
        return week;
    }
}

然后分组。

            var dataBase = new[]{
            new MyClass(new DateTime(2018,1,1),"Jane",10),
            new MyClass(new DateTime(2017,1,1),"Jane",1),
            new MyClass(new DateTime(2016,1,1),"Jane",10),
            new MyClass(new DateTime(2018,2,1),"Jane",1),
            new MyClass(new DateTime(2018,2,2),"Jane",10),
            new MyClass(new DateTime(2017,2,2),"Jane",1),
            new MyClass(new DateTime(2016,2,2),"Jane",10),
            new MyClass(new DateTime(2018,2,8),"Jane",1),
            new MyClass(new DateTime(2017,2,8),"Jane",10),
            new MyClass(new DateTime(2016,2,8),"Jane",1),
            new MyClass(new DateTime(2018,3,1),"Jane",10),
            new MyClass(new DateTime(2017,3,1),"Jane",1),
            new MyClass(new DateTime(2016,3,1),"Jane",10),
        };

        var myCollection = new MyClasses(dataBase);

                    while (true)
        {
            Console.WriteLine("Group By What");
            var type = Console.ReadLine();

            var enumType = (MyClasses.GroupType) Enum.Parse(typeof(MyClasses.GroupType), $"GroupBy{type.UppercaseFirst()}");

            foreach (var g in myCollection.GetGroupedValue(enumType))
            {
                Console.WriteLine(g.Key);
            }
        }
    }

您可以创建自己的收藏集,以完成您想要的事情。看起来可能像这样:

public class MyClasses : Collection<MyClass>
    {
        public enum GroupType
        {
            GroupByYear = 0,
            GroupByMonth = 1,
            GroupByWeek = 2
        }
        public MyClasses(IEnumerable<MyClass> classes)
        {
            foreach (var myClass in classes)
            {
                Add(myClass);
            }
        }

        public IEnumerable<IGrouping<object, MyClass>> GroupByYear => this.GroupBy(x => x.Date.Year);

        public IEnumerable<IGrouping<object, MyClass>> GroupByMonth => this.GroupBy(x => new { x.Date.Year, x.Date.Month});

        public IEnumerable<IGrouping<object, MyClass>> GroupByWeek => this.GroupBy(x => new { x.Date.Year, x.Date.WeekNumber()});

        public IEnumerable<IGrouping<object, MyClass>> GetGroupedValue(GroupType groupType)
        {
            var propertyName = groupType.ToString();
            var property = GetType().GetProperty(propertyName);
            return property?.GetValue(this) as IEnumerable<IGrouping<object, MyClass>>;
        }
    }

这将按照您要求的日期分组。

更新:

答案 1 :(得分:0)

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

我创建了一个通用组合密钥,其中包含一个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实现来进行分组。