Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add handling for DateOnly/TimeOnly. #198

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/ReleaseNotes.md
Original file line number Diff line number Diff line change
@@ -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.

Expand Down
59 changes: 59 additions & 0 deletions source/Sylvan.Data.Excel.Tests/ExcelDataReaderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<double>("Value")
.Add<DateOnly>("Date")
.Add<DateTime>("DateTime")
.Add<TimeSpan>("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<double>(value);
Assert.IsType<DateOnly>(edr.GetValue(1));
Assert.IsType<DateTime>(edr.GetValue(2));
Assert.IsType<TimeSpan>(edr.GetValue(3));

}

#endif

#if ASYNC

[Fact]
Expand Down
138 changes: 77 additions & 61 deletions source/Sylvan.Data.Excel/ExcelDataReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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<TimeSpan>(ordinal);
}
break;
case ExcelDataType.String:
return GetString(ordinal);
}
throw new NotSupportedException();
}

/// <inheritdoc/>
public sealed override object GetValue(int ordinal)
Expand Down Expand Up @@ -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<TimeSpan>(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<DateOnly>(ordinal);
}

if (schemaType == typeof(TimeOnly))
{
return GetFieldValue<TimeOnly>(ordinal);
}
#endif

break;
}
throw new NotSupportedException();
Expand Down Expand Up @@ -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();
}

/// <inheritdoc/>
Expand Down
31 changes: 15 additions & 16 deletions source/Sylvan.Data.Excel/ExcelFormat.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand All @@ -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.
Expand Down
49 changes: 49 additions & 0 deletions source/Sylvan.Data.Excel/FieldAccessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,33 @@ public override TimeSpan GetValue(ExcelDataReader reader, int ordinal)
}
}

#if DATE_ONLY

sealed class DateOnlyAccessor : FieldAccessor<DateOnly>
{
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<TimeOnly>
{
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<Guid>
{
internal static readonly GuidAccessor Instance = new GuidAccessor();
Expand Down Expand Up @@ -304,6 +331,10 @@ sealed partial class ExcelDataAccessor :
IFieldAccessor<decimal>,
IFieldAccessor<DateTime>,
IFieldAccessor<TimeSpan>,
#if DATE_ONLY
IFieldAccessor<DateOnly>,
IFieldAccessor<TimeOnly>,
#endif
IFieldAccessor<Guid>,
IFieldAccessor<Stream>,
IFieldAccessor<TextReader>,
Expand Down Expand Up @@ -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?
Expand Down Expand Up @@ -417,6 +452,20 @@ TimeSpan IFieldAccessor<TimeSpan>.GetValue(ExcelDataReader reader, int ordinal)
return reader.GetTimeSpan(ordinal);
}

#if DATE_ONLY

DateOnly IFieldAccessor<DateOnly>.GetValue(ExcelDataReader reader, int ordinal)
{
return DateOnlyAccessor.Instance.GetValue(reader, ordinal);
}

TimeOnly IFieldAccessor<TimeOnly>.GetValue(ExcelDataReader reader, int ordinal)
{
return TimeOnlyAccessor.Instance.GetValue(reader, ordinal);
}

#endif

decimal IFieldAccessor<decimal>.GetValue(ExcelDataReader reader, int ordinal)
{
return reader.GetDecimal(ordinal);
Expand Down
3 changes: 2 additions & 1 deletion source/Sylvan.Data.Excel/Sylvan.Data.Excel.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
<PropertyGroup>
<TargetFrameworks>net6.0;netstandard2.1;netstandard2.0</TargetFrameworks>
<LangVersion>latest</LangVersion>
<VersionPrefix>0.4.25</VersionPrefix>
<VersionPrefix>0.4.26</VersionPrefix>
<VersionSuffix>b0001</VersionSuffix>
<Description>A cross-platform .NET library for reading Excel data files.</Description>
<PackageTags>excel;xls;xlsx;xlsb;datareader</PackageTags>
<Nullable>enable</Nullable>
Expand Down
Loading