From f1f66ccccba487d1560089fef91e2facc0d1f1bf Mon Sep 17 00:00:00 2001 From: jgilbert2017 Date: Fri, 12 Nov 2021 11:01:20 -0500 Subject: [PATCH] #518 Allow user defined ReadHeader method to control DataTable column creation and header parsing. Example use would be to combine multiple header rows. --- .../ExcelDataReaderExtensions.cs | 48 +++++++++---- .../ExcelDataTableConfiguration.cs | 11 ++- .../ExcelOpenXmlReaderTest.cs | 64 ++++++++++++++++++ test/Resources/Test_git_issue_518.xlsx | Bin 0 -> 8307 bytes 4 files changed, 107 insertions(+), 16 deletions(-) create mode 100644 test/Resources/Test_git_issue_518.xlsx diff --git a/src/ExcelDataReader.DataSet/ExcelDataReaderExtensions.cs b/src/ExcelDataReader.DataSet/ExcelDataReaderExtensions.cs index 9ac5b187..7dbfaa25 100644 --- a/src/ExcelDataReader.DataSet/ExcelDataReaderExtensions.cs +++ b/src/ExcelDataReader.DataSet/ExcelDataReaderExtensions.cs @@ -82,27 +82,45 @@ private static DataTable AsDataTable(IExcelDataReader self, ExcelDataTableConfig configuration.ReadHeaderRow(self); } - for (var i = 0; i < self.FieldCount; i++) + if (configuration.ReadHeader != null) { - if (configuration.FilterColumn != null && !configuration.FilterColumn(self, i)) + var dict = configuration.ReadHeader(self); + foreach (var kvp in dict) { - continue; + var columnIndex = kvp.Key; + var name = kvp.Value; + + // if a column already exists with the name append _i to the duplicates + var columnName = GetUniqueColumnName(result, name); + var column = new DataColumn(columnName, typeof(object)) { Caption = name }; + result.Columns.Add(column); + columnIndices.Add(columnIndex); } + } + else + { + for (var i = 0; i < self.FieldCount; i++) + { + if (configuration.FilterColumn != null && !configuration.FilterColumn(self, i)) + { + continue; + } - var name = configuration.UseHeaderRow - ? Convert.ToString(self.GetValue(i), CultureInfo.CurrentCulture) - : null; + var name = configuration.UseHeaderRow + ? Convert.ToString(self.GetValue(i), CultureInfo.CurrentCulture) + : null; - if (string.IsNullOrEmpty(name)) - { - name = configuration.EmptyColumnNamePrefix + i; - } + if (string.IsNullOrEmpty(name)) + { + name = configuration.EmptyColumnNamePrefix + i; + } - // if a column already exists with the name append _i to the duplicates - var columnName = GetUniqueColumnName(result, name); - var column = new DataColumn(columnName, typeof(object)) { Caption = name }; - result.Columns.Add(column); - columnIndices.Add(i); + // if a column already exists with the name append _i to the duplicates + var columnName = GetUniqueColumnName(result, name); + var column = new DataColumn(columnName, typeof(object)) { Caption = name }; + result.Columns.Add(column); + columnIndices.Add(i); + } } result.BeginLoadData(); diff --git a/src/ExcelDataReader.DataSet/ExcelDataTableConfiguration.cs b/src/ExcelDataReader.DataSet/ExcelDataTableConfiguration.cs index 4fe58463..0b22489a 100644 --- a/src/ExcelDataReader.DataSet/ExcelDataTableConfiguration.cs +++ b/src/ExcelDataReader.DataSet/ExcelDataTableConfiguration.cs @@ -1,4 +1,4 @@ -namespace ExcelDataReader +namespace ExcelDataReader { /// /// Processing configuration options and callbacks for AsDataTable(). @@ -20,6 +20,15 @@ public class ExcelDataTableConfiguration /// public Action ReadHeaderRow { get; set; } + /// + /// Gets or sets a callback to allow a custom implementation of header reading. + /// The returned dictionary will be used to construct the resulting DataTable. + /// Each element of the dictionary specifies an index and column name pair. + /// An example use of this would be to combine multiple header rows. + /// NOTE: If this field is set, UseHeaderRow, EmptyColumnNamePrefix, and FilterColumn are ignored. + /// + public Func> ReadHeader { get; set; } + /// /// Gets or sets a callback to determine whether to include the current row in the DataTable. /// diff --git a/test/ExcelDataReader.Tests/ExcelOpenXmlReaderTest.cs b/test/ExcelDataReader.Tests/ExcelOpenXmlReaderTest.cs index 0879349b..4c6d1731 100644 --- a/test/ExcelDataReader.Tests/ExcelOpenXmlReaderTest.cs +++ b/test/ExcelDataReader.Tests/ExcelOpenXmlReaderTest.cs @@ -388,5 +388,69 @@ public void GitIssue425() Assert.That(dataSet.Tables[0].Rows[1].ItemArray[0], Is.EqualTo("text")); Assert.That(dataSet.Tables[0].Rows[2].ItemArray[0], Is.EqualTo("text text")); } + + [Test] + public void GitIssue518MultipleHeaderRows() + { + using (var reader = OpenReader("Test_git_issue_518")) + { + var dataSet = reader.AsDataSet(new ExcelDataSetConfiguration() + { + ConfigureDataTable = _ => new ExcelDataTableConfiguration() + { + UseHeaderRow = true, + ReadHeader = self => + { + var headerNames = new List(); + + // read first header row + for (var i = 0; i < self.FieldCount; i++) + { + var s = Convert.ToString(self.GetValue(i)); + headerNames.Add(s); + } + + // append second header row + if (!self.Read()) { + throw new Exception(); + } + var result = new Dictionary(); + for (var i = 0; i < self.FieldCount; i++) + { + var first = headerNames[i]; + var second = Convert.ToString(self.GetValue(i)); + string name; + if (first.Length == 0) + { + name = second; + } + else if (second.Length == 0) + { + name = first; + } + else + { + name = first + " " + second; + } + if (string.IsNullOrEmpty(name)) + { + name = "Column" + i; + } + result.Add(i, name); + } + return result; + } + } + }); + + var columns = dataSet.Tables[0].Columns; + Assert.That(columns[0].ColumnName.ToString(), Is.EqualTo("ColName1 A")); + Assert.That(columns[1].ColumnName.ToString(), Is.EqualTo("ColName1 B")); + Assert.That(columns[2].ColumnName.ToString(), Is.EqualTo("ColName2 B")); + Assert.That(columns[3].ColumnName.ToString(), Is.EqualTo("FirstOnly")); + Assert.That(columns[4].ColumnName.ToString(), Is.EqualTo("SecondOnly")); + Assert.That(columns[5].ColumnName.ToString(), Is.EqualTo("Another One")); + } + } } } diff --git a/test/Resources/Test_git_issue_518.xlsx b/test/Resources/Test_git_issue_518.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..1e678c96533f849509f34b3b5eb685e78f47fe89 GIT binary patch literal 8307 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