Dictionary to DataTable

后端 未结 2 1399
[愿得一人]
[愿得一人] 2021-01-14 08:56

I have a very strange issue, and no clue which way I should take to fix it.

I have an IEnumerable> and it can cont

2条回答
  •  猫巷女王i
    2021-01-14 09:18

    Linq is a good candidate for this job. I still think you should rethink about design, this is such a horrible thing to do. This should do (and without any hard coding):

    var child1 = new List>
    {
        new Dictionary { { "ChildName", "John" }, { "ChildAge", 10 } }
    };
    
    var child2 = new List>
    {
        new Dictionary { { "ChildName", "Tony" }, { "ChildAge", 12 } }
    };
    
    var parent = new List>
    {
        new Dictionary 
        { 
            { "Name", "Mike" },
            { "LastName", "Tyson" },
            { "child1", child1 },
            { "child2", child2 }
        },
        new Dictionary
        {
            { "Name", "Lykke" },
            { "LastName", "Li" },
            { "child1", child1 },
        },
        new Dictionary
        { 
            { "Name", "Mike" },
            { "LastName", "Oldfield" }
        }
    };
    
    CreateTable(parent);
    
    static DataTable CreateTable(IEnumerable> parents)
    {
        var table = new DataTable();
    
        foreach (var parent in parents)
        {
            var children = parent.Values
                                 .OfType>>()
                                 .ToArray();
    
            var length = children.Any() ? children.Length : 1;
    
            var parentEntries = parent.Where(x => x.Value is string)
                                      .Repeat(length)
                                      .ToLookup(x => x.Key, x => x.Value);
            var childEntries = children.SelectMany(x => x.First())
                                       .ToLookup(x => x.Key, x => x.Value);
    
            var allEntries = parentEntries.Concat(childEntries)
                                          .ToDictionary(x => x.Key, x => x.ToArray());
    
            var headers = allEntries.Select(x => x.Key)
                                    .Except(table.Columns
                                                 .Cast()
                                                 .Select(x => x.ColumnName))
                                    .Select(x => new DataColumn(x))
                                    .ToArray();
            table.Columns.AddRange(headers);
    
            var addedRows = new int[length];
            for (int i = 0; i < length; i++)
                addedRows[i] = table.Rows.IndexOf(table.Rows.Add());
    
            foreach (DataColumn col in table.Columns)
            {
                object[] columnRows;
                if (!allEntries.TryGetValue(col.ColumnName, out columnRows))
                    continue;
    
                for (int i = 0; i < addedRows.Length; i++)
                    table.Rows[addedRows[i]][col] = columnRows[i];
            }
        }
    
        return table;
    }
    

    This is one extension method I've used:

    public static IEnumerable Repeat(this IEnumerable source, int times)
    {
        source = source.ToArray();
        return Enumerable.Range(0, times).SelectMany(_ => source);
    }
    

    You can create the addedRows variable in a more idiomatic fashion (which I prefer) but may be that's little less readable for others. In a single line, like this:

    var addedRows = Enumerable.Range(0, length)
                              .Select(x => new
                              {
                                  relativeIndex = x,
                                  actualIndex = table.Rows.IndexOf(table.Rows.Add())
                              })
                              .ToArray();
    

    The tricky part here is to get the pivoting right. No big deal in our case since we can utilize indexers. Do test with a set of examples and let me know if this is buggy..


    One another way of doing it is to precalculate the headers (data table columns before the loop) as it's not going to change anyway. But that also means one extra round of enumeration. As to which is more efficient, you will have to test it.. I find the first one more elegant though.

    static DataTable CreateTable(IEnumerable> parents)
    {
        var table = new DataTable();
    
        //excuse the meaningless variable names
    
        var c = parents.FirstOrDefault(x => x.Values
                                             .OfType>>()
                                             .Any());
        var p = c ?? parents.FirstOrDefault();
        if (p == null)
            return table;
    
        var headers = p.Where(x => x.Value is string)
                       .Select(x => x.Key)
                       .Concat(c == null ? 
                               Enumerable.Empty() : 
                               c.Values
                                .OfType>>()
                                .First()
                                .SelectMany(x => x.Keys))
                       .Select(x => new DataColumn(x))
                       .ToArray();
        table.Columns.AddRange(headers);
    
        foreach (var parent in parents)
        {
            var children = parent.Values
                                 .OfType>>()
                                 .ToArray();
    
            var length = children.Any() ? children.Length : 1;
    
            var parentEntries = parent.Where(x => x.Value is string)
                                      .Repeat(length)
                                      .ToLookup(x => x.Key, x => x.Value);
            var childEntries = children.SelectMany(x => x.First())
                                       .ToLookup(x => x.Key, x => x.Value);
    
            var allEntries = parentEntries.Concat(childEntries)
                                          .ToDictionary(x => x.Key, x => x.ToArray());
    
            var addedRows = Enumerable.Range(0, length)
                                      .Select(x => new
                                      {
                                          relativeIndex = x,
                                          actualIndex = table.Rows.IndexOf(table.Rows.Add())
                                      })
                                      .ToArray();
    
            foreach (DataColumn col in table.Columns)
            {
                object[] columnRows;
                if (!allEntries.TryGetValue(col.ColumnName, out columnRows))
                    continue;
    
                foreach (var row in addedRows)
                    table.Rows[row.actualIndex][col] = columnRows[row.relativeIndex];
            }
        }
    
        return table;
    }
    

提交回复
热议问题