【问题标题】:Linq generic GroupBy date and other fields at the same timeLinq通用GroupBy日期和其他字段同时
【发布时间】:2018-07-05 13:03:05
【问题描述】:

我希望能够使用 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&lt;DateGroupKey&gt; 的类。我可以像这样使用扩展方法:

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

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

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') 方法。

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

【问题讨论】:

  • 这样的东西是否不符合您的需求(伪代码):results.GroupBy(x =&gt; x.Date).ThenBy(x =&gt; x.ProductGroup).ThenBy(x =&gt; x.Variant) 看看documentation
  • 对我来说,这看起来像纯粹的XY problem
  • 这与订购有关 - 不是分组?还是我错过了什么:)

标签: c# linq


【解决方案1】:

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

我的模型类

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>>;
        }
    }

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

更新:

【讨论】:

  • 感谢您的输入 - 但是,这将按天分组。我正在寻找的是在运行时切换“分组模式”的可能性,即从按月分组到按周分组(因此是 DateGroupType 枚举)。很抱歉,如果原始帖子中的内容不清楚。
  • 所以只是为了确保我理解您的意思是您希望用户能够在日月年周之间切换?
  • 是的,正是:)
  • 这里有多个问题。首先,如果时间跨度是几年,分组将是错误的。例如,2018 年 1 月将与 2017 年 1 月组合在一起,依此类推。其次,每个可扩展性非常差的 Dto 类型都需要一个集合实现。对此的解决方案可能是让所有 Dto 实现一个包含 DateTime 属性的“ITimestamped”接口,然后实现一个 Collection,但是这会给我留下原来的问题,即不仅按日期分组,还分组其他属性不同的 Dto。
  • 我目前正在用“复合分组密钥”之类的东西摆弄一个可能的解决方案 - 无论我成功与否,我都会回复:)
【解决方案2】:

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

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

【讨论】:

    猜你喜欢
    • 2012-01-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-04-12
    • 2020-09-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多