Support Query Dynamic and IEnumerable lazy loading

This commit is contained in:
wei 2021-03-13 00:51:29 +08:00
parent c784c4195a
commit 63fb261b02
11 changed files with 586 additions and 286 deletions

View File

@ -3,14 +3,11 @@
using MiniExcelLibs.OpenXml; using MiniExcelLibs.OpenXml;
using MiniExcelLibs.Zip; using MiniExcelLibs.Zip;
using System; using System;
using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Data; using System.Data;
using System.IO; using System.IO;
using System.IO.Compression; using System.IO.Compression;
using System.Linq;
using System.Text; using System.Text;
using System.Xml.Linq;
public static partial class MiniExcel public static partial class MiniExcel
{ {
@ -163,112 +160,9 @@
CreateXlsxFile(filePath, defaultFiles); CreateXlsxFile(filePath, defaultFiles);
} }
public static Dictionary<int, Dictionary<int, object>> Query(string path) public static IEnumerable<dynamic> Query(this Stream stream, bool UseHeaderRow = false)
{ {
using (var stream = File.OpenRead(path)) return new ExcelOpenXmlSheetReader().QueryImpl(stream, UseHeaderRow);
{
return stream.Query();
}
}
public static Dictionary<int, Dictionary<int, object>> Query(this Stream stream)
{
using (var reader = new ExcelOpenXmlReader(stream))
{
var d = new Dictionary<int, Dictionary<int, object>>();
while (reader.Read())
{
var dic = new Dictionary<int, object>();
for (int i = 0; i < reader.FieldCount; i++)
{
var v = reader.GetValue(i);
dic.Add(i, v);
}
d.Add(reader.CurrentRowIndex, dic);
}
return d;
}
}
internal static Worksheet GetFirstSheet(Stream stream)
{
using (ZipArchive archive = new ZipArchive(stream, ZipArchiveMode.Read, false, UTF8Encoding.UTF8))
{
var rows = new Dictionary<int, Dictionary<int, object>>();
//sharedStrings must in memory cache
Dictionary<int, string> GetSharedStrings()
{
var sharedStringsEntry = archive.Entries.SingleOrDefault(w => w.FullName == "xl/sharedStrings.xml");
var xml = ConvertToString(sharedStringsEntry);
var xl = XElement.Parse(xml);
var ts = xl.Descendants(ExcelOpenXmlXName.T).Select((s, i) => new { i, v = s.Value?.ToString() })
.ToDictionary(s => s.i, s => s.v)
;
return ts;
}
var sharedStrings = GetSharedStrings();
var rowIndexMaximum = int.MinValue;
var columnIndexMaximum = int.MinValue;
//notice: for performance just read first one and no care the order
var firstSheetEntry = archive.Entries.First(w => w.FullName.StartsWith("xl/worksheets/", StringComparison.OrdinalIgnoreCase));
{
var xml = ConvertToString(firstSheetEntry);
var xl = XElement.Parse(xml);
foreach (var row in xl.Descendants(ExcelOpenXmlXName.Row))
{
//
var datarow = new Dictionary<int, object>();
{
var r = row.Attribute("r")?.Value?.ToString();
var rowIndex = int.MinValue;
if (int.TryParse(r, out var _rowIndex))
rowIndex = _rowIndex - 1; // The row attribute is 1 - based
rowIndexMaximum = Math.Max(rowIndexMaximum, rowIndex);
rows.Add(rowIndex, datarow);
}
foreach (var cell in row.Descendants(ExcelOpenXmlXName.C))
{
var t = cell.Attribute("t")?.Value?.ToString();
var v = cell.Descendants(ExcelOpenXmlXName.V).SingleOrDefault()?.Value;
if (t == "s")
{
if (!string.IsNullOrEmpty(v))
v = sharedStrings[int.Parse(v)];
}
var r = cell.Attribute("r")?.Value?.ToString();
{
var cellIndex = ExcelOpenXmlUtils.GetCellColumnIndex(r) - 1;
columnIndexMaximum = Math.Max(columnIndexMaximum, cellIndex);
datarow.Add(cellIndex, v);
}
}
}
}
return new Worksheet {
Rows = rows,
FieldCount = columnIndexMaximum + 1,
RowCount = rowIndexMaximum + 1
};
}
}
private static string ConvertToString(ZipArchiveEntry entry)
{
using (var eStream = entry.Open())
using (var reader = new StreamReader(eStream))
return reader.ReadToEnd();
} }
private readonly static UTF8Encoding Utf8WithBom = new System.Text.UTF8Encoding(true); private readonly static UTF8Encoding Utf8WithBom = new System.Text.UTF8Encoding(true);

View File

@ -1,125 +0,0 @@
namespace MiniExcelLibs.OpenXml
{
using System;
using System.Collections.Generic;
using System.Data;
using System.IO;
using System.Linq;
internal class ExcelOpenXmlReader : IDataReader
{
private static Worksheet _Sheet ;
private Dictionary<int, Dictionary<int, object>> _Rows { get { return _Sheet.Rows; } }
public ExcelOpenXmlReader(Stream stream)
{
_Sheet = MiniExcel.GetFirstSheet(stream);
}
public int RowCount { get { return _Sheet.RowCount; } }
public int FieldCount { get { return _Sheet.FieldCount; } }
public int Depth { get; private set; }
public int CurrentRowIndex { get { return Depth - 1; } }
public object this[int i] => GetValue(i);
public object this[string name] => GetValue(GetOrdinal(name));
public bool Read()
{
if (Depth == RowCount)
return false;
Depth++;
return true;
}
public string GetName(int i) => ExcelOpenXmlUtils.ConvertColumnName(i + 1);
public int GetOrdinal(string name) => ExcelOpenXmlUtils.GetCellColumnIndex(name);
public object GetValue(int i)
{
//if (CurrentRowIndex < 0)
// throw new InvalidOperationException("Invalid attempt to read when no data is present.");
if (!_Rows.Keys.Contains(CurrentRowIndex))
return null;
if (_Rows[this.CurrentRowIndex].TryGetValue(i, out var v))
return v;
return null;
}
public int GetValues(object[] values)
{
return this.Depth;
}
//TODO: multiple sheets
public bool NextResult() => false;
public void Dispose() { }
public void Close() { }
public int RecordsAffected => throw new NotImplementedException();
bool IDataReader.IsClosed => this.RowCount - 1 == this.Depth;
public string GetString(int i) => (string)GetValue(i);
public bool GetBoolean(int i) => (bool)GetValue(i);
public byte GetByte(int i) => (byte)GetValue(i);
public long GetBytes(int i, long fieldOffset, byte[] buffer, int bufferoffset, int length) => throw new NotImplementedException();
public char GetChar(int i) => (char)GetValue(i);
public long GetChars(int i, long fieldoffset, char[] buffer, int bufferoffset, int length) => throw new NotImplementedException();
public IDataReader GetData(int i) => throw new NotImplementedException();
public string GetDataTypeName(int i) => throw new NotImplementedException();
public DateTime GetDateTime(int i) => (DateTime)GetValue(i);
public decimal GetDecimal(int i) => (decimal)GetValue(i);
public double GetDouble(int i) => (double)GetValue(i);
public Type GetFieldType(int i)
{
var v = GetValue(i);
return v == null ? typeof(string) : v.GetType();
}
public float GetFloat(int i) => (float)GetValue(i);
public Guid GetGuid(int i) => (Guid)GetValue(i);
public short GetInt16(int i) => (short)GetValue(i);
public int GetInt32(int i) => (int)GetValue(i);
public long GetInt64(int i) => (long)GetValue(i);
public DataTable GetSchemaTable()
{
var dataTable = new DataTable("SchemaTable");
dataTable.Locale = System.Globalization.CultureInfo.InvariantCulture;
dataTable.Columns.Add("ColumnName", typeof(string));
dataTable.Columns.Add("ColumnOrdinal", typeof(int));
for (int i = 0; i < this.FieldCount; i++)
{
dataTable.Rows.Add(this.GetName(i), i);
}
DataColumnCollection columns = dataTable.Columns;
foreach (DataColumn item in columns)
{
item.ReadOnly = true;
}
return dataTable;
}
public bool IsDBNull(int i) => GetValue(i) == null;
}
}

View File

@ -0,0 +1,246 @@
using MiniExcelLibs.Utils;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Text;
using System.Xml;
using System.Xml.Linq;
namespace MiniExcelLibs.OpenXml
{
internal class ExcelOpenXmlSheetReader
{
internal Dictionary<int, string> GetSharedStrings(ZipArchiveEntry sharedStringsEntry)
{
var xl = XElement.Load(sharedStringsEntry.Open());
var ts = xl.Descendants(ExcelOpenXmlXName.T).Select((s, i) => new { i, v = s.Value?.ToString() })
.ToDictionary(s => s.i, s => s.v)
;
return ts;
}
private static Dictionary<int, string> _SharedStrings;
internal IEnumerable<object> QueryImpl(Stream stream, bool UseHeaderRow = false)
{
using (ZipArchive archive = new ZipArchive(stream, ZipArchiveMode.Read, false, UTF8Encoding.UTF8))
{
var e = archive.Entries.SingleOrDefault(w => w.FullName == "xl/sharedStrings.xml");
_SharedStrings = GetSharedStrings(e);
var firstSheetEntry = archive.Entries.First(w => w.FullName.StartsWith("xl/worksheets/", StringComparison.OrdinalIgnoreCase));
using (var firstSheetEntryStream = firstSheetEntry.Open())
{
using (XmlReader reader = XmlReader.Create(firstSheetEntryStream, XmlSettings))
{
var ns = "http://schemas.openxmlformats.org/spreadsheetml/2006/main";
if (!reader.IsStartElement("worksheet", ns))
yield break;
if (!XmlReaderHelper.ReadFirstContent(reader))
yield break;
var maxRowIndex = -1;
var maxColumnIndex = -1;
while (!reader.EOF)
{
//TODO: will dimension after sheetData?
//this method logic depends on dimension to get maxcolumnIndex, if without dimension then it need to foreach all rows first time to get maxColumn and maxRowColumn
if (reader.IsStartElement("dimension", ns))
{
var @ref = reader.GetAttribute("ref");
if (string.IsNullOrEmpty(@ref))
throw new InvalidOperationException("Without sheet dimension data");
var rs = @ref.Split(':');
if (ReferenceHelper.ParseReference(rs[1], out int cIndex, out int rIndex))
{
maxColumnIndex = cIndex - 1;
maxRowIndex = rIndex - 1;
}
else
throw new InvalidOperationException("Invaild sheet dimension start data");
}
if (reader.IsStartElement("sheetData", ns))
{
if (!XmlReaderHelper.ReadFirstContent(reader))
{
continue;
}
Dictionary<int, string> headRows = new Dictionary<int, string>();
int rowIndex = -1;
int nextRowIndex = 0;
while (!reader.EOF)
{
if (reader.IsStartElement("row", ns))
{
nextRowIndex = rowIndex + 1;
if (int.TryParse(reader.GetAttribute("r"), out int arValue))
rowIndex = arValue - 1; // The row attribute is 1-based
else
rowIndex++;
if (!XmlReaderHelper.ReadFirstContent(reader))
continue;
// fill empty rows
{
if (nextRowIndex < rowIndex)
{
for (int i = nextRowIndex; i < rowIndex; i++)
if (UseHeaderRow)
yield return Helpers.GetEmptyExpandoObject(headRows);
else
yield return Helpers.GetEmptyExpandoObject(maxColumnIndex);
}
}
// Set Cells
{
var cell = UseHeaderRow ? Helpers.GetEmptyExpandoObject(headRows) : Helpers.GetEmptyExpandoObject(maxColumnIndex);
var columnIndex = 0;
while (!reader.EOF)
{
if (reader.IsStartElement("c", ns))
{
var cellValue = ReadCell(reader, columnIndex, out var _columnIndex);
columnIndex = _columnIndex;
//if not using First Head then using 1,2,3 as index
if (UseHeaderRow)
{
if (rowIndex == 0)
headRows.Add(columnIndex, cellValue.ToString());
else
cell[headRows[columnIndex]] = cellValue;
}
else
cell[columnIndex.ToString()] = cellValue;
}
else if (!XmlReaderHelper.SkipContent(reader))
break;
}
if (UseHeaderRow && rowIndex == 0)
continue;
yield return cell;
}
}
else if (!XmlReaderHelper.SkipContent(reader))
{
break;
}
}
}
else if (!XmlReaderHelper.SkipContent(reader))
{
break;
}
}
}
}
}
}
private object ReadCell(XmlReader reader, int nextColumnIndex, out int columnIndex)
{
var aT = reader.GetAttribute("t");
var aR = reader.GetAttribute("r");
//TODO:need to check only need nextColumnIndex or columnIndex
if (ReferenceHelper.ParseReference(aR, out int referenceColumn, out _))
columnIndex = referenceColumn - 1; // ParseReference is 1-based
else
columnIndex = nextColumnIndex;
if (!XmlReaderHelper.ReadFirstContent(reader))
return null;
object value = null;
while (!reader.EOF)
{
if (reader.IsStartElement("v", "http://schemas.openxmlformats.org/spreadsheetml/2006/main"))
{
string rawValue = reader.ReadElementContentAsString();
if (!string.IsNullOrEmpty(rawValue))
ConvertCellValue(rawValue, aT, out value);
}
else if (reader.IsStartElement("is", "http://schemas.openxmlformats.org/spreadsheetml/2006/main"))
{
string rawValue = StringHelper.ReadStringItem(reader);
if (!string.IsNullOrEmpty(rawValue))
ConvertCellValue(rawValue, aT, out value);
}
else if (!XmlReaderHelper.SkipContent(reader))
{
break;
}
}
return value;
}
private void ConvertCellValue(string rawValue, string aT, out object value)
{
const NumberStyles style = NumberStyles.Any;
var invariantCulture = CultureInfo.InvariantCulture;
switch (aT)
{
case "s": //// if string
if (int.TryParse(rawValue, style, invariantCulture, out var sstIndex))
{
if (_SharedStrings.ContainsKey(sstIndex))
value = _SharedStrings[sstIndex];
else
value = sstIndex;
return;
}
value = rawValue;
return;
case "inlineStr": //// if string inline
case "str": //// if cached formula string
value = Helpers.ConvertEscapeChars(rawValue);
return;
case "b": //// boolean
value = rawValue == "1";
return;
case "d": //// ISO 8601 date
if (DateTime.TryParseExact(rawValue, "yyyy-MM-dd", invariantCulture, DateTimeStyles.AllowLeadingWhite | DateTimeStyles.AllowTrailingWhite, out var date))
{
value = date;
return;
}
value = rawValue;
return;
case "e": //// error
value = rawValue;
return;
default:
if (double.TryParse(rawValue, style, invariantCulture, out double number))
{
value = number;
return;
}
value = rawValue;
return;
}
}
private static readonly XmlReaderSettings XmlSettings = new XmlReaderSettings
{
IgnoreComments = true,
IgnoreWhitespace = true,
XmlResolver = null,
};
}
}

View File

@ -1,10 +0,0 @@
namespace MiniExcelLibs.OpenXml
{
using System.Collections.Generic;
internal class Worksheet
{
public int RowCount { get; set; }
public int FieldCount { get; set; }
public Dictionary<int, Dictionary<int, object>> Rows { get; set; }
}
}

View File

@ -0,0 +1,81 @@
/*
Code from ExcelDataReader : https://github.com/ExcelDataReader/ExcelDataReader/blob/master/src/ExcelDataReader/Core/Helpers.cs
*/
namespace MiniExcelLibs.Utils
{
using System;
internal static class DateTimeHelper
{
// All OA dates must be greater than (not >=) OADateMinAsDouble
public const double OADateMinAsDouble = -657435.0;
// All OA dates must be less than (not <=) OADateMaxAsDouble
public const double OADateMaxAsDouble = 2958466.0;
// From DateTime class to enable OADate in PCL
// Number of 100ns ticks per time unit
private const long TicksPerMillisecond = 10000;
private const long TicksPerSecond = TicksPerMillisecond * 1000;
private const long TicksPerMinute = TicksPerSecond * 60;
private const long TicksPerHour = TicksPerMinute * 60;
private const long TicksPerDay = TicksPerHour * 24;
// Number of milliseconds per time unit
private const int MillisPerSecond = 1000;
private const int MillisPerMinute = MillisPerSecond * 60;
private const int MillisPerHour = MillisPerMinute * 60;
private const int MillisPerDay = MillisPerHour * 24;
// Number of days in a non-leap year
private const int DaysPerYear = 365;
// Number of days in 4 years
private const int DaysPer4Years = DaysPerYear * 4 + 1;
// Number of days in 100 years
private const int DaysPer100Years = DaysPer4Years * 25 - 1;
// Number of days in 400 years
private const int DaysPer400Years = DaysPer100Years * 4 + 1;
// Number of days from 1/1/0001 to 12/30/1899
private const int DaysTo1899 = DaysPer400Years * 4 + DaysPer100Years * 3 - 367;
// Number of days from 1/1/0001 to 12/31/9999
private const int DaysTo10000 = DaysPer400Years * 25 - 366;
private const long MaxMillis = (long)DaysTo10000 * MillisPerDay;
private const long DoubleDateOffset = DaysTo1899 * TicksPerDay;
public static DateTime FromOADate(double d)
{
return new DateTime(DoubleDateToTicks(d), DateTimeKind.Unspecified);
}
// duplicated from DateTime
internal static long DoubleDateToTicks(double value)
{
if (value >= OADateMaxAsDouble || value <= OADateMinAsDouble)
throw new ArgumentException("Invalid OA Date");
long millis = (long)(value * MillisPerDay + (value >= 0 ? 0.5 : -0.5));
// The interesting thing here is when you have a value like 12.5 it all positive 12 days and 12 hours from 01/01/1899
// However if you a value of -12.25 it is minus 12 days but still positive 6 hours, almost as though you meant -11.75 all negative
// This line below fixes up the millis in the negative case
if (millis < 0)
{
millis -= millis % MillisPerDay * 2;
}
millis += DoubleDateOffset / TicksPerMillisecond;
if (millis < 0 || millis >= MaxMillis)
throw new ArgumentException("OA Date out of range");
return millis * TicksPerMillisecond;
}
}
}

View File

@ -0,0 +1,73 @@
namespace MiniExcelLibs.Utils
{
using System;
using System.Collections.Generic;
using System.Dynamic;
using System.Globalization;
using System.Text.RegularExpressions;
internal static class Helpers
{
private static readonly Regex EscapeRegex = new Regex("_x([0-9A-F]{4,4})_");
public static IDictionary<string, object> GetEmptyExpandoObject(int maxColumnIndex)
{
// TODO: strong type mapping can ignore this
// TODO: it can recode better performance
var cell = (IDictionary<string, object>)new ExpandoObject();
for (int i = 0; i <= maxColumnIndex; i++)
cell.Add(i.ToString(), DBNull.Value);
return cell;
}
public static IDictionary<string, object> GetEmptyExpandoObject(Dictionary<int, string> hearrows)
{
// TODO: strong type mapping can ignore this
// TODO: it can recode better performance
var cell = (IDictionary<string, object>)new ExpandoObject();
foreach (var hr in hearrows)
cell.Add(hr.Value, DBNull.Value);
return cell;
}
public static string ConvertEscapeChars(string input)
{
return EscapeRegex.Replace(input, m => ((char)uint.Parse(m.Groups[1].Value, NumberStyles.HexNumber)).ToString());
}
/// <summary>
/// Convert a double from Excel to an OA DateTime double.
/// The returned value is normalized to the '1900' date mode and adjusted for the 1900 leap year bug.
/// </summary>
public static double AdjustOADateTime(double value, bool date1904)
{
if (!date1904)
{
// Workaround for 1900 leap year bug in Excel
if (value >= 0.0 && value < 60.0)
return value + 1;
}
else
{
return value + 1462.0;
}
return value;
}
public static bool IsValidOADateTime(double value)
{
return value > DateTimeHelper.OADateMinAsDouble && value < DateTimeHelper.OADateMaxAsDouble;
}
public static object ConvertFromOATime(double value, bool date1904)
{
var dateValue = AdjustOADateTime(value, date1904);
if (IsValidOADateTime(dateValue))
return DateTimeHelper.FromOADate(dateValue);
return value;
}
}
}

View File

@ -0,0 +1,56 @@
namespace MiniExcelLibs.Utils
{
using System.Globalization;
internal static class ReferenceHelper
{
/// <summary>
/// Logic for the Excel dimensions. Ex: A15
/// </summary>
/// <param name="value">The value.</param>
/// <param name="column">The column, 1-based.</param>
/// <param name="row">The row, 1-based.</param>
public static bool ParseReference(string value, out int column, out int row)
{
column = 0;
var position = 0;
const int offset = 'A' - 1;
if (value != null)
{
while (position < value.Length)
{
var c = value[position];
if (c >= 'A' && c <= 'Z')
{
position++;
column *= 26;
column += c - offset;
continue;
}
if (char.IsDigit(c))
break;
position = 0;
break;
}
}
if (position == 0)
{
column = 0;
row = 0;
return false;
}
if (!int.TryParse(value.Substring(position), NumberStyles.None, CultureInfo.InvariantCulture, out row))
{
return false;
}
return true;
}
}
}

View File

@ -0,0 +1,59 @@
namespace MiniExcelLibs.Utils
{
using System.Xml;
internal static class StringHelper
{
public static string ReadStringItem(XmlReader reader)
{
string result = string.Empty;
if (!XmlReaderHelper.ReadFirstContent(reader))
{
return result;
}
while (!reader.EOF)
{
if (reader.IsStartElement("t", "http://schemas.openxmlformats.org/spreadsheetml/2006/main"))
{
// There are multiple <t> in a <si>. Concatenate <t> within an <si>.
result += reader.ReadElementContentAsString();
}
else if (reader.IsStartElement("r", "http://schemas.openxmlformats.org/spreadsheetml/2006/main"))
{
result += ReadRichTextRun(reader);
}
else if (!XmlReaderHelper.SkipContent(reader))
{
break;
}
}
return result;
}
private static string ReadRichTextRun(XmlReader reader)
{
string result = string.Empty;
if (!XmlReaderHelper.ReadFirstContent(reader))
{
return result;
}
while (!reader.EOF)
{
if (reader.IsStartElement("t", "http://schemas.openxmlformats.org/spreadsheetml/2006/main"))
{
result += reader.ReadElementContentAsString();
}
else if (!XmlReaderHelper.SkipContent(reader))
{
break;
}
}
return result;
}
}
}

View File

@ -0,0 +1,33 @@
namespace MiniExcelLibs.Utils
{
using System.Xml;
internal static class XmlReaderHelper
{
public static bool ReadFirstContent(XmlReader xmlReader)
{
if (xmlReader.IsEmptyElement)
{
xmlReader.Read();
return false;
}
xmlReader.MoveToContent();
xmlReader.Read();
return true;
}
public static bool SkipContent(XmlReader xmlReader)
{
if (xmlReader.NodeType == XmlNodeType.EndElement)
{
xmlReader.Read();
return false;
}
xmlReader.Skip();
return true;
}
}
}

View File

@ -1,15 +1,14 @@
using Xunit; using Xunit;
using MiniExcelLibs;
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO; using System.IO;
using OfficeOpenXml; using OfficeOpenXml;
using ClosedXML.Excel; using ClosedXML.Excel;
using System.IO.Packaging; using System.IO.Packaging;
using System.Data; using System.Data;
using ExcelDataReader;
using System.Collections.Generic;
using System.Dynamic;
namespace MiniExcelLibs.Tests namespace MiniExcelLibs.Tests
{ {
@ -22,50 +21,42 @@ namespace MiniExcelLibs.Tests
using (var stream = File.OpenRead(path)) using (var stream = File.OpenRead(path))
{ {
var rows = stream.Query(); var rows = stream.Query();
foreach (var item in rows)
{
}
}
}
Assert.Equal("a", rows[0][0]); [Fact()]
Assert.Equal("b", rows[0][1]); public void QueryExcelDataReaderCheckTest()
Assert.Equal("c", rows[0][2]); {
Assert.Equal("d", rows[0][3]); #if NETCOREAPP3_1 || NET5_0
System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance);
#endif
var path = @"..\..\..\..\..\samples\xlsx\TestCenterEmptyRow\TestCenterEmptyRow.xlsx";
Assert.Equal("1", rows[1][0]); DataSet exceldatareaderResult;
Assert.Null(rows[1][1]); using (var stream = File.OpenRead(path))
Assert.Equal("3", rows[1][2]); using (var reader = ExcelReaderFactory.CreateReader(stream))
Assert.Null(rows[1][3]); {
exceldatareaderResult = reader.AsDataSet();
Assert.Null(rows[2][0]);
Assert.Equal("2", rows[2][1]);
Assert.Null(rows[2][2]);
Assert.Equal("4", rows[2][3]);
Assert.Null(rows[3][0]);
Assert.Null(rows[3][1]);
Assert.Null(rows[3][2]);
Assert.Null(rows[3][3]);
} }
using (var stream = File.OpenRead(path))
{ {
var rows = MiniExcel.Query(path); var rows = stream.Query().ToList();
foreach (IDictionary<string, object> row in rows)
Assert.Equal("a", rows[0][0]); {
Assert.Equal("b", rows[0][1]); var rowIndex = rows.IndexOf(row);
Assert.Equal("c", rows[0][2]); var keys = row.Keys;
Assert.Equal("d", rows[0][3]); foreach (var key in keys)
{
Assert.Equal("1", rows[1][0]); var eV = exceldatareaderResult.Tables[0].Rows[rowIndex][int.Parse(key)];
Assert.Null(rows[1][1]); var v = row[key];
Assert.Equal("3", rows[1][2]); Assert.Equal(eV, v);
Assert.Null(rows[1][3]); }
}
Assert.Null(rows[2][0]);
Assert.Equal("2", rows[2][1]);
Assert.Null(rows[2][2]);
Assert.Equal("4", rows[2][3]);
Assert.Null(rows[3][0]);
Assert.Null(rows[3][1]);
Assert.Null(rows[3][2]);
Assert.Null(rows[3][3]);
} }
} }

View File

@ -15,6 +15,8 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="ClosedXML" Version="0.95.4" /> <PackageReference Include="ClosedXML" Version="0.95.4" />
<PackageReference Include="EPPlus" Version="4.5.3.3" /> <PackageReference Include="EPPlus" Version="4.5.3.3" />
<PackageReference Include="ExcelDataReader" Version="3.6.0" />
<PackageReference Include="ExcelDataReader.DataSet" Version="3.6.0" />
<PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1"> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>