diff --git a/Directory.build.props b/Directory.build.props
index d79eee79..089d42c6 100644
--- a/Directory.build.props
+++ b/Directory.build.props
@@ -9,7 +9,7 @@
- 3.8.0
+ 3.9.0
net462;netstandard2.0;netstandard2.1;net8.0
latest-recommended
..\ExcelDataReader.snk
@@ -24,7 +24,9 @@
$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb
MIT
README.md
- true
+ true
+ true
+ true
@@ -49,7 +51,6 @@
-
diff --git a/README.md b/README.md
index 9374a76e..77eea849 100644
--- a/README.md
+++ b/README.md
@@ -138,6 +138,13 @@ var reader = ExcelReaderFactory.CreateReader(stream, new ExcelReaderConfiguratio
// Default: 0 - analyzes the entire file (CSV only, has no effect on other
// formats)
AnalyzeInitialCsvRows = 0,
+
+ // Gets or sets the culture to use when mapping locale-dependent built-in
+ // number format indices to format strings. Affects indices 14 (short date)
+ // and 22 (short date and time). When null (the default), hardcoded format
+ // strings are used for backward compatibility.
+ // (XLS and XLSX/XLSB only, has no effect on CSV)
+ Culture = CultureInfo.CurrentCulture,
});
```
@@ -184,7 +191,11 @@ var result = reader.AsDataSet(new ExcelDataSetConfiguration()
// headers.
FilterColumn = (rowReader, columnIndex) => {
return true;
- }
+ },
+
+ // Gets or sets a value indicating whether merged cells should be filled
+ // with their top-left cell's value. Default: false
+ FillMergedCellsValue = false,
}
});
```
diff --git a/src/ExcelDataReader.Benchmarks/ExcelDataReader.Benchmarks.csproj b/src/ExcelDataReader.Benchmarks/ExcelDataReader.Benchmarks.csproj
index 1b67a3d7..f5c1b95d 100644
--- a/src/ExcelDataReader.Benchmarks/ExcelDataReader.Benchmarks.csproj
+++ b/src/ExcelDataReader.Benchmarks/ExcelDataReader.Benchmarks.csproj
@@ -15,14 +15,15 @@
+
-
-
-
-
+
+
+
+
\ No newline at end of file
diff --git a/src/ExcelDataReader.Benchmarks/ReadAllStrings.cs b/src/ExcelDataReader.Benchmarks/ReadAllStrings.cs
new file mode 100644
index 00000000..17e385fa
--- /dev/null
+++ b/src/ExcelDataReader.Benchmarks/ReadAllStrings.cs
@@ -0,0 +1,56 @@
+using System.Text;
+using BenchmarkDotNet.Attributes;
+
+namespace ExcelDataReader.Benchmarks;
+
+///
+/// Benchmarks for reading all string cell values, exercising SST lookup performance.
+/// For XLS, this validates the lazy string cache (issue #525): strings are decoded and cached
+/// on first access in GetString(), releasing the raw byte[] backing after the first lookup.
+/// Repeated access of the same SST index returns the cached string with zero allocation.
+///
+[MemoryDiagnoser]
+public class ReadAllStrings
+{
+ [GlobalSetup]
+ public void Setup()
+ {
+ Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
+ }
+
+ [Benchmark]
+ public string ReadAllStringsXlsx()
+ {
+ return ReadStrings(ExcelReaderFactory.CreateReader(
+ typeof(OpenXmlFile).Assembly.GetManifestResourceStream("ExcelDataReader.Benchmarks.Test10x10000.xlsx")));
+ }
+
+ [Benchmark]
+ public string ReadAllStringsXlsb()
+ {
+ return ReadStrings(ExcelReaderFactory.CreateReader(
+ typeof(OpenXmlFile).Assembly.GetManifestResourceStream("ExcelDataReader.Benchmarks.Test10x10000.xlsb")));
+ }
+
+ [Benchmark]
+ public string ReadAllStringsXls()
+ {
+ return ReadStrings(ExcelReaderFactory.CreateReader(
+ typeof(OpenXmlFile).Assembly.GetManifestResourceStream("ExcelDataReader.Benchmarks.Test10x10000.xls")));
+ }
+
+ private static string ReadStrings(IExcelDataReader reader)
+ {
+ using (reader)
+ {
+ string last = null;
+ while (reader.Read())
+ {
+ for (var i = 0; i < reader.FieldCount; i++)
+ last = reader.GetString(i);
+ }
+
+ return last;
+ }
+ }
+}
diff --git a/src/ExcelDataReader.Benchmarks/SinglePassRead.cs b/src/ExcelDataReader.Benchmarks/SinglePassRead.cs
new file mode 100644
index 00000000..d13a1d5c
--- /dev/null
+++ b/src/ExcelDataReader.Benchmarks/SinglePassRead.cs
@@ -0,0 +1,78 @@
+using System.Text;
+using BenchmarkDotNet.Attributes;
+
+namespace ExcelDataReader.Benchmarks;
+
+[MemoryDiagnoser]
+public class SinglePassRead
+{
+ private static readonly ExcelReaderConfiguration MultiPassConfig = new();
+ private static readonly ExcelReaderConfiguration SinglePassConfig = new() { SinglePassMode = true };
+
+ [GlobalSetup]
+ public void Setup()
+ {
+ Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
+ }
+
+ [Benchmark]
+ public void ReadMultiPassXlsx()
+ {
+ using var reader = ExcelReaderFactory.CreateReader(
+ typeof(OpenXmlFile).Assembly.GetManifestResourceStream("ExcelDataReader.Benchmarks.Test10x10000.xlsx"),
+ MultiPassConfig);
+ Read(reader);
+ }
+
+ [Benchmark]
+ public void ReadSinglePassXlsx()
+ {
+ using var reader = ExcelReaderFactory.CreateReader(
+ typeof(OpenXmlFile).Assembly.GetManifestResourceStream("ExcelDataReader.Benchmarks.Test10x10000.xlsx"),
+ SinglePassConfig);
+ Read(reader);
+ }
+
+ [Benchmark]
+ public void ReadMultiPassXlsb()
+ {
+ using var reader = ExcelReaderFactory.CreateReader(
+ typeof(OpenXmlFile).Assembly.GetManifestResourceStream("ExcelDataReader.Benchmarks.Test10x10000.xlsb"),
+ MultiPassConfig);
+ Read(reader);
+ }
+
+ [Benchmark]
+ public void ReadSinglePassXlsb()
+ {
+ using var reader = ExcelReaderFactory.CreateReader(
+ typeof(OpenXmlFile).Assembly.GetManifestResourceStream("ExcelDataReader.Benchmarks.Test10x10000.xlsb"),
+ SinglePassConfig);
+ Read(reader);
+ }
+
+ [Benchmark]
+ public void ReadMultiPassXls()
+ {
+ using var reader = ExcelReaderFactory.CreateReader(
+ typeof(OpenXmlFile).Assembly.GetManifestResourceStream("ExcelDataReader.Benchmarks.Test10x10000.xls"),
+ MultiPassConfig);
+ Read(reader);
+ }
+
+ [Benchmark]
+ public void ReadSinglePassXls()
+ {
+ using var reader = ExcelReaderFactory.CreateReader(
+ typeof(OpenXmlFile).Assembly.GetManifestResourceStream("ExcelDataReader.Benchmarks.Test10x10000.xls"),
+ SinglePassConfig);
+ Read(reader);
+ }
+
+ private static void Read(IExcelDataReader reader)
+ {
+ while (reader.Read())
+ {
+ }
+ }
+}
diff --git a/src/ExcelDataReader.DataSet/ExcelDataReaderExtensions.cs b/src/ExcelDataReader.DataSet/ExcelDataReaderExtensions.cs
index 1598420d..b40916aa 100644
--- a/src/ExcelDataReader.DataSet/ExcelDataReaderExtensions.cs
+++ b/src/ExcelDataReader.DataSet/ExcelDataReaderExtensions.cs
@@ -68,13 +68,26 @@ private static string GetUniqueColumnName(DataTable table, string name)
private static DataTable AsDataTable(IExcelDataReader self, ExcelDataTableConfiguration configuration)
{
- var result = new DataTable { TableName = self.Name };
+ var result = new DataTable { TableName = self.Name, CaseSensitive = configuration.CaseSentitive };
result.ExtendedProperties.Add("visiblestate", self.VisibleState);
var first = true;
var emptyRows = 0;
+ List mergedCellsList = [];
+ Dictionary<(int Row, int Column), object> mergeCellValue = [];
+
+ // If need to fill merged cells, check the next row have merged cells
+ var nextRowHaveMergedCell = false;
+ if (configuration.FillMergedCellsValue)
+ {
+ mergedCellsList = self.MergeCells.OrderBy(c => c.FromRow).ThenBy(c => c.FromColumn).ToList();
+ }
+
+ int rowIndex = -1;
+ int lastCheckedFieldCount = 0;
List columnIndices = [];
while (self.Read())
{
+ rowIndex++;
if (first)
{
if (configuration.UseHeaderRow && configuration.ReadHeaderRow != null)
@@ -82,7 +95,7 @@ private static DataTable AsDataTable(IExcelDataReader self, ExcelDataTableConfig
configuration.ReadHeaderRow(self);
}
- if (configuration.ReadHeader != null)
+ if (configuration.ReadHeader != null)
{
var dict = configuration.ReadHeader(self);
foreach (var kvp in dict)
@@ -96,8 +109,8 @@ private static DataTable AsDataTable(IExcelDataReader self, ExcelDataTableConfig
result.Columns.Add(column);
columnIndices.Add(columnIndex);
}
- }
- else
+ }
+ else
{
for (var i = 0; i < self.FieldCount; i++)
{
@@ -124,6 +137,7 @@ private static DataTable AsDataTable(IExcelDataReader self, ExcelDataTableConfig
}
result.BeginLoadData();
+ lastCheckedFieldCount = self.FieldCount;
first = false;
if (configuration.UseHeaderRow)
@@ -131,13 +145,33 @@ private static DataTable AsDataTable(IExcelDataReader self, ExcelDataTableConfig
continue;
}
}
+ else if (configuration.ReadHeader == null && self.FieldCount > lastCheckedFieldCount)
+ {
+ // Grow DataTable columns dynamically when FieldCount increases (e.g. single-pass mode)
+ for (var i = lastCheckedFieldCount; i < self.FieldCount; i++)
+ {
+ if (configuration.FilterColumn != null && !configuration.FilterColumn(self, i))
+ {
+ continue;
+ }
+
+ var name = configuration.EmptyColumnNamePrefix + i;
+ var columnName = GetUniqueColumnName(result, name);
+ var column = new DataColumn(columnName, typeof(object)) { Caption = name };
+ result.Columns.Add(column);
+ columnIndices.Add(i);
+ }
+
+ lastCheckedFieldCount = self.FieldCount;
+ }
if (configuration.FilterRow != null && !configuration.FilterRow(self))
{
continue;
}
- if (IsEmptyRow(self, configuration))
+ // if next row is containing merged cells, skip the empty row check
+ if (!nextRowHaveMergedCell && IsEmptyRow(self, configuration))
{
emptyRows++;
continue;
@@ -157,6 +191,36 @@ private static DataTable AsDataTable(IExcelDataReader self, ExcelDataTableConfig
var columnIndex = columnIndices[i];
var value = self.GetValue(columnIndex);
+ if (configuration.FillMergedCellsValue)
+ {
+ var range = mergedCellsList.Find(range => range.FromRow <= rowIndex &&
+ range.ToRow >= rowIndex &&
+ range.FromColumn <= columnIndex &&
+ range.ToColumn >= columnIndex);
+ if (range != null)
+ {
+ if (mergeCellValue.TryGetValue((range.FromRow, range.FromColumn), out var mergedValue))
+ {
+ value = mergedValue;
+ }
+ else
+ {
+ mergeCellValue[(range.FromRow, range.FromColumn)] = value;
+ }
+
+ // mark next row is in merged range, skip empty row check and to fill row
+ if (rowIndex < range.ToRow)
+ {
+ nextRowHaveMergedCell = true;
+ }
+ else if (rowIndex == range.ToRow && columnIndex == range.ToColumn)
+ {
+ mergedCellsList.Remove(range);
+ nextRowHaveMergedCell = false;
+ }
+ }
+ }
+
if (configuration.TransformValue != null)
{
var transformedValue = configuration.TransformValue(self, i, value);
diff --git a/src/ExcelDataReader.DataSet/ExcelDataTableConfiguration.cs b/src/ExcelDataReader.DataSet/ExcelDataTableConfiguration.cs
index 04243d4f..9f52f7e6 100644
--- a/src/ExcelDataReader.DataSet/ExcelDataTableConfiguration.cs
+++ b/src/ExcelDataReader.DataSet/ExcelDataTableConfiguration.cs
@@ -5,6 +5,11 @@ namespace ExcelDataReader;
///
public class ExcelDataTableConfiguration
{
+ ///
+ /// Gets or sets a value indicating whether the DataTable should be case sensitive. Useful when setting a primary key after loading from Excel.
+ ///
+ public bool CaseSentitive { get; set; }
+
///
/// Gets or sets a value indicating the prefix of generated column names.
///
@@ -43,4 +48,9 @@ public class ExcelDataTableConfiguration
/// Gets or sets a callback to determine whether to transform the cell value.
///
public Func TransformValue { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether merged cells should be filled with their top-left cell's value.
+ ///
+ public bool FillMergedCellsValue { get; set; }
}
diff --git a/src/ExcelDataReader.DataSet/packages.lock.json b/src/ExcelDataReader.DataSet/packages.lock.json
index 684cac8a..a0024132 100644
--- a/src/ExcelDataReader.DataSet/packages.lock.json
+++ b/src/ExcelDataReader.DataSet/packages.lock.json
@@ -11,16 +11,6 @@
"Microsoft.NETFramework.ReferenceAssemblies.net462": "1.0.3"
}
},
- "Microsoft.SourceLink.GitHub": {
- "type": "Direct",
- "requested": "[8.0.0, )",
- "resolved": "8.0.0",
- "contentHash": "G5q7OqtwIyGTkeIOAc3u2ZuV/kicQaec5EaRnc0pIeSnh9LUjj+PYQrJYBURvDt7twGl2PKA7nSN0kz1Zw5bnQ==",
- "dependencies": {
- "Microsoft.Build.Tasks.Git": "8.0.0",
- "Microsoft.SourceLink.Common": "8.0.0"
- }
- },
"PolySharp": {
"type": "Direct",
"requested": "[1.15.0, )",
@@ -42,21 +32,11 @@
"resolved": "4.5.0",
"contentHash": "okurQJO6NRE/apDIP23ajJ0hpiNmJ+f0BwOlB/cSqTLQlw5upkf+5+96+iG2Jw40G1fCVCyPz/FhIABUjMR+RQ=="
},
- "Microsoft.Build.Tasks.Git": {
- "type": "Transitive",
- "resolved": "8.0.0",
- "contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ=="
- },
"Microsoft.NETFramework.ReferenceAssemblies.net462": {
"type": "Transitive",
"resolved": "1.0.3",
"contentHash": "IzAV30z22ESCeQfxP29oVf4qEo8fBGXLXSU6oacv/9Iqe6PzgHDKCaWfwMBak7bSJQM0F5boXWoZS+kChztRIQ=="
},
- "Microsoft.SourceLink.Common": {
- "type": "Transitive",
- "resolved": "8.0.0",
- "contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw=="
- },
"StyleCop.Analyzers.Unstable": {
"type": "Transitive",
"resolved": "1.2.0.556",
@@ -70,16 +50,6 @@
}
},
".NETStandard,Version=v2.0": {
- "Microsoft.SourceLink.GitHub": {
- "type": "Direct",
- "requested": "[8.0.0, )",
- "resolved": "8.0.0",
- "contentHash": "G5q7OqtwIyGTkeIOAc3u2ZuV/kicQaec5EaRnc0pIeSnh9LUjj+PYQrJYBURvDt7twGl2PKA7nSN0kz1Zw5bnQ==",
- "dependencies": {
- "Microsoft.Build.Tasks.Git": "8.0.0",
- "Microsoft.SourceLink.Common": "8.0.0"
- }
- },
"NETStandard.Library": {
"type": "Direct",
"requested": "[2.0.3, )",
@@ -104,21 +74,11 @@
"StyleCop.Analyzers.Unstable": "1.2.0.556"
}
},
- "Microsoft.Build.Tasks.Git": {
- "type": "Transitive",
- "resolved": "8.0.0",
- "contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ=="
- },
"Microsoft.NETCore.Platforms": {
"type": "Transitive",
"resolved": "1.1.0",
"contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A=="
},
- "Microsoft.SourceLink.Common": {
- "type": "Transitive",
- "resolved": "8.0.0",
- "contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw=="
- },
"StyleCop.Analyzers.Unstable": {
"type": "Transitive",
"resolved": "1.2.0.556",
@@ -129,16 +89,6 @@
}
},
".NETStandard,Version=v2.1": {
- "Microsoft.SourceLink.GitHub": {
- "type": "Direct",
- "requested": "[8.0.0, )",
- "resolved": "8.0.0",
- "contentHash": "G5q7OqtwIyGTkeIOAc3u2ZuV/kicQaec5EaRnc0pIeSnh9LUjj+PYQrJYBURvDt7twGl2PKA7nSN0kz1Zw5bnQ==",
- "dependencies": {
- "Microsoft.Build.Tasks.Git": "8.0.0",
- "Microsoft.SourceLink.Common": "8.0.0"
- }
- },
"PolySharp": {
"type": "Direct",
"requested": "[1.15.0, )",
@@ -154,16 +104,6 @@
"StyleCop.Analyzers.Unstable": "1.2.0.556"
}
},
- "Microsoft.Build.Tasks.Git": {
- "type": "Transitive",
- "resolved": "8.0.0",
- "contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ=="
- },
- "Microsoft.SourceLink.Common": {
- "type": "Transitive",
- "resolved": "8.0.0",
- "contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw=="
- },
"StyleCop.Analyzers.Unstable": {
"type": "Transitive",
"resolved": "1.2.0.556",
@@ -174,15 +114,11 @@
}
},
"net8.0": {
- "Microsoft.SourceLink.GitHub": {
+ "Microsoft.NET.ILLink.Tasks": {
"type": "Direct",
- "requested": "[8.0.0, )",
- "resolved": "8.0.0",
- "contentHash": "G5q7OqtwIyGTkeIOAc3u2ZuV/kicQaec5EaRnc0pIeSnh9LUjj+PYQrJYBURvDt7twGl2PKA7nSN0kz1Zw5bnQ==",
- "dependencies": {
- "Microsoft.Build.Tasks.Git": "8.0.0",
- "Microsoft.SourceLink.Common": "8.0.0"
- }
+ "requested": "[8.0.25, )",
+ "resolved": "8.0.25",
+ "contentHash": "sqX4nmBft05ivqKvUT4nxaN8rT3apCLt9SWFkfRrQPwra1zPwFknQAw1lleuMCKOCLvVmOWwrC2iPSm9RiXZUg=="
},
"PolySharp": {
"type": "Direct",
@@ -199,16 +135,6 @@
"StyleCop.Analyzers.Unstable": "1.2.0.556"
}
},
- "Microsoft.Build.Tasks.Git": {
- "type": "Transitive",
- "resolved": "8.0.0",
- "contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ=="
- },
- "Microsoft.SourceLink.Common": {
- "type": "Transitive",
- "resolved": "8.0.0",
- "contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw=="
- },
"StyleCop.Analyzers.Unstable": {
"type": "Transitive",
"resolved": "1.2.0.556",
diff --git a/src/ExcelDataReader.Sample/Form1.Designer.cs b/src/ExcelDataReader.Sample/Form1.Designer.cs
index 7d717f16..e2647ad6 100644
--- a/src/ExcelDataReader.Sample/Form1.Designer.cs
+++ b/src/ExcelDataReader.Sample/Form1.Designer.cs
@@ -33,162 +33,177 @@ protected override void Dispose(bool disposing)
///
private void InitializeComponent()
{
- this.openFileDialog1 = new System.Windows.Forms.OpenFileDialog();
- this.button1 = new System.Windows.Forms.Button();
- this.textBox1 = new System.Windows.Forms.TextBox();
- this.button2 = new System.Windows.Forms.Button();
- this.dataSet1 = new System.Data.DataSet();
- this.dataGridView1 = new System.Windows.Forms.DataGridView();
- this.sheetCombo = new System.Windows.Forms.ComboBox();
- this.Sheet = new System.Windows.Forms.Label();
- this.firstRowNamesCheckBox = new System.Windows.Forms.CheckBox();
- this.statusStrip1 = new System.Windows.Forms.StatusStrip();
- this.toolStripStatusLabel1 = new System.Windows.Forms.ToolStripStatusLabel();
- this.label1 = new System.Windows.Forms.Label();
- ((System.ComponentModel.ISupportInitialize)(this.dataSet1)).BeginInit();
- ((System.ComponentModel.ISupportInitialize)(this.dataGridView1)).BeginInit();
- this.statusStrip1.SuspendLayout();
- this.SuspendLayout();
- //
- // openFileDialog1
- //
- this.openFileDialog1.FileName = "openFileDialog1";
- this.openFileDialog1.Filter = "Supported files|*.xls;*.xlsx;*.xlsb;*.csv|xls|*.xls|xlsx|*.xlsx|xlsb|*.xlsb|csv|*" +
-".csv|All|*.*";
- //
- // button1
- //
- this.button1.Location = new System.Drawing.Point(345, 5);
- this.button1.Margin = new System.Windows.Forms.Padding(2);
- this.button1.Name = "button1";
- this.button1.Size = new System.Drawing.Size(70, 21);
- this.button1.TabIndex = 0;
- this.button1.Text = "Select file";
- this.button1.UseVisualStyleBackColor = true;
- this.button1.Click += new System.EventHandler(this.Button1Click);
- //
- // textBox1
- //
- this.textBox1.Location = new System.Drawing.Point(46, 6);
- this.textBox1.Margin = new System.Windows.Forms.Padding(2);
- this.textBox1.Name = "textBox1";
- this.textBox1.Size = new System.Drawing.Size(295, 20);
- this.textBox1.TabIndex = 1;
- //
- // button2
- //
- this.button2.Location = new System.Drawing.Point(11, 51);
- this.button2.Margin = new System.Windows.Forms.Padding(2);
- this.button2.Name = "button2";
- this.button2.Size = new System.Drawing.Size(73, 29);
- this.button2.TabIndex = 2;
- this.button2.Text = "Process";
- this.button2.UseVisualStyleBackColor = true;
- this.button2.Click += new System.EventHandler(this.Button2Click);
- //
- // dataSet1
- //
- this.dataSet1.DataSetName = "NewDataSet";
- //
- // dataGridView1
- //
- this.dataGridView1.AllowUserToAddRows = false;
- this.dataGridView1.AllowUserToDeleteRows = false;
- this.dataGridView1.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
- | System.Windows.Forms.AnchorStyles.Left)
- | System.Windows.Forms.AnchorStyles.Right)));
- this.dataGridView1.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
- this.dataGridView1.Location = new System.Drawing.Point(11, 114);
- this.dataGridView1.Margin = new System.Windows.Forms.Padding(2);
- this.dataGridView1.Name = "dataGridView1";
- this.dataGridView1.ReadOnly = true;
- this.dataGridView1.RowTemplate.Height = 24;
- this.dataGridView1.Size = new System.Drawing.Size(865, 426);
- this.dataGridView1.TabIndex = 3;
- //
- // sheetCombo
- //
- this.sheetCombo.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
- this.sheetCombo.FormattingEnabled = true;
- this.sheetCombo.Location = new System.Drawing.Point(88, 89);
- this.sheetCombo.Margin = new System.Windows.Forms.Padding(2);
- this.sheetCombo.Name = "sheetCombo";
- this.sheetCombo.Size = new System.Drawing.Size(253, 21);
- this.sheetCombo.TabIndex = 4;
- this.sheetCombo.SelectedIndexChanged += new System.EventHandler(this.SheetComboSelectedIndexChanged);
- //
- // Sheet
- //
- this.Sheet.AutoSize = true;
- this.Sheet.Location = new System.Drawing.Point(12, 92);
- this.Sheet.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0);
- this.Sheet.Name = "Sheet";
- this.Sheet.Size = new System.Drawing.Size(72, 13);
- this.Sheet.TabIndex = 5;
- this.Sheet.Text = "Choose sheet";
- //
- // firstRowNamesCheckBox
- //
- this.firstRowNamesCheckBox.AutoSize = true;
- this.firstRowNamesCheckBox.Checked = true;
- this.firstRowNamesCheckBox.CheckState = System.Windows.Forms.CheckState.Checked;
- this.firstRowNamesCheckBox.Location = new System.Drawing.Point(15, 30);
- this.firstRowNamesCheckBox.Margin = new System.Windows.Forms.Padding(2);
- this.firstRowNamesCheckBox.Name = "firstRowNamesCheckBox";
- this.firstRowNamesCheckBox.Size = new System.Drawing.Size(176, 17);
- this.firstRowNamesCheckBox.TabIndex = 6;
- this.firstRowNamesCheckBox.Text = "first row contains column names";
- this.firstRowNamesCheckBox.UseVisualStyleBackColor = true;
- //
- // statusStrip1
- //
- this.statusStrip1.ImageScalingSize = new System.Drawing.Size(20, 20);
- this.statusStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
- this.toolStripStatusLabel1});
- this.statusStrip1.Location = new System.Drawing.Point(0, 552);
- this.statusStrip1.Name = "statusStrip1";
- this.statusStrip1.Padding = new System.Windows.Forms.Padding(1, 0, 10, 0);
- this.statusStrip1.Size = new System.Drawing.Size(887, 22);
- this.statusStrip1.TabIndex = 7;
- this.statusStrip1.Text = "statusStrip1";
- //
- // toolStripStatusLabel1
- //
- this.toolStripStatusLabel1.Name = "toolStripStatusLabel1";
- this.toolStripStatusLabel1.Size = new System.Drawing.Size(0, 17);
- //
- // label1
- //
- this.label1.AutoSize = true;
- this.label1.Location = new System.Drawing.Point(12, 9);
- this.label1.Name = "label1";
- this.label1.Size = new System.Drawing.Size(29, 13);
- this.label1.TabIndex = 8;
- this.label1.Text = "Path";
- //
- // Form1
- //
- this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
- this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
- this.ClientSize = new System.Drawing.Size(887, 574);
- this.Controls.Add(this.label1);
- this.Controls.Add(this.statusStrip1);
- this.Controls.Add(this.firstRowNamesCheckBox);
- this.Controls.Add(this.Sheet);
- this.Controls.Add(this.sheetCombo);
- this.Controls.Add(this.dataGridView1);
- this.Controls.Add(this.button2);
- this.Controls.Add(this.textBox1);
- this.Controls.Add(this.button1);
- this.Margin = new System.Windows.Forms.Padding(2);
- this.Name = "Form1";
- this.Text = "Form1";
- ((System.ComponentModel.ISupportInitialize)(this.dataSet1)).EndInit();
- ((System.ComponentModel.ISupportInitialize)(this.dataGridView1)).EndInit();
- this.statusStrip1.ResumeLayout(false);
- this.statusStrip1.PerformLayout();
- this.ResumeLayout(false);
- this.PerformLayout();
+ this.openFileDialog1 = new System.Windows.Forms.OpenFileDialog();
+ this.button1 = new System.Windows.Forms.Button();
+ this.textBox1 = new System.Windows.Forms.TextBox();
+ this.button2 = new System.Windows.Forms.Button();
+ this.dataSet1 = new System.Data.DataSet();
+ this.dataGridView1 = new System.Windows.Forms.DataGridView();
+ this.sheetCombo = new System.Windows.Forms.ComboBox();
+ this.Sheet = new System.Windows.Forms.Label();
+ this.firstRowNamesCheckBox = new System.Windows.Forms.CheckBox();
+ this.statusStrip1 = new System.Windows.Forms.StatusStrip();
+ this.toolStripStatusLabel1 = new System.Windows.Forms.ToolStripStatusLabel();
+ this.label1 = new System.Windows.Forms.Label();
+ this.fillMergeCellCheckBox = new System.Windows.Forms.CheckBox();
+ ((System.ComponentModel.ISupportInitialize)(this.dataSet1)).BeginInit();
+ ((System.ComponentModel.ISupportInitialize)(this.dataGridView1)).BeginInit();
+ this.statusStrip1.SuspendLayout();
+ this.SuspendLayout();
+ //
+ // openFileDialog1
+ //
+ this.openFileDialog1.FileName = "openFileDialog1";
+ this.openFileDialog1.Filter = "Supported files|*.xls;*.xlsx;*.xlsb;*.csv|xls|*.xls|xlsx|*.xlsx|xlsb|*.xlsb|csv|*" +
+ ".csv|All|*.*";
+ //
+ // button1
+ //
+ this.button1.Location = new System.Drawing.Point(345, 5);
+ this.button1.Margin = new System.Windows.Forms.Padding(2);
+ this.button1.Name = "button1";
+ this.button1.Size = new System.Drawing.Size(70, 19);
+ this.button1.TabIndex = 0;
+ this.button1.Text = "Select file";
+ this.button1.UseVisualStyleBackColor = true;
+ this.button1.Click += new System.EventHandler(this.Button1Click);
+ //
+ // textBox1
+ //
+ this.textBox1.Location = new System.Drawing.Point(46, 6);
+ this.textBox1.Margin = new System.Windows.Forms.Padding(2);
+ this.textBox1.Name = "textBox1";
+ this.textBox1.Size = new System.Drawing.Size(295, 21);
+ this.textBox1.TabIndex = 1;
+ //
+ // button2
+ //
+ this.button2.Location = new System.Drawing.Point(11, 47);
+ this.button2.Margin = new System.Windows.Forms.Padding(2);
+ this.button2.Name = "button2";
+ this.button2.Size = new System.Drawing.Size(73, 27);
+ this.button2.TabIndex = 2;
+ this.button2.Text = "Process";
+ this.button2.UseVisualStyleBackColor = true;
+ this.button2.Click += new System.EventHandler(this.Button2Click);
+ //
+ // dataSet1
+ //
+ this.dataSet1.DataSetName = "NewDataSet";
+ //
+ // dataGridView1
+ //
+ this.dataGridView1.AllowUserToAddRows = false;
+ this.dataGridView1.AllowUserToDeleteRows = false;
+ this.dataGridView1.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
+ | System.Windows.Forms.AnchorStyles.Left)
+ | System.Windows.Forms.AnchorStyles.Right)));
+ this.dataGridView1.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
+ this.dataGridView1.Location = new System.Drawing.Point(11, 105);
+ this.dataGridView1.Margin = new System.Windows.Forms.Padding(2);
+ this.dataGridView1.Name = "dataGridView1";
+ this.dataGridView1.ReadOnly = true;
+ this.dataGridView1.RowTemplate.Height = 24;
+ this.dataGridView1.Size = new System.Drawing.Size(865, 393);
+ this.dataGridView1.TabIndex = 3;
+ //
+ // sheetCombo
+ //
+ this.sheetCombo.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
+ this.sheetCombo.FormattingEnabled = true;
+ this.sheetCombo.Location = new System.Drawing.Point(88, 82);
+ this.sheetCombo.Margin = new System.Windows.Forms.Padding(2);
+ this.sheetCombo.Name = "sheetCombo";
+ this.sheetCombo.Size = new System.Drawing.Size(253, 20);
+ this.sheetCombo.TabIndex = 4;
+ this.sheetCombo.SelectedIndexChanged += new System.EventHandler(this.SheetComboSelectedIndexChanged);
+ //
+ // Sheet
+ //
+ this.Sheet.AutoSize = true;
+ this.Sheet.Location = new System.Drawing.Point(12, 85);
+ this.Sheet.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0);
+ this.Sheet.Name = "Sheet";
+ this.Sheet.Size = new System.Drawing.Size(77, 12);
+ this.Sheet.TabIndex = 5;
+ this.Sheet.Text = "Choose sheet";
+ //
+ // firstRowNamesCheckBox
+ //
+ this.firstRowNamesCheckBox.AutoSize = true;
+ this.firstRowNamesCheckBox.Checked = true;
+ this.firstRowNamesCheckBox.CheckState = System.Windows.Forms.CheckState.Checked;
+ this.firstRowNamesCheckBox.Location = new System.Drawing.Point(15, 28);
+ this.firstRowNamesCheckBox.Margin = new System.Windows.Forms.Padding(2);
+ this.firstRowNamesCheckBox.Name = "firstRowNamesCheckBox";
+ this.firstRowNamesCheckBox.Size = new System.Drawing.Size(210, 16);
+ this.firstRowNamesCheckBox.TabIndex = 6;
+ this.firstRowNamesCheckBox.Text = "first row contains column names";
+ this.firstRowNamesCheckBox.UseVisualStyleBackColor = true;
+ //
+ // statusStrip1
+ //
+ this.statusStrip1.ImageScalingSize = new System.Drawing.Size(20, 20);
+ this.statusStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
+ this.toolStripStatusLabel1});
+ this.statusStrip1.Location = new System.Drawing.Point(0, 508);
+ this.statusStrip1.Name = "statusStrip1";
+ this.statusStrip1.Padding = new System.Windows.Forms.Padding(1, 0, 10, 0);
+ this.statusStrip1.Size = new System.Drawing.Size(887, 22);
+ this.statusStrip1.TabIndex = 7;
+ this.statusStrip1.Text = "statusStrip1";
+ //
+ // toolStripStatusLabel1
+ //
+ this.toolStripStatusLabel1.Name = "toolStripStatusLabel1";
+ this.toolStripStatusLabel1.Size = new System.Drawing.Size(0, 17);
+ //
+ // label1
+ //
+ this.label1.AutoSize = true;
+ this.label1.Location = new System.Drawing.Point(12, 8);
+ this.label1.Name = "label1";
+ this.label1.Size = new System.Drawing.Size(29, 12);
+ this.label1.TabIndex = 8;
+ this.label1.Text = "Path";
+ //
+ // fillMergeCellCheckBox
+ //
+ this.fillMergeCellCheckBox.AutoSize = true;
+ this.fillMergeCellCheckBox.Checked = true;
+ this.fillMergeCellCheckBox.CheckState = System.Windows.Forms.CheckState.Checked;
+ this.fillMergeCellCheckBox.Location = new System.Drawing.Point(229, 28);
+ this.fillMergeCellCheckBox.Margin = new System.Windows.Forms.Padding(2);
+ this.fillMergeCellCheckBox.Name = "fillMergeCellCheckBox";
+ this.fillMergeCellCheckBox.Size = new System.Drawing.Size(228, 16);
+ this.fillMergeCellCheckBox.TabIndex = 9;
+ this.fillMergeCellCheckBox.Text = "fill merged cells by left-top cell";
+ this.fillMergeCellCheckBox.UseVisualStyleBackColor = true;
+ //
+ // Form1
+ //
+ this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 12F);
+ this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
+ this.ClientSize = new System.Drawing.Size(887, 530);
+ this.Controls.Add(this.fillMergeCellCheckBox);
+ this.Controls.Add(this.label1);
+ this.Controls.Add(this.statusStrip1);
+ this.Controls.Add(this.firstRowNamesCheckBox);
+ this.Controls.Add(this.Sheet);
+ this.Controls.Add(this.sheetCombo);
+ this.Controls.Add(this.dataGridView1);
+ this.Controls.Add(this.button2);
+ this.Controls.Add(this.textBox1);
+ this.Controls.Add(this.button1);
+ this.Margin = new System.Windows.Forms.Padding(2);
+ this.Name = "Form1";
+ this.Text = "Form1";
+ ((System.ComponentModel.ISupportInitialize)(this.dataSet1)).EndInit();
+ ((System.ComponentModel.ISupportInitialize)(this.dataGridView1)).EndInit();
+ this.statusStrip1.ResumeLayout(false);
+ this.statusStrip1.PerformLayout();
+ this.ResumeLayout(false);
+ this.PerformLayout();
}
@@ -207,4 +222,5 @@ private void InitializeComponent()
private ToolStripStatusLabel toolStripStatusLabel1;
private Label label1;
private DataSet ds;
+ private CheckBox fillMergeCellCheckBox;
}
diff --git a/src/ExcelDataReader.Sample/Form1.cs b/src/ExcelDataReader.Sample/Form1.cs
index a4205ad4..46a14ace 100644
--- a/src/ExcelDataReader.Sample/Form1.cs
+++ b/src/ExcelDataReader.Sample/Form1.cs
@@ -53,7 +53,7 @@ private void Button2Click(object sender, EventArgs e)
var sw = new Stopwatch();
sw.Start();
- using IExcelDataReader reader = string.Equals(Path.GetExtension(textBox1.Text), ".csv", StringComparison.OrdinalIgnoreCase) ?
+ using IExcelDataReader reader = string.Equals(Path.GetExtension(textBox1.Text), ".csv", StringComparison.OrdinalIgnoreCase) ?
ExcelReaderFactory.CreateCsvReader(stream) : ExcelReaderFactory.CreateReader(stream);
var openTiming = sw.ElapsedMilliseconds;
@@ -63,7 +63,8 @@ private void Button2Click(object sender, EventArgs e)
UseColumnDataType = false,
ConfigureDataTable = (tableReader) => new ExcelDataTableConfiguration()
{
- UseHeaderRow = firstRowNamesCheckBox.Checked
+ UseHeaderRow = firstRowNamesCheckBox.Checked,
+ FillMergedCellsValue = fillMergeCellCheckBox.Checked
}
});
@@ -75,7 +76,7 @@ private void Button2Click(object sender, EventArgs e)
if (tablenames.Count > 0)
sheetCombo.SelectedIndex = 0;
}
- catch (Exception ex)
+ catch (Exception ex)
{
MessageBox.Show(ex.ToString(), ex.Message, MessageBoxButtons.OK, MessageBoxIcon.Error);
}
diff --git a/src/ExcelDataReader.Sample/packages.lock.json b/src/ExcelDataReader.Sample/packages.lock.json
index 751d7d61..f570cde6 100644
--- a/src/ExcelDataReader.Sample/packages.lock.json
+++ b/src/ExcelDataReader.Sample/packages.lock.json
@@ -11,16 +11,6 @@
"Microsoft.NETFramework.ReferenceAssemblies.net462": "1.0.3"
}
},
- "Microsoft.SourceLink.GitHub": {
- "type": "Direct",
- "requested": "[8.0.0, )",
- "resolved": "8.0.0",
- "contentHash": "G5q7OqtwIyGTkeIOAc3u2ZuV/kicQaec5EaRnc0pIeSnh9LUjj+PYQrJYBURvDt7twGl2PKA7nSN0kz1Zw5bnQ==",
- "dependencies": {
- "Microsoft.Build.Tasks.Git": "8.0.0",
- "Microsoft.SourceLink.Common": "8.0.0"
- }
- },
"StyleCop.Analyzers": {
"type": "Direct",
"requested": "[1.2.0-beta.556, )",
@@ -30,21 +20,11 @@
"StyleCop.Analyzers.Unstable": "1.2.0.556"
}
},
- "Microsoft.Build.Tasks.Git": {
- "type": "Transitive",
- "resolved": "8.0.0",
- "contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ=="
- },
"Microsoft.NETFramework.ReferenceAssemblies.net462": {
"type": "Transitive",
"resolved": "1.0.3",
"contentHash": "IzAV30z22ESCeQfxP29oVf4qEo8fBGXLXSU6oacv/9Iqe6PzgHDKCaWfwMBak7bSJQM0F5boXWoZS+kChztRIQ=="
},
- "Microsoft.SourceLink.Common": {
- "type": "Transitive",
- "resolved": "8.0.0",
- "contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw=="
- },
"StyleCop.Analyzers.Unstable": {
"type": "Transitive",
"resolved": "1.2.0.556",
@@ -64,23 +44,13 @@
"exceldatareader.dataset": {
"type": "Project",
"dependencies": {
- "ExcelDataReader": "[3.8.0, )",
+ "ExcelDataReader": "[3.9.0, )",
"System.ValueTuple": "[4.5.0, )"
}
}
},
".NETFramework,Version=v4.6.2/win-x86": {},
"net8.0-windows7.0": {
- "Microsoft.SourceLink.GitHub": {
- "type": "Direct",
- "requested": "[8.0.0, )",
- "resolved": "8.0.0",
- "contentHash": "G5q7OqtwIyGTkeIOAc3u2ZuV/kicQaec5EaRnc0pIeSnh9LUjj+PYQrJYBURvDt7twGl2PKA7nSN0kz1Zw5bnQ==",
- "dependencies": {
- "Microsoft.Build.Tasks.Git": "8.0.0",
- "Microsoft.SourceLink.Common": "8.0.0"
- }
- },
"StyleCop.Analyzers": {
"type": "Direct",
"requested": "[1.2.0-beta.556, )",
@@ -90,16 +60,6 @@
"StyleCop.Analyzers.Unstable": "1.2.0.556"
}
},
- "Microsoft.Build.Tasks.Git": {
- "type": "Transitive",
- "resolved": "8.0.0",
- "contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ=="
- },
- "Microsoft.SourceLink.Common": {
- "type": "Transitive",
- "resolved": "8.0.0",
- "contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw=="
- },
"StyleCop.Analyzers.Unstable": {
"type": "Transitive",
"resolved": "1.2.0.556",
@@ -111,7 +71,7 @@
"exceldatareader.dataset": {
"type": "Project",
"dependencies": {
- "ExcelDataReader": "[3.8.0, )"
+ "ExcelDataReader": "[3.9.0, )"
}
}
},
diff --git a/src/ExcelDataReader.Tests/ExcelBinaryReaderTest.cs b/src/ExcelDataReader.Tests/ExcelBinaryReaderTest.cs
index 1531b60d..b702b050 100644
--- a/src/ExcelDataReader.Tests/ExcelBinaryReaderTest.cs
+++ b/src/ExcelDataReader.Tests/ExcelBinaryReaderTest.cs
@@ -658,7 +658,7 @@ public void GitIssue368Header()
// - Non-standard header with size=6, and version=0
// - Mixes record identifiers from different BIFF versions:
// - Uses NUMBER (BIFF3-8) and NUMBER_OLD (BIFF2) records
- // - Uses LABEL (BIFF3-5) and LABEL_OLD (BIFF2) records
+ // - Uses LABEL (BIFF3-5) and LABEL_V2 (BIFF2) records
// - Uses RK (BIFF3-5) and INTEGER (BIFF2) records
// - Uses FORMAT_V23 (BIFF2-3) and FORMAT (BIFF4-8) records
using var reader = ExcelReaderFactory.CreateBinaryReader(Configuration.GetTestWorkbook("Test_git_issue_368_header.xls"));
@@ -1093,6 +1093,71 @@ public void GitIssue642_ActiveSheet_SingleWorksheet()
Assert.That(dataSet.Tables[0].TableName, Is.EqualTo("List1"));
}
+ [Test]
+ public void GitIssue525_SstMaterializationReturnsCorrectStrings()
+ {
+ // Verify SST strings are correctly accessible after lazy caching is initialized.
+ // XlsWorkbook.ReadWorkbookGlobals() calls SST.Flush() which allocates the cache array.
+ using var reader = ExcelReaderFactory.CreateBinaryReader(Configuration.GetTestWorkbook("Test10x10.xls"));
+ reader.Read();
+ Assert.That(reader.GetString(0), Is.EqualTo("col1"));
+ Assert.That(reader.GetString(4), Is.EqualTo("col5"));
+ Assert.That(reader.GetString(9), Is.Null); // column 9 is empty in first row
+
+ // Repeated lookups of the same SST index must return the same value
+ Assert.That(reader.GetString(0), Is.EqualTo("col1"));
+
+ // Verify subsequent rows also resolve correctly
+ reader.Read();
+ Assert.That(reader.GetString(0), Is.EqualTo("10x10"));
+ }
+
+ [Test]
+ public void ClipboardDimension()
+ {
+ using var excelReader = ExcelReaderFactory.CreateBinaryReader(Configuration.GetTestWorkbook("Test_Clipboard_Biff8.xls"));
+
+ Assert.That(excelReader.Dimension.FromRow, Is.EqualTo(10));
+ Assert.That(excelReader.Dimension.ToRow, Is.EqualTo(11));
+ Assert.That(excelReader.Dimension.FromColumn, Is.EqualTo(6));
+ Assert.That(excelReader.Dimension.ToColumn, Is.EqualTo(7));
+ }
+
+ public void Read_XlsExcel20()
+ {
+ using var stream = Configuration.GetTestWorkbook(Path.Combine("xls", "SIMPLE.XLS"));
+ using var reader = OpenReader(stream);
+
+ reader.Read();
+ Assert.That(reader.GetString(0), Is.EqualTo("Value_A1"));
+ Assert.That(reader.GetString(1), Is.EqualTo("Value_B1"));
+
+ reader.Read();
+ Assert.That(reader.GetString(0), Is.EqualTo("Value_A2"));
+ Assert.That(reader.GetString(1), Is.EqualTo("Value_B2"));
+ }
+
+ [Test]
+ public void Read_XlsmExcel20()
+ {
+ using var stream = Configuration.GetTestWorkbook(Path.Combine("xls", "MACRO1.XLM"));
+ using var reader = OpenReader(stream);
+
+ reader.Read();
+ Assert.That(reader.GetValue(0), Is.EqualTo("Record1"));
+ Assert.That(reader.GetValue(2), Is.EqualTo(null));
+
+ reader.Read();
+ Assert.That(reader.GetValue(0), Is.EqualTo(double.NaN));
+ Assert.That(reader.GetValue(2), Is.EqualTo(1));
+ Assert.That(reader.GetValue(3), Is.EqualTo(3));
+
+ reader.Read();
+ Assert.That(reader.GetValue(0), Is.EqualTo(double.NaN));
+ Assert.That(reader.GetValue(2), Is.EqualTo(2));
+ Assert.That(reader.GetValue(3), Is.EqualTo(4));
+ }
+
protected override IExcelDataReader OpenReader(Stream stream, ExcelReaderConfiguration configuration = null)
{
return ExcelReaderFactory.CreateBinaryReader(stream, configuration);
diff --git a/src/ExcelDataReader.Tests/ExcelCsvReaderTest.cs b/src/ExcelDataReader.Tests/ExcelCsvReaderTest.cs
index 23f51482..645510e2 100644
--- a/src/ExcelDataReader.Tests/ExcelCsvReaderTest.cs
+++ b/src/ExcelDataReader.Tests/ExcelCsvReaderTest.cs
@@ -10,7 +10,7 @@ public class ExcelCsvReaderTest
[Test]
public void CsvCommaInQuotes()
{
- using var excelReader = ExcelReaderFactory.CreateCsvReader(Configuration.GetTestWorkbook("csv\\comma_in_quotes.csv"));
+ using var excelReader = ExcelReaderFactory.CreateCsvReader(Configuration.GetTestWorkbook(Path.Combine("csv", "comma_in_quotes.csv")));
var ds = excelReader.AsDataSet();
Assert.That(ds.Tables[0].Rows[0][0], Is.EqualTo("first"));
Assert.That(ds.Tables[0].Rows[0][1], Is.EqualTo("last"));
@@ -28,7 +28,7 @@ public void CsvCommaInQuotes()
[Test]
public void CsvEscapedQuotes()
{
- using var excelReader = ExcelReaderFactory.CreateCsvReader(Configuration.GetTestWorkbook("csv\\escaped_quotes.csv"));
+ using var excelReader = ExcelReaderFactory.CreateCsvReader(Configuration.GetTestWorkbook(Path.Combine("csv", "escaped_quotes.csv")));
var ds = excelReader.AsDataSet();
Assert.That(ds.Tables[0].Rows[0][0], Is.EqualTo("a"));
Assert.That(ds.Tables[0].Rows[0][1], Is.EqualTo("b"));
@@ -140,7 +140,7 @@ static void TestNewlines(string newLine)
[Test]
public void CsvWhitespaceNull()
{
- using var excelReader = ExcelReaderFactory.CreateCsvReader(Configuration.GetTestWorkbook("csv\\simple_whitespace_null.csv"));
+ using var excelReader = ExcelReaderFactory.CreateCsvReader(Configuration.GetTestWorkbook(Path.Combine("csv", "simple_whitespace_null.csv")));
var ds = excelReader.AsDataSet();
Assert.That(ds.Tables[0].Rows[0][0], Is.EqualTo("a")); // ignore spaces
Assert.That(ds.Tables[0].Rows[0][1], Is.EqualTo("\0b\0"));
@@ -154,11 +154,11 @@ public void CsvWhitespaceNull()
[Test]
public void CsvEncoding()
{
- TestEncoding("csv\\utf8.csv", "ʤ");
- TestEncoding("csv\\utf8_bom.csv", "ʤ");
- TestEncoding("csv\\utf16le_bom.csv", "ʤ");
- TestEncoding("csv\\utf16be_bom.csv", "ʤ");
- TestEncoding("csv\\cp1252.csv", "æøå");
+ TestEncoding(Path.Combine("csv", "utf8.csv"), "ʤ");
+ TestEncoding(Path.Combine("csv", "utf8_bom.csv"), "ʤ");
+ TestEncoding(Path.Combine("csv", "utf16le_bom.csv"), "ʤ");
+ TestEncoding(Path.Combine("csv", "utf16be_bom.csv"), "ʤ");
+ TestEncoding(Path.Combine("csv", "cp1252.csv"), "æøå");
static void TestEncoding(string workbook, string specialString)
{
@@ -186,7 +186,7 @@ public void CsvWrongEncoding()
FallbackEncoding = Encoding.UTF8
};
- using (ExcelReaderFactory.CreateCsvReader(Configuration.GetTestWorkbook("csv\\cp1252.csv"), configuration))
+ using (ExcelReaderFactory.CreateCsvReader(Configuration.GetTestWorkbook(Path.Combine("csv", "cp1252.csv")), configuration))
{
}
});
@@ -195,7 +195,7 @@ public void CsvWrongEncoding()
[Test]
public void CsvBigSheet()
{
- using var excelReader = ExcelReaderFactory.CreateCsvReader(Configuration.GetTestWorkbook("csv\\MOCK_DATA.csv"));
+ using var excelReader = ExcelReaderFactory.CreateCsvReader(Configuration.GetTestWorkbook(Path.Combine("csv", "MOCK_DATA.csv")));
var ds = excelReader.AsDataSet();
Assert.That(ds.Tables[0].Rows[0][0], Is.EqualTo("id"));
Assert.That(ds.Tables[0].Rows[1000][5], Is.EqualTo("111.4.88.155"));
@@ -249,7 +249,7 @@ public void TestNoSeparator(ExcelReaderConfiguration configuration)
[Test]
public void GitIssue323DoubleClose()
{
- using var reader = ExcelReaderFactory.CreateCsvReader(Configuration.GetTestWorkbook("csv\\MOCK_DATA.csv"));
+ using var reader = ExcelReaderFactory.CreateCsvReader(Configuration.GetTestWorkbook(Path.Combine("csv", "MOCK_DATA.csv")));
reader.Read();
reader.Close();
}
@@ -257,7 +257,7 @@ public void GitIssue323DoubleClose()
[Test]
public void GitIssue333EanQuotes()
{
- using var reader = ExcelReaderFactory.CreateCsvReader(Configuration.GetTestWorkbook("csv\\ean.txt"));
+ using var reader = ExcelReaderFactory.CreateCsvReader(Configuration.GetTestWorkbook(Path.Combine("csv", "ean.txt")));
reader.Read();
Assert.That(reader.RowCount, Is.EqualTo(2));
Assert.That(reader.FieldCount, Is.EqualTo(24));
@@ -281,7 +281,7 @@ public void GitIssue351LastLineWithoutLineFeed()
[Test]
public void ColumnWidthsTest()
{
- using var reader = ExcelReaderFactory.CreateCsvReader(Configuration.GetTestWorkbook("csv\\column_widths_test.csv"));
+ using var reader = ExcelReaderFactory.CreateCsvReader(Configuration.GetTestWorkbook(Path.Combine("csv", "column_widths_test.csv")));
reader.Read();
Assert.That(reader.GetColumnWidth(0), Is.EqualTo(8.43));
Assert.That(reader.GetColumnWidth(1), Is.EqualTo(8.43));
@@ -304,7 +304,7 @@ public void CsvDisposed()
{
// Verify the file stream is closed and disposed by the reader
{
- var stream = Configuration.GetTestWorkbook("csv\\MOCK_DATA.csv");
+ var stream = Configuration.GetTestWorkbook(Path.Combine("csv", "MOCK_DATA.csv"));
using (IExcelDataReader excelReader = ExcelReaderFactory.CreateCsvReader(stream))
{
_ = excelReader.AsDataSet();
@@ -319,7 +319,7 @@ public void CsvLeaveOpen()
{
// Verify the file stream is not disposed by the reader
{
- var stream = Configuration.GetTestWorkbook("csv\\MOCK_DATA.csv");
+ var stream = Configuration.GetTestWorkbook(Path.Combine("csv", "MOCK_DATA.csv"));
using (IExcelDataReader excelReader = ExcelReaderFactory.CreateCsvReader(stream, new ExcelReaderConfiguration()
{
LeaveOpen = true
@@ -338,7 +338,7 @@ public void CsvLeaveOpen()
public void CsvRowCountAnalyzeRowsThrows()
{
{
- var stream = Configuration.GetTestWorkbook("csv\\MOCK_DATA.csv");
+ var stream = Configuration.GetTestWorkbook(Path.Combine("csv", "MOCK_DATA.csv"));
using IExcelDataReader reader = ExcelReaderFactory.CreateCsvReader(stream, new ExcelReaderConfiguration()
{
AnalyzeInitialCsvRows = 100
@@ -353,7 +353,7 @@ public void CsvRowCountAnalyzeRowsThrows()
[Test]
public void GitIssue578()
{
- var stream = Configuration.GetTestWorkbook(@"csv\Test_git_issue578.csv");
+ var stream = Configuration.GetTestWorkbook(Path.Combine("csv", "Test_git_issue578.csv"));
using IExcelDataReader excelReader = ExcelReaderFactory.CreateCsvReader(stream, new ExcelReaderConfiguration());
excelReader.Read();
var values = new object[excelReader.FieldCount];
@@ -467,7 +467,7 @@ public void GitIssue463()
[Test]
public void GitIssue642_ActiveSheet()
{
- using var reader = ExcelReaderFactory.CreateCsvReader(Configuration.GetTestWorkbook("csv\\MOCK_DATA.csv"));
+ using var reader = ExcelReaderFactory.CreateCsvReader(Configuration.GetTestWorkbook(Path.Combine("csv", "MOCK_DATA.csv")));
var dataSet = reader.AsDataSet(new ExcelDataSetConfiguration()
{
FilterSheet = (tableReader, sheetIndex) => tableReader.IsActiveSheet
@@ -562,7 +562,7 @@ public void GitIssue566_ParseCsvWithoutTrimmingWhiteSpace()
TrimWhiteSpace = false,
};
- using var excelReader = ExcelReaderFactory.CreateCsvReader(Configuration.GetTestWorkbook("csv\\test_issue_566.csv"), keepSpaceConfig);
+ using var excelReader = ExcelReaderFactory.CreateCsvReader(Configuration.GetTestWorkbook(Path.Combine("csv", "test_issue_566.csv")), keepSpaceConfig);
var ds = excelReader.AsDataSet();
Assert.That(ds.Tables[0].Rows[0][0], Is.EqualTo("c1"));
diff --git a/src/ExcelDataReader.Tests/ExcelDataReader.Tests.csproj b/src/ExcelDataReader.Tests/ExcelDataReader.Tests.csproj
index 4d442380..10862905 100644
--- a/src/ExcelDataReader.Tests/ExcelDataReader.Tests.csproj
+++ b/src/ExcelDataReader.Tests/ExcelDataReader.Tests.csproj
@@ -2,7 +2,8 @@
ExcelDataReader.Tests
- net462;net8.0
+ net8.0
+ $(TargetFrameworks);net462
ExcelDataReader.Tests
ExcelDataReader.Tests
true
diff --git a/src/ExcelDataReader.Tests/ExcelOpenXmlBinaryReaderTest.cs b/src/ExcelDataReader.Tests/ExcelOpenXmlBinaryReaderTest.cs
index 06e15562..aa171816 100644
--- a/src/ExcelDataReader.Tests/ExcelOpenXmlBinaryReaderTest.cs
+++ b/src/ExcelDataReader.Tests/ExcelOpenXmlBinaryReaderTest.cs
@@ -38,6 +38,17 @@ public void GitIssue642_ActiveSheet_SingleWorksheet()
Assert.That(dataSet.Tables[0].TableName, Is.EqualTo("List1"));
}
+ [Test]
+ public void ClipboardDimension()
+ {
+ using var excelReader = OpenReader("Test_Clipboard_Biff12");
+
+ Assert.That(excelReader.Dimension.FromRow, Is.EqualTo(10));
+ Assert.That(excelReader.Dimension.ToRow, Is.EqualTo(11));
+ Assert.That(excelReader.Dimension.FromColumn, Is.EqualTo(6));
+ Assert.That(excelReader.Dimension.ToColumn, Is.EqualTo(7));
+ }
+
///
protected override Stream OpenStream(string name)
{
diff --git a/src/ExcelDataReader.Tests/ExcelOpenXmlReaderBase.cs b/src/ExcelDataReader.Tests/ExcelOpenXmlReaderBase.cs
index a1ba31db..d9fef677 100644
--- a/src/ExcelDataReader.Tests/ExcelOpenXmlReaderBase.cs
+++ b/src/ExcelDataReader.Tests/ExcelOpenXmlReaderBase.cs
@@ -4,6 +4,21 @@ namespace ExcelDataReader.Tests;
public abstract class ExcelOpenXmlReaderBase : ExcelTestBase
{
+ [Test]
+ public void GitIssue525_XlsxSstPreallocationReturnsCorrectStrings()
+ {
+ // Verify that pre-allocating XlsxSST based on uniqueCount doesn't break SST string resolution.
+ // XlsxWorkbook.ReadSharedStrings() sets SST.Capacity = uniqueCount from
+ using var reader = OpenReader("Test10x10");
+ reader.Read();
+ Assert.That(reader.GetString(0), Is.EqualTo("col1"));
+ Assert.That(reader.GetString(4), Is.EqualTo("col5"));
+ Assert.That(reader.GetString(9), Is.Null); // column 9 is empty in first row
+
+ reader.Read();
+ Assert.That(reader.GetString(0), Is.EqualTo("10x10"));
+ }
+
[Test]
public void GitIssue14InvalidOADate()
{
diff --git a/src/ExcelDataReader.Tests/ExcelOpenXmlReaderTest.cs b/src/ExcelDataReader.Tests/ExcelOpenXmlReaderTest.cs
index e029c3a5..e87252ab 100644
--- a/src/ExcelDataReader.Tests/ExcelOpenXmlReaderTest.cs
+++ b/src/ExcelDataReader.Tests/ExcelOpenXmlReaderTest.cs
@@ -552,6 +552,98 @@ public void GitIssue711_RowHeightParsing(string filename)
Assert.That(actualRowsHeights, Is.EqualTo(expectedRowHeights));
}
+ [Test]
+ public void GitIssue461_Format14WithEnUsCultureReturnsCorrectFormatString()
+ {
+ using var stream = Configuration.GetTestWorkbook("Test_git_issue_461.xlsx");
+ using var reader = ExcelReaderFactory.CreateOpenXmlReader(stream, new ExcelReaderConfiguration
+ {
+ Culture = new System.Globalization.CultureInfo("en-US"),
+ });
+
+ Assert.That(reader.Read(), Is.True);
+ Assert.That(reader.GetNumberFormatString(0), Is.EqualTo("m/d/yyyy"));
+ }
+
+ [Test]
+ public void GitIssue461_Format14WithEnGbCultureReturnsCorrectFormatString()
+ {
+ using var stream = Configuration.GetTestWorkbook("Test_git_issue_461.xlsx");
+ using var reader = ExcelReaderFactory.CreateOpenXmlReader(stream, new ExcelReaderConfiguration
+ {
+ Culture = new System.Globalization.CultureInfo("en-GB"),
+ });
+
+ Assert.That(reader.Read(), Is.True);
+ Assert.That(reader.GetNumberFormatString(0), Is.EqualTo("dd/mm/yyyy"));
+ }
+
+ [Test]
+ public void GitIssue461_Format14DefaultBehaviorUnchanged()
+ {
+ using var stream = Configuration.GetTestWorkbook("Test_git_issue_461.xlsx");
+ using var reader = ExcelReaderFactory.CreateOpenXmlReader(stream);
+
+ Assert.That(reader.Read(), Is.True);
+ Assert.That(reader.GetNumberFormatString(0), Is.EqualTo("d/m/yyyy"));
+ }
+
+ [Test]
+ public void GitIssue461_Formats15To17WithEnUsCultureUsesSlashSeparator()
+ {
+ using var stream = Configuration.GetTestWorkbook("Test_git_issue_461.xlsx");
+ using var reader = ExcelReaderFactory.CreateOpenXmlReader(stream, new ExcelReaderConfiguration
+ {
+ Culture = new System.Globalization.CultureInfo("en-US"),
+ });
+
+ Assert.That(reader.Read(), Is.True);
+ Assert.That(reader.GetNumberFormatString(1), Is.EqualTo("d/mmm/yy")); // format 15
+ Assert.That(reader.GetNumberFormatString(2), Is.EqualTo("d/mmm")); // format 16
+ Assert.That(reader.GetNumberFormatString(3), Is.EqualTo("mmm/yy")); // format 17
+ }
+
+ [Test]
+ public void GitIssue461_Formats15To17WithDeDeCultureUsesDotSeparator()
+ {
+ using var stream = Configuration.GetTestWorkbook("Test_git_issue_461.xlsx");
+ using var reader = ExcelReaderFactory.CreateOpenXmlReader(stream, new ExcelReaderConfiguration
+ {
+ Culture = new System.Globalization.CultureInfo("de-DE"),
+ });
+
+ Assert.That(reader.Read(), Is.True);
+ Assert.That(reader.GetNumberFormatString(1), Is.EqualTo("d.mmm.yy")); // format 15
+ Assert.That(reader.GetNumberFormatString(2), Is.EqualTo("d.mmm")); // format 16
+ Assert.That(reader.GetNumberFormatString(3), Is.EqualTo("mmm.yy")); // format 17
+ }
+
+ [Test]
+ public void GitIssue461_Formats15To17DefaultBehaviorUnchanged()
+ {
+ using var stream = Configuration.GetTestWorkbook("Test_git_issue_461.xlsx");
+ using var reader = ExcelReaderFactory.CreateOpenXmlReader(stream);
+
+ Assert.That(reader.Read(), Is.True);
+ Assert.That(reader.GetNumberFormatString(1), Is.EqualTo("d-mmm-yy")); // format 15
+ Assert.That(reader.GetNumberFormatString(2), Is.EqualTo("d-mmm")); // format 16
+ Assert.That(reader.GetNumberFormatString(3), Is.EqualTo("mmm-yy")); // format 17
+ }
+
+ [Test]
+ public void GitIssue461_CellValueIsDateTimeRegardlessOfCulture()
+ {
+ // Whether culture is set or not, cells with format 14 should be returned as DateTime
+ using var stream = Configuration.GetTestWorkbook("Test_git_issue_461.xlsx");
+ using var reader = ExcelReaderFactory.CreateOpenXmlReader(stream, new ExcelReaderConfiguration
+ {
+ Culture = new System.Globalization.CultureInfo("en-US"),
+ });
+
+ Assert.That(reader.Read(), Is.True);
+ Assert.That(reader.GetValue(0), Is.EqualTo(new DateTime(2023, 1, 1)));
+ }
+
protected override IExcelDataReader OpenReader(Stream stream, ExcelReaderConfiguration configuration = null)
=> ExcelReaderFactory.CreateOpenXmlReader(stream, configuration);
diff --git a/src/ExcelDataReader.Tests/ExcelOpenXmlStrictReaderTest.cs b/src/ExcelDataReader.Tests/ExcelOpenXmlStrictReaderTest.cs
index 22c45f44..565cec0f 100644
--- a/src/ExcelDataReader.Tests/ExcelOpenXmlStrictReaderTest.cs
+++ b/src/ExcelDataReader.Tests/ExcelOpenXmlStrictReaderTest.cs
@@ -29,6 +29,6 @@ protected override IExcelDataReader OpenReader(Stream stream, ExcelReaderConfigu
protected override Stream OpenStream(string name)
{
- return Configuration.GetTestWorkbook("strict\\" + name + ".xlsx");
+ return Configuration.GetTestWorkbook(Path.Combine("strict", name + ".xlsx"));
}
}
\ No newline at end of file
diff --git a/src/ExcelDataReader.Tests/ExcelTestBase.cs b/src/ExcelDataReader.Tests/ExcelTestBase.cs
index d198a889..18a9d6a5 100644
--- a/src/ExcelDataReader.Tests/ExcelTestBase.cs
+++ b/src/ExcelDataReader.Tests/ExcelTestBase.cs
@@ -159,7 +159,7 @@ public void MergedCells()
using var excelReader = OpenReader("Test_MergedCell");
excelReader.Read();
- Assert.That(excelReader.MergeCells, Is.EquivalentTo(new[]
+ Assert.That(excelReader.MergeCells, Is.EquivalentTo(new[]
{
new[] { 1, 2, 0, 1 },
new[] { 1, 5, 2, 2 },
@@ -192,7 +192,7 @@ public void OpenXmlLeaveOpen()
public void RowHeight()
{
using var reader = OpenReader("CollapsedHide");
-
+
// Verify the row heights are set when expected, and converted to points from twips
reader.Read();
Assert.That(reader.RowHeight, Is.EqualTo(15));
@@ -298,7 +298,7 @@ public void GitIssue82Date1900()
// 15/06/2009
// 4/19/2013 (=TODAY() when file was saved)
using var excelReader = OpenReader("roo_1900_base");
-
+
DataSet result = excelReader.AsDataSet();
Assert.That((DateTime)result.Tables[0].Rows[0][0], Is.EqualTo(new DateTime(2009, 6, 15)));
Assert.That((DateTime)result.Tables[0].Rows[1][0], Is.EqualTo(GitIssue82TodayDate));
@@ -337,7 +337,7 @@ public void IssueDecimal1109Test()
double val2 = (double)dataSet.Tables[0].Rows[0][1];
Assert.That(val2, Is.EqualTo(val1));
}
-
+
[Test]
public void IssueEncoding1520Test()
{
@@ -402,7 +402,7 @@ public void GitIssue270EmptyRowsAtTheEnd()
Assert.That(rowCount, Is.EqualTo(65536));
}
}
-
+
[Test]
public void GitIssue283IsoFormatTimeSpan()
{
@@ -467,7 +467,7 @@ public void GitIssue329Error()
public void Issue4031NullColumn()
{
using IExcelDataReader excelReader = OpenReader("Test_Issue_4031_NullColumn");
-
+
// DataSet dataSet = excelReader.AsDataSet(true);
excelReader.Read();
Assert.That(excelReader.GetValue(0), Is.Null);
@@ -576,7 +576,7 @@ public void Issue11435Colors()
public void Issue11479BlankSheet()
{
using IExcelDataReader excelReader = OpenReader("Test_Issue_11479_BlankSheet");
-
+
// DataSet result = excelReader.AsDataSet(true);
excelReader.Read();
Assert.That(excelReader.FieldCount, Is.EqualTo(5));
@@ -994,7 +994,7 @@ public void GitIssue694ExcelTimeFormatTimeSpanFormulaInvalidResult()
[Test]
public void GitIssue574VerticalAlignment()
- {
+ {
using var reader = OpenReader("Test_git_issue_574");
reader.Read();
@@ -1003,6 +1003,101 @@ public void GitIssue574VerticalAlignment()
Assert.That(reader.GetCellStyle(2).VerticalAlignment, Is.EqualTo(VerticalAlignment.Bottom));
}
+ [Test]
+ public void GitIssue618_SinglePassMode_RowCountThrows()
+ {
+ using var reader = OpenReader(OpenStream("Test10x10"), new ExcelReaderConfiguration { SinglePassMode = true });
+ Assert.Throws(() => _ = reader.RowCount);
+ reader.Read();
+ Assert.Throws(() => _ = reader.RowCount);
+ }
+
+ [Test]
+ public void GitIssue618_SinglePassMode_AsDataSet()
+ {
+ using var reader = OpenReader(OpenStream("Test10x10"), new ExcelReaderConfiguration { SinglePassMode = true });
+ var dataSet = reader.AsDataSet();
+ Assert.That(dataSet.Tables[0].Rows.Count, Is.EqualTo(10));
+ Assert.That(dataSet.Tables[0].Columns.Count, Is.EqualTo(10));
+ Assert.That(dataSet.Tables[0].Rows[1][0], Is.EqualTo("10x10"));
+ Assert.That(dataSet.Tables[0].Rows[9][9], Is.EqualTo("10x27"));
+ }
+
+ [Test]
+ public void GitIssue618_SinglePassMode_FieldCountGrows()
+ {
+ using var reader = OpenReader(OpenStream("Test10x10"), new ExcelReaderConfiguration { SinglePassMode = true });
+ Assert.That(reader.FieldCount, Is.Zero);
+ reader.Read();
+ Assert.That(reader.FieldCount, Is.EqualTo(9));
+ while (reader.Read())
+ {
+ }
+
+ Assert.That(reader.FieldCount, Is.GreaterThanOrEqualTo(10));
+ }
+
+ public void GitIssue541BuiltinFormat55IsDate()
+ {
+ using var reader = OpenReader("Test_git_issue_541");
+ Assert.That(reader.Read(), Is.True);
+ Assert.That(reader.GetValue(0), Is.EqualTo(new DateTime(2021, 1, 15)));
+ }
+
+ public void AsDataSetTestFillEmptyCellsInMergedRangeNotUseHeaderRow()
+ {
+ using IExcelDataReader excelReader = OpenReader("Test_MergedCell");
+ DataSet result = excelReader.AsDataSet(new ExcelDataSetConfiguration
+ {
+ ConfigureDataTable = _ => new ExcelDataTableConfiguration
+ {
+ FillMergedCellsValue = true
+ }
+ });
+
+ Assert.That(result != null, Is.True);
+ Assert.That(result.Tables.Count, Is.EqualTo(1));
+ Assert.That(result.Tables[0].Rows.Count, Is.EqualTo(7));
+ Assert.That(result.Tables[0].Columns.Count, Is.EqualTo(3));
+
+ Assert.That(result.Tables[0].Rows[1][0], Is.EqualTo("Merge Cell 1"));
+ Assert.That(result.Tables[0].Rows[2][0], Is.EqualTo("Merge Cell 1"));
+ Assert.That(result.Tables[0].Rows[1][1], Is.EqualTo("Merge Cell 1"));
+ Assert.That(result.Tables[0].Rows[2][1], Is.EqualTo("Merge Cell 1"));
+ Assert.That(result.Tables[0].Rows[5][2], Is.EqualTo("Merge Cell 2"));
+ Assert.That(result.Tables[0].Rows[4][0], Is.EqualTo("Merge Cell 3"));
+ Assert.That(result.Tables[0].Rows[6][1], Is.EqualTo("Merge Cell 4"));
+ Assert.That(result.Tables[0].Rows[6][2], Is.EqualTo("Merge Cell 4"));
+ }
+
+ [Test]
+ public void AsDataSetTestFillEmptyCellsInMergedRangeUseHeaderRow()
+ {
+ using IExcelDataReader excelReader = OpenReader("Test_MergedCell");
+ DataSet result = excelReader.AsDataSet(new ExcelDataSetConfiguration
+ {
+ ConfigureDataTable = _ => new ExcelDataTableConfiguration
+ {
+ UseHeaderRow = true,
+ FillMergedCellsValue = true
+ }
+ });
+
+ Assert.That(result != null, Is.True);
+ Assert.That(result.Tables.Count, Is.EqualTo(1));
+ Assert.That(result.Tables[0].Rows.Count, Is.EqualTo(6));
+ Assert.That(result.Tables[0].Columns.Count, Is.EqualTo(3));
+
+ Assert.That(result.Tables[0].Rows[0][0], Is.EqualTo("Merge Cell 1"));
+ Assert.That(result.Tables[0].Rows[1][0], Is.EqualTo("Merge Cell 1"));
+ Assert.That(result.Tables[0].Rows[0][1], Is.EqualTo("Merge Cell 1"));
+ Assert.That(result.Tables[0].Rows[1][1], Is.EqualTo("Merge Cell 1"));
+ Assert.That(result.Tables[0].Rows[4][2], Is.EqualTo("Merge Cell 2"));
+ Assert.That(result.Tables[0].Rows[3][0], Is.EqualTo("Merge Cell 3"));
+ Assert.That(result.Tables[0].Rows[5][1], Is.EqualTo("Merge Cell 4"));
+ Assert.That(result.Tables[0].Rows[5][2], Is.EqualTo("Merge Cell 4"));
+ }
+
protected IExcelDataReader OpenReader(string name)
{
return OpenReader(OpenStream(name));
diff --git a/src/ExcelDataReader.Tests/FormatReaderTest.cs b/src/ExcelDataReader.Tests/FormatReaderTest.cs
index e5463f5c..4643e2f9 100644
--- a/src/ExcelDataReader.Tests/FormatReaderTest.cs
+++ b/src/ExcelDataReader.Tests/FormatReaderTest.cs
@@ -1,4 +1,4 @@
-using ExcelDataReader.Core.NumberFormat;
+using ExcelDataReader.Core.NumberFormat;
namespace ExcelDataReader.Tests;
@@ -487,4 +487,98 @@ static void TestValid(string format)
Assert.That(to.IsValid, Is.True, $"Invalid format: {format}");
}
}
+
+ [Test]
+ public void GitIssue541LocaleFormatStringsDetectedAsDate()
+ {
+ // Locale-specific format strings containing unquoted CJK / Thai characters
+ // should be detected as date/time after the implicit-literal parser fix.
+
+ // ja-jp formats (unquoted characters)
+ Assert.That(IsDate("[$-411]ge.m.d"), Is.True);
+ Assert.That(IsDate("[$-411]ggge年m月d日"), Is.True);
+ Assert.That(IsDate("[$-0411]YYYY 年 M 月"), Is.True);
+
+ // zh-tw / zh-cn time formats (AM/PM equivalent followed by hour/minute)
+ Assert.That(IsDate("上午/下午hh\"時\"mm\"分\""), Is.True);
+ Assert.That(IsDate("上午/下午h\"时\"mm\"分\""), Is.True);
+
+ static bool IsDate(string formatString)
+ {
+ var format = new NumberFormatString(formatString);
+ return format.IsDateTimeFormat;
+ }
+ }
+
+ [Test]
+ public void GitIssue541BuiltinFormatIndicesDetectedAsDate()
+ {
+ // Verify that the representative format strings stored for locale-specific built-in
+ // indices are each detected as date/time (i.e. IsDateTimeFormat = true).
+ // These are the ja-jp format strings used as fallbacks for indices 27-36 and 50-58,
+ // and the ASCII equivalents used for Thai date indices 71-81.
+ string[] expectedDateFormatStrings =
+ [
+ @"[$-411]ge.m.d", // 27, 36, 50, 57
+ "[$-411]ggge\"年\"m\"月\"d\"日\"", // 28, 29, 51, 54, 58
+ "m/d/yy", // 30
+ "yyyy\"年\"m\"月\"d\"日\"", // 31
+ "h\"時\"mm\"分\"", // 32
+ "h\"時\"mm\"分\"ss\"秒\"", // 33
+ "yyyy\"年\"m\"月\"", // 34, 52, 55
+ "m\"月\"d\"日\"", // 35, 53, 56
+ "d/m/yyyy", // 71
+ "d-mmm-yy", // 72
+ "d-mmm", // 73
+ "mmm-yy", // 74
+ "h:mm", // 75
+ "h:mm:ss", // 76
+ "d/m/yyyy h:mm", // 77
+ "mm:ss", // 78
+ "mm:ss.0", // 80
+ "d/m/yyyy", // 81
+ ];
+
+ foreach (var formatString in expectedDateFormatStrings)
+ {
+ var format = new NumberFormatString(formatString);
+ Assert.That(format.IsDateTimeFormat, Is.True, $"Format string '{formatString}' not detected as date/time");
+ }
+
+ // Thai format 79 uses [h]:mm:ss (elapsed time) → IsTimeSpanFormat, not IsDateTimeFormat
+ var format79 = new NumberFormatString("[h]:mm:ss");
+ Assert.That(format79.IsTimeSpanFormat, Is.True, "Format '[h]:mm:ss' not detected as TimeSpan");
+
+ // Thai number formats (indices 59-62, 67-70) are NOT date formats.
+ string[] expectedNumberFormatStrings = ["0", "0.00", "#,##0", "#,##0.00", "0%", "0.00%", "# ?/?", "# ??/??"];
+ foreach (var formatString in expectedNumberFormatStrings)
+ {
+ var format = new NumberFormatString(formatString);
+ Assert.That(format.IsDateTimeFormat, Is.False, $"Format string '{formatString}' incorrectly detected as date/time");
+ }
+ }
+
+ ///
+ /// Verifies that the format strings produced by the culture-specific conversion (for locales such
+ /// as en-US and en-GB) are themselves valid date/time format strings that ExcelDataReader will
+ /// recognise as date values rather than raw doubles.
+ ///
+ [Test]
+ public void GitIssue461_CultureDerivedFormatStringsAreValidDateFormats()
+ {
+ // Expected outputs of DatePatternToExcel / TimePatternToExcel for common locales
+ Assert.That(IsDateFormatString("m/d/yyyy"), Is.True, "en-US short date (format 14)");
+ Assert.That(IsDateFormatString("dd/mm/yyyy"), Is.True, "en-GB short date (format 14)");
+ Assert.That(IsDateFormatString("dd.mm.yyyy"), Is.True, "de-DE short date (format 14)");
+ Assert.That(IsDateFormatString("d/mmm/yy"), Is.True, "en-US format 15");
+ Assert.That(IsDateFormatString("d.mmm.yy"), Is.True, "de-DE format 15");
+ Assert.That(IsDateFormatString("d/mmm"), Is.True, "en-US format 16");
+ Assert.That(IsDateFormatString("mmm/yy"), Is.True, "en-US format 17");
+ Assert.That(IsDateFormatString("m/d/yyyy h:mm AM/PM"), Is.True, "en-US short date+time (format 22)");
+ Assert.That(IsDateFormatString("dd/mm/yyyy hh:mm"), Is.True, "en-GB short date+time (format 22)");
+ Assert.That(IsDateFormatString("dd.mm.yyyy hh:mm"), Is.True, "de-DE short date+time (format 22)");
+
+ static bool IsDateFormatString(string formatString) =>
+ new NumberFormatString(formatString).IsDateTimeFormat;
+ }
}
diff --git a/src/ExcelDataReader.Tests/packages.lock.json b/src/ExcelDataReader.Tests/packages.lock.json
index aa77734c..40662d09 100644
--- a/src/ExcelDataReader.Tests/packages.lock.json
+++ b/src/ExcelDataReader.Tests/packages.lock.json
@@ -20,16 +20,6 @@
"Microsoft.NETFramework.ReferenceAssemblies.net462": "1.0.3"
}
},
- "Microsoft.SourceLink.GitHub": {
- "type": "Direct",
- "requested": "[8.0.0, )",
- "resolved": "8.0.0",
- "contentHash": "G5q7OqtwIyGTkeIOAc3u2ZuV/kicQaec5EaRnc0pIeSnh9LUjj+PYQrJYBURvDt7twGl2PKA7nSN0kz1Zw5bnQ==",
- "dependencies": {
- "Microsoft.Build.Tasks.Git": "8.0.0",
- "Microsoft.SourceLink.Common": "8.0.0"
- }
- },
"NUnit": {
"type": "Direct",
"requested": "[4.4.0, )",
@@ -73,11 +63,6 @@
"System.Diagnostics.DiagnosticSource": "5.0.0"
}
},
- "Microsoft.Build.Tasks.Git": {
- "type": "Transitive",
- "resolved": "8.0.0",
- "contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ=="
- },
"Microsoft.CodeCoverage": {
"type": "Transitive",
"resolved": "17.14.1",
@@ -88,11 +73,6 @@
"resolved": "1.0.3",
"contentHash": "IzAV30z22ESCeQfxP29oVf4qEo8fBGXLXSU6oacv/9Iqe6PzgHDKCaWfwMBak7bSJQM0F5boXWoZS+kChztRIQ=="
},
- "Microsoft.SourceLink.Common": {
- "type": "Transitive",
- "resolved": "8.0.0",
- "contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw=="
- },
"Microsoft.Testing.Extensions.Telemetry": {
"type": "Transitive",
"resolved": "1.7.3",
@@ -222,7 +202,7 @@
"exceldatareader.dataset": {
"type": "Project",
"dependencies": {
- "ExcelDataReader": "[3.8.0, )",
+ "ExcelDataReader": "[3.9.0, )",
"System.ValueTuple": "[4.5.0, )"
}
}
@@ -238,16 +218,6 @@
"Microsoft.TestPlatform.TestHost": "17.14.1"
}
},
- "Microsoft.SourceLink.GitHub": {
- "type": "Direct",
- "requested": "[8.0.0, )",
- "resolved": "8.0.0",
- "contentHash": "G5q7OqtwIyGTkeIOAc3u2ZuV/kicQaec5EaRnc0pIeSnh9LUjj+PYQrJYBURvDt7twGl2PKA7nSN0kz1Zw5bnQ==",
- "dependencies": {
- "Microsoft.Build.Tasks.Git": "8.0.0",
- "Microsoft.SourceLink.Common": "8.0.0"
- }
- },
"NUnit": {
"type": "Direct",
"requested": "[4.4.0, )",
@@ -287,21 +257,11 @@
"System.Diagnostics.DiagnosticSource": "5.0.0"
}
},
- "Microsoft.Build.Tasks.Git": {
- "type": "Transitive",
- "resolved": "8.0.0",
- "contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ=="
- },
"Microsoft.CodeCoverage": {
"type": "Transitive",
"resolved": "17.14.1",
"contentHash": "pmTrhfFIoplzFVbhVwUquT+77CbGH+h4/3mBpdmIlYtBi9nAB+kKI6dN3A/nV4DFi3wLLx/BlHIPK+MkbQ6Tpg=="
},
- "Microsoft.SourceLink.Common": {
- "type": "Transitive",
- "resolved": "8.0.0",
- "contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw=="
- },
"Microsoft.Testing.Extensions.Telemetry": {
"type": "Transitive",
"resolved": "1.7.3",
@@ -400,7 +360,7 @@
"exceldatareader.dataset": {
"type": "Project",
"dependencies": {
- "ExcelDataReader": "[3.8.0, )"
+ "ExcelDataReader": "[3.9.0, )"
}
}
}
diff --git a/src/ExcelDataReader/Core/BinaryFormat/Enums.cs b/src/ExcelDataReader/Core/BinaryFormat/Enums.cs
index b4ea01e8..b612016d 100644
--- a/src/ExcelDataReader/Core/BinaryFormat/Enums.cs
+++ b/src/ExcelDataReader/Core/BinaryFormat/Enums.cs
@@ -8,15 +8,26 @@ internal enum BIFFTYPE : ushort
Worksheet = 0x0010,
Chart = 0x0020,
#pragma warning disable SA1300 // Element must begin with upper-case letter
- v4MacroSheet = 0x0040,
+ MacroSheet = 0x0040,
v4WorkbookGlobals = 0x0100
#pragma warning restore SA1300 // Element must begin with upper-case letter
}
internal enum BIFFRECORDTYPE : ushort
{
- INTERFACEHDR = 0x00E1,
+ EXTERNCOUNT = 0x0016,
+ EXTERNSHEET = 0x0017,
+ DEFINEDNAME_V2 = 0x0018, // BIFF2. Defined name record.
+ BUILTINFMTCOUNT_V2 = 0x001F, // BIFF2. Number of number of following FORMAT records that contain built-in number format.
+ WINDOW2_V2 = 0x003E, // BIFF2. Window 2 record.
+ CRN = 0x005A,
+ FILESHARING = 0x005B,
+ WRITEPROTECT = 0x0086,
+ UNKNOWN191 = 0x00BF, // Not documented.
+ UNKNOWN192 = 0x00C0, // Not documented.
MMS = 0x00C1,
+ OBPROJ = 0x00D3,
+ INTERFACEHDR = 0x00E1,
MERGECELLS = 0x00E5, // Record containing list of merged cell ranges
INTERFACEEND = 0x00E2,
WRITEACCESS = 0x005C,
@@ -24,6 +35,11 @@ internal enum BIFFRECORDTYPE : ushort
DSF = 0x0161,
TABID = 0x013D,
FNGROUPCOUNT = 0x009C,
+ COLWIDTH = 0x0024,
+ LEFTMARGIN = 0x0026,
+ RIGHTMARGIN = 0x0027,
+ TOPMARGIN = 0x0028,
+ BOTTOMMARGIN = 0x0029,
FILEPASS = 0x002F,
WINDOWPROTECT = 0x0019,
PROTECT = 0x0012,
@@ -33,12 +49,15 @@ internal enum BIFFRECORDTYPE : ushort
WINDOW1 = 0x003D,
BACKUP = 0x0040,
HIDEOBJ = 0x008D,
- RECORD1904 = 0x0022,
+ PALETTE = 0x0092,
+ DATE1904 = 0x0022,
REFRESHALL = 0x01B7,
BOOKBOOL = 0x00DA,
FONT = 0x0031, // Font record, BIFF2, 5 and later
+ FONT2 = 0x0032, // Font record, BIFF2, unknown usage.
+
FONT_V34 = 0x0231, // Font record, BIFF3, 4
FORMAT = 0x041E, // Format record, BIFF4 and later
@@ -55,6 +74,8 @@ internal enum BIFFRECORDTYPE : ushort
IXFE = 0x0044, // Index to XF, BIFF2
+ BUILTINFMTCOUNT = 0x0056, // BIFF3+. Number of number of following FORMAT records that contain built-in number format.
+
STYLE = 0x0293,
BOUNDSHEET = 0x0085,
COUNTRY = 0x008C,
@@ -71,7 +92,7 @@ internal enum BIFFRECORDTYPE : ushort
BOF_V4 = 0x0409, // BOF Id for BIFF4
EOF = 0x000A, // End of block started with BOF
-
+ INDEX_V2 = 0x000B, // Index record for BIFF2
CALCCOUNT = 0x000C,
CALCMODE = 0x000D,
PRECISION = 0x000E,
@@ -91,15 +112,17 @@ internal enum BIFFRECORDTYPE : ushort
HCENTER = 0x0083,
VCENTER = 0x0084,
PRINTSETUP = 0x00A1,
- DFAULTCOLWIDTH = 0x0055,
+ DEFAULTCOLWIDTH = 0x0055,
DIMENSIONS = 0x0200, // Size of area used for data
DIMENSIONS_V2 = 0x0000, // BIFF2
ROW_V2 = 0x0008, // Row record
ROW = 0x0208, // Row record
- WINDOW2 = 0x023E,
SELECTION = 0x001D,
+ OBNOMACROS = 0x1BD,
+ EXCEL9FILE = 0x01C0,
+ RECALCID = 0x01C1,
INDEX = 0x020B, // Index record, unsure about signature
DBCELL = 0x00D7, // DBCell record, unsure about signature
@@ -120,7 +143,7 @@ internal enum BIFFRECORDTYPE : ushort
LABEL = 0x0204, // String cell (up to 255 symbols)
- LABEL_OLD = 0x0004, // String cell (up to 255 symbols), old format
+ LABEL_V2 = 0x0004, // String cell (up to 255 symbols), old format
LABELSST = 0x00FD, // String cell with value from SST (for BIFF8)
@@ -184,7 +207,7 @@ internal enum BIFFRECORDTYPE : ushort
XL5MODIFY = 0x0162,
OBJ = 0x005D,
NOTE = 0x001C,
-
+
// SXEXT = 0x00DC,
VERTICALPAGEBREAKS = 0x001A,
XCT = 0x0059,
@@ -195,5 +218,22 @@ internal enum BIFFRECORDTYPE : ushort
///
UNCALCED = 0x005E,
QUICKTIP = 0x0800,
- COLINFO = 0x007D
+ COLINFO = 0x007D,
+ DEFINEDNAME = 0x0218, // Defined name record.
+ WINDOW2 = 0x023E, // BIFF3+. Window 2 record.
+ BOOKEXT = 0x0863,
+ HFPICTURE = 0x0866,
+ XFCRC = 0x087C,
+ XFEXT = 0x087D,
+ COMPAT12 = 0x088C,
+ STYLEEXT = 0x892,
+ THEME = 0x0896,
+ GUIDTYPELIB = 0x0897,
+ DXF = 0x088D,
+ TABLESTYLES = 0x88E,
+ MTRSETTINGS = 0x089A,
+ COMPRESSPICTURES = 0x089B,
+ FORCEFULLCALCULATION = 0x08A3,
+ UNKNOWN2262 = 0x08D6, // Not documented.
+ CRTCLIENT = 0x105C
}
diff --git a/src/ExcelDataReader/Core/BinaryFormat/XlsBiffBOF.cs b/src/ExcelDataReader/Core/BinaryFormat/XlsBiffBOF.cs
index a407d7ec..33d949a6 100644
--- a/src/ExcelDataReader/Core/BinaryFormat/XlsBiffBOF.cs
+++ b/src/ExcelDataReader/Core/BinaryFormat/XlsBiffBOF.cs
@@ -19,60 +19,4 @@ internal XlsBiffBOF(byte[] bytes)
/// Gets the type of the BIFF block.
///
public BIFFTYPE Type => (BIFFTYPE)ReadUInt16(0x2);
-
- ///
- /// Gets the creation Id.
- ///
- /// Not used before BIFF5.
- public ushort CreationId
- {
- get
- {
- if (RecordSize < 6)
- return 0;
- return ReadUInt16(0x4);
- }
- }
-
- ///
- /// Gets the creation year.
- ///
- /// Not used before BIFF5.
- public ushort CreationYear
- {
- get
- {
- if (RecordSize < 8)
- return 0;
- return ReadUInt16(0x6);
- }
- }
-
- ///
- /// Gets the file history flag.
- ///
- /// Not used before BIFF8.
- public uint HistoryFlag
- {
- get
- {
- if (RecordSize < 12)
- return 0;
- return ReadUInt32(0x8);
- }
- }
-
- ///
- /// Gets the minimum Excel version to open this file.
- ///
- /// Not used before BIFF8.
- public uint MinVersionToOpen
- {
- get
- {
- if (RecordSize < 16)
- return 0;
- return ReadUInt32(0xC);
- }
- }
}
\ No newline at end of file
diff --git a/src/ExcelDataReader/Core/BinaryFormat/XlsBiffBlankCell.cs b/src/ExcelDataReader/Core/BinaryFormat/XlsBiffBlankCell.cs
index 60361eb6..32aea75b 100644
--- a/src/ExcelDataReader/Core/BinaryFormat/XlsBiffBlankCell.cs
+++ b/src/ExcelDataReader/Core/BinaryFormat/XlsBiffBlankCell.cs
@@ -38,7 +38,7 @@ internal XlsBiffBlankCell(byte[] bytes)
///
public bool IsBiff2Cell => Id switch
{
- BIFFRECORDTYPE.NUMBER_OLD or BIFFRECORDTYPE.INTEGER_OLD or BIFFRECORDTYPE.LABEL_OLD or BIFFRECORDTYPE.BLANK_OLD or BIFFRECORDTYPE.BOOLERR_OLD => true,
+ BIFFRECORDTYPE.NUMBER_OLD or BIFFRECORDTYPE.INTEGER_OLD or BIFFRECORDTYPE.LABEL_V2 or BIFFRECORDTYPE.BLANK_OLD or BIFFRECORDTYPE.BOOLERR_OLD => true,
_ => false,
};
}
\ No newline at end of file
diff --git a/src/ExcelDataReader/Core/BinaryFormat/XlsBiffLabelCell.cs b/src/ExcelDataReader/Core/BinaryFormat/XlsBiffLabelCell.cs
index 62e55c1f..aa313941 100644
--- a/src/ExcelDataReader/Core/BinaryFormat/XlsBiffLabelCell.cs
+++ b/src/ExcelDataReader/Core/BinaryFormat/XlsBiffLabelCell.cs
@@ -13,7 +13,7 @@ internal sealed class XlsBiffLabelCell : XlsBiffBlankCell
internal XlsBiffLabelCell(byte[] bytes, int biffVersion)
: base(bytes)
{
- if (Id == BIFFRECORDTYPE.LABEL_OLD)
+ if (Id == BIFFRECORDTYPE.LABEL_V2)
{
// BIFF2
_xlsString = new XlsShortByteString(bytes, ContentOffset + 7);
diff --git a/src/ExcelDataReader/Core/BinaryFormat/XlsBiffMSODrawing.cs b/src/ExcelDataReader/Core/BinaryFormat/XlsBiffMSODrawing.cs
deleted file mode 100644
index 34c60aba..00000000
--- a/src/ExcelDataReader/Core/BinaryFormat/XlsBiffMSODrawing.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-namespace ExcelDataReader.Core.BinaryFormat;
-
-///
-/// Represents MSO Drawing record.
-///
-internal sealed class XlsBiffMSODrawing : XlsBiffRecord
-{
- internal XlsBiffMSODrawing(byte[] bytes)
- : base(bytes)
- {
- }
-}
diff --git a/src/ExcelDataReader/Core/BinaryFormat/XlsBiffMulBlankCell.cs b/src/ExcelDataReader/Core/BinaryFormat/XlsBiffMulBlankCell.cs
deleted file mode 100644
index ae69bb38..00000000
--- a/src/ExcelDataReader/Core/BinaryFormat/XlsBiffMulBlankCell.cs
+++ /dev/null
@@ -1,32 +0,0 @@
-namespace ExcelDataReader.Core.BinaryFormat;
-
-///
-/// Represents multiple Blank cell.
-///
-internal sealed class XlsBiffMulBlankCell : XlsBiffBlankCell
-{
- internal XlsBiffMulBlankCell(byte[] bytes)
- : base(bytes)
- {
- }
-
- public override bool IsEmpty => true;
-
- ///
- /// Gets the zero-based index of last described column.
- ///
- public ushort LastColumnIndex => ReadUInt16(RecordSize - 2);
-
- ///
- /// Returns format forspecified column, column must be between ColumnIndex and LastColumnIndex.
- ///
- /// Index of column.
- /// Format.
- public ushort GetXF(ushort columnIdx)
- {
- int ofs = 4 + 6 * (columnIdx - ColumnIndex);
- if (ofs > RecordSize - 2)
- return 0;
- return ReadUInt16(ofs);
- }
-}
\ No newline at end of file
diff --git a/src/ExcelDataReader/Core/BinaryFormat/XlsBiffQuickTip.cs b/src/ExcelDataReader/Core/BinaryFormat/XlsBiffQuickTip.cs
deleted file mode 100644
index a087a0f0..00000000
--- a/src/ExcelDataReader/Core/BinaryFormat/XlsBiffQuickTip.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-namespace ExcelDataReader.Core.BinaryFormat;
-
-///
-/// For now QuickTip will do nothing, it seems to have a different.
-///
-internal sealed class XlsBiffQuickTip : XlsBiffRecord
-{
- internal XlsBiffQuickTip(byte[] bytes)
- : base(bytes)
- {
- }
-}
\ No newline at end of file
diff --git a/src/ExcelDataReader/Core/BinaryFormat/XlsBiffRecord.cs b/src/ExcelDataReader/Core/BinaryFormat/XlsBiffRecord.cs
index ae99991a..22249759 100644
--- a/src/ExcelDataReader/Core/BinaryFormat/XlsBiffRecord.cs
+++ b/src/ExcelDataReader/Core/BinaryFormat/XlsBiffRecord.cs
@@ -6,8 +6,16 @@ namespace ExcelDataReader.Core.BinaryFormat;
///
internal class XlsBiffRecord
{
+ // Records with a total buffer size at or below this threshold are allocated with
+ // new byte[] rather than rented from ArrayPool. The pool's per-operation overhead
+ // (bucket lookup + thread-local push/pop) exceeds the benefit for tiny buffers.
+ // Rented buffers always have Length > SmallRecordPoolThreshold because ArrayPool
+ // rounds up to the next power-of-two bucket (≥128 when totalSize > 64), so the
+ // check in Return() reliably distinguishes heap-allocated from pool-rented arrays.
+ internal const int SmallRecordPoolThreshold = 64;
+
protected const int ContentOffset = 4;
-
+
public XlsBiffRecord(byte[] bytes)
{
if (bytes.Length < 4)
@@ -35,7 +43,10 @@ public XlsBiffRecord(byte[] bytes)
#if NETSTANDARD2_1_OR_GREATER || NET8_0_OR_GREATER
public virtual void Return()
{
- System.Buffers.ArrayPool.Shared.Return(Bytes);
+ // Only buffers rented from ArrayPool need to be returned.
+ // Heap-allocated small records (Bytes.Length <= SmallRecordPoolThreshold) are GC-managed.
+ if (Bytes.Length > SmallRecordPoolThreshold)
+ System.Buffers.ArrayPool.Shared.Return(Bytes);
}
#endif
@@ -74,13 +85,6 @@ public long ReadInt64(int offset)
return BitConverter.ToInt64(Bytes, ContentOffset + offset);
}
- public byte[] ReadArray(int offset, int size)
- {
- byte[] tmp = new byte[size];
- Buffer.BlockCopy(Bytes, ContentOffset + offset, tmp, 0, size);
- return tmp;
- }
-
public float ReadFloat(int offset)
{
return BitConverter.ToSingle(Bytes, ContentOffset + offset);
diff --git a/src/ExcelDataReader/Core/BinaryFormat/XlsBiffSST.cs b/src/ExcelDataReader/Core/BinaryFormat/XlsBiffSST.cs
index 98c6f0e0..1fad9bf9 100644
--- a/src/ExcelDataReader/Core/BinaryFormat/XlsBiffSST.cs
+++ b/src/ExcelDataReader/Core/BinaryFormat/XlsBiffSST.cs
@@ -7,8 +7,9 @@ namespace ExcelDataReader.Core.BinaryFormat;
///
internal sealed class XlsBiffSST : XlsBiffRecord
{
- private readonly List _strings = [];
private readonly XlsSSTReader _reader = new();
+ private string[] _materializedStrings;
+ private List _strings = [];
internal XlsBiffSST(byte[] bytes)
: base(bytes)
@@ -54,20 +55,36 @@ public void Flush()
{
_strings.Add(str);
}
+
+ _materializedStrings = new string[_strings.Count];
}
///
- /// Returns string at specified index.
+ /// Returns string at specified index, caching it on first access and releasing the raw byte buffer.
///
/// Index of string to get.
/// Workbook encoding.
/// string value if it was found, empty string otherwise.
public string GetString(uint sstIndex, Encoding encoding)
{
- if (sstIndex < _strings.Count)
- return _strings[(int)sstIndex].GetValue(encoding);
+ if (_materializedStrings == null)
+ {
+ if (sstIndex < _strings.Count)
+ return _strings[(int)sstIndex].GetValue(encoding);
+ return null;
+ }
+
+ if (sstIndex >= (uint)_materializedStrings.Length)
+ return null;
+
+ var cached = _materializedStrings[sstIndex];
+ if (cached != null)
+ return cached;
- return null; // #VALUE error
+ var s = _strings[(int)sstIndex].GetValue(encoding);
+ _materializedStrings[sstIndex] = s;
+ _strings[(int)sstIndex] = null;
+ return s;
}
///
diff --git a/src/ExcelDataReader/Core/BinaryFormat/XlsBiffStream.cs b/src/ExcelDataReader/Core/BinaryFormat/XlsBiffStream.cs
index f6b86b8f..87bcdf66 100644
--- a/src/ExcelDataReader/Core/BinaryFormat/XlsBiffStream.cs
+++ b/src/ExcelDataReader/Core/BinaryFormat/XlsBiffStream.cs
@@ -10,7 +10,17 @@ namespace ExcelDataReader.Core.BinaryFormat;
///
internal sealed class XlsBiffStream : IDisposable
{
- private byte[] _headerBuffer = new byte[4];
+ // Read-ahead buffer: a single BaseStream.Read() fills 1 024 bytes that then serve
+ // ~70 sequential small records (avg 14 bytes each) without further CompoundStream calls.
+ // Any Seek() sets _readAheadStart == _readAheadEnd to invalidate it.
+ // Uses only byte[], Buffer.BlockCopy and Stream.Read — available on all target frameworks.
+ private const int ReadAheadSize = 1024;
+
+ private readonly byte[] _headerBuffer = new byte[4];
+
+ private readonly byte[] _readAheadBuffer = new byte[ReadAheadSize];
+ private int _readAheadStart;
+ private int _readAheadEnd;
public XlsBiffStream(Stream baseStream, int offset = 0, int explicitVersion = 0, BIFFTYPE? defaultType = null, string password = null, byte[] secretKey = null, EncryptionInfo encryption = null)
{
@@ -78,7 +88,11 @@ record = Read();
///
/// Gets or sets the current position in BIFF stream.
///
- public int Position { get => (int)BaseStream.Position; set => Seek(value, SeekOrigin.Begin); }
+ public int Position
+ {
+ get => (int)BaseStream.Position - (_readAheadEnd - _readAheadStart);
+ set => Seek(value, SeekOrigin.Begin);
+ }
public Stream BaseStream { get; }
@@ -105,6 +119,9 @@ record = Read();
/// Offset origin.
public void Seek(int offset, SeekOrigin origin)
{
+ // Discard buffered bytes: they belong to the old position.
+ _readAheadStart = 0;
+ _readAheadEnd = 0;
BaseStream.Seek(offset, origin);
if (Position < 0)
@@ -146,20 +163,28 @@ record = null;
/// The record -or- null.
public XlsBiffRecord GetRecord(Stream stream)
{
- var recordOffset = (int)stream.Position;
- stream.ReadAtLeast(_headerBuffer, 0, 4);
+ // Capture the logical record start before consuming header bytes from the read-ahead
+ // buffer; this is the value DecryptRecord() needs for its block-number calculation.
+ var recordOffset = Position;
+ ReadFromReadAhead(_headerBuffer, 0, 4);
// Does this work on a big endian system?
var id = (BIFFRECORDTYPE)BitConverter.ToUInt16(_headerBuffer, 0);
ushort recordSize = BitConverter.ToUInt16(_headerBuffer, 2);
#if NETSTANDARD2_1_OR_GREATER || NET8_0_OR_GREATER
- var bytes = System.Buffers.ArrayPool.Shared.Rent(4 + recordSize);
+ // Records at or below SmallRecordPoolThreshold bytes use a plain heap allocation.
+ // ArrayPool.Rent+Return overhead exceeds the benefit for these tiny buffers.
+ // Rented buffers always have Length > SmallRecordPoolThreshold (pool rounds up to
+ // the next power-of-two bucket), so XlsBiffRecord.Return() can tell them apart.
+ var bytes = (4 + recordSize) <= XlsBiffRecord.SmallRecordPoolThreshold
+ ? new byte[4 + recordSize]
+ : System.Buffers.ArrayPool.Shared.Rent(4 + recordSize);
#else
var bytes = new byte[4 + recordSize];
#endif
Array.Copy(_headerBuffer, bytes, 4);
- stream.ReadAtLeast(bytes, 4, recordSize);
+ ReadFromReadAhead(bytes, 4, recordSize);
if (SecretKey != null)
DecryptRecord(recordOffset, id, bytes, 4 + recordSize);
@@ -194,8 +219,8 @@ public XlsBiffRecord GetRecord(Stream stream)
case BIFFRECORDTYPE.BLANK_OLD:
return new XlsBiffBlankCell(bytes);
case BIFFRECORDTYPE.MULBLANK:
- return new XlsBiffMulBlankCell(bytes);
- case BIFFRECORDTYPE.LABEL_OLD:
+ return new XlsBiffBlankCell(bytes);
+ case BIFFRECORDTYPE.LABEL_V2:
case BIFFRECORDTYPE.LABEL:
case BIFFRECORDTYPE.RSTRING:
return new XlsBiffLabelCell(bytes, biffVersion);
@@ -229,27 +254,15 @@ public XlsBiffRecord GetRecord(Stream stream)
case BIFFRECORDTYPE.BOUNDSHEET:
return new XlsBiffBoundSheet(bytes, biffVersion);
case BIFFRECORDTYPE.WINDOW1:
- return new XlsBiffWindow1(bytes);
+ return new XlsBiffRecord(bytes);
case BIFFRECORDTYPE.CODEPAGE:
- return new XlsBiffSimpleValueRecord(bytes);
case BIFFRECORDTYPE.FNGROUPCOUNT:
- return new XlsBiffSimpleValueRecord(bytes);
- case BIFFRECORDTYPE.RECORD1904:
- return new XlsBiffSimpleValueRecord(bytes);
+ case BIFFRECORDTYPE.DATE1904:
case BIFFRECORDTYPE.BOOKBOOL:
- return new XlsBiffSimpleValueRecord(bytes);
case BIFFRECORDTYPE.BACKUP:
- return new XlsBiffSimpleValueRecord(bytes);
case BIFFRECORDTYPE.HIDEOBJ:
- return new XlsBiffSimpleValueRecord(bytes);
case BIFFRECORDTYPE.USESELFS:
return new XlsBiffSimpleValueRecord(bytes);
- case BIFFRECORDTYPE.UNCALCED:
- return new XlsBiffUncalced(bytes);
- case BIFFRECORDTYPE.QUICKTIP:
- return new XlsBiffQuickTip(bytes);
- case BIFFRECORDTYPE.MSODRAWING:
- return new XlsBiffMSODrawing(bytes);
case BIFFRECORDTYPE.FILEPASS:
return new XlsBiffFilePass(bytes, biffVersion);
case BIFFRECORDTYPE.HEADER:
@@ -306,6 +319,51 @@ private static int GetBiffVersion(XlsBiffBOF bof)
return 0;
}
+ // Serves 'count' bytes into dest[destOffset..], draining the read-ahead buffer first.
+ // When the buffer is exhausted it is refilled with a single BaseStream.Read(1024).
+ // For large bodies that exceed the remaining buffer the tail is read directly from
+ // BaseStream in one call (avoids per-1024-chunk overhead for SST / other big records).
+ private void ReadFromReadAhead(byte[] dest, int destOffset, int count)
+ {
+ // Fast path: serve from the in-memory buffer when possible.
+ int available = _readAheadEnd - _readAheadStart;
+ if (available > 0)
+ {
+ int fromBuffer = Math.Min(available, count);
+ Buffer.BlockCopy(_readAheadBuffer, _readAheadStart, dest, destOffset, fromBuffer);
+ _readAheadStart += fromBuffer;
+
+ if (fromBuffer == count)
+ return;
+
+ destOffset += fromBuffer;
+ count -= fromBuffer;
+ }
+
+ // Buffer was exhausted before satisfying the request.
+ // For small leftovers refill the buffer and serve from it;
+ // for large reads go directly to BaseStream to avoid per-chunk overhead.
+ if (count <= ReadAheadSize)
+ {
+ _readAheadStart = 0;
+ _readAheadEnd = BaseStream.Read(_readAheadBuffer, 0, ReadAheadSize);
+ if (_readAheadEnd < count)
+ throw new EndOfStreamException();
+
+ Buffer.BlockCopy(_readAheadBuffer, 0, dest, destOffset, count);
+ _readAheadStart = count;
+ }
+ else
+ {
+ // Large record body: read directly into the caller's buffer.
+ // The read-ahead buffer remains empty (_readAheadStart == _readAheadEnd == 0)
+ // and will be refilled on the next call.
+ _readAheadStart = 0;
+ _readAheadEnd = 0;
+ BaseStream.ReadAtLeast(dest, destOffset, count);
+ }
+ }
+
///
/// Create an ICryptoTransform instance to decrypt a 1024-byte block.
///
@@ -323,8 +381,20 @@ private void CreateBlockDecryptor(int blockNumber)
///
private void AlignBlockDecryptor(int blockOffset)
{
+#if NETSTANDARD2_1_OR_GREATER || NET8_0_OR_GREATER
+ var bytes = System.Buffers.ArrayPool.Shared.Rent(blockOffset);
+ try
+ {
+ CryptoHelpers.DecryptBytes(CipherTransform, bytes, blockOffset);
+ }
+ finally
+ {
+ System.Buffers.ArrayPool.Shared.Return(bytes);
+ }
+#else
var bytes = new byte[blockOffset];
- CryptoHelpers.DecryptBytes(CipherTransform, bytes, bytes.Length);
+ CryptoHelpers.DecryptBytes(CipherTransform, bytes, blockOffset);
+#endif
}
private void DecryptRecord(int startPosition, BIFFRECORDTYPE id, byte[] bytes, int recordSize)
@@ -343,44 +413,57 @@ private void DecryptRecord(int startPosition, BIFFRECORDTYPE id, byte[] bytes, i
break;
}
- var position = 0;
- while (position < recordSize)
+ // Max chunk size per iteration is 1024 (one encryption block boundary).
+ // Rent both buffers once and reuse across iterations to avoid per-iteration allocations.
+#if NETSTANDARD2_1_OR_GREATER || NET8_0_OR_GREATER
+ var inputBlock = System.Buffers.ArrayPool.Shared.Rent(1024);
+ var outputBlock = System.Buffers.ArrayPool.Shared.Rent(1024);
+#else
+ var inputBlock = new byte[1024];
+ var outputBlock = new byte[1024];
+#endif
+ try
{
- var offset = startPosition + position;
- int blockNumber = offset / 1024;
- var blockOffset = offset % 1024;
-
- if (blockNumber != CipherBlock)
+ var position = 0;
+ while (position < recordSize)
{
- CreateBlockDecryptor(blockNumber);
- }
+ var offset = startPosition + position;
+ int blockNumber = offset / 1024;
+ var blockOffset = offset % 1024;
- if (Encryption.IsXor)
- {
- // Bypass everything and hook into the XorTransform instance to set the XorArrayIndex pr record.
- // This is a hack to use the XorTransform otherwise transparently to the other encryption methods.
- var xorTransform = (XorManaged.XorTransform)CipherTransform;
- xorTransform.XorArrayIndex = offset + recordSize - 4;
- }
+ if (blockNumber != CipherBlock)
+ {
+ CreateBlockDecryptor(blockNumber);
+ }
- // Decrypt at most up to the next 1024 byte boundary
- var chunkSize = Math.Min(recordSize - position, 1024 - blockOffset);
+ if (Encryption.IsXor)
+ {
+ // Bypass everything and hook into the XorTransform instance to set the XorArrayIndex pr record.
+ // This is a hack to use the XorTransform otherwise transparently to the other encryption methods.
+ var xorTransform = (XorManaged.XorTransform)CipherTransform;
+ xorTransform.XorArrayIndex = offset + recordSize - 4;
+ }
-#if NETSTANDARD2_1_OR_GREATER || NET8_0_OR_GREATER
- var block = System.Buffers.ArrayPool.Shared.Rent(chunkSize);
-#else
- var block = new byte[chunkSize];
-#endif
+ // Decrypt at most up to the next 1024 byte boundary
+ var chunkSize = Math.Min(recordSize - position, 1024 - blockOffset);
- Array.Copy(bytes, position, block, 0, chunkSize);
+ Array.Copy(bytes, position, inputBlock, 0, chunkSize);
+ CryptoHelpers.DecryptBytes(CipherTransform, inputBlock, chunkSize, outputBlock);
- var decryptedblock = CryptoHelpers.DecryptBytes(CipherTransform, block, chunkSize);
- for (var i = 0; i < decryptedblock.Length; i++)
- {
- if (position >= startDecrypt)
- bytes[position] = decryptedblock[i];
- position++;
+ for (var i = 0; i < chunkSize; i++)
+ {
+ if (position >= startDecrypt)
+ bytes[position] = outputBlock[i];
+ position++;
+ }
}
}
+ finally
+ {
+#if NETSTANDARD2_1_OR_GREATER || NET8_0_OR_GREATER
+ System.Buffers.ArrayPool.Shared.Return(inputBlock);
+ System.Buffers.ArrayPool.Shared.Return(outputBlock);
+#endif
+ }
}
}
diff --git a/src/ExcelDataReader/Core/BinaryFormat/XlsBiffUncalced.cs b/src/ExcelDataReader/Core/BinaryFormat/XlsBiffUncalced.cs
deleted file mode 100644
index 8caa1ac7..00000000
--- a/src/ExcelDataReader/Core/BinaryFormat/XlsBiffUncalced.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-namespace ExcelDataReader.Core.BinaryFormat;
-
-///
-/// If present the Calculate Message was in the status bar when Excel saved the file.
-/// This occurs if the sheet changed, the Manual calculation option was on, and the Recalculate Before Save option was off.
-///
-internal sealed class XlsBiffUncalced : XlsBiffRecord
-{
- internal XlsBiffUncalced(byte[] bytes)
- : base(bytes)
- {
- }
-}
\ No newline at end of file
diff --git a/src/ExcelDataReader/Core/BinaryFormat/XlsBiffWindow1.cs b/src/ExcelDataReader/Core/BinaryFormat/XlsBiffWindow1.cs
deleted file mode 100644
index 257bd525..00000000
--- a/src/ExcelDataReader/Core/BinaryFormat/XlsBiffWindow1.cs
+++ /dev/null
@@ -1,71 +0,0 @@
-namespace ExcelDataReader.Core.BinaryFormat;
-
-///
-/// Represents Workbook's global window description.
-///
-internal sealed class XlsBiffWindow1 : XlsBiffRecord
-{
- internal XlsBiffWindow1(byte[] bytes)
- : base(bytes)
- {
- }
-
- [Flags]
- public enum Window1Flags : ushort
- {
- Hidden = 0x1,
- Minimized = 0x2,
-
- // (Reserved) = 0x4,
- HScrollVisible = 0x8,
- VScrollVisible = 0x10,
- WorkbookTabs = 0x20
-
- // (Other bits are reserved)
- }
-
- ///
- /// Gets the X position of a window.
- ///
- public ushort Left => ReadUInt16(0x0);
-
- ///
- /// Gets the Y position of a window.
- ///
- public ushort Top => ReadUInt16(0x2);
-
- ///
- /// Gets the width of the window.
- ///
- public ushort Width => ReadUInt16(0x4);
-
- ///
- /// Gets the height of the window.
- ///
- public ushort Height => ReadUInt16(0x6);
-
- ///
- /// Gets the window flags.
- ///
- public Window1Flags Flags => (Window1Flags)ReadUInt16(0x8);
-
- ///
- /// Gets the active workbook tab (zero-based).
- ///
- public ushort ActiveTab => ReadUInt16(0xA);
-
- ///
- /// Gets the first visible workbook tab (zero-based).
- ///
- public ushort FirstVisibleTab => ReadUInt16(0xC);
-
- ///
- /// Gets the number of selected workbook tabs.
- ///
- public ushort SelectedTabCount => ReadUInt16(0xE);
-
- ///
- /// Gets the workbook tab width to horizontal scrollbar width.
- ///
- public ushort TabRatio => ReadUInt16(0x10);
-}
\ No newline at end of file
diff --git a/src/ExcelDataReader/Core/BinaryFormat/XlsByteString.cs b/src/ExcelDataReader/Core/BinaryFormat/XlsByteString.cs
index 6c7b3002..ec5eb603 100644
--- a/src/ExcelDataReader/Core/BinaryFormat/XlsByteString.cs
+++ b/src/ExcelDataReader/Core/BinaryFormat/XlsByteString.cs
@@ -20,14 +20,7 @@ internal sealed class XlsByteString(byte[] bytes, uint offset) : IXlsString
///
public string GetValue(Encoding encoding)
{
- var stringBytes = ReadArray(0x2, CharacterCount * (Helpers.IsSingleByteEncoding(encoding) ? 1 : 2));
- return encoding.GetString(stringBytes, 0, stringBytes.Length);
- }
-
- public byte[] ReadArray(int offset, int size)
- {
- byte[] tmp = new byte[size];
- Buffer.BlockCopy(_bytes, (int)(_offset + offset), tmp, 0, size);
- return tmp;
+ int byteCount = CharacterCount * (Helpers.IsSingleByteEncoding(encoding) ? 1 : 2);
+ return encoding.GetString(_bytes, (int)_offset + 2, byteCount);
}
}
\ No newline at end of file
diff --git a/src/ExcelDataReader/Core/BinaryFormat/XlsInternalString.cs b/src/ExcelDataReader/Core/BinaryFormat/XlsInternalString.cs
index e8a0aa69..e73d89ab 100644
--- a/src/ExcelDataReader/Core/BinaryFormat/XlsInternalString.cs
+++ b/src/ExcelDataReader/Core/BinaryFormat/XlsInternalString.cs
@@ -7,10 +7,5 @@ namespace ExcelDataReader.Core.BinaryFormat;
///
internal sealed class XlsInternalString(string value) : IXlsString
{
- private readonly string stringValue = value;
-
- public string GetValue(Encoding encoding)
- {
- return stringValue;
- }
+ public string GetValue(Encoding encoding) => value;
}
diff --git a/src/ExcelDataReader/Core/BinaryFormat/XlsShortByteString.cs b/src/ExcelDataReader/Core/BinaryFormat/XlsShortByteString.cs
index fd6119e0..be7df796 100644
--- a/src/ExcelDataReader/Core/BinaryFormat/XlsShortByteString.cs
+++ b/src/ExcelDataReader/Core/BinaryFormat/XlsShortByteString.cs
@@ -12,14 +12,6 @@ internal sealed class XlsShortByteString(byte[] bytes, uint offset) : IXlsString
public ushort CharacterCount => _bytes[_offset];
- public string GetValue(Encoding encoding)
- {
- // Supposedly this is never multibyte, but technically could be
- if (!Helpers.IsSingleByteEncoding(encoding))
- {
- return encoding.GetString(_bytes, (int)_offset + 1, CharacterCount * 2);
- }
-
- return encoding.GetString(_bytes, (int)_offset + 1, CharacterCount);
- }
+ public string GetValue(Encoding encoding) =>
+ encoding.GetString(_bytes, (int)_offset + 1, CharacterCount * (Helpers.IsSingleByteEncoding(encoding) ? 1 : 2));
}
diff --git a/src/ExcelDataReader/Core/BinaryFormat/XlsShortUnicodeString.cs b/src/ExcelDataReader/Core/BinaryFormat/XlsShortUnicodeString.cs
index 78821e97..46a3e677 100644
--- a/src/ExcelDataReader/Core/BinaryFormat/XlsShortUnicodeString.cs
+++ b/src/ExcelDataReader/Core/BinaryFormat/XlsShortUnicodeString.cs
@@ -1,4 +1,5 @@
-using System.Text;
+using System.Text;
+using ExcelDataReader.Exceptions;
namespace ExcelDataReader.Core.BinaryFormat;
@@ -25,17 +26,32 @@ public string GetValue(Encoding encoding)
return string.Empty;
}
- if (IsMultiByte)
+ int available = _bytes.Length - (int)_offset - 2;
+ int needed = IsMultiByte ? CharacterCount * 2 : CharacterCount;
+ if (available < needed)
{
- return Encoding.Unicode.GetString(_bytes, (int)_offset + 2, CharacterCount * 2);
+ throw new ExcelReaderException(Errors.ErrorBiffStringSize);
}
- byte[] bytes = new byte[CharacterCount * 2];
- for (int i = 0; i < CharacterCount; i++)
+ if (IsMultiByte)
{
- bytes[i * 2] = _bytes[_offset + 2 + i];
+ return Encoding.Unicode.GetString(_bytes, (int)_offset + 2, CharacterCount * 2);
}
- return Encoding.Unicode.GetString(bytes);
+ // In BIFF8 single-byte strings each byte value IS the Unicode code point,
+ // so a direct (char)byte cast is the exact and encoding-free conversion.
+#if NETSTANDARD2_1_OR_GREATER || NET8_0_OR_GREATER
+ return string.Create(CharacterCount, (Bytes: _bytes, Start: (int)_offset + 2), static (span, state) =>
+ {
+ for (int i = 0; i < span.Length; i++)
+ span[i] = (char)state.Bytes[state.Start + i];
+ });
+#else
+ var chars = new char[CharacterCount];
+ int start = (int)_offset + 2;
+ for (int i = 0; i < CharacterCount; i++)
+ chars[i] = (char)_bytes[start + i];
+ return new string(chars);
+#endif
}
}
diff --git a/src/ExcelDataReader/Core/BinaryFormat/XlsUnicodeString.cs b/src/ExcelDataReader/Core/BinaryFormat/XlsUnicodeString.cs
index 656fe54f..ebce7307 100644
--- a/src/ExcelDataReader/Core/BinaryFormat/XlsUnicodeString.cs
+++ b/src/ExcelDataReader/Core/BinaryFormat/XlsUnicodeString.cs
@@ -1,4 +1,5 @@
using System.Text;
+using ExcelDataReader.Exceptions;
namespace ExcelDataReader.Core.BinaryFormat;
@@ -25,17 +26,32 @@ public string GetValue(Encoding encoding)
return string.Empty;
}
- if (IsMultiByte)
+ int available = _bytes.Length - (int)_offset - 3;
+ int needed = IsMultiByte ? CharacterCount * 2 : CharacterCount;
+ if (available < needed)
{
- return Encoding.Unicode.GetString(_bytes, (int)_offset + 3, CharacterCount * 2);
+ throw new ExcelReaderException(Errors.ErrorBiffStringSize);
}
- byte[] bytes = new byte[CharacterCount * 2];
- for (int i = 0; i < CharacterCount; i++)
+ if (IsMultiByte)
{
- bytes[i * 2] = _bytes[_offset + 3 + i];
+ return Encoding.Unicode.GetString(_bytes, (int)_offset + 3, CharacterCount * 2);
}
- return Encoding.Unicode.GetString(bytes);
+ // In BIFF8 single-byte strings each byte value IS the Unicode code point,
+ // so a direct (char)byte cast is the exact and encoding-free conversion.
+#if NETSTANDARD2_1_OR_GREATER || NET8_0_OR_GREATER
+ return string.Create(CharacterCount, (Bytes: _bytes, Start: (int)_offset + 3), static (span, state) =>
+ {
+ for (int i = 0; i < span.Length; i++)
+ span[i] = (char)state.Bytes[state.Start + i];
+ });
+#else
+ var chars = new char[CharacterCount];
+ int start = (int)_offset + 3;
+ for (int i = 0; i < CharacterCount; i++)
+ chars[i] = (char)_bytes[start + i];
+ return new string(chars);
+#endif
}
}
diff --git a/src/ExcelDataReader/Core/BinaryFormat/XlsWorkbook.cs b/src/ExcelDataReader/Core/BinaryFormat/XlsWorkbook.cs
index 64218631..a63df23a 100644
--- a/src/ExcelDataReader/Core/BinaryFormat/XlsWorkbook.cs
+++ b/src/ExcelDataReader/Core/BinaryFormat/XlsWorkbook.cs
@@ -23,18 +23,21 @@ internal XlsWorkbook(Stream stream, string password, Encoding fallbackEncoding)
Encryption = biffStream.Encryption;
Encoding = biffStream.BiffVersion == 8 ? Encoding.Unicode : fallbackEncoding;
- if (biffStream.BiffType == BIFFTYPE.WorkbookGlobals)
+ switch (biffStream.BiffType)
{
- ReadWorkbookGlobals(biffStream);
- }
- else if (biffStream.BiffType == BIFFTYPE.Worksheet)
- {
- // set up 'virtual' bound sheet pointing at this
- Sheets.Add(new XlsBiffBoundSheet(0, XlsBiffBoundSheet.SheetType.Worksheet, XlsBiffBoundSheet.SheetVisibility.Visible, "Sheet"));
- }
- else
- {
- throw new ExcelReaderException(Errors.ErrorWorkbookGlobalsInvalidData);
+ case BIFFTYPE.WorkbookGlobals:
+ ReadWorkbookGlobals(biffStream);
+ break;
+ case BIFFTYPE.Worksheet:
+ // set up 'virtual' bound sheet pointing at this
+ Sheets.Add(new XlsBiffBoundSheet(0, XlsBiffBoundSheet.SheetType.Worksheet, XlsBiffBoundSheet.SheetVisibility.Visible, "Sheet"));
+ break;
+ case BIFFTYPE.MacroSheet:
+ // set up 'virtual' bound sheet pointing at this
+ Sheets.Add(new XlsBiffBoundSheet(0, XlsBiffBoundSheet.SheetType.MacroSheet, XlsBiffBoundSheet.SheetVisibility.Visible, "Sheet"));
+ break;
+ default:
+ throw new ExcelReaderException(Errors.ErrorWorkbookGlobalsInvalidData);
}
}
@@ -131,7 +134,7 @@ public IEnumerable ReadWorksheets()
{
for (var i = 0; i < Sheets.Count; ++i)
{
- yield return new XlsWorksheet(this, Sheets[i], Stream);
+ yield return new XlsWorksheet(this, Sheets[i], Stream, SinglePassMode);
}
}
@@ -171,7 +174,7 @@ private void ReadWorkbookGlobals(XlsBiffStream biffStream)
CodePage = codePage;
Encoding = EncodingHelper.GetEncoding(CodePage.Value);
break;
- case XlsBiffSimpleValueRecord is1904 when rec.Id == BIFFRECORDTYPE.RECORD1904:
+ case XlsBiffSimpleValueRecord is1904 when rec.Id == BIFFRECORDTYPE.DATE1904:
IsDate1904 = is1904.Value == 1;
break;
case XlsBiffFont font:
diff --git a/src/ExcelDataReader/Core/BinaryFormat/XlsWorksheet.cs b/src/ExcelDataReader/Core/BinaryFormat/XlsWorksheet.cs
index 2c50fccb..3ba6838b 100644
--- a/src/ExcelDataReader/Core/BinaryFormat/XlsWorksheet.cs
+++ b/src/ExcelDataReader/Core/BinaryFormat/XlsWorksheet.cs
@@ -1,5 +1,5 @@
using System.Text;
-using ExcelDataReader.Log;
+using ExcelDataReader.Exceptions;
namespace ExcelDataReader.Core.BinaryFormat;
@@ -8,7 +8,7 @@ namespace ExcelDataReader.Core.BinaryFormat;
///
internal sealed class XlsWorksheet : IWorksheet
{
- public XlsWorksheet(XlsWorkbook workbook, XlsBiffBoundSheet refSheet, Stream stream)
+ public XlsWorksheet(XlsWorkbook workbook, XlsBiffBoundSheet refSheet, Stream stream, bool singlePassMode = false)
{
Workbook = workbook;
Stream = stream;
@@ -27,6 +27,7 @@ public XlsWorksheet(XlsWorkbook workbook, XlsBiffBoundSheet refSheet, Stream str
XlsBiffBoundSheet.SheetVisibility.VeryHidden => "veryhidden",
_ => "visible",
};
+ SinglePassMode = singlePassMode;
ReadWorksheetGlobals();
}
@@ -80,23 +81,44 @@ public XlsWorksheet(XlsWorkbook workbook, XlsBiffBoundSheet refSheet, Stream str
public int RowCount { get; private set; }
+ public CellRange Dimension { get; private set; }
+
public bool IsDate1904 { get; private set; }
+ public bool SinglePassMode { get; }
+
public XlsWorkbook Workbook { get; }
public IEnumerable ReadRows()
{
var rowIndex = 0;
using var biffStream = new XlsBiffStream(Stream, (int)DataOffset, Workbook.BiffVersion, BIFFTYPE.Worksheet, secretKey: Workbook.SecretKey, encryption: Workbook.Encryption);
- foreach (var rowBlock in ReadWorksheetRows(biffStream))
+
+ if (SinglePassMode)
{
- for (; rowIndex < rowBlock.RowIndex; ++rowIndex)
+ foreach (var row in ReadWorksheetRowsSinglePass(biffStream))
{
- yield return new Row(rowIndex, DefaultRowHeight / 20.0, []);
+ for (; rowIndex < row.RowIndex; ++rowIndex)
+ {
+ yield return new Row(rowIndex, DefaultRowHeight / 20.0, []);
+ }
+
+ rowIndex++;
+ yield return row;
}
+ }
+ else
+ {
+ foreach (var rowBlock in ReadWorksheetRows(biffStream))
+ {
+ for (; rowIndex < rowBlock.RowIndex; ++rowIndex)
+ {
+ yield return new Row(rowIndex, DefaultRowHeight / 20.0, []);
+ }
- rowIndex++;
- yield return rowBlock;
+ rowIndex++;
+ yield return rowBlock;
+ }
}
}
@@ -134,41 +156,93 @@ private void GetBlockSize(int startRow, out int blockRowCount, out int minOffset
private IEnumerable ReadWorksheetRows(XlsBiffStream biffStream)
{
var rowIndex = 0;
+ List> cellListPool = [];
- while (rowIndex < RowCount)
+ // blockRowCount is declared here so the finally block can see its current value
+ // for the early-disposal safety clear (blockRowCount == 0 after normal completion).
+ var blockRowCount = 0;
+
+#if NETSTANDARD2_1_OR_GREATER || NET8_0_OR_GREATER
+ // Rent once for the lifetime of the loop. For most files blockRowCount stays at
+ // Math.Min(32, RowCount) throughout, so a single Rent/Return replaces the previous
+ // 313 Rent + 313 clearArray:true Return cycles for a 10 000-row worksheet.
+ var block = System.Buffers.ArrayPool.Shared.Rent(Math.Max(1, Math.Min(32, RowCount)));
+ try
+ {
+#else
+ // On older targets allocate once and grow only when overlapping rows force a larger block.
+ var block = new Row?[Math.Max(1, Math.Min(32, RowCount))];
{
- GetBlockSize(rowIndex, out var blockRowCount, out var minOffset, out var maxOffset);
+#endif
+ while (rowIndex < RowCount)
+ {
+ GetBlockSize(rowIndex, out blockRowCount, out var minOffset, out var maxOffset);
- var block = ReadNextBlock(biffStream, rowIndex, blockRowCount, minOffset, maxOffset);
+#if NETSTANDARD2_1_OR_GREATER || NET8_0_OR_GREATER
+ if (blockRowCount > block.Length)
+ {
+ // Rare: overlapping rows require a larger block. The current array is already
+ // clean (all slots default), so return it without an extra clear pass.
+ System.Buffers.ArrayPool.Shared.Return(block, clearArray: false);
+ block = System.Buffers.ArrayPool.Shared.Rent(blockRowCount);
+ }
+#else
+ if (blockRowCount > block.Length)
+ block = new Row?[blockRowCount];
+#endif
- for (var i = 0; i < blockRowCount; ++i)
- {
- if (block.Rows.TryGetValue(rowIndex + i, out var row))
+ ReadNextBlock(biffStream, rowIndex, blockRowCount, minOffset, maxOffset, block, cellListPool);
+
+ for (var i = 0; i < blockRowCount; ++i)
{
- yield return row;
+ if (block[i].HasValue)
+ {
+ yield return block[i]!.Value;
+
+ // Safe: consumer has already processed the row's cells before MoveNext resumed.
+ var consumed = block[i]!.Value.Cells;
+
+ // release List reference immediately so the pool
+ // return below does not need a bulk Array.Clear pass
+ block[i] = default;
+ consumed.Clear();
+ cellListPool.Add(consumed);
+ }
}
+
+ rowIndex += blockRowCount;
}
- rowIndex += blockRowCount;
+ blockRowCount = 0; // signal to finally: normal completion, all slots already cleared
+ }
+#if NETSTANDARD2_1_OR_GREATER || NET8_0_OR_GREATER
+ finally
+ {
+ // On early disposal blockRowCount > 0 and some slots in the current block may
+ // still hold live Row? values; clear them so the pool does not retain references.
+ // On normal completion blockRowCount == 0, so this loop is a no-op.
+ for (var j = 0; j < blockRowCount; ++j)
+ block[j] = default;
+
+ System.Buffers.ArrayPool.Shared.Return(block, clearArray: false);
}
+#endif
}
- private XlsRowBlock ReadNextBlock(XlsBiffStream biffStream, int startRow, int rows, int minOffset, int maxOffset)
+ private void ReadNextBlock(XlsBiffStream biffStream, int startRow, int rows, int minOffset, int maxOffset, Row?[] result, List> cellListPool)
{
- var result = new XlsRowBlock();
-
// Ensure rows with physical records are initialized with height
for (var i = 0; i < rows; i++)
{
if (RowOffsetMap.TryGetValue(startRow + i, out _))
{
- EnsureRow(result, startRow + i);
+ EnsureRow(result, startRow, startRow + i, cellListPool);
}
}
if (minOffset == int.MaxValue)
{
- return result;
+ return;
}
biffStream.Position = minOffset;
@@ -184,18 +258,17 @@ private XlsRowBlock ReadNextBlock(XlsBiffStream biffStream, int startRow, int ro
if (rec is XlsBiffBlankCell cell)
{
- var currentRow = EnsureRow(result, cell.RowIndex);
+ var cellList = EnsureRow(result, startRow, cell.RowIndex, cellListPool);
if (cell.Id == BIFFRECORDTYPE.MULRK)
{
- var cellValues = ReadMultiCell(cell);
- currentRow.Cells.AddRange(cellValues);
+ ReadMultiCell(cell, cellList);
}
else
{
var xfIndex = GetXfIndexForCell(cell, ixfe);
var cellValue = ReadSingleCell(biffStream, cell, xfIndex);
- currentRow.Cells.Add(cellValue);
+ cellList.Add(cellValue);
}
ixfe = null;
@@ -206,13 +279,12 @@ private XlsRowBlock ReadNextBlock(XlsBiffStream biffStream, int startRow, int ro
#endif
}
-
- return result;
}
- private Row EnsureRow(XlsRowBlock result, int rowIndex)
+ private List EnsureRow(Row?[] result, int startRow, int rowIndex, List> cellListPool)
{
- if (!result.Rows.TryGetValue(rowIndex, out var currentRow))
+ int i = rowIndex - startRow;
+ if (!result[i].HasValue)
{
var height = DefaultRowHeight / 20.0;
if (RowOffsetMap.TryGetValue(rowIndex, out var rowOffset) && rowOffset.RowHeight != null)
@@ -220,32 +292,36 @@ private Row EnsureRow(XlsRowBlock result, int rowIndex)
height = (rowOffset.UseDefaultRowHeight ? DefaultRowHeight : rowOffset.RowHeight.Value) / 20.0;
}
- currentRow = new Row(rowIndex, height, []);
+ List cells;
+ if (cellListPool.Count > 0)
+ {
+ cells = cellListPool[^1];
+ cellListPool.RemoveAt(cellListPool.Count - 1);
+ }
+ else
+ {
+ cells = [];
+ }
- result.Rows.Add(rowIndex, currentRow);
+ result[i] = new Row(rowIndex, height, cells);
}
- return currentRow;
+ return result[i]!.Value.Cells;
}
- private IEnumerable ReadMultiCell(XlsBiffBlankCell cell)
+ private void ReadMultiCell(XlsBiffBlankCell cell, List cellList)
{
- LogManager.Log(this).Debug("ReadMultiCell {0}", cell.Id);
-
switch (cell.Id)
{
case BIFFRECORDTYPE.MULRK:
-
XlsBiffMulRKCell rkCell = (XlsBiffMulRKCell)cell;
ushort lastColumnIndex = rkCell.LastColumnIndex;
for (ushort j = cell.ColumnIndex; j <= lastColumnIndex; j++)
{
var xfIndex = rkCell.GetXF(j);
var effectiveStyle = Workbook.GetEffectiveCellStyle(xfIndex, cell.Format);
-
var value = TryConvertOADateTime(rkCell.GetValue(j), effectiveStyle.NumberFormatIndex);
- LogManager.Log(this).Debug("CELL[{0}] = {1}", j, value);
- yield return new Cell(j, value, effectiveStyle, null);
+ cellList.Add(new Cell(j, value, effectiveStyle, null));
}
break;
@@ -257,8 +333,6 @@ private IEnumerable ReadMultiCell(XlsBiffBlankCell cell)
///
private Cell ReadSingleCell(XlsBiffStream biffStream, XlsBiffBlankCell cell, int xfIndex)
{
- LogManager.Log(this).Debug("ReadSingleCell {0}", cell.Id);
-
var effectiveStyle = Workbook.GetEffectiveCellStyle(xfIndex, cell.Format);
var numberFormatIndex = effectiveStyle.NumberFormatIndex;
@@ -287,11 +361,16 @@ private Cell ReadSingleCell(XlsBiffStream biffStream, XlsBiffBlankCell cell, int
value = TryConvertOADateTime(((XlsBiffNumberCell)cell).Value, numberFormatIndex);
break;
case BIFFRECORDTYPE.LABEL:
- case BIFFRECORDTYPE.LABEL_OLD:
+ case BIFFRECORDTYPE.LABEL_V2:
case BIFFRECORDTYPE.RSTRING:
value = GetLabelString((XlsBiffLabelCell)cell, effectiveStyle);
break;
case BIFFRECORDTYPE.LABELSST:
+ if (Workbook.SST == null)
+ {
+ throw new ExcelReaderException(Errors.ErrorWorkbookGlobalsInvalidData);
+ }
+
value = Workbook.SST.GetString(((XlsBiffLabelSSTCell)cell).SSTIndex, Encoding);
break;
case BIFFRECORDTYPE.RK:
@@ -434,7 +513,7 @@ private void ReadWorksheetGlobals()
using var biffStream = new XlsBiffStream(Stream, (int)DataOffset, Workbook.BiffVersion, BIFFTYPE.Worksheet, secretKey: Workbook.SecretKey, encryption: Workbook.Encryption);
// Check the expected BOF record was found in the BIFF stream
- if (biffStream.BiffVersion == 0 || biffStream.BiffType != BIFFTYPE.Worksheet)
+ if (biffStream.BiffVersion == 0 || (biffStream.BiffType != BIFFTYPE.Worksheet && biffStream.BiffType != BIFFTYPE.MacroSheet))
return;
XlsBiffHeaderFooterString header = null;
@@ -452,18 +531,19 @@ private void ReadWorksheetGlobals()
var recordOffset = biffStream.Position;
var rec = biffStream.Read();
- while (rec != null && rec is not XlsBiffEof)
+ var stopAtCells = false;
+ while (!stopAtCells && rec != null && rec is not XlsBiffEof)
{
switch (rec)
{
case XlsBiffDimensions dims:
- // FieldCount = dims.LastColumn;
RowCount = (int)dims.LastRow;
+ Dimension = new CellRange(dims.FirstColumn, (int)dims.FirstRow, dims.LastColumn - 1, (int)dims.LastRow - 1);
break;
case XlsBiffDefaultRowHeight defaultRowHeightRecord:
DefaultRowHeight = defaultRowHeightRecord.RowHeight;
break;
- case XlsBiffSimpleValueRecord is1904 when rec.Id == BIFFRECORDTYPE.RECORD1904:
+ case XlsBiffSimpleValueRecord is1904 when rec.Id == BIFFRECORDTYPE.DATE1904:
IsDate1904 = is1904.Value == 1;
break;
case XlsBiffXF xf when rec.Id is BIFFRECORDTYPE.XF_V2 or BIFFRECORDTYPE.XF_V3 or BIFFRECORDTYPE.XF_V4:
@@ -506,12 +586,26 @@ private void ReadWorksheetGlobals()
CodeName = codeName.GetValue(Encoding);
break;
case XlsBiffRow row:
+ if (SinglePassMode)
+ {
+ // In single-pass mode, stop before processing cell data;
+ // ReadRows() will stream cells in one forward pass
+ stopAtCells = true;
+ break;
+ }
+
SetMinMaxRow(row.RowIndex, row);
// Count rows by row records without affecting the overlap in OffsetMap
maxRowCountFromRowRecord = Math.Max(maxRowCountFromRowRecord, row.RowIndex + 1);
break;
case XlsBiffBlankCell cell:
+ if (SinglePassMode)
+ {
+ stopAtCells = true;
+ break;
+ }
+
if (!cell.IsEmpty)
{
if (cell is XlsBiffMulRKCell mcell)
@@ -559,7 +653,10 @@ private void ReadWorksheetGlobals()
if (mergeCells.Count > 0)
MergeCells = mergeCells.ToArray();
- FieldCount = maxCellColumn;
+ if (!SinglePassMode)
+ {
+ FieldCount = maxCellColumn;
+ }
maxRowCount = Math.Max(maxRowCount, maxRowCountFromRowRecord);
if (RowCount < maxRowCount)
@@ -587,6 +684,84 @@ private void SetMinMaxRow(int rowIndex, XlsBiffRow row)
rowOffset.RowHeight = row.RowHeight;
}
+ private IEnumerable ReadWorksheetRowsSinglePass(XlsBiffStream biffStream)
+ {
+ int lastYieldedRowIndex = -1;
+ int currentRowIndex = -1;
+ List currentRowCells = [];
+ Dictionary rowHeights = [];
+ ushort? ixfe = null;
+
+ var rec = biffStream.Read();
+ while (rec != null && rec is not XlsBiffEof)
+ {
+ switch (rec)
+ {
+ case XlsBiffRow row:
+ rowHeights[row.RowIndex] = row.UseDefaultRowHeight
+ ? DefaultRowHeight / 20.0
+ : row.RowHeight / 20.0;
+ break;
+ case XlsBiffBlankCell cell:
+ {
+ if (cell.RowIndex <= lastYieldedRowIndex)
+ {
+ throw new InvalidOperationException(
+ "File contains out-of-order cells. Use SinglePassMode = false to read this file.");
+ }
+
+ if (cell.RowIndex != currentRowIndex)
+ {
+ if (currentRowIndex != -1)
+ {
+ var height = rowHeights.TryGetValue(currentRowIndex, out var h) ? h : DefaultRowHeight / 20.0;
+ yield return new Row(currentRowIndex, height, currentRowCells);
+ lastYieldedRowIndex = currentRowIndex;
+ rowHeights.Remove(lastYieldedRowIndex);
+ currentRowCells.Clear();
+ }
+
+ currentRowIndex = cell.RowIndex;
+ }
+
+ if (!cell.IsEmpty)
+ {
+ if (cell.Id == BIFFRECORDTYPE.MULRK)
+ {
+ ReadMultiCell(cell, currentRowCells);
+ }
+ else
+ {
+ var xfIndex = GetXfIndexForCell(cell, ixfe);
+ currentRowCells.Add(ReadSingleCell(biffStream, cell, xfIndex));
+ }
+ }
+
+ ixfe = null;
+ }
+
+ break;
+ case { Id: BIFFRECORDTYPE.IXFE }:
+ ixfe = rec.ReadUInt16(0);
+ break;
+ }
+
+#if NETSTANDARD2_1_OR_GREATER || NET8_0_OR_GREATER
+ rec.Return();
+#endif
+ rec = biffStream.Read();
+
+ if (rec is XlsBiffBOF)
+ break;
+ }
+
+ if (currentRowIndex != -1 && currentRowCells.Count > 0)
+ {
+ var height = rowHeights.TryGetValue(currentRowIndex, out var h) ? h : DefaultRowHeight / 20.0;
+ yield return new Row(currentRowIndex, height, currentRowCells);
+ }
+ }
+
private void SetMinMaxRowOffset(int rowIndex, int recordOffset, int maxOverlapRow)
{
if (!RowOffsetMap.TryGetValue(rowIndex, out var rowOffset))
@@ -616,9 +791,4 @@ internal sealed class XlsRowOffset
public int MaxOverlapRowIndex { get; set; }
}
-
- private sealed class XlsRowBlock
- {
- public Dictionary Rows { get; } = [];
- }
}
\ No newline at end of file
diff --git a/src/ExcelDataReader/Core/BuiltinNumberFormat.cs b/src/ExcelDataReader/Core/BuiltinNumberFormat.cs
index fc5ff3c1..1502a879 100644
--- a/src/ExcelDataReader/Core/BuiltinNumberFormat.cs
+++ b/src/ExcelDataReader/Core/BuiltinNumberFormat.cs
@@ -1,5 +1,7 @@
#nullable enable
+using System.Globalization;
+using System.Text;
using ExcelDataReader.Core.NumberFormat;
namespace ExcelDataReader.Core;
@@ -46,6 +48,53 @@ internal static class BuiltinNumberFormat
{ 47, new NumberFormatString("mm:ss.0") },
{ 48, new NumberFormatString("##0.0E+0") },
{ 49, new NumberFormatString("@") },
+
+ // Locale-specific built-in formats.
+ // Indices 27–36 and 50–58 vary by locale (ja-jp, ko-kr, zh-tw, zh-cn) but are
+ // always date/time formats. The ja-jp strings are stored as a representative
+ // fallback; files from other locales may display differently via GetNumberFormatString()
+ // but will still be correctly identified as date/time values.
+ { 27, new NumberFormatString(@"[$-411]ge.m.d") },
+ { 28, new NumberFormatString("[$-411]ggge\"年\"m\"月\"d\"日\"") },
+ { 29, new NumberFormatString("[$-411]ggge\"年\"m\"月\"d\"日\"") },
+ { 30, new NumberFormatString("m/d/yy") },
+ { 31, new NumberFormatString("yyyy\"年\"m\"月\"d\"日\"") },
+ { 32, new NumberFormatString("h\"時\"mm\"分\"") },
+ { 33, new NumberFormatString("h\"時\"mm\"分\"ss\"秒\"") },
+ { 34, new NumberFormatString("yyyy\"年\"m\"月\"") },
+ { 35, new NumberFormatString("m\"月\"d\"日\"") },
+ { 36, new NumberFormatString(@"[$-411]ge.m.d") },
+ { 50, new NumberFormatString(@"[$-411]ge.m.d") },
+ { 51, new NumberFormatString("[$-411]ggge\"年\"m\"月\"d\"日\"") },
+ { 52, new NumberFormatString("yyyy\"年\"m\"月\"") },
+ { 53, new NumberFormatString("m\"月\"d\"日\"") },
+ { 54, new NumberFormatString("[$-411]ggge\"年\"m\"月\"d\"日\"") },
+ { 55, new NumberFormatString("yyyy\"年\"m\"月\"") },
+ { 56, new NumberFormatString("m\"月\"d\"日\"") },
+ { 57, new NumberFormatString(@"[$-411]ge.m.d") },
+ { 58, new NumberFormatString("[$-411]ggge\"年\"m\"月\"d\"日\"") },
+
+ // 59–62 and 67–70 are Thai-digit number formats; 71–81 are Thai date/time formats.
+ // ASCII equivalents are used so the format strings parse correctly.
+ { 59, new NumberFormatString("0") },
+ { 60, new NumberFormatString("0.00") },
+ { 61, new NumberFormatString("#,##0") },
+ { 62, new NumberFormatString("#,##0.00") },
+ { 67, new NumberFormatString("0%") },
+ { 68, new NumberFormatString("0.00%") },
+ { 69, new NumberFormatString("# ?/?") },
+ { 70, new NumberFormatString("# ??/??") },
+ { 71, new NumberFormatString("d/m/yyyy") },
+ { 72, new NumberFormatString("d-mmm-yy") },
+ { 73, new NumberFormatString("d-mmm") },
+ { 74, new NumberFormatString("mmm-yy") },
+ { 75, new NumberFormatString("h:mm") },
+ { 76, new NumberFormatString("h:mm:ss") },
+ { 77, new NumberFormatString("d/m/yyyy h:mm") },
+ { 78, new NumberFormatString("mm:ss") },
+ { 79, new NumberFormatString("[h]:mm:ss") },
+ { 80, new NumberFormatString("mm:ss.0") },
+ { 81, new NumberFormatString("d/m/yyyy") },
};
public static NumberFormatString? GetBuiltinNumberFormat(int numFmtId)
@@ -55,4 +104,126 @@ internal static class BuiltinNumberFormat
return null;
}
+
+ public static NumberFormatString? GetBuiltinNumberFormat(int numFmtId, CultureInfo culture)
+ {
+ if (numFmtId == 14)
+ return new NumberFormatString(DatePatternToExcel(culture.DateTimeFormat.ShortDatePattern));
+
+ if (numFmtId == 15 || numFmtId == 16 || numFmtId == 17)
+ {
+ var sep = EscapeExcelLiteral(culture.DateTimeFormat.DateSeparator);
+ return numFmtId switch
+ {
+ 15 => new NumberFormatString($"d{sep}mmm{sep}yy"),
+ 16 => new NumberFormatString($"d{sep}mmm"),
+ _ => new NumberFormatString($"mmm{sep}yy"),
+ };
+ }
+
+ if (numFmtId == 22)
+ {
+ var date = DatePatternToExcel(culture.DateTimeFormat.ShortDatePattern);
+ var time = TimePatternToExcel(culture.DateTimeFormat.ShortTimePattern);
+ return new NumberFormatString($"{date} {time}");
+ }
+
+ if (Formats.TryGetValue(numFmtId, out var result))
+ return result;
+
+ return null;
+ }
+
+ // Wraps a date separator in double quotes if it contains any letter that would be
+ // misinterpreted as a date/time format specifier in an Excel format string.
+ // In practice, date separators are always punctuation (/, ., -) so quoting is rarely needed.
+ private static string EscapeExcelLiteral(string s)
+ {
+ foreach (char c in s)
+ {
+ if (char.IsLetter(c) || c == '"')
+ return $"\"{s}\"";
+ }
+
+ return s;
+ }
+
+ // Converts a .NET date pattern (e.g. "M/d/yyyy") to an Excel format string (e.g. "m/d/yyyy").
+ // .NET uses uppercase M for months; Excel uses lowercase m.
+ // Single-quoted literals 'text' are converted to double-quoted "text" without modifying their content.
+ // Backslash-escaped characters \x are converted to "x".
+ private static string DatePatternToExcel(string dotNetPattern)
+ {
+ var sb = new StringBuilder(dotNetPattern.Length + 4);
+ for (int i = 0; i < dotNetPattern.Length; i++)
+ {
+ char c = dotNetPattern[i];
+ if (c == '\'')
+ {
+ sb.Append('"');
+ i++;
+ while (i < dotNetPattern.Length && dotNetPattern[i] != '\'')
+ sb.Append(dotNetPattern[i++]);
+ sb.Append('"');
+ }
+ else if (c == '\\' && i + 1 < dotNetPattern.Length)
+ {
+ sb.Append('"').Append(dotNetPattern[++i]).Append('"');
+ }
+ else if (c == 'M')
+ {
+ sb.Append('m');
+ }
+ else
+ {
+ sb.Append(c);
+ }
+ }
+
+ return sb.ToString();
+ }
+
+ // Converts a .NET time pattern (e.g. "h:mm tt") to an Excel format string (e.g. "h:mm AM/PM").
+ // .NET uses uppercase H for 24-hour clock; Excel uses lowercase h.
+ // .NET uses tt/t for AM/PM designator; Excel uses AM/PM and A/P.
+ // Single-quoted literals and backslash escapes are handled as in DatePatternToExcel.
+ private static string TimePatternToExcel(string dotNetPattern)
+ {
+ var sb = new StringBuilder(dotNetPattern.Length + 8);
+ for (int i = 0; i < dotNetPattern.Length; i++)
+ {
+ char c = dotNetPattern[i];
+ if (c == '\'')
+ {
+ sb.Append('"');
+ i++;
+ while (i < dotNetPattern.Length && dotNetPattern[i] != '\'')
+ sb.Append(dotNetPattern[i++]);
+ sb.Append('"');
+ }
+ else if (c == '\\' && i + 1 < dotNetPattern.Length)
+ {
+ sb.Append('"').Append(dotNetPattern[++i]).Append('"');
+ }
+ else if (c == 'H')
+ {
+ sb.Append('h');
+ }
+ else if (c == 't' && i + 1 < dotNetPattern.Length && dotNetPattern[i + 1] == 't')
+ {
+ sb.Append("AM/PM");
+ i++;
+ }
+ else if (c == 't')
+ {
+ sb.Append("A/P");
+ }
+ else
+ {
+ sb.Append(c);
+ }
+ }
+
+ return sb.ToString();
+ }
}
diff --git a/src/ExcelDataReader/Core/Cell.cs b/src/ExcelDataReader/Core/Cell.cs
index 09f01cef..ffacc1e0 100644
--- a/src/ExcelDataReader/Core/Cell.cs
+++ b/src/ExcelDataReader/Core/Cell.cs
@@ -12,4 +12,4 @@ namespace ExcelDataReader.Core;
/// the Cell XF, with optional overrides from a Cell Style XF.
///
/// Cell error -or- .
-internal sealed record Cell(int ColumnIndex, object? Value, ExtendedFormat EffectiveStyle, CellError? Error);
+internal readonly record struct Cell(int ColumnIndex, object? Value, ExtendedFormat EffectiveStyle, CellError? Error);
diff --git a/src/ExcelDataReader/Core/CommonWorkbook.cs b/src/ExcelDataReader/Core/CommonWorkbook.cs
index 619a1dcb..6a740cc2 100644
--- a/src/ExcelDataReader/Core/CommonWorkbook.cs
+++ b/src/ExcelDataReader/Core/CommonWorkbook.cs
@@ -1,4 +1,7 @@
-using ExcelDataReader.Core.NumberFormat;
+#nullable enable
+
+using System.Globalization;
+using ExcelDataReader.Core.NumberFormat;
namespace ExcelDataReader.Core;
@@ -23,6 +26,14 @@ internal class CommonWorkbook
///
public List CellStyleExtendedFormats { get; } = [];
+ public bool SinglePassMode { get; set; }
+
+ ///
+ /// Gets or sets the culture to use for locale-dependent built-in number format indices.
+ /// When null (the default), hardcoded format strings are used.
+ ///
+ public CultureInfo? Culture { get; set; }
+
private NumberFormatString GeneralNumberFormat { get; } = new("General");
public ExtendedFormat GetEffectiveCellStyle(int xfIndex, int numberFormatFromCell)
@@ -54,9 +65,14 @@ public NumberFormatString GetNumberFormatString(int numberFormatIndex)
return numberFormat;
}
- numberFormat = BuiltinNumberFormat.GetBuiltinNumberFormat(numberFormatIndex);
+ numberFormat = Culture != null
+ ? BuiltinNumberFormat.GetBuiltinNumberFormat(numberFormatIndex, Culture)
+#pragma warning disable CA1304 // Intentional: null Culture means use hardcoded formats for backward compatibility
+ : BuiltinNumberFormat.GetBuiltinNumberFormat(numberFormatIndex);
+#pragma warning restore CA1304
if (numberFormat != null)
{
+ Formats[numberFormatIndex] = numberFormat; // cache for future lookups
return numberFormat;
}
diff --git a/src/ExcelDataReader/Core/CompoundFormat/CompoundStream.cs b/src/ExcelDataReader/Core/CompoundFormat/CompoundStream.cs
index f4e49c7f..fa4bedd5 100644
--- a/src/ExcelDataReader/Core/CompoundFormat/CompoundStream.cs
+++ b/src/ExcelDataReader/Core/CompoundFormat/CompoundStream.cs
@@ -4,6 +4,10 @@ namespace ExcelDataReader.Core.CompoundFormat;
internal sealed class CompoundStream : Stream
{
+ private readonly byte[] _sectorBuffer;
+
+ private int _sectorBufferValidLength;
+
public CompoundStream(CompoundDocument document, Stream baseStream, List sectorChain, int length, bool leaveOpen)
{
Document = document;
@@ -12,6 +16,7 @@ public CompoundStream(CompoundDocument document, Stream baseStream, List s
LeaveOpen = leaveOpen;
Length = length;
SectorChain = sectorChain;
+ _sectorBuffer = new byte[Document.Header.SectorSize];
ReadSector();
}
@@ -27,10 +32,12 @@ public CompoundStream(CompoundDocument document, Stream baseStream, uint baseSec
{
SectorChain = CompoundDocument.GetSectorChain(baseSector, Document.MiniSectorTable);
RootSectorChain = CompoundDocument.GetSectorChain(Document.RootEntry.StreamFirstSector, Document.SectorTable);
+ _sectorBuffer = new byte[Document.Header.MiniSectorSize];
}
else
{
SectorChain = CompoundDocument.GetSectorChain(baseSector, Document.SectorTable);
+ _sectorBuffer = new byte[Document.Header.SectorSize];
}
ReadSector();
@@ -48,7 +55,7 @@ public CompoundStream(CompoundDocument document, Stream baseStream, uint baseSec
public override long Length { get; }
- public override long Position { get => Offset - SectorBytes.Length + SectorOffset; set => Seek(value, SeekOrigin.Begin); }
+ public override long Position { get => Offset - _sectorBufferValidLength + SectorOffset; set => Seek(value, SeekOrigin.Begin); }
private Stream BaseStream { get; set; }
@@ -64,8 +71,6 @@ public CompoundStream(CompoundDocument document, Stream baseStream, uint baseSec
private int SectorOffset { get; set; }
- private byte[] SectorBytes { get; set; }
-
public override void Flush()
{
}
@@ -75,14 +80,14 @@ public override int Read(byte[] buffer, int offset, int count)
int index = 0;
while (index < count && Position < Length)
{
- if (SectorOffset == SectorBytes.Length)
+ if (SectorOffset == _sectorBufferValidLength)
{
ReadSector();
SectorOffset = 0;
}
- var chunkSize = Math.Min(count - index, SectorBytes.Length - SectorOffset);
- Array.Copy(SectorBytes, SectorOffset, buffer, offset + index, chunkSize);
+ var chunkSize = Math.Min(count - index, _sectorBufferValidLength - SectorOffset);
+ Array.Copy(_sectorBuffer, SectorOffset, buffer, offset + index, chunkSize);
index += chunkSize;
SectorOffset += chunkSize;
}
@@ -161,12 +166,12 @@ private void ReadMiniSector()
BaseStream.Seek(Document.GetSectorOffset(rootSector) + rootOffset, SeekOrigin.Begin);
var chunkSize = (int)Math.Min(Length - Offset, Document.Header.MiniSectorSize);
- SectorBytes = new byte[chunkSize];
- if (BaseStream.ReadAtLeast(SectorBytes, 0, chunkSize) < chunkSize)
+ if (BaseStream.ReadAtLeast(_sectorBuffer, 0, chunkSize) < chunkSize)
{
throw new CompoundDocumentException(Errors.ErrorEndOfFile);
}
+ _sectorBufferValidLength = chunkSize;
Offset += chunkSize;
SectorChainOffset++;
}
@@ -177,12 +182,12 @@ private void ReadRegularSector()
BaseStream.Seek(Document.GetSectorOffset(sector), SeekOrigin.Begin);
var chunkSize = (int)Math.Min(Length - Offset, Document.Header.SectorSize);
- SectorBytes = new byte[chunkSize];
- if (BaseStream.ReadAtLeast(SectorBytes, 0, chunkSize) < chunkSize)
+ if (BaseStream.ReadAtLeast(_sectorBuffer, 0, chunkSize) < chunkSize)
{
throw new CompoundDocumentException(Errors.ErrorEndOfFile);
}
+ _sectorBufferValidLength = chunkSize;
Offset += chunkSize;
SectorChainOffset++;
}
diff --git a/src/ExcelDataReader/Core/CsvFormat/CsvParser.cs b/src/ExcelDataReader/Core/CsvFormat/CsvParser.cs
index dd3b5869..bf198ac5 100644
--- a/src/ExcelDataReader/Core/CsvFormat/CsvParser.cs
+++ b/src/ExcelDataReader/Core/CsvFormat/CsvParser.cs
@@ -236,7 +236,7 @@ private bool ReadLinebreak(char c)
private void AddValueToRow()
{
RowResult.Add(ValueResult.ToString(0, ValueResult.Length - TrailingWhitespaceCount));
- ValueResult = new StringBuilder();
+ ValueResult.Clear();
TrailingWhitespaceCount = 0;
}
diff --git a/src/ExcelDataReader/Core/CsvFormat/CsvWorksheet.cs b/src/ExcelDataReader/Core/CsvFormat/CsvWorksheet.cs
index 0e8a27e9..944fc9bc 100644
--- a/src/ExcelDataReader/Core/CsvFormat/CsvWorksheet.cs
+++ b/src/ExcelDataReader/Core/CsvFormat/CsvWorksheet.cs
@@ -65,6 +65,8 @@ public int RowCount
}
}
+ public CellRange Dimension => null;
+
public Stream Stream { get; }
public Encoding Encoding { get; }
diff --git a/src/ExcelDataReader/Core/Helpers.cs b/src/ExcelDataReader/Core/Helpers.cs
index d3598fb8..0f706f21 100644
--- a/src/ExcelDataReader/Core/Helpers.cs
+++ b/src/ExcelDataReader/Core/Helpers.cs
@@ -32,6 +32,11 @@ public static bool IsSingleByteEncoding(Encoding encoding)
public static string ConvertEscapeChars(string input)
{
+ // Fast rejection: the escape pattern always starts with "_x". For typical
+ // spreadsheet strings this short-circuits before the regex engine is entered.
+ if (input.IndexOf("_x", StringComparison.Ordinal) < 0)
+ return input;
+
return EscapeRegex().Replace(input, m => ((char)uint.Parse(m.Groups[1].Value, NumberStyles.HexNumber, CultureInfo.InvariantCulture)).ToString());
}
diff --git a/src/ExcelDataReader/Core/IWorksheet.cs b/src/ExcelDataReader/Core/IWorksheet.cs
index 215a90e9..4e39163b 100644
--- a/src/ExcelDataReader/Core/IWorksheet.cs
+++ b/src/ExcelDataReader/Core/IWorksheet.cs
@@ -17,6 +17,8 @@ internal interface IWorksheet
int RowCount { get; }
+ CellRange Dimension { get; }
+
CellRange[] MergeCells { get; }
List ColumnWidths { get; }
diff --git a/src/ExcelDataReader/Core/NumberFormat/Parser.cs b/src/ExcelDataReader/Core/NumberFormat/Parser.cs
index 85d40f76..f2910650 100644
--- a/src/ExcelDataReader/Core/NumberFormat/Parser.cs
+++ b/src/ExcelDataReader/Core/NumberFormat/Parser.cs
@@ -248,7 +248,19 @@ private static string ReadToken(Tokenizer reader, out bool syntaxError)
return reader.Substring(offset, length);
}
- syntaxError = reader.Position < reader.Length;
+ // Treat any unrecognised character as an implicit literal (single-char token).
+ // This matches Excel behaviour: characters that are not format metacharacters
+ // are passed through as literals (e.g. unquoted CJK/Thai characters in
+ // locale-specific built-in format strings such as [$-411]yyyy"年"m"月").
+ if (reader.Position < reader.Length)
+ {
+ reader.Advance(1);
+ syntaxError = false;
+ var length = reader.Position - offset;
+ return reader.Substring(offset, length);
+ }
+
+ syntaxError = false;
return null;
}
diff --git a/src/ExcelDataReader/Core/OfficeCrypto/CryptoHelpers.cs b/src/ExcelDataReader/Core/OfficeCrypto/CryptoHelpers.cs
index d67da26f..cba4d201 100644
--- a/src/ExcelDataReader/Core/OfficeCrypto/CryptoHelpers.cs
+++ b/src/ExcelDataReader/Core/OfficeCrypto/CryptoHelpers.cs
@@ -81,4 +81,11 @@ public static byte[] DecryptBytes(ICryptoTransform transform, byte[] bytes, int
csDecrypt.ReadAtLeast(result, 0, length);
return result;
}
+
+ public static void DecryptBytes(ICryptoTransform transform, byte[] bytes, int chunkSize, byte[] output)
+ {
+ using MemoryStream msDecrypt = new(bytes, 0, chunkSize);
+ using CryptoStream csDecrypt = new(msDecrypt, transform, CryptoStreamMode.Read);
+ csDecrypt.ReadAtLeast(output, 0, chunkSize);
+ }
}
diff --git a/src/ExcelDataReader/Core/OpenXmlFormat/BinaryFormat/BiffReader.cs b/src/ExcelDataReader/Core/OpenXmlFormat/BinaryFormat/BiffReader.cs
index 18ef788f..66999354 100644
--- a/src/ExcelDataReader/Core/OpenXmlFormat/BinaryFormat/BiffReader.cs
+++ b/src/ExcelDataReader/Core/OpenXmlFormat/BinaryFormat/BiffReader.cs
@@ -8,7 +8,7 @@ namespace ExcelDataReader.Core.OpenXmlFormat.BinaryFormat;
internal abstract class BiffReader(Stream stream) : RecordReader
{
- private readonly byte[] _buffer = new byte[128];
+ private readonly byte[] _buffer = new byte[512];
private Stream Stream { get; } = stream ?? throw new ArgumentNullException(nameof(stream));
@@ -18,11 +18,28 @@ internal abstract class BiffReader(Stream stream) : RecordReader
!TryReadVariableValue(out var recordLength))
return null;
+#if NETSTANDARD2_1_OR_GREATER || NET8_0_OR_GREATER
+ bool rented = recordLength >= _buffer.Length;
+ byte[] buffer = rented
+ ? System.Buffers.ArrayPool.Shared.Rent((int)recordLength)
+ : _buffer;
+#else
byte[] buffer = recordLength < _buffer.Length ? _buffer : new byte[recordLength];
- if (Stream.ReadAtLeast(buffer, 0, (int)recordLength) != recordLength)
- return null;
+#endif
+ try
+ {
+ if (Stream.ReadAtLeast(buffer, 0, (int)recordLength) != recordLength)
+ return null;
- return ReadOverride(buffer, recordId, recordLength);
+ return ReadOverride(buffer, recordId, recordLength);
+ }
+ finally
+ {
+#if NETSTANDARD2_1_OR_GREATER || NET8_0_OR_GREATER
+ if (rented)
+ System.Buffers.ArrayPool.Shared.Return(buffer);
+#endif
+ }
}
protected static uint GetDWord(byte[] buffer, uint offset)
@@ -52,10 +69,11 @@ protected static ushort GetWord(byte[] buffer, uint offset)
protected static string GetString(byte[] buffer, uint offset, uint length)
{
- StringBuilder sb = new((int)length);
- for (uint i = offset; i < offset + 2 * length; i += 2)
- sb.Append((char)GetWord(buffer, i));
- return sb.ToString();
+#if NETSTANDARD2_1_OR_GREATER || NET8_0_OR_GREATER
+ return Encoding.Unicode.GetString(buffer.AsSpan((int)offset, (int)(length * 2)));
+#else
+ return Encoding.Unicode.GetString(buffer, (int)offset, (int)(length * 2));
+#endif
}
protected static string? GetNullableString(byte[] buffer, ref uint offset)
@@ -64,11 +82,13 @@ protected static string GetString(byte[] buffer, uint offset, uint length)
offset += 4;
if (length == uint.MaxValue)
return null;
- StringBuilder sb = new((int)length);
- uint end = offset + length * 2;
- for (; offset < end; offset += 2)
- sb.Append((char)GetWord(buffer, offset));
- return sb.ToString();
+#if NETSTANDARD2_1_OR_GREATER || NET8_0_OR_GREATER
+ string result = Encoding.Unicode.GetString(buffer.AsSpan((int)offset, (int)(length * 2)));
+#else
+ string result = Encoding.Unicode.GetString(buffer, (int)offset, (int)(length * 2));
+#endif
+ offset += length * 2;
+ return result;
}
protected static double GetRkNumber(byte[] buffer, uint offset)
diff --git a/src/ExcelDataReader/Core/OpenXmlFormat/BinaryFormat/BiffWorksheetReader.cs b/src/ExcelDataReader/Core/OpenXmlFormat/BinaryFormat/BiffWorksheetReader.cs
index ffcc8cd2..9ec01d03 100644
--- a/src/ExcelDataReader/Core/OpenXmlFormat/BinaryFormat/BiffWorksheetReader.cs
+++ b/src/ExcelDataReader/Core/OpenXmlFormat/BinaryFormat/BiffWorksheetReader.cs
@@ -4,11 +4,11 @@ namespace ExcelDataReader.Core.OpenXmlFormat.BinaryFormat;
internal sealed class BiffWorksheetReader(Stream stream, bool preparing) : BiffReader(stream)
{
- private const uint Row = 0x00;
+ private const uint Row = 0x00;
private const uint Blank = 0x01;
private const uint Number = 0x02;
- private const uint BoolError = 0x03;
- private const uint Bool = 0x04;
+ private const uint BoolError = 0x03;
+ private const uint Bool = 0x04;
private const uint Float = 0x05;
private const uint String = 0x06;
private const uint SharedString = 0x07;
@@ -24,6 +24,7 @@ internal sealed class BiffWorksheetReader(Stream stream, bool preparing) : BiffR
private const uint SheetDataBegin = 0x91;
private const uint SheetDataEnd = 0x92;
private const uint SheetPr = 0x93;
+ private const uint SheetDim = 0x94; // BrtWsDim - worksheet dimensions
private const uint SheetFormatPr = 0x1E5;
// private const uint ColumnsBegin = 0x186;
@@ -36,14 +37,68 @@ internal sealed class BiffWorksheetReader(Stream stream, bool preparing) : BiffR
// private const uint MergeCellsEnd = 178;
private const uint MergeCell = 176;
+ // State for pre-scan mode: track max indices while suppressing per-cell record allocations
+ private int _scanMaxRowIndex = int.MinValue;
+ private int _scanMaxColumnIndex = int.MinValue;
+ private bool _inPreparingScan;
+
protected override Record ReadOverride(byte[] buffer, uint recordId, uint recordLength)
{
- switch (recordId)
+ switch (recordId)
{
case SheetDataBegin:
+ if (preparing)
+ {
+ _inPreparingScan = true;
+ return Record.Default; // suppress SheetDataBeginRecord; ScanResultRecord comes at SheetDataEnd
+ }
+
return new SheetDataBeginRecord();
case SheetDataEnd:
+ if (_inPreparingScan)
+ {
+ _inPreparingScan = false;
+ return new ScanResultRecord(_scanMaxRowIndex, _scanMaxColumnIndex);
+ }
+
return new SheetDataEndRecord();
+ case Row when _inPreparingScan:
+ {
+ int rowIndex = GetInt32(buffer, 0);
+ if (rowIndex > _scanMaxRowIndex)
+ {
+ _scanMaxRowIndex = rowIndex;
+ }
+
+ return Record.Default;
+ }
+
+ // Blank cells have null value and null error — not counted toward column maximum
+ case Blank when _inPreparingScan:
+ return Record.Default;
+
+ // All other cell types have a non-null value or non-null error — counted toward column maximum
+ case Number when _inPreparingScan:
+ case Float when _inPreparingScan:
+ case String when _inPreparingScan:
+ case SharedString when _inPreparingScan:
+ case FormulaNumber when _inPreparingScan:
+ case FormulaString when _inPreparingScan:
+ case FormulaBool when _inPreparingScan:
+ case BrtCellRString when _inPreparingScan:
+ case Bool when _inPreparingScan:
+ case BoolError when _inPreparingScan:
+ case FormulaError when _inPreparingScan:
+ {
+ int column = (int)GetDWord(buffer, 0);
+ if (column > _scanMaxColumnIndex)
+ {
+ _scanMaxColumnIndex = column;
+ }
+
+ return Record.Default;
+ }
+
case SheetPr: // BrtWsProp
{
// Must be between 0 and 31 characters
@@ -56,6 +111,15 @@ protected override Record ReadOverride(byte[] buffer, uint recordId, uint record
return new SheetPrRecord(codeName);
}
+ case SheetDim: // BrtWsDim - worksheet dimensions (rwFirst, rwLast, colFirst, colLast)
+ {
+ int dimFromRow = GetInt32(buffer, 0);
+ int dimToRow = GetInt32(buffer, 4);
+ int dimFromColumn = GetInt32(buffer, 8);
+ int dimToColumn = GetInt32(buffer, 12);
+ return new SheetDimRecord(new CellRange(dimFromColumn, dimFromRow, dimToColumn, dimToRow));
+ }
+
case SheetFormatPr: // BrtWsFmtInfo
{
// TODO Default column width
@@ -92,7 +156,7 @@ protected override Record ReadOverride(byte[] buffer, uint recordId, uint record
var footerEven = GetNullableString(buffer, ref offset);
var headerFirst = GetNullableString(buffer, ref offset);
var footerFirst = GetNullableString(buffer, ref offset);
- return new HeaderFooterRecord(new HeaderFooter(differentFirst, differentOddEven)
+ return new HeaderFooterRecord(new HeaderFooter(differentFirst, differentOddEven)
{
FirstHeader = headerFirst,
FirstFooter = footerFirst,
@@ -170,7 +234,7 @@ protected override Record ReadOverride(byte[] buffer, uint recordId, uint record
return Record.Default;
}
- CellRecord ReadCell(object value, CellError? errorValue = null)
+ CellRecord ReadCell(object value, CellError? errorValue = null)
{
int column = (int)GetDWord(buffer, 0);
uint xfIndex = GetDWord(buffer, 4) & 0xffffff;
diff --git a/src/ExcelDataReader/Core/OpenXmlFormat/Records/ScanResultRecord.cs b/src/ExcelDataReader/Core/OpenXmlFormat/Records/ScanResultRecord.cs
new file mode 100644
index 00000000..92864b28
--- /dev/null
+++ b/src/ExcelDataReader/Core/OpenXmlFormat/Records/ScanResultRecord.cs
@@ -0,0 +1,14 @@
+namespace ExcelDataReader.Core.OpenXmlFormat.Records;
+
+///
+/// Produced by worksheet readers in preparing (pre-scan) mode instead of individual
+/// RowHeaderRecord and CellRecord instances. Contains the maximum row and column indices
+/// found in the sheet data, allowing the worksheet constructor to determine FieldCount
+/// and RowCount without allocating per-cell objects.
+///
+internal sealed class ScanResultRecord(int maxRowIndex, int maxColumnIndex) : Record
+{
+ public int MaxRowIndex { get; } = maxRowIndex;
+
+ public int MaxColumnIndex { get; } = maxColumnIndex;
+}
diff --git a/src/ExcelDataReader/Core/OpenXmlFormat/Records/SheetDimRecord.cs b/src/ExcelDataReader/Core/OpenXmlFormat/Records/SheetDimRecord.cs
new file mode 100644
index 00000000..96315e35
--- /dev/null
+++ b/src/ExcelDataReader/Core/OpenXmlFormat/Records/SheetDimRecord.cs
@@ -0,0 +1,12 @@
+namespace ExcelDataReader.Core.OpenXmlFormat.Records
+{
+ internal sealed class SheetDimRecord : Record
+ {
+ public SheetDimRecord(CellRange range)
+ {
+ Range = range;
+ }
+
+ public CellRange Range { get; }
+ }
+}
diff --git a/src/ExcelDataReader/Core/OpenXmlFormat/Records/SstCountRecord.cs b/src/ExcelDataReader/Core/OpenXmlFormat/Records/SstCountRecord.cs
new file mode 100644
index 00000000..0b075233
--- /dev/null
+++ b/src/ExcelDataReader/Core/OpenXmlFormat/Records/SstCountRecord.cs
@@ -0,0 +1,6 @@
+namespace ExcelDataReader.Core.OpenXmlFormat.Records;
+
+internal sealed class SstCountRecord(int uniqueCount) : Record
+{
+ public int UniqueCount { get; } = uniqueCount;
+}
diff --git a/src/ExcelDataReader/Core/OpenXmlFormat/XlsxWorkbook.cs b/src/ExcelDataReader/Core/OpenXmlFormat/XlsxWorkbook.cs
index 82983f5e..68f9a194 100644
--- a/src/ExcelDataReader/Core/OpenXmlFormat/XlsxWorkbook.cs
+++ b/src/ExcelDataReader/Core/OpenXmlFormat/XlsxWorkbook.cs
@@ -24,7 +24,11 @@ public XlsxWorkbook(ZipWorker zipWorker)
private List Sheets { get; } = [];
- public IEnumerable ReadWorksheets() => Sheets.Select(sheet => new XlsxWorksheet(_zipWorker, this, sheet));
+ public IEnumerable ReadWorksheets()
+ {
+ foreach (var sheet in Sheets)
+ yield return new XlsxWorksheet(_zipWorker, this, sheet, SinglePassMode);
+ }
private void ReadWorkbook()
{
@@ -57,6 +61,9 @@ private void ReadSharedStrings()
{
switch (record)
{
+ case SstCountRecord countRecord:
+ SST.Capacity = countRecord.UniqueCount;
+ break;
case SharedStringRecord pr:
SST.Add(pr.Value);
break;
diff --git a/src/ExcelDataReader/Core/OpenXmlFormat/XlsxWorksheet.cs b/src/ExcelDataReader/Core/OpenXmlFormat/XlsxWorksheet.cs
index 575fe70c..bbbc640f 100644
--- a/src/ExcelDataReader/Core/OpenXmlFormat/XlsxWorksheet.cs
+++ b/src/ExcelDataReader/Core/OpenXmlFormat/XlsxWorksheet.cs
@@ -6,7 +6,7 @@ namespace ExcelDataReader.Core.OpenXmlFormat;
internal sealed class XlsxWorksheet : IWorksheet
{
- public XlsxWorksheet(ZipWorker document, XlsxWorkbook workbook, SheetRecord refSheet)
+ public XlsxWorksheet(ZipWorker document, XlsxWorkbook workbook, SheetRecord refSheet, bool singlePassMode = false)
{
Document = document;
Workbook = workbook;
@@ -19,7 +19,7 @@ public XlsxWorksheet(ZipWorker document, XlsxWorkbook workbook, SheetRecord refS
if (string.IsNullOrEmpty(Path))
return;
- using var sheetStream = Document.GetWorksheetReader(Path, true);
+ using var sheetStream = Document.GetWorksheetReader(Path, !singlePassMode);
if (sheetStream == null)
return;
@@ -30,24 +30,23 @@ public XlsxWorksheet(ZipWorker document, XlsxWorkbook workbook, SheetRecord refS
List columnWidths = [];
List cellRanges = [];
- bool inSheetData = false;
+ bool stop = false;
- while (sheetStream.Read() is { } record)
+ while (!stop && sheetStream.Read() is { } record)
{
switch (record)
{
- case SheetDataBeginRecord _:
- inSheetData = true;
- break;
- case SheetDataEndRecord _:
- inSheetData = false;
+ case SheetDimRecord dimRecord:
+ Dimension = dimRecord.Range;
break;
- case RowHeaderRecord row when inSheetData:
- rowIndexMaximum = Math.Max(rowIndexMaximum, row.RowIndex);
+ case SheetDataBeginRecord _ when singlePassMode:
+ // In single-pass mode stop before reading cells; ReadRows() will be the only pass
+ stop = true;
break;
- case CellRecord cell when inSheetData:
- if (cell.Value != null || cell.Error != null)
- columnIndexMaximum = Math.Max(columnIndexMaximum, cell.ColumnIndex);
+ case ScanResultRecord scan:
+ // Multi-pass: sheetData was scanned without allocating per-cell records
+ rowIndexMaximum = Math.Max(rowIndexMaximum, scan.MaxRowIndex);
+ columnIndexMaximum = Math.Max(columnIndexMaximum, scan.MaxColumnIndex);
break;
case ColumnRecord column:
columnWidths.Add(column.Column);
@@ -71,7 +70,12 @@ public XlsxWorksheet(ZipWorker document, XlsxWorkbook workbook, SheetRecord refS
ColumnWidths = columnWidths;
MergeCells = [.. cellRanges];
- if (rowIndexMaximum != int.MinValue && columnIndexMaximum != int.MinValue)
+ if (singlePassMode)
+ {
+ // In single-pass mode FieldCount comes from the dimension hint only (may be 0 if absent)
+ FieldCount = columnIndexMaximum != int.MinValue ? columnIndexMaximum + 1 : 0;
+ }
+ else if (rowIndexMaximum != int.MinValue && columnIndexMaximum != int.MinValue)
{
FieldCount = columnIndexMaximum + 1;
RowCount = rowIndexMaximum + 1;
@@ -82,6 +86,8 @@ public XlsxWorksheet(ZipWorker document, XlsxWorkbook workbook, SheetRecord refS
public int RowCount { get; }
+ public CellRange Dimension { get; private set; }
+
public string Name { get; }
public string CodeName { get; }
diff --git a/src/ExcelDataReader/Core/OpenXmlFormat/XmlFormat/XmlSharedStringsReader.cs b/src/ExcelDataReader/Core/OpenXmlFormat/XmlFormat/XmlSharedStringsReader.cs
index 3165d058..841f48ea 100644
--- a/src/ExcelDataReader/Core/OpenXmlFormat/XmlFormat/XmlSharedStringsReader.cs
+++ b/src/ExcelDataReader/Core/OpenXmlFormat/XmlFormat/XmlSharedStringsReader.cs
@@ -7,6 +7,7 @@ internal sealed class XmlSharedStringsReader(XmlReader reader) : XmlRecordReader
{
private const string ElementSst = "sst";
private const string ElementStringItem = "si";
+ private const string AttributeUniqueCount = "uniqueCount";
protected override IEnumerable ReadOverride()
{
@@ -15,6 +16,12 @@ protected override IEnumerable ReadOverride()
yield break;
}
+ var uniqueCountStr = Reader.GetAttribute(AttributeUniqueCount);
+ if (int.TryParse(uniqueCountStr, out var uniqueCount) && uniqueCount > 0)
+ {
+ yield return new SstCountRecord(uniqueCount);
+ }
+
if (!XmlReaderHelper.ReadFirstContent(Reader))
{
yield break;
@@ -22,7 +29,7 @@ protected override IEnumerable ReadOverride()
while (!Reader.EOF)
{
- if (Reader.IsStartElement(ElementStringItem, ProperNamespaces.NsSpreadsheetMl))
+ if (Reader.NodeType == XmlNodeType.Element && Reader.LocalName == ElementStringItem)
{
var value = StringHelper.ReadStringItem(Reader, ProperNamespaces.NsSpreadsheetMl);
yield return new SharedStringRecord(value);
diff --git a/src/ExcelDataReader/Core/OpenXmlFormat/XmlFormat/XmlWorksheetReader.cs b/src/ExcelDataReader/Core/OpenXmlFormat/XmlFormat/XmlWorksheetReader.cs
index 5bec55e0..74b3ecf2 100644
--- a/src/ExcelDataReader/Core/OpenXmlFormat/XmlFormat/XmlWorksheetReader.cs
+++ b/src/ExcelDataReader/Core/OpenXmlFormat/XmlFormat/XmlWorksheetReader.cs
@@ -46,6 +46,8 @@ internal sealed class XmlWorksheetReader(XmlReader reader, bool preparing) : Xml
private const string NMergeCell = "mergeCell";
+ private const string NDimension = "dimension";
+
private const string ACustomHeight = "customHeight";
private const string AHt = "ht";
@@ -65,59 +67,121 @@ protected override IEnumerable ReadOverride()
{
if (Reader.IsStartElement(NSheetData, ProperNamespaces.NsSpreadsheetMl))
{
- yield return new SheetDataBeginRecord();
- if (!XmlReaderHelper.ReadFirstContent(Reader))
+ if (preparing)
{
- yield return new SheetDataEndRecord();
- continue;
- }
+ // Fast pre-scan: read row/column indices directly without allocating per-cell records.
+ int maxRowIndex = int.MinValue;
+ int maxColumnIndex = int.MinValue;
- int rowIndex = -1;
- while (!Reader.EOF)
+ if (XmlReaderHelper.ReadFirstContent(Reader))
+ {
+ int seqRowIndex = -1;
+ while (!Reader.EOF)
+ {
+ if (Reader.NodeType == XmlNodeType.Element && Reader.LocalName == NRow)
+ {
+ // Mirror ReadOverride: use r attribute if valid, otherwise sequential
+ if (int.TryParse(Reader.GetAttribute(AR), out int arValue))
+ seqRowIndex = arValue - 1;
+ else
+ seqRowIndex++;
+
+ maxRowIndex = Math.Max(maxRowIndex, seqRowIndex);
+
+ if (XmlReaderHelper.ReadFirstContent(Reader))
+ {
+ int nextColumnIndex = 0;
+ while (!Reader.EOF)
+ {
+ if (Reader.NodeType == XmlNodeType.Element && Reader.LocalName == NC)
+ {
+ // Mirror ReadCell: use the r attribute if valid, otherwise sequential
+ int colIndex;
+ if (ReferenceHelper.ParseReference(Reader.GetAttribute(AR), out int referenceColumn, out _))
+ colIndex = referenceColumn - 1;
+ else
+ colIndex = nextColumnIndex;
+
+ nextColumnIndex = colIndex + 1;
+
+ // Only non-empty cells count toward the column maximum
+ if (!Reader.IsEmptyElement)
+ maxColumnIndex = Math.Max(maxColumnIndex, colIndex);
+
+ Reader.Skip();
+ }
+ else if (!XmlReaderHelper.SkipContent(Reader))
+ {
+ break;
+ }
+ }
+ }
+ }
+ else if (!XmlReaderHelper.SkipContent(Reader))
+ {
+ break;
+ }
+ }
+ }
+
+ yield return new ScanResultRecord(maxRowIndex, maxColumnIndex);
+ }
+ else
{
- if (Reader.IsStartElement(NRow, ProperNamespaces.NsSpreadsheetMl))
+ yield return new SheetDataBeginRecord();
+ if (!XmlReaderHelper.ReadFirstContent(Reader))
+ {
+ yield return new SheetDataEndRecord();
+ continue;
+ }
+
+ int rowIndex = -1;
+ while (!Reader.EOF)
{
- if (int.TryParse(Reader.GetAttribute(AR), out int arValue))
- rowIndex = arValue - 1; // The row attribute is 1-based
- else
- rowIndex++;
+ if (Reader.NodeType == XmlNodeType.Element && Reader.LocalName == NRow)
+ {
+ if (int.TryParse(Reader.GetAttribute(AR), out int arValue))
+ rowIndex = arValue - 1; // The row attribute is 1-based
+ else
+ rowIndex++;
#pragma warning disable CA1806 // Do not ignore method results
- int.TryParse(Reader.GetAttribute(AHidden), out int hidden);
- int.TryParse(Reader.GetAttribute(ACustomHeight), out int _);
+ int.TryParse(Reader.GetAttribute(AHidden), out int hidden);
+ int.TryParse(Reader.GetAttribute(ACustomHeight), out int _);
#pragma warning restore CA1806 // Do not ignore method results
- double? height = double.TryParse(Reader.GetAttribute(AHt), NumberStyles.Any, CultureInfo.InvariantCulture, out var ahtValue) ? Math.Abs(ahtValue) : null;
-
- yield return new RowHeaderRecord(rowIndex, hidden != 0, height);
+ double? height = double.TryParse(Reader.GetAttribute(AHt), NumberStyles.Any, CultureInfo.InvariantCulture, out var ahtValue) ? Math.Abs(ahtValue) : null;
- if (!XmlReaderHelper.ReadFirstContent(Reader))
- {
- continue;
- }
+ yield return new RowHeaderRecord(rowIndex, hidden != 0, height);
- int nextColumnIndex = 0;
- while (!Reader.EOF)
- {
- if (Reader.IsStartElement(NC, ProperNamespaces.NsSpreadsheetMl))
+ if (!XmlReaderHelper.ReadFirstContent(Reader))
{
- var cell = ReadCell(nextColumnIndex, ProperNamespaces.NsSpreadsheetMl);
- nextColumnIndex = cell.ColumnIndex + 1;
- yield return cell;
+ continue;
}
- else if (!XmlReaderHelper.SkipContent(Reader))
+
+ int nextColumnIndex = 0;
+ while (!Reader.EOF)
{
- break;
+ if (Reader.NodeType == XmlNodeType.Element && Reader.LocalName == NC)
+ {
+ var cell = ReadCell(nextColumnIndex, ProperNamespaces.NsSpreadsheetMl);
+ nextColumnIndex = cell.ColumnIndex + 1;
+ yield return cell;
+ }
+ else if (!XmlReaderHelper.SkipContent(Reader))
+ {
+ break;
+ }
}
}
+ else if (!XmlReaderHelper.SkipContent(Reader))
+ {
+ break;
+ }
}
- else if (!XmlReaderHelper.SkipContent(Reader))
- {
- break;
- }
- }
- yield return new SheetDataEndRecord();
+ yield return new SheetDataEndRecord();
+ }
}
else if (Reader.IsStartElement(NMergeCells, ProperNamespaces.NsSpreadsheetMl))
{
@@ -193,6 +257,17 @@ protected override IEnumerable ReadOverride()
Reader.Skip();
}
+ else if (Reader.IsStartElement(NDimension, ProperNamespaces.NsSpreadsheetMl))
+ {
+ var refAttr = Reader.GetAttribute(ARef);
+ if (refAttr != null)
+ {
+ var range = CellRange.Parse(refAttr);
+ yield return new SheetDimRecord(range);
+ }
+
+ Reader.Skip();
+ }
else if (!XmlReaderHelper.SkipContent(Reader))
{
break;
@@ -259,25 +334,6 @@ private CellRecord ReadCell(int nextColumnIndex, string nsSpreadsheetMl)
else
columnIndex = nextColumnIndex;
- if (preparing)
- {
- // We only care about columnIndex and if there is any content or not when preparing.
- if (!XmlReaderHelper.ReadFirstContent(Reader))
- {
- return new CellRecord(columnIndex, 0, null, null);
- }
-
- while (!Reader.EOF)
- {
- if (!XmlReaderHelper.SkipContent(Reader))
- {
- break;
- }
- }
-
- return new CellRecord(columnIndex, 0, string.Empty, null);
- }
-
var aS = Reader.GetAttribute(AS);
if (aS != null)
{
@@ -298,13 +354,13 @@ private CellRecord ReadCell(int nextColumnIndex, string nsSpreadsheetMl)
CellError? error = null;
while (!Reader.EOF)
{
- if (Reader.IsStartElement(NV, nsSpreadsheetMl))
+ if (Reader.NodeType == XmlNodeType.Element && Reader.LocalName == NV)
{
string rawValue = Reader.ReadElementContentAsString();
if (!string.IsNullOrEmpty(rawValue))
ConvertCellValue(rawValue, aT, out value, out error);
}
- else if (Reader.IsStartElement(NIs, nsSpreadsheetMl))
+ else if (Reader.NodeType == XmlNodeType.Element && Reader.LocalName == NIs)
{
string rawValue = StringHelper.ReadStringItem(Reader, nsSpreadsheetMl);
if (!string.IsNullOrEmpty(rawValue))
diff --git a/src/ExcelDataReader/Core/OpenXmlFormat/ZipWorker.cs b/src/ExcelDataReader/Core/OpenXmlFormat/ZipWorker.cs
index 91130d0e..28050027 100644
--- a/src/ExcelDataReader/Core/OpenXmlFormat/ZipWorker.cs
+++ b/src/ExcelDataReader/Core/OpenXmlFormat/ZipWorker.cs
@@ -14,10 +14,18 @@ internal sealed partial class ZipWorker : IDisposable
private const string Format = "xml";
private const string BinFormat = "bin";
- private static readonly XmlReaderSettings XmlSettings = new()
+ // Pre-populate the shared NameTable with every namespace URI and element/attribute name
+ // used by the xlsx/xlsb readers. When the XmlReader parses a namespace declaration it
+ // calls NameTable.Add() which returns the pre-existing atom, turning every subsequent
+ // Reader.NamespaceURI == someConstantString comparison into a cheap reference-equality
+ // check instead of a full 65-character string comparison.
+ private static readonly XmlNameTable XmlNameTable = BuildNameTable();
+
+ private static readonly XmlReaderSettings XmlSettings = new()
{
- IgnoreComments = true,
+ IgnoreComments = true,
IgnoreWhitespace = true,
+ NameTable = XmlNameTable,
};
private readonly Dictionary _entries = new(StringComparer.OrdinalIgnoreCase);
@@ -145,7 +153,7 @@ static string ResolvePath(string? basePath, string path)
return new XmlSharedStringsReader(XmlReader.Create(entry.Open(), XmlSettings));
if (entry.FullName.EndsWith(".bin", StringComparison.Ordinal))
- return new BiffSharedStringsReader(entry.Open());
+ return new BiffSharedStringsReader(OpenZipEntry(entry));
}
return null;
@@ -162,7 +170,7 @@ static string ResolvePath(string? basePath, string path)
return new XmlStylesReader(XmlReader.Create(entry.Open(), XmlSettings));
if (entry.FullName.EndsWith(".bin", StringComparison.Ordinal))
- return new BiffStylesReader(entry.Open());
+ return new BiffStylesReader(OpenZipEntry(entry));
}
return null;
@@ -178,7 +186,7 @@ static string ResolvePath(string? basePath, string path)
if (entry.FullName.EndsWith(".xml", StringComparison.Ordinal))
return new XmlWorkbookReader(XmlReader.Create(entry.Open(), XmlSettings), _worksheetRels);
else if (entry.FullName.EndsWith(".bin", StringComparison.Ordinal))
- return new BiffWorkbookReader(entry.Open(), _worksheetRels);
+ return new BiffWorkbookReader(OpenZipEntry(entry), _worksheetRels);
}
throw new Exceptions.HeaderException(Errors.ErrorZipNoOpenXml);
@@ -205,13 +213,67 @@ static string ResolvePath(string? basePath, string path)
return null;
}
- // for some reason, reading of zip entry is slow on NET Core.
- // fix that with usage of BufferedStream
-#if NETSTANDARD2_0_OR_GREATER || NET8_0_OR_GREATER
+ private static NameTable BuildNameTable()
+ {
+ var nt = new NameTable();
+
+ // Namespace URIs
+ nt.Add(XmlNamespaces.NsSpreadsheetMl);
+ nt.Add(XmlNamespaces.StrictNsSpreadsheetMl);
+ nt.Add(XmlNamespaces.NsDocumentRelationship);
+ nt.Add(XmlNamespaces.StrictNsDocumentRelationship);
+
+ // High-frequency element local names (worksheet inner loops)
+ nt.Add("worksheet");
+ nt.Add("sheetData");
+ nt.Add("row");
+ nt.Add("c");
+ nt.Add("v");
+ nt.Add("f");
+ nt.Add("is");
+ nt.Add("t");
+
+ // SST
+ nt.Add("sst");
+ nt.Add("si");
+ nt.Add("r");
+ nt.Add("rPh");
+ nt.Add("phoneticPr");
+
+ // Workbook
+ nt.Add("workbook");
+ nt.Add("sheets");
+ nt.Add("sheet");
+ nt.Add("bookViews");
+ nt.Add("workbookView");
+
+ // Styles
+ nt.Add("styleSheet");
+ nt.Add("cellXfs");
+ nt.Add("xf");
+ nt.Add("numFmts");
+ nt.Add("numFmt");
+
+ // Common structural elements
+ nt.Add("mergeCell");
+ nt.Add("mergeCells");
+ nt.Add("dimension");
+ nt.Add("col");
+ nt.Add("cols");
+ nt.Add("headerFooter");
+ nt.Add("sheetPr");
+ nt.Add("sheetFormatPr");
+
+ return nt;
+ }
+
+ // BufferedStream wrapping is essential for all targets: BiffReader.TryReadVariableValue reads
+ // the variable-length record header one byte at a time. Without buffering each of those reads
+ // hits the DeflateStream (and the zlib Inflater) directly. For a 10x10000 xlsb file the SST
+ // alone produces ~400 000 single-byte DeflateStream.Read calls; a 4 096-byte BufferedStream
+ // reduces that to ~100 Inflater interactions. The guard previously excluded net462; removing it
+ // gives the same benefit to net462 consumers of xlsb/xlsx files.
private static BufferedStream OpenZipEntry(ZipArchiveEntry zipEntry) => new(zipEntry.Open());
-#else
- private static Stream OpenZipEntry(ZipArchiveEntry zipEntry) => zipEntry.Open();
-#endif
private ZipArchiveEntry? FindEntry(string? name)
{
diff --git a/src/ExcelDataReader/Core/Row.cs b/src/ExcelDataReader/Core/Row.cs
index 2e837b04..5ea5069b 100644
--- a/src/ExcelDataReader/Core/Row.cs
+++ b/src/ExcelDataReader/Core/Row.cs
@@ -8,4 +8,4 @@ namespace ExcelDataReader.Core;
/// The zero-based row index.
/// The height of this row in points, zero if hidden or collapsed.
/// The cells.
-internal sealed record Row(int RowIndex, double Height, List Cells);
\ No newline at end of file
+internal readonly record struct Row(int RowIndex, double Height, List| Cells);
\ No newline at end of file
diff --git a/src/ExcelDataReader/Errors.cs b/src/ExcelDataReader/Errors.cs
index 5b8bef9b..97b8ce93 100644
--- a/src/ExcelDataReader/Errors.cs
+++ b/src/ExcelDataReader/Errors.cs
@@ -12,6 +12,7 @@ internal static class Errors
public const string ErrorHeaderSignature = "Invalid file signature.";
public const string ErrorHeaderOrder = "Invalid byte order specified in header.";
public const string ErrorBiffRecordSize = "Buffer size is less than minimum BIFF record size.";
+ public const string ErrorBiffStringSize = "Error reading BIFF string - record size is too small.";
public const string ErrorBiffIlegalBefore = "BIFF Stream error: Moving before stream start.";
public const string ErrorBiffIlegalAfter = "BIFF Stream error: Moving after stream end.";
diff --git a/src/ExcelDataReader/ExcelBinaryReader.cs b/src/ExcelDataReader/ExcelBinaryReader.cs
index 58fe1258..0aa5076d 100644
--- a/src/ExcelDataReader/ExcelBinaryReader.cs
+++ b/src/ExcelDataReader/ExcelBinaryReader.cs
@@ -1,3 +1,4 @@
+using System.Globalization;
using System.Text;
using ExcelDataReader.Core.BinaryFormat;
@@ -5,10 +6,13 @@ namespace ExcelDataReader;
internal sealed class ExcelBinaryReader : ExcelDataReader
{
- public ExcelBinaryReader(Stream stream, string password, Encoding fallbackEncoding)
+ public ExcelBinaryReader(Stream stream, string password, Encoding fallbackEncoding, CultureInfo culture = null, bool singlePassMode = false)
{
Workbook = new XlsWorkbook(stream, password, fallbackEncoding);
-
+ Workbook.Culture = culture;
+ Workbook.SinglePassMode = singlePassMode;
+ SinglePassMode = singlePassMode;
+
// By default, the data reader is positioned on the first result.
Reset();
}
diff --git a/src/ExcelDataReader/ExcelDataReader.cs b/src/ExcelDataReader/ExcelDataReader.cs
index 121e83f5..009855b0 100644
--- a/src/ExcelDataReader/ExcelDataReader.cs
+++ b/src/ExcelDataReader/ExcelDataReader.cs
@@ -1,4 +1,7 @@
using System.Data;
+#if NET8_0_OR_GREATER
+using System.Diagnostics.CodeAnalysis;
+#endif
using ExcelDataReader.Core;
namespace ExcelDataReader;
@@ -17,6 +20,7 @@ internal abstract class ExcelDataReader : IExcelDataReade
private IEnumerator _cachedWorksheetIterator;
private List _cachedWorksheets;
private int _idx;
+ private bool _singlePassMode;
~ExcelDataReader()
{
@@ -44,17 +48,25 @@ internal abstract class ExcelDataReader : IExcelDataReade
public bool IsClosed { get; private set; }
- public int FieldCount => _worksheetIterator?.Current?.FieldCount ?? 0;
+ public int FieldCount => _singlePassMode
+ ? Math.Max(_worksheetIterator?.Current?.FieldCount ?? 0, RowCells?.Length ?? 0)
+ : (_worksheetIterator?.Current?.FieldCount ?? 0);
- public int RowCount => _worksheetIterator?.Current?.RowCount ?? 0;
+ public int RowCount => _singlePassMode
+ ? throw new InvalidOperationException("RowCount is not available in SinglePassMode.")
+ : (_worksheetIterator?.Current?.RowCount ?? 0);
+
+ public CellRange Dimension => _worksheetIterator?.Current?.Dimension;
public int RecordsAffected => throw new NotSupportedException();
- public double RowHeight => _rowIterator?.Current?.Height ?? 0;
+ public double RowHeight => _rowIterator?.Current.Height ?? 0;
+
+ protected bool SinglePassMode { set => _singlePassMode = value; }
protected TWorkbook Workbook { get; set; }
- private Cell[] RowCells { get; set; }
+ private Cell?[] RowCells { get; set; }
public object this[int i] => GetValue(i);
@@ -82,6 +94,9 @@ public long GetChars(int i, long fieldoffset, char[] buffer, int bufferoffset, i
public double GetDouble(int i) => (double)GetValue(i);
+#if NET8_0_OR_GREATER
+ [return: DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicFields)]
+#endif
public Type GetFieldType(int i) => GetValue(i)?.GetType();
public float GetFloat(int i) => (float)GetValue(i);
@@ -339,16 +354,27 @@ private void ResetSheetData()
private void ReadCurrentRow()
{
- RowCells ??= new Cell[FieldCount];
+ RowCells ??= new Cell?[FieldCount];
Array.Clear(RowCells, 0, RowCells.Length);
foreach (var cell in _rowIterator.Current.Cells)
{
- if (cell.ColumnIndex < RowCells.Length)
+ if (cell.ColumnIndex >= RowCells.Length)
{
- RowCells[cell.ColumnIndex] = cell;
+ if (_singlePassMode)
+ {
+ var resized = RowCells;
+ Array.Resize(ref resized, cell.ColumnIndex + 1);
+ RowCells = resized;
+ }
+ else
+ {
+ continue;
+ }
}
+
+ RowCells[cell.ColumnIndex] = cell;
}
}
}
diff --git a/src/ExcelDataReader/ExcelOpenXmlReader.cs b/src/ExcelDataReader/ExcelOpenXmlReader.cs
index 944aad56..accd1be3 100644
--- a/src/ExcelDataReader/ExcelOpenXmlReader.cs
+++ b/src/ExcelDataReader/ExcelOpenXmlReader.cs
@@ -1,13 +1,17 @@
+using System.Globalization;
using ExcelDataReader.Core.OpenXmlFormat;
namespace ExcelDataReader;
internal sealed class ExcelOpenXmlReader : ExcelDataReader
{
- public ExcelOpenXmlReader(Stream stream)
+ public ExcelOpenXmlReader(Stream stream, CultureInfo culture = null, bool singlePassMode = false)
{
Document = new(stream);
Workbook = new XlsxWorkbook(Document);
+ Workbook.SinglePassMode = singlePassMode;
+ SinglePassMode = singlePassMode;
+ Workbook.Culture = culture;
// By default, the data reader is positioned on the first result.
Reset();
diff --git a/src/ExcelDataReader/ExcelReaderConfiguration.cs b/src/ExcelDataReader/ExcelReaderConfiguration.cs
index 78b4a560..21ffec1a 100644
--- a/src/ExcelDataReader/ExcelReaderConfiguration.cs
+++ b/src/ExcelDataReader/ExcelReaderConfiguration.cs
@@ -1,4 +1,5 @@
-using System.Text;
+using System.Globalization;
+using System.Text;
namespace ExcelDataReader;
@@ -44,4 +45,18 @@ public class ExcelReaderConfiguration
/// Default: 0 - analyzes the entire file (CSV only, has no effect on other formats).
///
public int AnalyzeInitialCsvRows { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether to skip the initial full-scan pass used to determine FieldCount and RowCount.
+ /// When true, RowCount throws InvalidOperationException and FieldCount reflects the maximum column index seen so far,
+ /// growing dynamically as rows are read. Default: false (XLS and XLSX/XLSB only, has no effect on CSV).
+ ///
+ public bool SinglePassMode { get; set; }
+
+ ///
+ /// Gets or sets the culture to use when mapping locale-dependent built-in number format indices
+ /// to format strings. Affects indices 14 (short date) and 22 (short date and time).
+ /// When null (the default), hardcoded format strings are used for backward compatibility.
+ ///
+ public CultureInfo Culture { get; set; }
}
\ No newline at end of file
diff --git a/src/ExcelDataReader/ExcelReaderFactory.cs b/src/ExcelDataReader/ExcelReaderFactory.cs
index a1051283..9bd5a104 100644
--- a/src/ExcelDataReader/ExcelReaderFactory.cs
+++ b/src/ExcelDataReader/ExcelReaderFactory.cs
@@ -42,12 +42,12 @@ public static IExcelDataReader CreateReader(Stream fileStream, ExcelReaderConfig
var document = new CompoundDocument(fileStream);
if (TryGetWorkbook(fileStream, document, out var stream))
{
- return new ExcelBinaryReader(stream, configuration.Password, configuration.FallbackEncoding);
+ return new ExcelBinaryReader(stream, configuration.Password, configuration.FallbackEncoding, configuration.Culture, configuration.SinglePassMode);
}
if (TryGetEncryptedPackage(fileStream, document, configuration.Password, out stream))
{
- return new ExcelOpenXmlReader(stream);
+ return new ExcelOpenXmlReader(stream, configuration.Culture, configuration.SinglePassMode);
}
throw new ExcelReaderException(Errors.ErrorStreamWorkbookNotFound);
@@ -55,13 +55,13 @@ public static IExcelDataReader CreateReader(Stream fileStream, ExcelReaderConfig
if (XlsWorkbook.IsRawBiffStream(probe))
{
- return new ExcelBinaryReader(fileStream, configuration.Password, configuration.FallbackEncoding);
+ return new ExcelBinaryReader(fileStream, configuration.Password, configuration.FallbackEncoding, configuration.Culture, configuration.SinglePassMode);
}
if (probe[0] == 0x50 && probe[1] == 0x4B)
{
// zip files start with 'PK'
- return new ExcelOpenXmlReader(fileStream);
+ return new ExcelOpenXmlReader(fileStream, configuration.Culture, configuration.SinglePassMode);
}
throw new HeaderException(Errors.ErrorHeaderSignature);
@@ -92,7 +92,7 @@ public static IExcelDataReader CreateBinaryReader(Stream fileStream, ExcelReader
var document = new CompoundDocument(fileStream);
if (TryGetWorkbook(fileStream, document, out var stream))
{
- return new ExcelBinaryReader(stream, configuration.Password, configuration.FallbackEncoding);
+ return new ExcelBinaryReader(stream, configuration.Password, configuration.FallbackEncoding, configuration.Culture, configuration.SinglePassMode);
}
else
{
@@ -101,7 +101,7 @@ public static IExcelDataReader CreateBinaryReader(Stream fileStream, ExcelReader
}
else if (XlsWorkbook.IsRawBiffStream(probe))
{
- return new ExcelBinaryReader(fileStream, configuration.Password, configuration.FallbackEncoding);
+ return new ExcelBinaryReader(fileStream, configuration.Password, configuration.FallbackEncoding, configuration.Culture, configuration.SinglePassMode);
}
else
{
@@ -135,7 +135,7 @@ public static IExcelDataReader CreateOpenXmlReader(Stream fileStream, ExcelReade
var document = new CompoundDocument(fileStream);
if (TryGetEncryptedPackage(fileStream, document, configuration.Password, out var stream))
{
- return new ExcelOpenXmlReader(stream);
+ return new ExcelOpenXmlReader(stream, configuration.Culture, configuration.SinglePassMode);
}
throw new ExcelReaderException(Errors.ErrorCompoundNoOpenXml);
@@ -144,7 +144,7 @@ public static IExcelDataReader CreateOpenXmlReader(Stream fileStream, ExcelReade
if (probe[0] == 0x50 && probe[1] == 0x4B)
{
// Zip files start with 'PK'
- return new ExcelOpenXmlReader(fileStream);
+ return new ExcelOpenXmlReader(fileStream, configuration.Culture, configuration.SinglePassMode);
}
throw new HeaderException(Errors.ErrorHeaderSignature);
diff --git a/src/ExcelDataReader/IExcelDataReader.cs b/src/ExcelDataReader/IExcelDataReader.cs
index 13f48a3b..34ed5564 100644
--- a/src/ExcelDataReader/IExcelDataReader.cs
+++ b/src/ExcelDataReader/IExcelDataReader.cs
@@ -52,6 +52,12 @@ public interface IExcelDataReader : IDataReader
///
int RowCount { get; }
+ ///
+ /// Gets the dimension of the current result.
+ ///
+ /// Main use case is analysis of clipboard data. Potentially unreliable in other use cases.
+ CellRange Dimension { get; }
+
///
/// Gets the height of the current row in points.
///
diff --git a/src/ExcelDataReader/packages.lock.json b/src/ExcelDataReader/packages.lock.json
index 36e27863..24ce0e9e 100644
--- a/src/ExcelDataReader/packages.lock.json
+++ b/src/ExcelDataReader/packages.lock.json
@@ -11,16 +11,6 @@
"Microsoft.NETFramework.ReferenceAssemblies.net462": "1.0.3"
}
},
- "Microsoft.SourceLink.GitHub": {
- "type": "Direct",
- "requested": "[8.0.0, )",
- "resolved": "8.0.0",
- "contentHash": "G5q7OqtwIyGTkeIOAc3u2ZuV/kicQaec5EaRnc0pIeSnh9LUjj+PYQrJYBURvDt7twGl2PKA7nSN0kz1Zw5bnQ==",
- "dependencies": {
- "Microsoft.Build.Tasks.Git": "8.0.0",
- "Microsoft.SourceLink.Common": "8.0.0"
- }
- },
"PolySharp": {
"type": "Direct",
"requested": "[1.15.0, )",
@@ -42,21 +32,11 @@
"resolved": "4.5.0",
"contentHash": "okurQJO6NRE/apDIP23ajJ0hpiNmJ+f0BwOlB/cSqTLQlw5upkf+5+96+iG2Jw40G1fCVCyPz/FhIABUjMR+RQ=="
},
- "Microsoft.Build.Tasks.Git": {
- "type": "Transitive",
- "resolved": "8.0.0",
- "contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ=="
- },
"Microsoft.NETFramework.ReferenceAssemblies.net462": {
"type": "Transitive",
"resolved": "1.0.3",
"contentHash": "IzAV30z22ESCeQfxP29oVf4qEo8fBGXLXSU6oacv/9Iqe6PzgHDKCaWfwMBak7bSJQM0F5boXWoZS+kChztRIQ=="
},
- "Microsoft.SourceLink.Common": {
- "type": "Transitive",
- "resolved": "8.0.0",
- "contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw=="
- },
"StyleCop.Analyzers.Unstable": {
"type": "Transitive",
"resolved": "1.2.0.556",
@@ -64,16 +44,6 @@
}
},
".NETStandard,Version=v2.0": {
- "Microsoft.SourceLink.GitHub": {
- "type": "Direct",
- "requested": "[8.0.0, )",
- "resolved": "8.0.0",
- "contentHash": "G5q7OqtwIyGTkeIOAc3u2ZuV/kicQaec5EaRnc0pIeSnh9LUjj+PYQrJYBURvDt7twGl2PKA7nSN0kz1Zw5bnQ==",
- "dependencies": {
- "Microsoft.Build.Tasks.Git": "8.0.0",
- "Microsoft.SourceLink.Common": "8.0.0"
- }
- },
"NETStandard.Library": {
"type": "Direct",
"requested": "[2.0.3, )",
@@ -98,21 +68,11 @@
"StyleCop.Analyzers.Unstable": "1.2.0.556"
}
},
- "Microsoft.Build.Tasks.Git": {
- "type": "Transitive",
- "resolved": "8.0.0",
- "contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ=="
- },
"Microsoft.NETCore.Platforms": {
"type": "Transitive",
"resolved": "1.1.0",
"contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A=="
},
- "Microsoft.SourceLink.Common": {
- "type": "Transitive",
- "resolved": "8.0.0",
- "contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw=="
- },
"StyleCop.Analyzers.Unstable": {
"type": "Transitive",
"resolved": "1.2.0.556",
@@ -120,16 +80,6 @@
}
},
".NETStandard,Version=v2.1": {
- "Microsoft.SourceLink.GitHub": {
- "type": "Direct",
- "requested": "[8.0.0, )",
- "resolved": "8.0.0",
- "contentHash": "G5q7OqtwIyGTkeIOAc3u2ZuV/kicQaec5EaRnc0pIeSnh9LUjj+PYQrJYBURvDt7twGl2PKA7nSN0kz1Zw5bnQ==",
- "dependencies": {
- "Microsoft.Build.Tasks.Git": "8.0.0",
- "Microsoft.SourceLink.Common": "8.0.0"
- }
- },
"PolySharp": {
"type": "Direct",
"requested": "[1.15.0, )",
@@ -145,16 +95,6 @@
"StyleCop.Analyzers.Unstable": "1.2.0.556"
}
},
- "Microsoft.Build.Tasks.Git": {
- "type": "Transitive",
- "resolved": "8.0.0",
- "contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ=="
- },
- "Microsoft.SourceLink.Common": {
- "type": "Transitive",
- "resolved": "8.0.0",
- "contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw=="
- },
"StyleCop.Analyzers.Unstable": {
"type": "Transitive",
"resolved": "1.2.0.556",
@@ -162,15 +102,11 @@
}
},
"net8.0": {
- "Microsoft.SourceLink.GitHub": {
+ "Microsoft.NET.ILLink.Tasks": {
"type": "Direct",
- "requested": "[8.0.0, )",
- "resolved": "8.0.0",
- "contentHash": "G5q7OqtwIyGTkeIOAc3u2ZuV/kicQaec5EaRnc0pIeSnh9LUjj+PYQrJYBURvDt7twGl2PKA7nSN0kz1Zw5bnQ==",
- "dependencies": {
- "Microsoft.Build.Tasks.Git": "8.0.0",
- "Microsoft.SourceLink.Common": "8.0.0"
- }
+ "requested": "[8.0.25, )",
+ "resolved": "8.0.25",
+ "contentHash": "sqX4nmBft05ivqKvUT4nxaN8rT3apCLt9SWFkfRrQPwra1zPwFknQAw1lleuMCKOCLvVmOWwrC2iPSm9RiXZUg=="
},
"PolySharp": {
"type": "Direct",
@@ -187,16 +123,6 @@
"StyleCop.Analyzers.Unstable": "1.2.0.556"
}
},
- "Microsoft.Build.Tasks.Git": {
- "type": "Transitive",
- "resolved": "8.0.0",
- "contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ=="
- },
- "Microsoft.SourceLink.Common": {
- "type": "Transitive",
- "resolved": "8.0.0",
- "contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw=="
- },
"StyleCop.Analyzers.Unstable": {
"type": "Transitive",
"resolved": "1.2.0.556",
diff --git a/src/TestData/Test_Clipboard_Biff12.xlsb b/src/TestData/Test_Clipboard_Biff12.xlsb
new file mode 100644
index 00000000..8d9b8210
Binary files /dev/null and b/src/TestData/Test_Clipboard_Biff12.xlsb differ
diff --git a/src/TestData/Test_Clipboard_Biff8.xls b/src/TestData/Test_Clipboard_Biff8.xls
new file mode 100644
index 00000000..30d8f0f4
Binary files /dev/null and b/src/TestData/Test_Clipboard_Biff8.xls differ
diff --git a/src/TestData/Test_git_issue_461.xlsx b/src/TestData/Test_git_issue_461.xlsx
new file mode 100644
index 00000000..58a7537a
Binary files /dev/null and b/src/TestData/Test_git_issue_461.xlsx differ
diff --git a/src/TestData/Test_git_issue_541.xls b/src/TestData/Test_git_issue_541.xls
new file mode 100644
index 00000000..04570dbe
Binary files /dev/null and b/src/TestData/Test_git_issue_541.xls differ
diff --git a/src/TestData/Test_git_issue_541.xlsb b/src/TestData/Test_git_issue_541.xlsb
new file mode 100644
index 00000000..48918514
Binary files /dev/null and b/src/TestData/Test_git_issue_541.xlsb differ
diff --git a/src/TestData/Test_git_issue_541.xlsx b/src/TestData/Test_git_issue_541.xlsx
new file mode 100644
index 00000000..d0bb1682
Binary files /dev/null and b/src/TestData/Test_git_issue_541.xlsx differ
diff --git a/src/TestData/strict/Test_git_issue_541.xlsx b/src/TestData/strict/Test_git_issue_541.xlsx
new file mode 100644
index 00000000..c5647424
Binary files /dev/null and b/src/TestData/strict/Test_git_issue_541.xlsx differ
diff --git a/src/TestData/xls/MACRO1.XLM b/src/TestData/xls/MACRO1.XLM
new file mode 100755
index 00000000..a66f2885
Binary files /dev/null and b/src/TestData/xls/MACRO1.XLM differ
diff --git a/src/TestData/xls/SIMPLE.XLS b/src/TestData/xls/SIMPLE.XLS
new file mode 100755
index 00000000..324ddf87
Binary files /dev/null and b/src/TestData/xls/SIMPLE.XLS differ
| | | | | | | | |