Issue for ExcelDataReader/ExcelDataReader

Type: bug report, with an offer to write the fix Status: FILED 2026-08-08 as #759 Verified on: develop @ c4acb4b (4.0.0) and the released 3.9.0 package

Kept here as the source of what was sent, so the claims stay checkable against the harness that produced them.

This was nearly two issues. The second one, that their output depends on the machine's timezone, did not survive being written up: the reproduction that killed it is in the second afterword to chapter 9. Verifying a finding against the other project rather than against your own rendering of it is the whole difference between a contribution and a waste of a maintainer's evening.


Title

OOXML strict: a cell holding a time with no date is resolved against the current date, so its value changes daily

Body

In the strict flavour of OOXML, a cell can carry an ISO 8601 time with no date component. ExcelDataReader resolves those against today's date, so reading the same file on two different days returns two different values.

The clearest way to see it is with two files already in the repository. LocaleTime.xlsx and strict/LocaleTime.xlsx are the same workbook saved in the transitional and strict formats:

LocaleTime.xlsx          row 1, column 1 -> 1899-12-31 01:34:00
strict/LocaleTime.xlsx   row 1, column 1 -> 2026-08-08 01:34:00   (today's date)

The first value is the one ExcelOpenXmlReaderLocaleTest already asserts:

Assert.That(dataSet.Tables[0].Rows[1][1], Is.EqualTo(new System.DateTime(1899, 12, 31, 1, 34, 0)));

The strict twin of that file is not covered, and it returns a different instant. Tomorrow it will return another one.

Reproduction

using System.Globalization;
using System.Text;
using ExcelDataReader;

Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);

foreach (var path in new[] { "LocaleTime.xlsx", "strict/LocaleTime.xlsx" })
{
    using var stream = File.Open(path, FileMode.Open, FileAccess.Read);
    using var reader = ExcelReaderFactory.CreateReader(stream);
    reader.Read();               // header row
    reader.Read();               // first data row
    var v = (DateTime)reader.GetValue(1);
    Console.WriteLine($"{path,-24} {v.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture)}");
}

Run from src/TestData. Observed on 2026-08-08:

LocaleTime.xlsx          1899-12-31 01:34:00
strict/LocaleTime.xlsx   2026-08-08 01:34:00

strict/Open.xlsx shows the same thing in column 10, and because "today" is itself a function of the host, the value also changes with the machine's timezone:

TZ=UTC          2026-08-08 11:00:00
TZ=Asia/Tokyo   2026-08-09 11:00:00

Cause

Core/OpenXmlFormat/XlsxWorksheet.cs:222:

if (numberFormat.IsDateTimeFormat && DateTime.TryParse(s, out DateTime dateTime))

The cell arrives here as a string because the t="d" branch in XmlWorksheetReader.ParseCellValue only accepts the exact yyyy-MM-dd form and passes anything else through unchanged. DateTime.TryParse then substitutes the current date when the input has no date component, which is documented .NET behaviour rather than a surprise.

The values in these files look like this, complete with the float error left over from converting a serial to text:

<c r="B2" s="4" t="d"><v>01:34:00.00000000000154875</v></c>

Possible fixes

Adding DateTimeStyles.NoCurrentDateDefault is the one-line version, and makes the result stable at 0001-01-01 01:34:00:

DateTime.TryParse(s, CultureInfo.InvariantCulture,
    DateTimeStyles.NoCurrentDateDefault | DateTimeStyles.AllowWhiteSpaces, out var dateTime)

CultureInfo.InvariantCulture belongs there regardless. The value is ISO 8601, so parsing it with CurrentCulture cannot be right even where it happens to agree.

That leaves strict and transitional disagreeing, though. Routing a time with no date through the same OADate conversion the transitional path uses would return 1899-12-31 01:34:00 for both, which matches the existing assertion and makes the two spellings of one workbook decode alike. That is a design call rather than a bug fix, so I have not assumed it.

Happy to open a PR for whichever you prefer.

Context

Found while building a differential harness for a Go port of this library. It compares the two implementations cell by cell across the 303 .xls and .xlsx files in src/TestData, which is also how I found several bugs in my own code. The corpus in this repository is the reason that was possible at all.

Writing is AI assisted. Thoughts and publishing are human-gated.

Rendered from jloor/go-excel-reader at 18e4904. The markdown in that repository is the source of truth; if this page disagrees with it, this page is stale.