如何根据键值组将字典拆分为多个字典?

Posted

技术标签:

【中文标题】如何根据键值组将字典拆分为多个字典?【英文标题】:How can I split a Dictionary into multiple dictionaries based on groups of key values? 【发布时间】:2021-04-15 19:26:42 【问题描述】:

我有一本字典 Dictionary<string, string>,其中包含一组 KeyValuePairs KVP。 出于此说明的目的,这些信息存储在字典中_test 这些打算用于使用 command.Parameters.AddWithValue(value.key, value.Value) 的 SQL INSERT 但是,这会将所有值插入到 SQL 的一行中,其中预期的结果是多行。_test 中的示例源;

[0] [Data, ONCE,UPON,A,TIME,UP,AN,ENORMOUS,GREEN,BEANSTALK,IN,THE,CLOUDS]
[1] [Ident, 123456789]
[2] [Time1, 2020-01-01T13:56:56.123]
[3] [Time2, 2020-01-01T13:58:02.356]
[4] [D, 57]
[5] [D1, 508,967,123,456,234,456,232,124,167,198,985,786]
[6] [AC, 1,1,1,1,2,2,2,4,3,3,1,1]

我希望将这些值根据出现在键 AC 下的值分组拆分为多个字典,因此输出将是; 然后我可以使用foreach 对每个字典进行交互以执行每个插入操作。

dict-1(包含基于 AC 为 1 的前 4 个值)

dictionary<string,string>

[0] [Data, ONCE,UPON,A,TIME]
[1] [Ident, 123456789]
[2] [Time1, 2020-01-01T13:56:56.123]
[3] [Time2, 2020-01-01T13:58:02.356]
[4] [D, 57]
[5] [D1, 508,967,123,456]
[6] [AC, 1,1,1,1]

dict-2(包含基于 AC 值为 2 的下 3 个值)

dictionary<string,string>

[0] [Data, UP,AN,ENORMOUS]
[1] [Ident, 123456789]
[2] [Time1, 2020-01-01T13:56:56.123]
[3] [Time2, 2020-01-01T13:58:02.356]
[4] [D, 57]
[5] [D1, 234,456,232]
[6] [AC, 2,2,2]

dict-3(包含基于 AC 4 的单个值)

dictionary<string,string>

[0] [Data, GREEN]
[1] [Ident, 123456789]
[2] [Time1, 2020-01-01T13:56:56.123]
[3] [Time2, 2020-01-01T13:58:02.356]
[4] [D, 57]
[5] [D1, 124]
[6] [AC, 4]

dict-4

dictionary<string,string>

[0] [Data, BEANSTALK,IN]
[1] [Ident, 123456789]
[2] [Time1, 2020-01-01T13:56:56.123]
[3] [Time2, 2020-01-01T13:58:02.356]
[4] [D, 57]
[5] [D1, 167,198]
[6] [AC, 3,3]

dict-5

dictionary<string,string>

[0] [Data, THE,CLOUDS]
[1] [Ident, 123456789]
[2] [Time1, 2020-01-01T13:56:56.123]
[3] [Time2, 2020-01-01T13:58:02.356]
[4] [D, 57]
[5] [D1, 985,786]
[6] [AC, 1,1]

他们是使用 Linq 方法或查询语法来做这件事的好方法吗?

【问题讨论】:

你有什么尝试吗?请向我们展示尝试某事的努力,否则看起来您向我们提出了您的任务要求。 这些打算用于 SQL INSERT 使用 - 不知道您的应用程序的上下文,但我希望有更好的方法将数据插入数据库然后手动使用VARCHAR 类型的所有参数构建 SQL 查询。表中的所有列都是VARCHAR 类型的吗? 您能解释一下您的初始字典是如何初始化的吗? var _test = new Dictionary&lt;string, string&gt; "Data", "ONCE,UPON,A,TIME,UP,AN,ENORMOUS,GREEN,BEANSTALK,IN,THE,CLOUDS", "Ident", "123456789", "Time1", "2020-01-01T13:56:56.123" "Time2", "2020-01-01T13:58:02.356" "D", "57" "D1", "508,967,123,456,234,456,232,124,167,198,985,786" "AC", "1,1,1,1,2,2,2,4,3,3,1,1" ; @Fabio CREATE TABLE [dbo].[Test]( [Data] [nvarchar](max) NULL, [Ident] [int] NULL, [Time1] [datetime2](7) NULL, [Time2] [datetime2](7) NULL, [D] [int] NULL, [D1] [nvarchar](max) NULL, [AC] [nvarchar](max) NULL, ) ON [PRIMARY] @Ivan Khorin public static Dictionary _test = new Dictionary();然后使用 stringbuilder 构建列和参数列表以构建 sql insert 语句 INSERT INTO test (xxx,xxx) VALUES (@xxx, @xxxx) 值通过在 _test 字典上使用 foreach 循环拉取 Key 和 Value 然后添加使用 sqlCommand.Parameters.AddWithValue(v.Key, v.Value) 值通过使用 _test.Add(xxxxxx) 添加到字典中 【参考方案1】:
using System;
using System.Collections.Generic;
using System.Linq;

var _test = new Dictionary<string, string>

     "Data", "ONCE,UPON,A,TIME,UP,AN,ENORMOUS,GREEN,BEANSTALK,IN,THE,CLOUDS",
     "Ident", "123456789",
     "Time1", "2020-01-01T13:56:56.123",
     "Time2", "2020-01-01T13:58:02.356",
     "D", "57",
     "D1", "508,967,123,456,234,456,232,124,167,198,985,786",
     "AC", "1,1,1,1,2,2,2,4,3,3,1,1",
;

var testWithLists = _test
    .ToDictionary(
        x => x.Key,
        y => y.Value.Split(',').ToList()
    );

var prevAC = string.Empty;
var result = new List<Dictionary<string, string>>();

var resultPart = new Dictionary<string, List<string>>();

for (int i = 0; i < testWithLists["AC"].Count; i++)

    var curAC = testWithLists["AC"][i];

    if ((curAC != prevAC && i != 0) || i == (testWithLists["AC"].Count - 1))
        result.Add(resultPart.ToDictionary(x => x.Key, y => string.Join(',', y.Value)));

    if (curAC != prevAC)
    
        resultPart = new Dictionary<string, List<string>>
        
             "Data", new List<string>() ,
             "Ident", testWithLists["Ident"] ,
             "Time1", testWithLists["Time1"] ,
             "Time2", testWithLists["Time2"] ,
             "D", testWithLists["D"] ,
             "D1", new List<string>() ,
             "AC", new List<string>() ,
        ;
    

    resultPart["Data"].Add(testWithLists["Data"][i]);
    resultPart["D1"].Add(testWithLists["D1"][i]);
    resultPart["AC"].Add(testWithLists["AC"][i]);   

    prevAC = curAC;

没有检查数据、d1、ac 长度是否相等,但它的工作方式与您预期的一样。 result 变量是 5 个字典的列表。

附:新的 C# 9.0 record 类型在这里可能很有用。

P.S.P.S.抱歉,解决方案不理想:)

【讨论】:

初步检查上述解决方案工作正常。由于编译错误,我不得不对string.Join(',' 进行一项更改,并将其从char 更改为带有双引号的字符串,但在更改后一切都很好。明天我会进一步查看,但希望感谢所有做出贡献的人,并感谢您 Ivan 提供上述解决方案 我使用 .NET 5.0,所以您的 .NET 版本可能没有以 char 作为参数的 Join() 重载。希望你可以使用我的代码。祝你好运!【参考方案2】:

使用我已经使用的一些扩展方法来处理IEnumerables,包括用于运行的 group by 运算符、APL Scan 运算符以及此处看到的一些基本实用方法:

public static class IEnumerableExt 
    // returns IEnumerables of runs
    public static IEnumerable<IEnumerable<T>> Runs<T>(this IEnumerable<T> items) 
        var cmp = EqualityComparer<T>.Default;
        using (var itemsEnum = items.GetEnumerator()) 
            bool notAtEnd;

            IEnumerable<T> NextRun() 
                T curItem;
                do 
                    curItem = itemsEnum.Current;
                    yield return curItem;
                    notAtEnd = itemsEnum.MoveNext();
                 while (notAtEnd && cmp.Equals(itemsEnum.Current, curItem));
            

            notAtEnd = itemsEnum.MoveNext();
            while (notAtEnd)
                yield return NextRun().ToList();
        
    

    // APL Scan operator (like Aggregate, but returns intermediate results)
    // T combineFn(T PrevResult, T CurItem)
    // First PrevResult = items.First()
    // First CurItem = items.Skip(1).First()
    // output is items.First(), combineFn(PrevResult, CurItem), ...
    public static IEnumerable<T> Scan<T>(this IEnumerable<T> items, Func<T, T, T> combineFn) 
        using (var itemsEnum = items.GetEnumerator()) 
            if (itemsEnum.MoveNext()) 
                T prevResult = itemsEnum.Current;

                for (; ; ) 
                    yield return prevResult;
                    if (!itemsEnum.MoveNext())
                        yield break;
                    prevResult = combineFn(prevResult, itemsEnum.Current);
                
            
        
    

    // prepend a sequence with a single element
    public static IEnumerable<T> FollowedBy<T>(this T first, IEnumerable<T> rest) => first.AsSingleton().Concat(rest);
    // Join strings together with a separator
    public static string Join(this IEnumerable<string> s, string sep) => String.Join(sep, s);

    // convert an element into a single element IEnumerable
    public static IEnumerable<T> AsSingleton<T>(this T item) => new[]  item ;

您可以创建要拆分的每个组合值的起始位置序列,然后创建表示要为每个可拆分键值收集的值的范围序列。

我使用了一个变量来指定可拆分的键名,以防它们将来发生变化,并且只是复制了所有不可拆分的键名/值对。

var splitKeyNames = new[]  "Data", "D1", "AC" ;
var splitValues = splitKeyNames.ToDictionary(n => n, n => _test[n].Split(','));

var starts = splitValues["AC"].Runs()
                .Select(run => run.Count())
                .Scan((acc, runCt) => acc + runCt);
var ranges = 0.FollowedBy(starts).Zip(starts, (start, end) => start .. end);

var ans = ranges.Select(range => _test.Keys.Where(key => !splitKeyNames.Contains(key))
                                           .Select(key => new  key, value = _test[key] )
                                           .Concat(
                                                splitKeyNames.Select(key => new 
                                                                                key,
                                                                                value = splitValues[key][range].Join(",")
                                                                            ))
                                           .ToDictionary(kv => kv.key, kv => kv.value)
                );

【讨论】:

以上是关于如何根据键值组将字典拆分为多个字典?的主要内容,如果未能解决你的问题,请参考以下文章

Python列表元素为字典时,如何根据其中某个相同的键值进行元素合并

将列表转换为字典,然后通过键值将多个字典合并为一个字典

如何根据字典中的键值逐行向熊猫数据框添加值?

如果它们共享任何键值对,如何合并来自不同列表的多个字典?

python根据字典自动生成一组省和市名

iOS:如何在一个数组中的多个字典中分隔键值?