C# list - 查找元素的出现并向列表添加位置计数器

C# list - finding the occurrence of elements and adding a position counter to the list

我需要一些关于列表操作难题的帮助。我不确定如何解决这个问题。

我有一个数据列表,我需要在日期分组且金额匹配的每一行中添加一个出现计数器 ID。

例如,在同一天,如果金额相同,则每次相同时增加计数器 ID

原始数据列表

text    | date       | amount
memo 01 | 2022-05-25 | 10
memo 02 | 2022-05-25 | 20
memo 03 | 2022-05-25 | 20
memo 04 | 2022-05-25 | 30
memo 05 | 2022-05-25 | 15
memo 06 | 2022-05-25 | 20
memo 07 | 2022-05-25 | 10
memo 08 | 2022-05-25 | 40
memo 09 | 2022-05-26 | 20
memo 10 | 2022-05-26 | 15
memo 11 | 2022-05-26 | 30
memo 12 | 2022-05-26 | 20

所需的输出(添加了出现次数计数器)

text    | date       | amount | occur
memo 01 | 2022-05-25 |   10   | 1
memo 02 | 2022-05-25 |   20   | 1
memo 03 | 2022-05-25 |   20   | 2
memo 04 | 2022-05-25 |   30   | 1
memo 05 | 2022-05-25 |   15   | 1
memo 06 | 2022-05-25 |   20   | 3
memo 07 | 2022-05-25 |   10   | 2
memo 08 | 2022-05-25 |   40   | 1
memo 09 | 2022-05-26 |   20   | 1
memo 10 | 2022-05-26 |   15   | 1
memo 11 | 2022-05-26 |   30   | 1
memo 12 | 2022-05-26 |   20   | 2

这是我创建数据测试列表的代码

var myList = new List<(string, DateTime, decimal)> 
     {
         ("memo 01",new DateTime(2022, 05, 25),10),
         ("memo 02",new DateTime(2022, 05, 25),20),
         ("memo 03",new DateTime(2022, 05, 25),20),
         ("memo 04",new DateTime(2022, 05, 25),30),
         ("memo 05",new DateTime(2022, 05, 25),15),
         ("memo 06",new DateTime(2022, 05, 25),20),
         ("memo 07",new DateTime(2022, 05, 25),10),
         ("memo 08",new DateTime(2022, 05, 25),40),
         ("memo 09",new DateTime(2022, 05, 26),20),
         ("memo 10",new DateTime(2022, 05, 26),15),
         ("memo 11",new DateTime(2022, 05, 26),30),
         ("memo 12",new DateTime(2022, 05, 26),20)
       };

myList.ForEach(x => Console.WriteLine($"{x.Item1} | {x.Item2.ToString("yyyy-MM-dd")} | {x.Item3}"));

您可以使用词典来计算出现次数:

  ...

  Dictionary<(DateTime, decimal), int> occs = new();

  foreach (var x in myList) {
    if (!occs.TryAdd((x.Item2, x.Item3), 1))
      occs[(x.Item2, x.Item3)] += 1;

    Console.WriteLine($"memo {x.Item1} | {x.Item2.ToString("yyyy-MM-dd")} | {x.Item3} | {occs[(x.Item2, x.Item3)]}");
  }

结果:

memo 01 | 2022-05-25 | 10 | 1
memo 02 | 2022-05-25 | 20 | 1
memo 03 | 2022-05-25 | 20 | 2
memo 04 | 2022-05-25 | 30 | 1
memo 05 | 2022-05-25 | 15 | 1
memo 06 | 2022-05-25 | 20 | 3
memo 07 | 2022-05-25 | 10 | 2
memo 08 | 2022-05-25 | 40 | 1
memo 09 | 2022-05-26 | 20 | 1
memo 10 | 2022-05-26 | 15 | 1
memo 11 | 2022-05-26 | 30 | 1
memo 12 | 2022-05-26 | 20 | 2

有很多方法可以达到你想要的结果:

var tempList = new List<(string, DateTime, decimal, int)>();
myList.ForEach(x => 
{
   x.Item4 = tempList.Where(y => y.Item2.Date == x.Item2.Date && y.Item3 == x.Item3 ).Count() + 1;
   tempList.Add(x);

   Console.WriteLine($"{x.Item1} | {x.Item2.ToString("yyyy-MM-dd")} | {x.Item3} | {x.Item4}");
});

使用词典,但为了更好地理解而进行了扩展。

    var myList = new List<(string text, DateTime date, decimal occurence)>
 {
     ("memo 01",new DateTime(2022, 05, 25),10),
     ("memo 02",new DateTime(2022, 05, 25),20),
     ("memo 03",new DateTime(2022, 05, 25),20),
     ("memo 04",new DateTime(2022, 05, 25),30),
     ("memo 05",new DateTime(2022, 05, 25),15),
     ("memo 06",new DateTime(2022, 05, 25),20),
     ("memo 07",new DateTime(2022, 05, 25),10),
     ("memo 08",new DateTime(2022, 05, 25),40),
     ("memo 09",new DateTime(2022, 05, 26),20),
     ("memo 10",new DateTime(2022, 05, 26),15),
     ("memo 11",new DateTime(2022, 05, 26),30),
     ("memo 12",new DateTime(2022, 05, 26),20)
   };
        //final output list
        var output = new List<(string text, DateTime date, decimal occurence, int count)>();

        //dictionary to store the counts
        var dictionary = new Dictionary<string, int>();
        foreach (var item in myList)
        {
            var key = $"{item.date}_{item.occurence}";
            if (dictionary.TryGetValue(key, out int count))
            {
                //populate the output list
                output.Add((item.text, item.date, item.occurence, count + 1));

                //update dictionary
                dictionary[key] += 1;
            }
            else
            {
                //populate the output list
                output.Add((item.text, item.date, item.occurence, 1));

                //update dictionary
                dictionary[key] = 1;
            }
        }

        output.ForEach(x => Console.WriteLine($"{x.text} | {x.date.ToString("yyyy-MM-dd")} | {x.occurence} | {x.count}"));

这里有一点object-oriented方法。

public class Transactions : IEnumerable<KeyValuePair<Transaction, int>>
{
    private readonly Dictionary<Transaction, int> _values = new();
    public void Add(Transaction transaction)
    {
        if (_values.ContainsKey(transaction))
        {
            _values[transaction] += 1;
        }
        else
        {
            _values.Add(transaction, 1);
        }
    }
    public IEnumerator<KeyValuePair<Transaction, int>> GetEnumerator()
    {
        return _values.GetEnumerator();
    }
    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}
public record Transaction(DateTime Date, decimal Amount);
//Program.cs
var myList = new List<Transaction>
{
    new(new DateTime(2022, 05, 25), 10),
    new(new DateTime(2022, 05, 25), 20),
    new(new DateTime(2022, 05, 25), 20),
    new(new DateTime(2022, 05, 25), 30),
    new(new DateTime(2022, 05, 25), 15),
    new(new DateTime(2022, 05, 25), 20),
    new(new DateTime(2022, 05, 25), 10),
    new(new DateTime(2022, 05, 25), 40),
    new(new DateTime(2022, 05, 26), 20),
    new(new DateTime(2022, 05, 26), 15),
    new(new DateTime(2022, 05, 26), 30),
    new(new DateTime(2022, 05, 26), 20)
};
var output = new Transactions();
myList.ForEach(item => output.Add(item));
foreach (var keyValuePair in output)
{
    Console.WriteLine(keyValuePair);
}

说明:我们这里有一条记录,可以自动比较值,然后我们创建一个自定义集合来处理新的插入。

这是我的更新 LINQ 唯一方法。

它使用 Aggregate 运算符来相当清楚地计算结果。它使用低效的 Count 运算符,但逻辑尽可能简单,因此对于短列表来说,这是一种非常干净的方法。

var output =
    list
        .Aggregate(
            new
            {
                counter = new List<(DateTime date, decimal amount)>(),
                output = new List<(string text, DateTime date, decimal amount, int occur)>()
            },
            (a, x) =>
            {
                a.counter.Add((x.date, x.amount));
                a.output.Add((x.text, x.date, x.amount, a.counter.Count(y => y == (x.date, x.amount))));
                return new { a.counter, a.output };
            },
            a => a.output);

从这个数据开始:

var list = new List<(string text, DateTime date, decimal amount)>
{
    ("memo 01",new DateTime(2022, 05, 25),10),
    ("memo 02",new DateTime(2022, 05, 25),20),
    ("memo 03",new DateTime(2022, 05, 25),20),
    ("memo 04",new DateTime(2022, 05, 25),30),
    ("memo 05",new DateTime(2022, 05, 25),15),
    ("memo 06",new DateTime(2022, 05, 25),20),
    ("memo 07",new DateTime(2022, 05, 25),10),
    ("memo 08",new DateTime(2022, 05, 25),40),
    ("memo 09",new DateTime(2022, 05, 26),20),
    ("memo 10",new DateTime(2022, 05, 26),15),
    ("memo 11",new DateTime(2022, 05, 26),30),
    ("memo 12",new DateTime(2022, 05, 26),20)
};

我得到这个结果:

经过进一步考虑,许多答案在尝试使用字典有效地跟踪计数器时需要丑陋的 if 语句。这是一种使代码更简单的方法。

从这个封装丑陋的扩展方法开始if:

public static class Ex
{
    public static int AddOrIncrement<T>(this Dictionary<T, int> source, T value)
    {
        if (source.ContainsKey(value))
            source[value] += 1;
        else
            source[value] = 1;
        return source[value];
    }
}

现在最后的代码很简单:

var output = new List<(string text, DateTime date, decimal amount, int occur)>();
var counter = new Dictionary<(DateTime date, decimal amount), int>();
foreach (var item in list)
{
    output.Add((item.text, item.date, item.amount, counter.AddOrIncrement((item.date, item.amount))));
}

这给出了正确的输出,与我的其他答案相同。


一分钱,一英镑...

所以,我上面的回答暴露了操作状态(counter 字典)。它没有很好地封装。我们也可以解决这个问题。

添加此方法:

IEnumerable<(string text, DateTime date, decimal amount, int occur)> Manipulate(IEnumerable<(string text, DateTime date, decimal amount)> source)
{
    var counter = new Dictionary<(DateTime date, decimal amount), int>();
    foreach (var item in source)
        yield return (item.text, item.date, item.amount, counter.AddOrIncrement((item.date, item.amount)));
}

现在可以在本地或作为 class 的一部分完成。

现在需要的代码是这样的:

var output = Manipulate(list).ToList();

与之前相同的结果。


让我们更进一步。

上面的Manipulate方法有点乱。让我们把它变成一个扩展方法。

这里是:

public static IEnumerable<R> Manipulate<T, K, R>(this IEnumerable<T> source, Func<T, K> key, Func<T, int, R> output)
{
    var counter = new Dictionary<K, int>();
    foreach (var item in source)
        yield return output(item, counter.AddOrIncrement(key(item)));
}

现在我认为我拥有最干净、最简单、最高效的输出:

var output =
    list
        .Manipulate(
            x => (x.date, x.amount),
            (x, n) => (x.text, x.date, x.amount, n))
        .ToList();

与之前相同的结果。


现在,Manipulate 作为一种扩展方法很不错,但不如 general-purpose 好。它的名字并没有告诉我们它的作用。让我们让它更有用一些,也让我们称它为 CountByKey 并让它这样做:

public static IEnumerable<(T item, int count)> CountByKey<T, K>(this IEnumerable<T> source, Func<T, K> key)
{
    var counter = new Dictionary<K, int>();
    foreach (var item in source)
        yield return (item, counter.AddOrIncrement(key(item)));
}

现在这终于用一个简单的通用方法确定了问题的意图,我们又回到了在 LINQ 查询中使用它:

var output =
    list
        .CountByKey(x => (x.date, x.amount))
        .Select(x => (x.item.text, x.item.date, x.item.amount, x.count))
        .ToList();

没有比这更简单,也更易读的了。


而且,当然,我们可以弹出 Manipulate 的原始签名,但名称为 CountByKey:

public static IEnumerable<R> CountByKey<T, K, R>(this IEnumerable<T> source, Func<T, K> key, Func<T, int, R> output)
    => source.CountByKey(key).Select(x => output(x.item, x.count));

最后我们得到这个:

var output =
    list
        .CountByKey(
            x => (x.date, x.amount),
            (x, n) => (x.text, x.date, x.amount, n))
        .ToList();

一个很好的清晰重载。