diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index fb4b295..8103f58 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -1,5 +1,9 @@ # Sylvan.Data.Excel Release Notes +_0.4.26_ +- Add handling for TimeSpan, DateOnly and TimeOnly, on supported runtimes. +- Time formatted date values will return a TimeSpan string when accessed as a string value. + _0.4.25_ - Fix some issues with reading Excel 95 .xls files. diff --git a/source/Sylvan.Data.Excel.Tests/ExcelDataReaderTests.cs b/source/Sylvan.Data.Excel.Tests/ExcelDataReaderTests.cs index 997402c..349592b 100644 --- a/source/Sylvan.Data.Excel.Tests/ExcelDataReaderTests.cs +++ b/source/Sylvan.Data.Excel.Tests/ExcelDataReaderTests.cs @@ -1461,6 +1461,65 @@ public void TrailingBlankNoSkip() Assert.False(edr.Read()); } + [Fact] + public void TimeSpanAsString() + { + var file = GetFile("DateTime"); + using var edr = ExcelDataReader.Create(file); + + edr.Read(); + var ts = edr.GetTimeSpan(3); + Assert.Equal(TimeSpan.Zero, ts); + var val = edr.GetValue(3); + Assert.Equal("00:00:00", val); + var str = edr.GetString(3); + Assert.Equal("00:00:00", str); + + edr.Read(); + ts = edr.GetTimeSpan(3); + Assert.Equal(TimeSpan.FromMinutes(144), ts); + val = edr.GetValue(3); + Assert.Equal("02:24:00", val); + str = edr.GetString(3); + Assert.Equal("02:24:00", str); + } + +#if NET6_0_OR_GREATER + + [Fact] + public void DateTimeGetValue() + { + var sb = + new Schema.Builder() + .Add("Value") + .Add("Date") + .Add("DateTime") + .Add("Time") + .Build(); + + var schema = new ExcelSchema(true, sb); + var opts = new ExcelDataReaderOptions { Schema = schema }; + var file = GetFile("DateTime"); + using var edr = ExcelDataReader.Create(file, opts); + + for (int i = 0; i < 10; i++) + { + // skip the rows with the invalid dates + edr.Read(); + } + + edr.Read(); + object value; + value = edr.GetValue(0); + Assert.IsType(value); + Assert.IsType(edr.GetValue(1)); + Assert.IsType(edr.GetValue(2)); + Assert.IsType(edr.GetValue(3)); + + } + +#endif + #if ASYNC [Fact] diff --git a/source/Sylvan.Data.Excel/ExcelDataReader.cs b/source/Sylvan.Data.Excel/ExcelDataReader.cs index 1d6cddb..618fc30 100644 --- a/source/Sylvan.Data.Excel/ExcelDataReader.cs +++ b/source/Sylvan.Data.Excel/ExcelDataReader.cs @@ -449,11 +449,11 @@ static ExcelDataType MapFieldType(FieldType t) default: // never case FieldType.Null: return ExcelDataType.Null; - case FieldType.Boolean: + case FieldType.Boolean: return ExcelDataType.Boolean; - case FieldType.DateTime: + case FieldType.DateTime: return ExcelDataType.DateTime; - case FieldType.Numeric: + case FieldType.Numeric: return ExcelDataType.Numeric; case FieldType.String: case FieldType.SharedString: @@ -574,7 +574,54 @@ public sealed override int GetValues(object[] values) return c; } - + object GetObjectValue(int ordinal) + { + // when the type of the column is object + // we'll treat it "dynamically" and try + // to return the most appropriate value. + var type = GetExcelDataType(ordinal); + switch (type) + { + case ExcelDataType.Boolean: + return GetBoolean(ordinal); + case ExcelDataType.DateTime: + return GetDateTime(ordinal); + case ExcelDataType.Error: + throw GetError(ordinal); + case ExcelDataType.Null: + return DBNull.Value; + case ExcelDataType.Numeric: + var fmt = GetFormat(ordinal); + var kind = fmt?.Kind ?? FormatKind.Number; + switch (kind) + { + case FormatKind.String: + case FormatKind.Number: + var doubleValue = GetDouble(ordinal); + unchecked + { + // Excel stores all values as double + // but we'll try to return it as the + // most "intuitive" type. + var int32Value = (int)doubleValue; + if (doubleValue == int32Value) + return int32Value; + var int64Value = (long)doubleValue; + if (doubleValue == int64Value) + return int64Value; + return doubleValue; + } + case FormatKind.Date: + return GetDateTime(ordinal); + case FormatKind.Time: + return GetFieldValue(ordinal); + } + break; + case ExcelDataType.String: + return GetString(ordinal); + } + throw new NotSupportedException(); + } /// public sealed override object GetValue(int ordinal) @@ -612,55 +659,29 @@ public sealed override object GetValue(int ordinal) { return GetGuid(ordinal); } + if (schemaType == typeof(object)) { - // when the type of the column is object - // we'll treat it "dynamically" and try - // to return the most appropriate value. - var type = GetExcelDataType(ordinal); - switch (type) - { - case ExcelDataType.Boolean: - return GetBoolean(ordinal); - case ExcelDataType.DateTime: - return GetDateTime(ordinal); - case ExcelDataType.Error: - throw GetError(ordinal); - case ExcelDataType.Null: - return DBNull.Value; - case ExcelDataType.Numeric: - var fmt = GetFormat(ordinal); - var kind = fmt?.Kind ?? FormatKind.Number; - switch (kind) - { - case FormatKind.String: - case FormatKind.Number: - var doubleValue = GetDouble(ordinal); - unchecked - { - // Excel stores all values as double - // but we'll try to return it as the - // most "intuitive" type. - var int32Value = (int)doubleValue; - if (doubleValue == int32Value) - return int32Value; - var int64Value = (long)doubleValue; - if (doubleValue == int64Value) - return int64Value; - return doubleValue; - } - case FormatKind.Date: - return GetDateTime(ordinal); - case FormatKind.Time: - return GetFieldValue(ordinal); - } - break; - case ExcelDataType.String: - return GetString(ordinal); - default: - throw new NotSupportedException(); - } + return GetObjectValue(ordinal); + } + + if (schemaType == typeof(TimeSpan)) + { + return GetTimeSpan(ordinal); + } + +#if DATE_ONLY + if (schemaType == typeof(DateOnly)) + { + return GetFieldValue(ordinal); } + + if (schemaType == typeof(TimeOnly)) + { + return GetFieldValue(ordinal); + } +#endif + break; } throw new NotSupportedException(); @@ -952,19 +973,14 @@ string ProcString(ref readonly FieldInfo fi) string FormatVal(int xfIdx, double val) { var fmtIdx = xfIdx >= this.xfMap.Length ? -1 : this.xfMap[xfIdx]; - if (fmtIdx == -1) - { - return val.ToString(); - } - - if (formats.TryGetValue(fmtIdx, out var fmt)) + if (fmtIdx != -1) { - return fmt.FormatValue(val, this.dateMode); - } - else - { - return val.ToString(); + if (formats.TryGetValue(fmtIdx, out var fmt)) + { + return fmt.FormatValue(val, this.dateMode); + } } + return val.ToString(); } /// diff --git a/source/Sylvan.Data.Excel/ExcelFormat.cs b/source/Sylvan.Data.Excel/ExcelFormat.cs index 74d12e0..18c4d39 100644 --- a/source/Sylvan.Data.Excel/ExcelFormat.cs +++ b/source/Sylvan.Data.Excel/ExcelFormat.cs @@ -248,13 +248,26 @@ internal ExcelFormat(string spec, FormatKind kind, string? format = null) internal string FormatValue(double value, ExcelDataReader.DateMode mode) { var kind = this.Kind; + DateTime dt; switch (kind) { case FormatKind.Number: return value.ToString("G"); - case FormatKind.Date: case FormatKind.Time: - if (ExcelDataReader.TryGetDate(this, value, mode, out var dt)) + // for values rendered as time (not including date) that are in the + // range 0-1 (which renders in Excel as 1900-01-00), + // allow these to be reported as just the time component. + if (value >= 0d && value < 1d) + { + // omit rendering the date when the value is in the range 0-1 + // this would render in Excel as the date + var fmt = "HH:mm:ss.FFFFFF"; + dt = DateTime.MinValue.AddDays(value); + return dt.ToString(fmt); + } + goto case FormatKind.Date; + case FormatKind.Date: + if (ExcelDataReader.TryGetDate(this, value, mode, out dt)) { if (dt.TimeOfDay == TimeSpan.Zero) { @@ -265,20 +278,6 @@ internal string FormatValue(double value, ExcelDataReader.DateMode mode) return IsoDate.ToStringIso(dt); } } - else - { - // for values rendered as time (not including date) that are in the - // range 0-1 (which renders in Excel as 1900-01-00), - // allow these to be reported as just the time component. - if (value < 1d && value >= 0d && Kind == FormatKind.Time) - { - // omit rendering the date when the value is in the range 0-1 - // this would render in Excel as the date - var fmt = "HH:mm:ss.FFFFFF"; - dt = DateTime.MinValue.AddDays(value); - return dt.ToString(fmt); - } - } // We arrive here for negative values which render in Excel as "########" (not meaningful) // or 1900-01-00 date, which isn't a valid date. // or 1900-02-29, which is a non-existent date. diff --git a/source/Sylvan.Data.Excel/FieldAccessor.cs b/source/Sylvan.Data.Excel/FieldAccessor.cs index d53d326..bdcb882 100644 --- a/source/Sylvan.Data.Excel/FieldAccessor.cs +++ b/source/Sylvan.Data.Excel/FieldAccessor.cs @@ -235,6 +235,33 @@ public override TimeSpan GetValue(ExcelDataReader reader, int ordinal) } } +#if DATE_ONLY + +sealed class DateOnlyAccessor : FieldAccessor +{ + internal static readonly DateOnlyAccessor Instance = new DateOnlyAccessor(); + + public override DateOnly GetValue(ExcelDataReader reader, int ordinal) + { + var dt = reader.GetDateTime(ordinal); + return new DateOnly(dt.Year, dt.Month, dt.Day); + } +} + +sealed class TimeOnlyAccessor : FieldAccessor +{ + internal static readonly TimeOnlyAccessor Instance = new TimeOnlyAccessor(); + + public override TimeOnly GetValue(ExcelDataReader reader, int ordinal) + { + var dt = reader.GetDateTime(ordinal); + return TimeOnly.FromDateTime(dt); + } +} + +#endif + + sealed class GuidAccessor : FieldAccessor { internal static readonly GuidAccessor Instance = new GuidAccessor(); @@ -304,6 +331,10 @@ sealed partial class ExcelDataAccessor : IFieldAccessor, IFieldAccessor, IFieldAccessor, +#if DATE_ONLY + IFieldAccessor, + IFieldAccessor, +#endif IFieldAccessor, IFieldAccessor, IFieldAccessor, @@ -332,6 +363,10 @@ static ExcelDataAccessor() {typeof(decimal), DecimalAccessor.Instance }, {typeof(DateTime), DateTimeAccessor.Instance }, {typeof(TimeSpan), TimeSpanAccessor.Instance }, +#if DATE_ONLY + {typeof(DateOnly), DateOnlyAccessor.Instance }, + {typeof(TimeOnly), TimeOnlyAccessor.Instance }, +#endif {typeof(Guid), GuidAccessor.Instance }, // TODO: add support for the following types? @@ -417,6 +452,20 @@ TimeSpan IFieldAccessor.GetValue(ExcelDataReader reader, int ordinal) return reader.GetTimeSpan(ordinal); } +#if DATE_ONLY + + DateOnly IFieldAccessor.GetValue(ExcelDataReader reader, int ordinal) + { + return DateOnlyAccessor.Instance.GetValue(reader, ordinal); + } + + TimeOnly IFieldAccessor.GetValue(ExcelDataReader reader, int ordinal) + { + return TimeOnlyAccessor.Instance.GetValue(reader, ordinal); + } + +#endif + decimal IFieldAccessor.GetValue(ExcelDataReader reader, int ordinal) { return reader.GetDecimal(ordinal); diff --git a/source/Sylvan.Data.Excel/Sylvan.Data.Excel.csproj b/source/Sylvan.Data.Excel/Sylvan.Data.Excel.csproj index 4b9e20b..7af1fd1 100644 --- a/source/Sylvan.Data.Excel/Sylvan.Data.Excel.csproj +++ b/source/Sylvan.Data.Excel/Sylvan.Data.Excel.csproj @@ -3,7 +3,8 @@ net6.0;netstandard2.1;netstandard2.0 latest - 0.4.25 + 0.4.26 + b0001 A cross-platform .NET library for reading Excel data files. excel;xls;xlsx;xlsb;datareader enable