【问题标题】:How to find out of order elements using LINQ in c#如何在 C# 中使用 LINQ 找出乱序元素
【发布时间】:2019-04-27 03:42:52
【问题描述】:

我在 c# 中有一个列表,其中包含两个日期时间字段:DatePaid 和 DateEntered。我想找到乱序元素。这很容易,老派。按 DatePaid 对列表进行排序,然后对列表执行 foreach,将 DateEntered 捕获到局部变量 (lastDateEntered) 中。每次通过循环,我们首先比较 DateEntered 和 lastDateEntered。如果 DateEntered 小于 lastDateEntered,则前一行将是乱序行。

| DatePaid | DateEntered | comments     |
|----------|-------------|--------------|
| 1/1/2019 | 1/1/2019    |              |
| 2/1/2019 | 2/2/2019    |              |
| 3/1/2019 | 3/1/2019    |              |
| 4/1/2019 | 5/2/2019    | out of order |
| 5/1/2019 | 5/1/2019    |              |

在 SQL 中(如果您不关心 SQL,请忽略本段),创建两个 CTE's(类似于子查询)很容易:一个按 DatePaid 排序,另一个按 DateEntered 排序。我们使用 Row_Number() 函数添加一个对 CTE 进行排序的额外字段。然后我们加入每个 CTE 的行号,然后只选择从一个 CTE 到另一个 CTE 日期不相等的行。类似于Oracle 12c - sql to find out of order rows,尽管答案没有使用 CTE。我想我可以通过 linq 做类似的事情来混淆我的方式,但我不确定它是否比 foreach 循环方法更容易。

有没有更好的临栖方式?

【问题讨论】:

  • 我不明白这个例子。 2019 年 2 月 2 日进场,2019 年 2 月 1 日付款怎么办?无论使用什么日期格式,都是在付款后进入...
  • 您是否需要确定哪个项目出现故障?或者只是列表不按顺序排列?
  • 为什么要对记录进行排序才能找到乱序的记录?通过对它们进行排序,您可以纠正它们的错误排序。我也不清楚列表中的日期是什么。格式是日-月-年还是月-日-年?
  • @TheodorZoulias 根据乱序评论,它必须是 M/D/Y。
  • 顺便说一句,LINQ to Objects 等效于RowNumber 是两个参数Selectlist.Select((item,rowNumber) => ...)

标签: c# linq


【解决方案1】:

使用基于 APL 扫描运算符的 LINQ 扩展(如Aggregate,仅返回中间结果),将列表的 prev 和 cur 元素组合起来以获得新值,解决方案很简单.

一、扩展方法:

// TRes combineFn(T prevValue, T curValue)
public static IEnumerable<TRes> ScanByPairs<T, TRes>(this IEnumerable<T> src, Func<T, T, TRes> combineFn) {
    using (var srce = src.GetEnumerator())
        if (srce.MoveNext()) {
            var prev = srce.Current;

            while (srce.MoveNext())
                yield return combineFn(prev, prev = srce.Current);
        }
}

现在您可以测试每个日期字段:

var ansdp = list.ScanByPairs((prev, cur) => new { OrderNotOkay = prev.DatePaid >= cur.DatePaid, prev })
              .Where(op => op.OrderNotOkay)
              .Select(op => op.prev)
              .ToList();

var ansde = list.ScanByPairs((prev, cur) => new { OrderNotOkay = prev.DateEntered >= cur.DateEntered, prev })
              .Where(op => op.OrderNotOkay)
              .Select(op => op.prev)
              .ToList();

(这给了我写WhereByPairs(及其许多同伴)的明显想法。)

如果你不想使用扩展方法,你可以使用 LINQ Zip 方法模拟同样的事情:

var ansde2 = list.Zip(list.Skip(1), (prev, cur) => new { OrderNotOkay = prev.DateEntered >= cur.DateEntered, prev })
                 .Where(op => op.OrderNotOkay)
                 .Select(op => op.prev)
                 .ToList();

当然,您可以将订单测试封装在扩展方法中:

public static class ListDateExt {
    public static IEnumerable<T> OutOfOrder<T, TField>(this IEnumerable<T> src, Func<T,TField> selectorFn, Comparer<TField> cmp = null) {
        cmp = cmp ?? Comparer<TField>.Default;
        return src.ScanByPairs((prev, cur) => new { OrderNotOkay = cmp.Compare(selectorFn(prev), selectorFn(cur)) >= 0, prev })
                  .Where(op => op.OrderNotOkay)
                  .Select(op => op.prev);
    }
}

然后您可以使用以下方法测试字段:

var ansdp = list.OutOfOrder(l => l.DatePaid).ToList();
var ansde = list.OutOfOrder(l => l.DateEntered).ToList();

【讨论】:

  • 您的ScanByPairs 扩展方法看起来与MoreLinqPairwise 方法非常相似。 ?
  • 我发现 MoreLinq(如 LINQ 和 C#)不像我一直希望的那样正交/通用(例如,为什么 Append 不将 params T[] 作为最后一个参数?为什么不GenerateByIndex 有一个带有最终值/计数的版本吗?(GenerateByIndex().Take() 对我来说似乎过于迟钝/冗长。)
  • 不过,它非常适合窃取源代码。 ? 你可以查看Atif 的实现here。它几乎与您的相同,只是返回嵌套函数的结果而不是直接产生。我不知道他为什么要这样做。
  • 顺便说一句 Append 使用 T[] 将与使用内置 Concat 相同。也许这就是他忽略它的原因。
  • @TheodorZoulias 查看代码,我发现它和我的一样。它们返回本地函数的模式允许在调用方法时出现无效参数的立即异常,而不是在流的第一次迭代时。 Append&lt;T&gt;(IEnumerable&lt;T&gt; head, params T[] tail) =&gt; head.Concat(tail) 的意义在于,你可以叫它list.Append(1,2,3)
【解决方案2】:

你可以试试这样的:

// your items
var items = new Item[]
{
    new Item(DateTime.Parse("2019-01-01"), DateTime.Parse("2019-01-01")),
    new Item(DateTime.Parse("2019-02-01"), DateTime.Parse("2019-02-02")),
    new Item(DateTime.Parse("2019-03-01"), DateTime.Parse("2019-03-01")),
    new Item(DateTime.Parse("2019-04-01"), DateTime.Parse("2019-05-02")),
    new Item(DateTime.Parse("2019-05-01"), DateTime.Parse("2019-05-01")),
    new Item(DateTime.Parse("2019-05-01"), DateTime.Parse("2019-05-01")),
    new Item(DateTime.Parse("2019-05-01"), DateTime.Parse("2019-05-01")),
    new Item(DateTime.Parse("2019-05-01"), DateTime.Parse("2019-05-01")),
    new Item(DateTime.Parse("2019-05-01"), DateTime.Parse("2019-05-01")),
};

// order the elements by DatePaid descending
// the aggregate accumulator will contain the last DateEntered
// and the list of elements out of order
var outOfOrder = items.OrderByDescending(i => i.DatePaid)
    .Aggregate(
        new { LastDate = DateTime.MaxValue, Accumulator = new Item[0] },
        (a, i) => new { LastDate = i.DateEntered, Accumulator = i.DateEntered <= a.LastDate ? a.Accumulator : a.Accumulator.Concat(new[] { i }).ToArray() });

它不是很优雅,但它显示了一种实现方式,您可能可以将其重构为更具可读性。

否则您可以对数组进行排序并执行以下操作:

// we have to materialize the array, so we can use it in the Select
var ordered = items.OrderByDescending(i => i.DatePaid).ToArray();
var outOfOrder = ordered
    .Select((item, index) => new { OutOfOrder = index > 0 && item.DateEntered > ordered[index - 1].DateEntered, Item = item })
    .Where(i => i.OutOfOrder)
    .Select(i => i.Item);

或者类似的东西:

var ordered = items.OrderByDescending(i => i.DatePaid);
var skipOrdered = ordered.Skip(1).Concat(new[] { new Item(DateTime.MinValue, DateTime.MinValue) });
var outOfOrder = ordered.Zip(skipOrdered, (old, curr) => new { old, curr })
    .Where(i => i.old.DateEntered < i.curr.DatePaid)
    .Select(i => i.curr);

但我不确定在性能方面是否比 foreach 循环好得多。

【讨论】:

    猜你喜欢
    • 2019-08-20
    • 2014-03-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-07-22
    • 2020-07-12
    相关资源
    最近更新 更多