diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index a055e91..b8b50ad 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -12,11 +12,11 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Setup .NET - uses: actions/setup-dotnet@v1.7.2 + uses: actions/setup-dotnet@v4 with: - dotnet-version: 6.0.100 + dotnet-version: 6.0.422 - name: Restore dependencies run: dotnet restore source - name: Build diff --git a/source/Sylvan.Data.Excel/ExcelDataReader+FieldInfo.cs b/source/Sylvan.Data.Excel/ExcelDataReader+FieldInfo.cs index 8ad42dc..a8cac01 100644 --- a/source/Sylvan.Data.Excel/ExcelDataReader+FieldInfo.cs +++ b/source/Sylvan.Data.Excel/ExcelDataReader+FieldInfo.cs @@ -1,27 +1,51 @@ using System; +using System.Runtime.InteropServices; namespace Sylvan.Data.Excel; partial class ExcelDataReader { + internal enum FieldType : int + { + Null = 0, + Numeric, + DateTime, + String, + SharedString, + Boolean, + Error, + } + + [StructLayout(LayoutKind.Explicit)] private protected struct FieldInfo { public static readonly FieldInfo Null = default; - public ExcelDataType type; - public bool isSS; - public string? strValue; - public int ssIdx; - public double numValue; - public DateTime dtValue; - public int xfIdx; + [FieldOffset(0)] + internal string? strValue; + + [FieldOffset(8)] + internal bool boolValue; + [FieldOffset(8)] + internal int ssIdx; + [FieldOffset(8)] + internal double numValue; + [FieldOffset(8)] + internal DateTime dtValue; + + [FieldOffset(16)] + internal FieldType type; + + [FieldOffset(20)] + internal int xfIdx; + internal bool IsEmptyValue { get { - return this.type == ExcelDataType.Null || (this.type == ExcelDataType.String && this.strValue?.Length == 0); + return this.type == FieldType.Null || (this.type == FieldType.String && this.strValue?.Length == 0); } } @@ -32,31 +56,31 @@ internal ExcelErrorCode ErrorCode internal bool BoolValue { - get { return numValue != 0d; } + get { return boolValue; } } public FieldInfo(string str) { this = default; - this.type = ExcelDataType.String; + this.type = FieldType.String; this.strValue = str; } public FieldInfo(bool b) { this = default; - this.type = ExcelDataType.Boolean; - this.numValue = b ? 1 : 0; + this.type = FieldType.Boolean; + this.boolValue = b; } public FieldInfo(ExcelErrorCode c) { this = default; - this.type = ExcelDataType.Error; + this.type = FieldType.Error; this.numValue = (double)c; } - public FieldInfo(uint val, ExcelDataType type) + public FieldInfo(uint val, FieldType type) { this = default; this.numValue = val; @@ -66,21 +90,23 @@ public FieldInfo(uint val, ExcelDataType type) public FieldInfo(double val, ushort ifIdx) { this = default; - this.type = ExcelDataType.Numeric; + this.type = FieldType.Numeric; this.numValue = val; this.xfIdx = ifIdx; } +#if DEBUG public override string ToString() { switch (type) { - case ExcelDataType.Numeric: + case FieldType.Numeric: return "Double: " + numValue; - case ExcelDataType.String: + case FieldType.String: return "String: " + strValue; } return "NULL"; } +#endif } } diff --git a/source/Sylvan.Data.Excel/ExcelDataReader.cs b/source/Sylvan.Data.Excel/ExcelDataReader.cs index f09017e..907e908 100644 --- a/source/Sylvan.Data.Excel/ExcelDataReader.cs +++ b/source/Sylvan.Data.Excel/ExcelDataReader.cs @@ -439,7 +439,28 @@ public ExcelDataType GetExcelDataType(int ordinal) ValidateAccess(); ValidateSheetRange(ordinal); ref readonly var cell = ref GetFieldValue(ordinal); - return cell.type; + return MapFieldType(cell.type); + } + + static ExcelDataType MapFieldType(FieldType t) + { + switch (t) + { + default: // never + case FieldType.Null: + return ExcelDataType.Null; + case FieldType.Boolean: + return ExcelDataType.Boolean; + case FieldType.DateTime: + return ExcelDataType.DateTime; + case FieldType.Numeric: + return ExcelDataType.Numeric; + case FieldType.String: + case FieldType.SharedString: + return ExcelDataType.String; + case FieldType.Error: + return ExcelDataType.Error; + } } /// @@ -681,7 +702,7 @@ public ExcelErrorCode GetFormulaError(int ordinal) { ValidateAccess(); var cell = GetFieldValue(ordinal); - if (cell.type == ExcelDataType.Error) + if (cell.type == FieldType.Error) return cell.ErrorCode; throw new InvalidOperationException(); } @@ -902,23 +923,27 @@ string GetStringRaw(int ordinal) switch (fi.type) { - case ExcelDataType.Error: + case FieldType.Error: if (errorAsNull) { return string.Empty; } throw GetError(ordinal); - case ExcelDataType.Boolean: + case FieldType.Boolean: return fi.BoolValue ? bool.TrueString : bool.FalseString; - case ExcelDataType.Numeric: + case FieldType.Numeric: return FormatVal(fi.xfIdx, fi.numValue); + case FieldType.String: + return fi.strValue ?? ""; + case FieldType.SharedString: + return GetSharedString(fi.ssIdx) ?? ""; } - return ProcString(fi); + return ProcString(in fi); } - string ProcString(in FieldInfo fi) + string ProcString(ref readonly FieldInfo fi) { - return (fi.isSS ? GetSharedString(fi.ssIdx) : fi.strValue) ?? string.Empty; + return (fi.type == FieldType.SharedString ? GetSharedString(fi.ssIdx) : fi.strValue) ?? string.Empty; } private protected abstract string GetSharedString(int idx); @@ -955,11 +980,12 @@ public sealed override double GetDouble(int ordinal) ref readonly var cell = ref GetFieldValue(ordinal); switch (cell.type) { - case ExcelDataType.String: - return double.Parse(ProcString(cell), culture); - case ExcelDataType.Numeric: + case FieldType.String: + case FieldType.SharedString: + return double.Parse(ProcString(in cell), culture); + case FieldType.Numeric: return cell.numValue; - case ExcelDataType.Error: + case FieldType.Error: throw Error(ordinal); } @@ -979,18 +1005,19 @@ public sealed override bool GetBoolean(int ordinal) ref readonly var fi = ref this.GetFieldValue(ordinal); switch (fi.type) { - case ExcelDataType.Boolean: + case FieldType.Boolean: return fi.BoolValue; - case ExcelDataType.Numeric: + case FieldType.Numeric: return this.GetDouble(ordinal) != 0; - case ExcelDataType.String: + case FieldType.String: + case FieldType.SharedString: var col = (uint)ordinal < this.columnSchema.Length ? this.columnSchema[ordinal] : null; var trueString = col?.TrueString ?? this.trueString; var falseString = col?.FalseString ?? this.falseString; - var strVal = ProcString(fi); + var strVal = ProcString(in fi); var c = StringComparer.OrdinalIgnoreCase; if (trueString != null && c.Equals(strVal, trueString)) @@ -1017,7 +1044,7 @@ public sealed override bool GetBoolean(int ordinal) if (trueString == null && falseString != null) return true; throw new InvalidCastException(); - case ExcelDataType.Error: + case FieldType.Error: var code = fi.ErrorCode; throw new ExcelFormulaException(ordinal, RowNumber, code); } diff --git a/source/Sylvan.Data.Excel/Xlsb/XlsbWorkbookReader.cs b/source/Sylvan.Data.Excel/Xlsb/XlsbWorkbookReader.cs index c3aa655..13389bd 100644 --- a/source/Sylvan.Data.Excel/Xlsb/XlsbWorkbookReader.cs +++ b/source/Sylvan.Data.Excel/Xlsb/XlsbWorkbookReader.cs @@ -350,7 +350,7 @@ int ParseRowValues() int count = 0; int notNull = 0; - ExcelDataType type = 0; + FieldType type = FieldType.Null; while (reader.RecordType != RecordType.Row) { @@ -385,7 +385,7 @@ static void EnsureCols(ref FieldInfo[] values, int c) EnsureCols(ref values, col); ref var fi = ref values[col]; - fi.type = ExcelDataType.Numeric; + fi.type = FieldType.Numeric; fi.numValue = d; fi.xfIdx = sf; notNull++; @@ -399,7 +399,7 @@ static void EnsureCols(ref FieldInfo[] values, int c) double d = reader.GetDouble(8); EnsureCols(ref values, col); ref var fi = ref values[col]; - fi.type = ExcelDataType.Numeric; + fi.type = FieldType.Numeric; fi.numValue = d; fi.xfIdx = sf; count = col + 1; @@ -425,42 +425,40 @@ static void EnsureCols(ref FieldInfo[] values, int c) switch (reader.RecordType) { case RecordType.CellBlank: - type = ExcelDataType.Null; + type = FieldType.Null; break; case RecordType.CellBool: case RecordType.CellFmlaBool: - type = ExcelDataType.Boolean; + type = FieldType.Boolean; fi = new FieldInfo(reader.GetByte(8) != 0); notNull++; break; case RecordType.CellError: case RecordType.CellFmlaError: - type = ExcelDataType.Error; + type = FieldType.Error; fi = new FieldInfo((ExcelErrorCode)reader.GetByte(8)); notNull++; break; case RecordType.CellIsst: - type = ExcelDataType.String; - var sstIdx = reader.GetInt32(8); - fi.isSS = true; + type = FieldType.SharedString; + var sstIdx = reader.GetInt32(8); fi.ssIdx = sstIdx; notNull++; break; case RecordType.CellSt: case RecordType.CellFmlaString: - type = ExcelDataType.String; + type = FieldType.String; fi.strValue = reader.GetString(8); if (fi.strValue.Length > 0) notNull++; break; case RecordType.CellFmlaNum: - type = ExcelDataType.Numeric; + type = FieldType.Numeric; fi.numValue = reader.GetDouble(8); notNull++; break; } - fi.type = type; fi.xfIdx = sf; count = col + 1; diff --git a/source/Sylvan.Data.Excel/Xlsx/XlsxWorkbookReader.cs b/source/Sylvan.Data.Excel/Xlsx/XlsxWorkbookReader.cs index 73c24a4..accda48 100644 --- a/source/Sylvan.Data.Excel/Xlsx/XlsxWorkbookReader.cs +++ b/source/Sylvan.Data.Excel/Xlsx/XlsxWorkbookReader.cs @@ -205,7 +205,7 @@ private protected override bool OpenWorksheet(int sheetIdx) CheckCharacters = false, CloseInput = true, ValidationType = ValidationType.None, - ValidationFlags = System.Xml.Schema.XmlSchemaValidationFlags.None, + ValidationFlags = System.Xml.Schema.XmlSchemaValidationFlags.None, #if SPAN NameTable = new SheetNameTable(), #endif @@ -549,14 +549,14 @@ static CellType GetCellType(char[] b, int l) if (ReadToDescendant(reader, "is")) { fi.strValue = ReadString(reader); - fi.type = ExcelDataType.String; + fi.type = FieldType.String; valueCount++; this.rowFieldCount = col + 1; } else { fi.strValue = string.Empty; - fi.type = ExcelDataType.Null; + fi.type = FieldType.Null; } } else @@ -570,7 +570,7 @@ static CellType GetCellType(char[] b, int l) switch (type) { case CellType.Numeric: - fi.type = ExcelDataType.Numeric; + fi.type = FieldType.Numeric; #if SPAN len = reader.ReadValueChunk(valueBuffer, 0, valueBuffer.Length); if (len < valueBuffer.Length && double.TryParse(valueBuffer.AsSpan(0, len), NumberStyles.Float, ci, out fi.numValue)) @@ -598,7 +598,7 @@ static CellType GetCellType(char[] b, int l) { throw new FormatException(); } - fi.type = ExcelDataType.DateTime; + fi.type = FieldType.DateTime; break; case CellType.SharedString: if (reader.NodeType == XmlNodeType.Text) @@ -610,15 +610,16 @@ static CellType GetCellType(char[] b, int l) { throw new FormatException(); } - fi.isSS = true; fi.ssIdx = strIdx; - //fi.strValue = GetSharedString(strIdx); + fi.type = FieldType.SharedString; } else { + // this handles an edge-case where the field is a shared string, + // but the index is empty. fi.strValue = string.Empty; + fi.type = FieldType.String; } - fi.type = ExcelDataType.String; break; case CellType.String: if (reader.NodeType == XmlNodeType.Text) @@ -629,20 +630,20 @@ static CellType GetCellType(char[] b, int l) s = s.Trim(); } fi.strValue = s; - fi.type = ExcelDataType.String; + fi.type = FieldType.String; } else { fi.strValue = string.Empty; - fi.type = ExcelDataType.Null; + fi.type = FieldType.Null; } break; case CellType.InlineString: fi.strValue = ReadString(reader); - fi.type = ExcelDataType.String; + fi.type = FieldType.String; if (fi.strValue.Length == 0) { - fi.type = ExcelDataType.Null; + fi.type = FieldType.Null; } break; case CellType.Boolean: @@ -651,7 +652,7 @@ static CellType GetCellType(char[] b, int l) { throw new FormatException(); } - fi.type = ExcelDataType.Boolean; + fi.type = FieldType.Boolean; fi = new FieldInfo(valueBuffer[0] != '0'); break; case CellType.Error: @@ -661,7 +662,7 @@ static CellType GetCellType(char[] b, int l) default: throw new InvalidDataException(); } - if (fi.type != ExcelDataType.Null) + if (fi.type != FieldType.Null) { valueCount++; this.rowFieldCount = col + 1;