Skip to content

GH-41488: [C++][Python] Apply timestamp_parsers as fallback when parsing CSV date and time columns - #50146

Open
pearu wants to merge 1 commit into
apache:mainfrom
pearu:pearu/fix-csv-date-time-parsers
Open

GH-41488: [C++][Python] Apply timestamp_parsers as fallback when parsing CSV date and time columns#50146
pearu wants to merge 1 commit into
apache:mainfrom
pearu:pearu/fix-csv-date-time-parsers

Conversation

@pearu

@pearupearu commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Rationale for this change

CSV columns explicitly typed as date32, date64, time32 or time64 can only be parsed from strict ISO-8601 strings: ConvertOptions::timestamp_parsers is consulted only for timestamp columns. Reading e.g. 15-OCT-15 into a date32 column fails even with timestamp_parsers=["%d-%b-%y"], and 7:55:00 (non-zero-padded hour) fails for time32[s]. Users currently work around this by declaring such columns as timestamp, reading, then casting back to the date/time type.

Effect on the issues collected in #41488:

What changes are included in this PR?

  • A new DateTimeWithParsersValueDecoder in csv/converter.cc, used for date32/date64/time32/time64 columns when timestamp_parsers is non-empty. It tries the built-in ISO-8601 parser first (preserving all existing behavior), then each configured parser in order. A timestamp produced by a fallback parser is floored to the day boundary for dates and reduced to the time of day for times, consistent with casting a timestamp to a date or time type. Values carrying a zone offset are rejected, as for zone-less timestamp columns. When no parsers are configured, the pre-existing decoder is used unchanged.
  • Type inference is deliberately unaffected: the Date/Time inference stages now explicitly use options with timestamp_parsers cleared, so inference keeps strict ISO-8601 semantics (otherwise a value with a time-of-day part could be inferred as a date and silently truncated). The existing test_timestamp_parsers Python test pins this behavior.
  • Documentation of the fallback and flooring semantics in ConvertOptions::timestamp_parsers (C++ and Python docstrings) and a new "Date and time parsing" section in the C++ CSV user guide.
  • C-locale name tables for the vendored musl strptime used on Windows, where nl_langinfo() is unavailable. Previously the %a/%A/%b/%B/%h/%p/%c/%r/%x/%X specifiers were compiled out on Windows, so the month-name formats from the original issue reports (%d-%b-%y) could not work there for any column type. The tables match musl's C locale, and name matching is case-insensitive as on glibc/musl/BSD. The fallback path is compiled and verified on Linux via the ARROW_TEST_FALLBACK_LANGINFO hook.

Are these changes tested?

Yes:

  • New C++ tests (Date32Conversion.UserDefinedParsers, Date64Conversion.UserDefinedParsers, Time32Conversion.UserDefinedParsers, Time64Conversion.UserDefinedParsers) covering custom formats, mixed ISO + custom values in one column (backward compatibility of ISO values when parsers are set), pre-epoch flooring with a time-of-day component (distinguishes floor from truncating division), time-of-day extraction from pre-epoch timestamps, zone-offset rejection, and error cases.
  • New Python tests with the reproducers from [C++] Unable to read date64 or date32 in specific format from CSV #28303 and CSV reader cannot parse dates or times #41488, plus an inference-unchanged guard.

Are there any user-facing changes?

Yes: ConvertOptions::timestamp_parsers now also applies, as a fallback after ISO-8601, to columns explicitly typed as date32/date64/time32/time64 (previously such values always errored). No breaking changes: behavior without timestamp_parsers is untouched, ISO values keep parsing when parsers are set, and type inference is unchanged. All language bindings gain the behavior without API changes.

AI usage disclosure

This PR was developed with AI assistance (Claude Code): the decoder, tests and documentation were AI-generated under my direction, then reviewed line-by-line and iterated on by me (design decisions: fallback-after-ISO semantics, silent flooring, inference isolation, and several implementation details adjusted during review). I own and can debug these changes.

🤖 Generated with Claude Code

@pearu

Copy link
Copy Markdown
ContributorAuthor

Two out-of-scope discoveries made while working on this, recorded here rather than folded into the PR to keep it minimal:

  1. MultipleParsersTimestampValueDecoder::Decode (pre-existing, csv/converter.cc) declares its zone_offset_present flag once outside the parser loop. The built-in parsers happen to write the out-parameter on every call (the strptime parser unconditionally, the ISO-8601 parser resets it to false before scanning), so this is currently harmless — but TimestampParser is a public interface, and a user-implemented parser that only writes the flag when an offset is found could observe a stale value from a previous loop iteration. The new decoder in this PR declares the flag per-iteration; the timestamp decoder could get the same two-line treatment as a MINOR follow-up.

  2. The "Timestamp inference/parsing" section of the C++ CSV user guide (docs/source/cpp/csv.rst) does not mention ConvertOptions::timestamp_parsers at all — custom timestamp parsing was undocumented in the user guide before the date/time subsection added here. A short paragraph there could be a docs follow-up.

@github-actionsgithub-actionsBot added the awaiting review Awaiting review label Jun 10, 2026
@github-actions

Copy link
Copy Markdown

⚠️ GitHub issue #41488has been automatically assigned in GitHub to PR creator.

@github-actions

Copy link
Copy Markdown

⚠️ GitHub issue #41488has been automatically assigned in GitHub to PR creator.

@pearu
pearuforce-pushed the pearu/fix-csv-date-time-parsers branch from 78ca3cb to e0af29eCompareJune 10, 2026 10:08
@pearu

pearu commented Jun 10, 2026

Copy link
Copy Markdown
ContributorAuthor

CI triage of the first run (3 failing jobs, all Windows): all three shared one root cause — the tests used the %d-%b-%y format from the original issue reproducer, but month names (%b) are not supported by the vendored musl strptime used on Windows: cpp/src/arrow/vendored/musl/strptime.c force-undefines HAVE_LANGINFO on _WIN32, which compiles out the %a/%A/%b/%B/%h/%c/%p/%r/%x/%X cases entirely. The feature code is unaffected; numeric-format and time tests passed on Windows.

Fixed (amended) by making numeric formats the primary test coverage and keeping the month-name reproducer guarded to non-Windows (#ifndef _WIN32 in C++, sys.platform != "win32" in Python, following the existing kStrptimeSupportsZone / test_strftime precedents).
UPDATE: sorry for this noise, Claude was too eager to post comments, it is better restrained now.

A third discovery for the list above: this %b limitation is pre-existing and applies equally to timestamp columns with timestamp_parsers on Windows — it just had no CI coverage because the existing timestamp tests only use numeric formats. Could deserve its own issue (either implementing C-locale month names in the vendored strptime, or documenting the limitation in timestamp_parsers docs).

@pearu
pearuforce-pushed the pearu/fix-csv-date-time-parsers branch from e0af29e to e75ececCompareJune 10, 2026 13:19
@github-actions

Copy link
Copy Markdown

⚠️ GitHub issue #41488has been automatically assigned in GitHub to PR creator.

@pearu
pearu marked this pull request as ready for review June 10, 2026 14:13
@pearu
pearu requested review from AlenkaF, raulcd and rok as code ownersJune 10, 2026 14:13
@pearu

Copy link
Copy Markdown
ContributorAuthor

The single CI failure (AMD64 Conda C++ AVX2) is unrelated to this PR: Gandiva's TestTime.TestCastTimestampWithTZ fails identically on main since this morning (passing at 4e25461, failing from ca47cd1 on — see e.g. this main run). castTIMESTAMP_utf8 returns 0 for the Canada/Pacific tz name, pointing at tz-database resolution in the CI conda environment — code this PR does not touch.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR extends the CSV reader’s ConvertOptions::timestamp_parsers behavior so that, when columns are explicitly typed as date32/date64/time32/time64, the reader first attempts the existing ISO-8601 parsing and then falls back to the user-provided timestamp parsers (with flooring/extracting semantics consistent with casting). It also keeps type inference strict (ISO-only) to avoid silent truncation.

Changes:

  • Add a new C++ CSV date/time value decoder that falls back to timestamp_parsers after ISO parsing and applies flooring/time-of-day extraction.
  • Ensure CSV type inference for date/time remains ISO-only even when timestamp_parsers are configured.
  • Update C++/Python docs and add C++/Python tests; improve vendored Windows strptime support for C-locale day/month names.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Show a summary per file
FileDescription
python/pyarrow/tests/test_csv.pyAdds Python coverage for date/time typed columns using timestamp_parsers fallback and inference guard.
python/pyarrow/_csv.pyxDocuments the new fallback behavior in Python ConvertOptions docstring.
docs/source/cpp/csv.rstAdds a “Date and time parsing” section documenting fallback + semantics.
cpp/src/arrow/vendored/musl/strptime.cAdds a C-locale nl_langinfo fallback table for Windows/testing to support %b/%B/%p/....
cpp/src/arrow/csv/options.hDocuments fallback semantics for timestamp_parsers in C++ API docs.
cpp/src/arrow/csv/inference_internal.hEnsures date/time inference ignores configured timestamp_parsers.
cpp/src/arrow/csv/converter.ccImplements fallback decoder + converter factory changes for date/time types.
cpp/src/arrow/csv/converter_test.ccAdds C++ tests for date/time fallback parsing behavior and edge cases.

Comment threadcpp/src/arrow/csv/converter.cc
Comment on lines +1188 to +1195
# Month names are parsed case-insensitively
rows = b"a\n15-OCT-15\n18-Jun-90\n"
opts = ConvertOptions(column_types={'a': pa.date32()},
timestamp_parsers=['%d-%b-%y'])
table = self.read_bytes(rows, convert_options=opts)
assert table.to_pydict() == {
'a': [date(2015, 10, 15), date(1990, 6, 18)],
}

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — I checked this and the tests are deterministic as written, so I've left them unchanged.

Neither the test binary nor pyarrow adopts the environment's LC_TIME: there is no setlocale(LC_ALL/LC_TIME, "") anywhere in Arrow's C++ (outside vendored code) or in pyarrow, and CPython coerces only LC_CTYPE at startup, never LC_TIME. So a process started with a non-English LC_TIME in the environment still runs strptime in the C locale. On glibc there is a second reason: strptime's %b/%B keeps the C-locale (English) month names as a fallback even under a non-English locale, so English abbreviations parse regardless (setlocale(LC_TIME, "fr_FR.UTF-8") followed by parsing "15-JUL-15" still succeeds).

Minimal check: CSV %b parsing is independent of the environment locale

Each child process below is started with a non-English LC_ALL/LC_TIME in its environment — exactly the scenario in the comment — and still parses the English month abbreviation correctly:

importosimportsubprocessimportsysCHILD=r"""import osimport pyarrow as pafrom pyarrow import csvfrom datetime import datedata = b"a\n15-JUL-15\n" # English abbreviated month name "JUL"opts = csv.ConvertOptions(column_types={"a": pa.date32()}, timestamp_parsers=["%d-%b-%y"])got = csv.read_csv(pa.py_buffer(data), convert_options=opts).to_pydict()assert got == {"a": [date(2015, 7, 15)]}, gotprint("OK with LC_ALL=%-14r ->" % os.environ.get("LC_ALL"), got)"""forlcin ("C", "fr_FR.UTF-8", "de_DE.UTF-8"):
env=dict(os.environ, LC_ALL=lc, LC_TIME=lc)
subprocess.run([sys.executable, "-c", CHILD], env=env, check=True)

Output (identical whether or not those locales are actually installed):

OK with LC_ALL='C' -> {'a': [datetime.date(2015, 7, 15)]}
OK with LC_ALL='fr_FR.UTF-8' -> {'a': [datetime.date(2015, 7, 15)]}
OK with LC_ALL='de_DE.UTF-8' -> {'a': [datetime.date(2015, 7, 15)]}

A genuinely locale-dependent case does remain — an application that itself calls setlocale(LC_ALL, "") under a non-English locale, on a libc whose strptime lacks that English fallback — but that is pre-existing (it affects timestamp columns too) and out of scope here. Happy to file a follow-up issue if that is worth tracking.

@github-actionsgithub-actionsBot added awaiting committer review Awaiting committer review and removed awaiting review Awaiting review labels Jun 22, 2026
@pearu
pearuforce-pushed the pearu/fix-csv-date-time-parsers branch from e75ecec to fcf4f95CompareJune 22, 2026 18:17
@pearu
pearu requested a review from pitrou as a code ownerJune 22, 2026 18:17
@pitrou

Copy link
Copy Markdown
Member

@jorisvandenbossche Do you want to take a look at this PR?

@manyiResearch

Copy link
Copy Markdown

Hi, a small quick question — I noticed that you mentioned multiple issues in the PR description. I was wondering, does this PR fix all of them? Thanks!

@pearu

pearu commented Jul 2, 2026

Copy link
Copy Markdown
ContributorAuthor

@manyifire Good question — mostly, but not every one in the same way:

So: yes for #28303/#33357 (and #26783/#26224), yes-on-Windows for #31816/#31971, and for #37180 only when timestamp_parsers is set explicitly (not the default).

…n parsing CSV date and time columns
CSV columns explicitly typed as date32, date64, time32 or time64 could
only be parsed from strict ISO-8601 strings; ConvertOptions::timestamp_parsers
was consulted only for timestamp columns.
Make the user-defined timestamp parsers act as a fallback for these
column types: the built-in ISO-8601 parser is tried first (preserving
existing behavior), then each configured parser in order. A timestamp
produced by a fallback parser is floored to the day boundary for dates
and reduced to the time of day for times, consistent with casting a
timestamp to a date or time type.
Type inference of date and time columns is deliberately unaffected:
inference keeps using strict ISO-8601 parsing, otherwise a value with a
time-of-day part could be inferred as a date and silently truncated.
Also provide C-locale name tables to the vendored musl strptime used on
Windows, where nl_langinfo() is unavailable: this makes %a/%A/%b/%B/%h/
%p/%c/%r/%x/%X work on Windows (matching musl's C locale), so that the
month-name formats from the original issue reports parse on all
platforms.
ClosesapacheGH-28303.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@pearu
pearuforce-pushed the pearu/fix-csv-date-time-parsers branch from fcf4f95 to 808f110CompareAugust 10, 2026 13:25
@pearu

Copy link
Copy Markdown
ContributorAuthor

Rebased onto current main and re-ran the CSV suite locally — arrow-csv-test passes 278/278, including the four new Date32/Date64/Time32/Time64Conversion.UserDefinedParsers cases.

Status recap for whoever picks this up:

  • Both automated-reviewer comments were addressed in June — std::ranges::transform replaced with a plain index loop, and the %b locale question answered with a reproducer showing the parsing is locale-independent as written.
  • The two failing jobs in the last CI run (AMD64 macOS 15-intel C++ and Python 3) were the same Homebrew infrastructure flake — brew install --formula aws-sdk-cpp hit a /usr/local/Cellar/cmake lock — unrelated to this change. The fresh run on the rebase should clear them.

@pitrou — you routed this to @jorisvandenbossche back in June and it has been quiet since. Is there anything I can do to make this easier to review? If reviewing the CSV converter change together with the vendored strptime C-locale tables is the sticking point, I'm happy to split the strptime part into its own PR.


🤖 Drafted by Claude Code (an AI agent) and reviewed & approved by pearu.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow ConvertOptions.timestamp_parsers for date types [C++] Unable to read date64 or date32 in specific format from CSV

4 participants

@pearu@pitrou@manyiResearch
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
GH-41488: [C++][Python] Apply timestamp_parsers as fallback when parsing CSV date and time columns by pearu · Pull Request #50146 · apache/arrow · GitHub
Skip to content

GH-41488: [C++][Python] Apply timestamp_parsers as fallback when parsing CSV date and time columns - #50146

Open
pearu wants to merge 1 commit into
apache:mainfrom
pearu:pearu/fix-csv-date-time-parsers
Open

GH-41488: [C++][Python] Apply timestamp_parsers as fallback when parsing CSV date and time columns#50146
pearu wants to merge 1 commit into
apache:mainfrom
pearu:pearu/fix-csv-date-time-parsers

Conversation

@pearu

@pearupearu commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Rationale for this change

CSV columns explicitly typed as date32, date64, time32 or time64 can only be parsed from strict ISO-8601 strings: ConvertOptions::timestamp_parsers is consulted only for timestamp columns. Reading e.g. 15-OCT-15 into a date32 column fails even with timestamp_parsers=["%d-%b-%y"], and 7:55:00 (non-zero-padded hour) fails for time32[s]. Users currently work around this by declaring such columns as timestamp, reading, then casting back to the date/time type.

Effect on the issues collected in #41488:

What changes are included in this PR?

  • A new DateTimeWithParsersValueDecoder in csv/converter.cc, used for date32/date64/time32/time64 columns when timestamp_parsers is non-empty. It tries the built-in ISO-8601 parser first (preserving all existing behavior), then each configured parser in order. A timestamp produced by a fallback parser is floored to the day boundary for dates and reduced to the time of day for times, consistent with casting a timestamp to a date or time type. Values carrying a zone offset are rejected, as for zone-less timestamp columns. When no parsers are configured, the pre-existing decoder is used unchanged.
  • Type inference is deliberately unaffected: the Date/Time inference stages now explicitly use options with timestamp_parsers cleared, so inference keeps strict ISO-8601 semantics (otherwise a value with a time-of-day part could be inferred as a date and silently truncated). The existing test_timestamp_parsers Python test pins this behavior.
  • Documentation of the fallback and flooring semantics in ConvertOptions::timestamp_parsers (C++ and Python docstrings) and a new "Date and time parsing" section in the C++ CSV user guide.
  • C-locale name tables for the vendored musl strptime used on Windows, where nl_langinfo() is unavailable. Previously the %a/%A/%b/%B/%h/%p/%c/%r/%x/%X specifiers were compiled out on Windows, so the month-name formats from the original issue reports (%d-%b-%y) could not work there for any column type. The tables match musl's C locale, and name matching is case-insensitive as on glibc/musl/BSD. The fallback path is compiled and verified on Linux via the ARROW_TEST_FALLBACK_LANGINFO hook.

Are these changes tested?

Yes:

  • New C++ tests (Date32Conversion.UserDefinedParsers, Date64Conversion.UserDefinedParsers, Time32Conversion.UserDefinedParsers, Time64Conversion.UserDefinedParsers) covering custom formats, mixed ISO + custom values in one column (backward compatibility of ISO values when parsers are set), pre-epoch flooring with a time-of-day component (distinguishes floor from truncating division), time-of-day extraction from pre-epoch timestamps, zone-offset rejection, and error cases.
  • New Python tests with the reproducers from [C++] Unable to read date64 or date32 in specific format from CSV #28303 and CSV reader cannot parse dates or times #41488, plus an inference-unchanged guard.

Are there any user-facing changes?

Yes: ConvertOptions::timestamp_parsers now also applies, as a fallback after ISO-8601, to columns explicitly typed as date32/date64/time32/time64 (previously such values always errored). No breaking changes: behavior without timestamp_parsers is untouched, ISO values keep parsing when parsers are set, and type inference is unchanged. All language bindings gain the behavior without API changes.

AI usage disclosure

This PR was developed with AI assistance (Claude Code): the decoder, tests and documentation were AI-generated under my direction, then reviewed line-by-line and iterated on by me (design decisions: fallback-after-ISO semantics, silent flooring, inference isolation, and several implementation details adjusted during review). I own and can debug these changes.

🤖 Generated with Claude Code

@pearu

Copy link
Copy Markdown
ContributorAuthor

Two out-of-scope discoveries made while working on this, recorded here rather than folded into the PR to keep it minimal:

  1. MultipleParsersTimestampValueDecoder::Decode (pre-existing, csv/converter.cc) declares its zone_offset_present flag once outside the parser loop. The built-in parsers happen to write the out-parameter on every call (the strptime parser unconditionally, the ISO-8601 parser resets it to false before scanning), so this is currently harmless — but TimestampParser is a public interface, and a user-implemented parser that only writes the flag when an offset is found could observe a stale value from a previous loop iteration. The new decoder in this PR declares the flag per-iteration; the timestamp decoder could get the same two-line treatment as a MINOR follow-up.

  2. The "Timestamp inference/parsing" section of the C++ CSV user guide (docs/source/cpp/csv.rst) does not mention ConvertOptions::timestamp_parsers at all — custom timestamp parsing was undocumented in the user guide before the date/time subsection added here. A short paragraph there could be a docs follow-up.

@github-actionsgithub-actionsBot added the awaiting review Awaiting review label Jun 10, 2026
@github-actions

Copy link
Copy Markdown

⚠️ GitHub issue #41488has been automatically assigned in GitHub to PR creator.

@github-actions

Copy link
Copy Markdown

⚠️ GitHub issue #41488has been automatically assigned in GitHub to PR creator.

@pearu
pearuforce-pushed the pearu/fix-csv-date-time-parsers branch from 78ca3cb to e0af29eCompareJune 10, 2026 10:08
@pearu

pearu commented Jun 10, 2026

Copy link
Copy Markdown
ContributorAuthor

CI triage of the first run (3 failing jobs, all Windows): all three shared one root cause — the tests used the %d-%b-%y format from the original issue reproducer, but month names (%b) are not supported by the vendored musl strptime used on Windows: cpp/src/arrow/vendored/musl/strptime.c force-undefines HAVE_LANGINFO on _WIN32, which compiles out the %a/%A/%b/%B/%h/%c/%p/%r/%x/%X cases entirely. The feature code is unaffected; numeric-format and time tests passed on Windows.

Fixed (amended) by making numeric formats the primary test coverage and keeping the month-name reproducer guarded to non-Windows (#ifndef _WIN32 in C++, sys.platform != "win32" in Python, following the existing kStrptimeSupportsZone / test_strftime precedents).
UPDATE: sorry for this noise, Claude was too eager to post comments, it is better restrained now.

A third discovery for the list above: this %b limitation is pre-existing and applies equally to timestamp columns with timestamp_parsers on Windows — it just had no CI coverage because the existing timestamp tests only use numeric formats. Could deserve its own issue (either implementing C-locale month names in the vendored strptime, or documenting the limitation in timestamp_parsers docs).

@pearu
pearuforce-pushed the pearu/fix-csv-date-time-parsers branch from e0af29e to e75ececCompareJune 10, 2026 13:19
@github-actions

Copy link
Copy Markdown

⚠️ GitHub issue #41488has been automatically assigned in GitHub to PR creator.

@pearu
pearu marked this pull request as ready for review June 10, 2026 14:13
@pearu
pearu requested review from AlenkaF, raulcd and rok as code ownersJune 10, 2026 14:13
@pearu

Copy link
Copy Markdown
ContributorAuthor

The single CI failure (AMD64 Conda C++ AVX2) is unrelated to this PR: Gandiva's TestTime.TestCastTimestampWithTZ fails identically on main since this morning (passing at 4e25461, failing from ca47cd1 on — see e.g. this main run). castTIMESTAMP_utf8 returns 0 for the Canada/Pacific tz name, pointing at tz-database resolution in the CI conda environment — code this PR does not touch.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR extends the CSV reader’s ConvertOptions::timestamp_parsers behavior so that, when columns are explicitly typed as date32/date64/time32/time64, the reader first attempts the existing ISO-8601 parsing and then falls back to the user-provided timestamp parsers (with flooring/extracting semantics consistent with casting). It also keeps type inference strict (ISO-only) to avoid silent truncation.

Changes:

  • Add a new C++ CSV date/time value decoder that falls back to timestamp_parsers after ISO parsing and applies flooring/time-of-day extraction.
  • Ensure CSV type inference for date/time remains ISO-only even when timestamp_parsers are configured.
  • Update C++/Python docs and add C++/Python tests; improve vendored Windows strptime support for C-locale day/month names.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Show a summary per file
FileDescription
python/pyarrow/tests/test_csv.pyAdds Python coverage for date/time typed columns using timestamp_parsers fallback and inference guard.
python/pyarrow/_csv.pyxDocuments the new fallback behavior in Python ConvertOptions docstring.
docs/source/cpp/csv.rstAdds a “Date and time parsing” section documenting fallback + semantics.
cpp/src/arrow/vendored/musl/strptime.cAdds a C-locale nl_langinfo fallback table for Windows/testing to support %b/%B/%p/....
cpp/src/arrow/csv/options.hDocuments fallback semantics for timestamp_parsers in C++ API docs.
cpp/src/arrow/csv/inference_internal.hEnsures date/time inference ignores configured timestamp_parsers.
cpp/src/arrow/csv/converter.ccImplements fallback decoder + converter factory changes for date/time types.
cpp/src/arrow/csv/converter_test.ccAdds C++ tests for date/time fallback parsing behavior and edge cases.

Comment threadcpp/src/arrow/csv/converter.cc
Comment on lines +1188 to +1195
# Month names are parsed case-insensitively
rows = b"a\n15-OCT-15\n18-Jun-90\n"
opts = ConvertOptions(column_types={'a': pa.date32()},
timestamp_parsers=['%d-%b-%y'])
table = self.read_bytes(rows, convert_options=opts)
assert table.to_pydict() == {
'a': [date(2015, 10, 15), date(1990, 6, 18)],
}

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — I checked this and the tests are deterministic as written, so I've left them unchanged.

Neither the test binary nor pyarrow adopts the environment's LC_TIME: there is no setlocale(LC_ALL/LC_TIME, "") anywhere in Arrow's C++ (outside vendored code) or in pyarrow, and CPython coerces only LC_CTYPE at startup, never LC_TIME. So a process started with a non-English LC_TIME in the environment still runs strptime in the C locale. On glibc there is a second reason: strptime's %b/%B keeps the C-locale (English) month names as a fallback even under a non-English locale, so English abbreviations parse regardless (setlocale(LC_TIME, "fr_FR.UTF-8") followed by parsing "15-JUL-15" still succeeds).

Minimal check: CSV %b parsing is independent of the environment locale

Each child process below is started with a non-English LC_ALL/LC_TIME in its environment — exactly the scenario in the comment — and still parses the English month abbreviation correctly:

importosimportsubprocessimportsysCHILD=r"""import osimport pyarrow as pafrom pyarrow import csvfrom datetime import datedata = b"a\n15-JUL-15\n" # English abbreviated month name "JUL"opts = csv.ConvertOptions(column_types={"a": pa.date32()}, timestamp_parsers=["%d-%b-%y"])got = csv.read_csv(pa.py_buffer(data), convert_options=opts).to_pydict()assert got == {"a": [date(2015, 7, 15)]}, gotprint("OK with LC_ALL=%-14r ->" % os.environ.get("LC_ALL"), got)"""forlcin ("C", "fr_FR.UTF-8", "de_DE.UTF-8"):
env=dict(os.environ, LC_ALL=lc, LC_TIME=lc)
subprocess.run([sys.executable, "-c", CHILD], env=env, check=True)

Output (identical whether or not those locales are actually installed):

OK with LC_ALL='C' -> {'a': [datetime.date(2015, 7, 15)]}
OK with LC_ALL='fr_FR.UTF-8' -> {'a': [datetime.date(2015, 7, 15)]}
OK with LC_ALL='de_DE.UTF-8' -> {'a': [datetime.date(2015, 7, 15)]}

A genuinely locale-dependent case does remain — an application that itself calls setlocale(LC_ALL, "") under a non-English locale, on a libc whose strptime lacks that English fallback — but that is pre-existing (it affects timestamp columns too) and out of scope here. Happy to file a follow-up issue if that is worth tracking.

@github-actionsgithub-actionsBot added awaiting committer review Awaiting committer review and removed awaiting review Awaiting review labels Jun 22, 2026
@pearu
pearuforce-pushed the pearu/fix-csv-date-time-parsers branch from e75ecec to fcf4f95CompareJune 22, 2026 18:17
@pearu
pearu requested a review from pitrou as a code ownerJune 22, 2026 18:17
@pitrou

Copy link
Copy Markdown
Member

@jorisvandenbossche Do you want to take a look at this PR?

@manyiResearch

Copy link
Copy Markdown

Hi, a small quick question — I noticed that you mentioned multiple issues in the PR description. I was wondering, does this PR fix all of them? Thanks!

@pearu

pearu commented Jul 2, 2026

Copy link
Copy Markdown
ContributorAuthor

@manyifire Good question — mostly, but not every one in the same way:

So: yes for #28303/#33357 (and #26783/#26224), yes-on-Windows for #31816/#31971, and for #37180 only when timestamp_parsers is set explicitly (not the default).

…n parsing CSV date and time columns
CSV columns explicitly typed as date32, date64, time32 or time64 could
only be parsed from strict ISO-8601 strings; ConvertOptions::timestamp_parsers
was consulted only for timestamp columns.
Make the user-defined timestamp parsers act as a fallback for these
column types: the built-in ISO-8601 parser is tried first (preserving
existing behavior), then each configured parser in order. A timestamp
produced by a fallback parser is floored to the day boundary for dates
and reduced to the time of day for times, consistent with casting a
timestamp to a date or time type.
Type inference of date and time columns is deliberately unaffected:
inference keeps using strict ISO-8601 parsing, otherwise a value with a
time-of-day part could be inferred as a date and silently truncated.
Also provide C-locale name tables to the vendored musl strptime used on
Windows, where nl_langinfo() is unavailable: this makes %a/%A/%b/%B/%h/
%p/%c/%r/%x/%X work on Windows (matching musl's C locale), so that the
month-name formats from the original issue reports parse on all
platforms.
ClosesapacheGH-28303.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@pearu
pearuforce-pushed the pearu/fix-csv-date-time-parsers branch from fcf4f95 to 808f110CompareAugust 10, 2026 13:25
@pearu

Copy link
Copy Markdown
ContributorAuthor

Rebased onto current main and re-ran the CSV suite locally — arrow-csv-test passes 278/278, including the four new Date32/Date64/Time32/Time64Conversion.UserDefinedParsers cases.

Status recap for whoever picks this up:

  • Both automated-reviewer comments were addressed in June — std::ranges::transform replaced with a plain index loop, and the %b locale question answered with a reproducer showing the parsing is locale-independent as written.
  • The two failing jobs in the last CI run (AMD64 macOS 15-intel C++ and Python 3) were the same Homebrew infrastructure flake — brew install --formula aws-sdk-cpp hit a /usr/local/Cellar/cmake lock — unrelated to this change. The fresh run on the rebase should clear them.

@pitrou — you routed this to @jorisvandenbossche back in June and it has been quiet since. Is there anything I can do to make this easier to review? If reviewing the CSV converter change together with the vendored strptime C-locale tables is the sticking point, I'm happy to split the strptime part into its own PR.


🤖 Drafted by Claude Code (an AI agent) and reviewed & approved by pearu.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow ConvertOptions.timestamp_parsers for date types [C++] Unable to read date64 or date32 in specific format from CSV

4 participants

@pearu@pitrou@manyiResearch
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GH-41488: [C++][Python] Apply timestamp_parsers as fallback when parsing CSV date and time columns by pearu · Pull Request #50146 · apache/arrow · GitHub
Skip to content

GH-41488: [C++][Python] Apply timestamp_parsers as fallback when parsing CSV date and time columns - #50146

Open
pearu wants to merge 1 commit into
apache:mainfrom
pearu:pearu/fix-csv-date-time-parsers
Open

GH-41488: [C++][Python] Apply timestamp_parsers as fallback when parsing CSV date and time columns#50146
pearu wants to merge 1 commit into
apache:mainfrom
pearu:pearu/fix-csv-date-time-parsers

Conversation

@pearu

@pearupearu commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Rationale for this change

CSV columns explicitly typed as date32, date64, time32 or time64 can only be parsed from strict ISO-8601 strings: ConvertOptions::timestamp_parsers is consulted only for timestamp columns. Reading e.g. 15-OCT-15 into a date32 column fails even with timestamp_parsers=["%d-%b-%y"], and 7:55:00 (non-zero-padded hour) fails for time32[s]. Users currently work around this by declaring such columns as timestamp, reading, then casting back to the date/time type.

Effect on the issues collected in #41488:

What changes are included in this PR?

  • A new DateTimeWithParsersValueDecoder in csv/converter.cc, used for date32/date64/time32/time64 columns when timestamp_parsers is non-empty. It tries the built-in ISO-8601 parser first (preserving all existing behavior), then each configured parser in order. A timestamp produced by a fallback parser is floored to the day boundary for dates and reduced to the time of day for times, consistent with casting a timestamp to a date or time type. Values carrying a zone offset are rejected, as for zone-less timestamp columns. When no parsers are configured, the pre-existing decoder is used unchanged.
  • Type inference is deliberately unaffected: the Date/Time inference stages now explicitly use options with timestamp_parsers cleared, so inference keeps strict ISO-8601 semantics (otherwise a value with a time-of-day part could be inferred as a date and silently truncated). The existing test_timestamp_parsers Python test pins this behavior.
  • Documentation of the fallback and flooring semantics in ConvertOptions::timestamp_parsers (C++ and Python docstrings) and a new "Date and time parsing" section in the C++ CSV user guide.
  • C-locale name tables for the vendored musl strptime used on Windows, where nl_langinfo() is unavailable. Previously the %a/%A/%b/%B/%h/%p/%c/%r/%x/%X specifiers were compiled out on Windows, so the month-name formats from the original issue reports (%d-%b-%y) could not work there for any column type. The tables match musl's C locale, and name matching is case-insensitive as on glibc/musl/BSD. The fallback path is compiled and verified on Linux via the ARROW_TEST_FALLBACK_LANGINFO hook.

Are these changes tested?

Yes:

  • New C++ tests (Date32Conversion.UserDefinedParsers, Date64Conversion.UserDefinedParsers, Time32Conversion.UserDefinedParsers, Time64Conversion.UserDefinedParsers) covering custom formats, mixed ISO + custom values in one column (backward compatibility of ISO values when parsers are set), pre-epoch flooring with a time-of-day component (distinguishes floor from truncating division), time-of-day extraction from pre-epoch timestamps, zone-offset rejection, and error cases.
  • New Python tests with the reproducers from [C++] Unable to read date64 or date32 in specific format from CSV #28303 and CSV reader cannot parse dates or times #41488, plus an inference-unchanged guard.

Are there any user-facing changes?

Yes: ConvertOptions::timestamp_parsers now also applies, as a fallback after ISO-8601, to columns explicitly typed as date32/date64/time32/time64 (previously such values always errored). No breaking changes: behavior without timestamp_parsers is untouched, ISO values keep parsing when parsers are set, and type inference is unchanged. All language bindings gain the behavior without API changes.

AI usage disclosure

This PR was developed with AI assistance (Claude Code): the decoder, tests and documentation were AI-generated under my direction, then reviewed line-by-line and iterated on by me (design decisions: fallback-after-ISO semantics, silent flooring, inference isolation, and several implementation details adjusted during review). I own and can debug these changes.

🤖 Generated with Claude Code

@pearu

Copy link
Copy Markdown
ContributorAuthor

Two out-of-scope discoveries made while working on this, recorded here rather than folded into the PR to keep it minimal:

  1. MultipleParsersTimestampValueDecoder::Decode (pre-existing, csv/converter.cc) declares its zone_offset_present flag once outside the parser loop. The built-in parsers happen to write the out-parameter on every call (the strptime parser unconditionally, the ISO-8601 parser resets it to false before scanning), so this is currently harmless — but TimestampParser is a public interface, and a user-implemented parser that only writes the flag when an offset is found could observe a stale value from a previous loop iteration. The new decoder in this PR declares the flag per-iteration; the timestamp decoder could get the same two-line treatment as a MINOR follow-up.

  2. The "Timestamp inference/parsing" section of the C++ CSV user guide (docs/source/cpp/csv.rst) does not mention ConvertOptions::timestamp_parsers at all — custom timestamp parsing was undocumented in the user guide before the date/time subsection added here. A short paragraph there could be a docs follow-up.

@github-actionsgithub-actionsBot added the awaiting review Awaiting review label Jun 10, 2026
@github-actions

Copy link
Copy Markdown

⚠️ GitHub issue #41488has been automatically assigned in GitHub to PR creator.

@github-actions

Copy link
Copy Markdown

⚠️ GitHub issue #41488has been automatically assigned in GitHub to PR creator.

@pearu
pearuforce-pushed the pearu/fix-csv-date-time-parsers branch from 78ca3cb to e0af29eCompareJune 10, 2026 10:08
@pearu

pearu commented Jun 10, 2026

Copy link
Copy Markdown
ContributorAuthor

CI triage of the first run (3 failing jobs, all Windows): all three shared one root cause — the tests used the %d-%b-%y format from the original issue reproducer, but month names (%b) are not supported by the vendored musl strptime used on Windows: cpp/src/arrow/vendored/musl/strptime.c force-undefines HAVE_LANGINFO on _WIN32, which compiles out the %a/%A/%b/%B/%h/%c/%p/%r/%x/%X cases entirely. The feature code is unaffected; numeric-format and time tests passed on Windows.

Fixed (amended) by making numeric formats the primary test coverage and keeping the month-name reproducer guarded to non-Windows (#ifndef _WIN32 in C++, sys.platform != "win32" in Python, following the existing kStrptimeSupportsZone / test_strftime precedents).
UPDATE: sorry for this noise, Claude was too eager to post comments, it is better restrained now.

A third discovery for the list above: this %b limitation is pre-existing and applies equally to timestamp columns with timestamp_parsers on Windows — it just had no CI coverage because the existing timestamp tests only use numeric formats. Could deserve its own issue (either implementing C-locale month names in the vendored strptime, or documenting the limitation in timestamp_parsers docs).

@pearu
pearuforce-pushed the pearu/fix-csv-date-time-parsers branch from e0af29e to e75ececCompareJune 10, 2026 13:19
@github-actions

Copy link
Copy Markdown

⚠️ GitHub issue #41488has been automatically assigned in GitHub to PR creator.

@pearu
pearu marked this pull request as ready for review June 10, 2026 14:13
@pearu
pearu requested review from AlenkaF, raulcd and rok as code ownersJune 10, 2026 14:13
@pearu

Copy link
Copy Markdown
ContributorAuthor

The single CI failure (AMD64 Conda C++ AVX2) is unrelated to this PR: Gandiva's TestTime.TestCastTimestampWithTZ fails identically on main since this morning (passing at 4e25461, failing from ca47cd1 on — see e.g. this main run). castTIMESTAMP_utf8 returns 0 for the Canada/Pacific tz name, pointing at tz-database resolution in the CI conda environment — code this PR does not touch.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR extends the CSV reader’s ConvertOptions::timestamp_parsers behavior so that, when columns are explicitly typed as date32/date64/time32/time64, the reader first attempts the existing ISO-8601 parsing and then falls back to the user-provided timestamp parsers (with flooring/extracting semantics consistent with casting). It also keeps type inference strict (ISO-only) to avoid silent truncation.

Changes:

  • Add a new C++ CSV date/time value decoder that falls back to timestamp_parsers after ISO parsing and applies flooring/time-of-day extraction.
  • Ensure CSV type inference for date/time remains ISO-only even when timestamp_parsers are configured.
  • Update C++/Python docs and add C++/Python tests; improve vendored Windows strptime support for C-locale day/month names.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Show a summary per file
FileDescription
python/pyarrow/tests/test_csv.pyAdds Python coverage for date/time typed columns using timestamp_parsers fallback and inference guard.
python/pyarrow/_csv.pyxDocuments the new fallback behavior in Python ConvertOptions docstring.
docs/source/cpp/csv.rstAdds a “Date and time parsing” section documenting fallback + semantics.
cpp/src/arrow/vendored/musl/strptime.cAdds a C-locale nl_langinfo fallback table for Windows/testing to support %b/%B/%p/....
cpp/src/arrow/csv/options.hDocuments fallback semantics for timestamp_parsers in C++ API docs.
cpp/src/arrow/csv/inference_internal.hEnsures date/time inference ignores configured timestamp_parsers.
cpp/src/arrow/csv/converter.ccImplements fallback decoder + converter factory changes for date/time types.
cpp/src/arrow/csv/converter_test.ccAdds C++ tests for date/time fallback parsing behavior and edge cases.

Comment threadcpp/src/arrow/csv/converter.cc
Comment on lines +1188 to +1195
# Month names are parsed case-insensitively
rows = b"a\n15-OCT-15\n18-Jun-90\n"
opts = ConvertOptions(column_types={'a': pa.date32()},
timestamp_parsers=['%d-%b-%y'])
table = self.read_bytes(rows, convert_options=opts)
assert table.to_pydict() == {
'a': [date(2015, 10, 15), date(1990, 6, 18)],
}

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — I checked this and the tests are deterministic as written, so I've left them unchanged.

Neither the test binary nor pyarrow adopts the environment's LC_TIME: there is no setlocale(LC_ALL/LC_TIME, "") anywhere in Arrow's C++ (outside vendored code) or in pyarrow, and CPython coerces only LC_CTYPE at startup, never LC_TIME. So a process started with a non-English LC_TIME in the environment still runs strptime in the C locale. On glibc there is a second reason: strptime's %b/%B keeps the C-locale (English) month names as a fallback even under a non-English locale, so English abbreviations parse regardless (setlocale(LC_TIME, "fr_FR.UTF-8") followed by parsing "15-JUL-15" still succeeds).

Minimal check: CSV %b parsing is independent of the environment locale

Each child process below is started with a non-English LC_ALL/LC_TIME in its environment — exactly the scenario in the comment — and still parses the English month abbreviation correctly:

importosimportsubprocessimportsysCHILD=r"""import osimport pyarrow as pafrom pyarrow import csvfrom datetime import datedata = b"a\n15-JUL-15\n" # English abbreviated month name "JUL"opts = csv.ConvertOptions(column_types={"a": pa.date32()}, timestamp_parsers=["%d-%b-%y"])got = csv.read_csv(pa.py_buffer(data), convert_options=opts).to_pydict()assert got == {"a": [date(2015, 7, 15)]}, gotprint("OK with LC_ALL=%-14r ->" % os.environ.get("LC_ALL"), got)"""forlcin ("C", "fr_FR.UTF-8", "de_DE.UTF-8"):
env=dict(os.environ, LC_ALL=lc, LC_TIME=lc)
subprocess.run([sys.executable, "-c", CHILD], env=env, check=True)

Output (identical whether or not those locales are actually installed):

OK with LC_ALL='C' -> {'a': [datetime.date(2015, 7, 15)]}
OK with LC_ALL='fr_FR.UTF-8' -> {'a': [datetime.date(2015, 7, 15)]}
OK with LC_ALL='de_DE.UTF-8' -> {'a': [datetime.date(2015, 7, 15)]}

A genuinely locale-dependent case does remain — an application that itself calls setlocale(LC_ALL, "") under a non-English locale, on a libc whose strptime lacks that English fallback — but that is pre-existing (it affects timestamp columns too) and out of scope here. Happy to file a follow-up issue if that is worth tracking.

@github-actionsgithub-actionsBot added awaiting committer review Awaiting committer review and removed awaiting review Awaiting review labels Jun 22, 2026
@pearu
pearuforce-pushed the pearu/fix-csv-date-time-parsers branch from e75ecec to fcf4f95CompareJune 22, 2026 18:17
@pearu
pearu requested a review from pitrou as a code ownerJune 22, 2026 18:17
@pitrou

Copy link
Copy Markdown
Member

@jorisvandenbossche Do you want to take a look at this PR?

@manyiResearch

Copy link
Copy Markdown

Hi, a small quick question — I noticed that you mentioned multiple issues in the PR description. I was wondering, does this PR fix all of them? Thanks!

@pearu

pearu commented Jul 2, 2026

Copy link
Copy Markdown
ContributorAuthor

@manyifire Good question — mostly, but not every one in the same way:

So: yes for #28303/#33357 (and #26783/#26224), yes-on-Windows for #31816/#31971, and for #37180 only when timestamp_parsers is set explicitly (not the default).

…n parsing CSV date and time columns
CSV columns explicitly typed as date32, date64, time32 or time64 could
only be parsed from strict ISO-8601 strings; ConvertOptions::timestamp_parsers
was consulted only for timestamp columns.
Make the user-defined timestamp parsers act as a fallback for these
column types: the built-in ISO-8601 parser is tried first (preserving
existing behavior), then each configured parser in order. A timestamp
produced by a fallback parser is floored to the day boundary for dates
and reduced to the time of day for times, consistent with casting a
timestamp to a date or time type.
Type inference of date and time columns is deliberately unaffected:
inference keeps using strict ISO-8601 parsing, otherwise a value with a
time-of-day part could be inferred as a date and silently truncated.
Also provide C-locale name tables to the vendored musl strptime used on
Windows, where nl_langinfo() is unavailable: this makes %a/%A/%b/%B/%h/
%p/%c/%r/%x/%X work on Windows (matching musl's C locale), so that the
month-name formats from the original issue reports parse on all
platforms.
ClosesapacheGH-28303.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@pearu
pearuforce-pushed the pearu/fix-csv-date-time-parsers branch from fcf4f95 to 808f110CompareAugust 10, 2026 13:25
@pearu

Copy link
Copy Markdown
ContributorAuthor

Rebased onto current main and re-ran the CSV suite locally — arrow-csv-test passes 278/278, including the four new Date32/Date64/Time32/Time64Conversion.UserDefinedParsers cases.

Status recap for whoever picks this up:

  • Both automated-reviewer comments were addressed in June — std::ranges::transform replaced with a plain index loop, and the %b locale question answered with a reproducer showing the parsing is locale-independent as written.
  • The two failing jobs in the last CI run (AMD64 macOS 15-intel C++ and Python 3) were the same Homebrew infrastructure flake — brew install --formula aws-sdk-cpp hit a /usr/local/Cellar/cmake lock — unrelated to this change. The fresh run on the rebase should clear them.

@pitrou — you routed this to @jorisvandenbossche back in June and it has been quiet since. Is there anything I can do to make this easier to review? If reviewing the CSV converter change together with the vendored strptime C-locale tables is the sticking point, I'm happy to split the strptime part into its own PR.


🤖 Drafted by Claude Code (an AI agent) and reviewed & approved by pearu.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow ConvertOptions.timestamp_parsers for date types [C++] Unable to read date64 or date32 in specific format from CSV

4 participants

@pearu@pitrou@manyiResearch
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GH-41488: [C++][Python] Apply timestamp_parsers as fallback when parsing CSV date and time columns by pearu · Pull Request #50146 · apache/arrow · GitHub
Skip to content

GH-41488: [C++][Python] Apply timestamp_parsers as fallback when parsing CSV date and time columns - #50146

Open
pearu wants to merge 1 commit into
apache:mainfrom
pearu:pearu/fix-csv-date-time-parsers
Open

GH-41488: [C++][Python] Apply timestamp_parsers as fallback when parsing CSV date and time columns#50146
pearu wants to merge 1 commit into
apache:mainfrom
pearu:pearu/fix-csv-date-time-parsers

Conversation

@pearu

@pearupearu commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Rationale for this change

CSV columns explicitly typed as date32, date64, time32 or time64 can only be parsed from strict ISO-8601 strings: ConvertOptions::timestamp_parsers is consulted only for timestamp columns. Reading e.g. 15-OCT-15 into a date32 column fails even with timestamp_parsers=["%d-%b-%y"], and 7:55:00 (non-zero-padded hour) fails for time32[s]. Users currently work around this by declaring such columns as timestamp, reading, then casting back to the date/time type.

Effect on the issues collected in #41488:

What changes are included in this PR?

  • A new DateTimeWithParsersValueDecoder in csv/converter.cc, used for date32/date64/time32/time64 columns when timestamp_parsers is non-empty. It tries the built-in ISO-8601 parser first (preserving all existing behavior), then each configured parser in order. A timestamp produced by a fallback parser is floored to the day boundary for dates and reduced to the time of day for times, consistent with casting a timestamp to a date or time type. Values carrying a zone offset are rejected, as for zone-less timestamp columns. When no parsers are configured, the pre-existing decoder is used unchanged.
  • Type inference is deliberately unaffected: the Date/Time inference stages now explicitly use options with timestamp_parsers cleared, so inference keeps strict ISO-8601 semantics (otherwise a value with a time-of-day part could be inferred as a date and silently truncated). The existing test_timestamp_parsers Python test pins this behavior.
  • Documentation of the fallback and flooring semantics in ConvertOptions::timestamp_parsers (C++ and Python docstrings) and a new "Date and time parsing" section in the C++ CSV user guide.
  • C-locale name tables for the vendored musl strptime used on Windows, where nl_langinfo() is unavailable. Previously the %a/%A/%b/%B/%h/%p/%c/%r/%x/%X specifiers were compiled out on Windows, so the month-name formats from the original issue reports (%d-%b-%y) could not work there for any column type. The tables match musl's C locale, and name matching is case-insensitive as on glibc/musl/BSD. The fallback path is compiled and verified on Linux via the ARROW_TEST_FALLBACK_LANGINFO hook.

Are these changes tested?

Yes:

  • New C++ tests (Date32Conversion.UserDefinedParsers, Date64Conversion.UserDefinedParsers, Time32Conversion.UserDefinedParsers, Time64Conversion.UserDefinedParsers) covering custom formats, mixed ISO + custom values in one column (backward compatibility of ISO values when parsers are set), pre-epoch flooring with a time-of-day component (distinguishes floor from truncating division), time-of-day extraction from pre-epoch timestamps, zone-offset rejection, and error cases.
  • New Python tests with the reproducers from [C++] Unable to read date64 or date32 in specific format from CSV #28303 and CSV reader cannot parse dates or times #41488, plus an inference-unchanged guard.

Are there any user-facing changes?

Yes: ConvertOptions::timestamp_parsers now also applies, as a fallback after ISO-8601, to columns explicitly typed as date32/date64/time32/time64 (previously such values always errored). No breaking changes: behavior without timestamp_parsers is untouched, ISO values keep parsing when parsers are set, and type inference is unchanged. All language bindings gain the behavior without API changes.

AI usage disclosure

This PR was developed with AI assistance (Claude Code): the decoder, tests and documentation were AI-generated under my direction, then reviewed line-by-line and iterated on by me (design decisions: fallback-after-ISO semantics, silent flooring, inference isolation, and several implementation details adjusted during review). I own and can debug these changes.

🤖 Generated with Claude Code

@pearu

Copy link
Copy Markdown
ContributorAuthor

Two out-of-scope discoveries made while working on this, recorded here rather than folded into the PR to keep it minimal:

  1. MultipleParsersTimestampValueDecoder::Decode (pre-existing, csv/converter.cc) declares its zone_offset_present flag once outside the parser loop. The built-in parsers happen to write the out-parameter on every call (the strptime parser unconditionally, the ISO-8601 parser resets it to false before scanning), so this is currently harmless — but TimestampParser is a public interface, and a user-implemented parser that only writes the flag when an offset is found could observe a stale value from a previous loop iteration. The new decoder in this PR declares the flag per-iteration; the timestamp decoder could get the same two-line treatment as a MINOR follow-up.

  2. The "Timestamp inference/parsing" section of the C++ CSV user guide (docs/source/cpp/csv.rst) does not mention ConvertOptions::timestamp_parsers at all — custom timestamp parsing was undocumented in the user guide before the date/time subsection added here. A short paragraph there could be a docs follow-up.

@github-actionsgithub-actionsBot added the awaiting review Awaiting review label Jun 10, 2026
@github-actions

Copy link
Copy Markdown

⚠️ GitHub issue #41488has been automatically assigned in GitHub to PR creator.

@github-actions

Copy link
Copy Markdown

⚠️ GitHub issue #41488has been automatically assigned in GitHub to PR creator.

@pearu
pearuforce-pushed the pearu/fix-csv-date-time-parsers branch from 78ca3cb to e0af29eCompareJune 10, 2026 10:08
@pearu

pearu commented Jun 10, 2026

Copy link
Copy Markdown
ContributorAuthor

CI triage of the first run (3 failing jobs, all Windows): all three shared one root cause — the tests used the %d-%b-%y format from the original issue reproducer, but month names (%b) are not supported by the vendored musl strptime used on Windows: cpp/src/arrow/vendored/musl/strptime.c force-undefines HAVE_LANGINFO on _WIN32, which compiles out the %a/%A/%b/%B/%h/%c/%p/%r/%x/%X cases entirely. The feature code is unaffected; numeric-format and time tests passed on Windows.

Fixed (amended) by making numeric formats the primary test coverage and keeping the month-name reproducer guarded to non-Windows (#ifndef _WIN32 in C++, sys.platform != "win32" in Python, following the existing kStrptimeSupportsZone / test_strftime precedents).
UPDATE: sorry for this noise, Claude was too eager to post comments, it is better restrained now.

A third discovery for the list above: this %b limitation is pre-existing and applies equally to timestamp columns with timestamp_parsers on Windows — it just had no CI coverage because the existing timestamp tests only use numeric formats. Could deserve its own issue (either implementing C-locale month names in the vendored strptime, or documenting the limitation in timestamp_parsers docs).

@pearu
pearuforce-pushed the pearu/fix-csv-date-time-parsers branch from e0af29e to e75ececCompareJune 10, 2026 13:19
@github-actions

Copy link
Copy Markdown

⚠️ GitHub issue #41488has been automatically assigned in GitHub to PR creator.

@pearu
pearu marked this pull request as ready for review June 10, 2026 14:13
@pearu
pearu requested review from AlenkaF, raulcd and rok as code ownersJune 10, 2026 14:13
@pearu

Copy link
Copy Markdown
ContributorAuthor

The single CI failure (AMD64 Conda C++ AVX2) is unrelated to this PR: Gandiva's TestTime.TestCastTimestampWithTZ fails identically on main since this morning (passing at 4e25461, failing from ca47cd1 on — see e.g. this main run). castTIMESTAMP_utf8 returns 0 for the Canada/Pacific tz name, pointing at tz-database resolution in the CI conda environment — code this PR does not touch.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR extends the CSV reader’s ConvertOptions::timestamp_parsers behavior so that, when columns are explicitly typed as date32/date64/time32/time64, the reader first attempts the existing ISO-8601 parsing and then falls back to the user-provided timestamp parsers (with flooring/extracting semantics consistent with casting). It also keeps type inference strict (ISO-only) to avoid silent truncation.

Changes:

  • Add a new C++ CSV date/time value decoder that falls back to timestamp_parsers after ISO parsing and applies flooring/time-of-day extraction.
  • Ensure CSV type inference for date/time remains ISO-only even when timestamp_parsers are configured.
  • Update C++/Python docs and add C++/Python tests; improve vendored Windows strptime support for C-locale day/month names.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Show a summary per file
FileDescription
python/pyarrow/tests/test_csv.pyAdds Python coverage for date/time typed columns using timestamp_parsers fallback and inference guard.
python/pyarrow/_csv.pyxDocuments the new fallback behavior in Python ConvertOptions docstring.
docs/source/cpp/csv.rstAdds a “Date and time parsing” section documenting fallback + semantics.
cpp/src/arrow/vendored/musl/strptime.cAdds a C-locale nl_langinfo fallback table for Windows/testing to support %b/%B/%p/....
cpp/src/arrow/csv/options.hDocuments fallback semantics for timestamp_parsers in C++ API docs.
cpp/src/arrow/csv/inference_internal.hEnsures date/time inference ignores configured timestamp_parsers.
cpp/src/arrow/csv/converter.ccImplements fallback decoder + converter factory changes for date/time types.
cpp/src/arrow/csv/converter_test.ccAdds C++ tests for date/time fallback parsing behavior and edge cases.

Comment threadcpp/src/arrow/csv/converter.cc
Comment on lines +1188 to +1195
# Month names are parsed case-insensitively
rows = b"a\n15-OCT-15\n18-Jun-90\n"
opts = ConvertOptions(column_types={'a': pa.date32()},
timestamp_parsers=['%d-%b-%y'])
table = self.read_bytes(rows, convert_options=opts)
assert table.to_pydict() == {
'a': [date(2015, 10, 15), date(1990, 6, 18)],
}

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — I checked this and the tests are deterministic as written, so I've left them unchanged.

Neither the test binary nor pyarrow adopts the environment's LC_TIME: there is no setlocale(LC_ALL/LC_TIME, "") anywhere in Arrow's C++ (outside vendored code) or in pyarrow, and CPython coerces only LC_CTYPE at startup, never LC_TIME. So a process started with a non-English LC_TIME in the environment still runs strptime in the C locale. On glibc there is a second reason: strptime's %b/%B keeps the C-locale (English) month names as a fallback even under a non-English locale, so English abbreviations parse regardless (setlocale(LC_TIME, "fr_FR.UTF-8") followed by parsing "15-JUL-15" still succeeds).

Minimal check: CSV %b parsing is independent of the environment locale

Each child process below is started with a non-English LC_ALL/LC_TIME in its environment — exactly the scenario in the comment — and still parses the English month abbreviation correctly:

importosimportsubprocessimportsysCHILD=r"""import osimport pyarrow as pafrom pyarrow import csvfrom datetime import datedata = b"a\n15-JUL-15\n" # English abbreviated month name "JUL"opts = csv.ConvertOptions(column_types={"a": pa.date32()}, timestamp_parsers=["%d-%b-%y"])got = csv.read_csv(pa.py_buffer(data), convert_options=opts).to_pydict()assert got == {"a": [date(2015, 7, 15)]}, gotprint("OK with LC_ALL=%-14r ->" % os.environ.get("LC_ALL"), got)"""forlcin ("C", "fr_FR.UTF-8", "de_DE.UTF-8"):
env=dict(os.environ, LC_ALL=lc, LC_TIME=lc)
subprocess.run([sys.executable, "-c", CHILD], env=env, check=True)

Output (identical whether or not those locales are actually installed):

OK with LC_ALL='C' -> {'a': [datetime.date(2015, 7, 15)]}
OK with LC_ALL='fr_FR.UTF-8' -> {'a': [datetime.date(2015, 7, 15)]}
OK with LC_ALL='de_DE.UTF-8' -> {'a': [datetime.date(2015, 7, 15)]}

A genuinely locale-dependent case does remain — an application that itself calls setlocale(LC_ALL, "") under a non-English locale, on a libc whose strptime lacks that English fallback — but that is pre-existing (it affects timestamp columns too) and out of scope here. Happy to file a follow-up issue if that is worth tracking.

@github-actionsgithub-actionsBot added awaiting committer review Awaiting committer review and removed awaiting review Awaiting review labels Jun 22, 2026
@pearu
pearuforce-pushed the pearu/fix-csv-date-time-parsers branch from e75ecec to fcf4f95CompareJune 22, 2026 18:17
@pearu
pearu requested a review from pitrou as a code ownerJune 22, 2026 18:17
@pitrou

Copy link
Copy Markdown
Member

@jorisvandenbossche Do you want to take a look at this PR?

@manyiResearch

Copy link
Copy Markdown

Hi, a small quick question — I noticed that you mentioned multiple issues in the PR description. I was wondering, does this PR fix all of them? Thanks!

@pearu

pearu commented Jul 2, 2026

Copy link
Copy Markdown
ContributorAuthor

@manyifire Good question — mostly, but not every one in the same way:

So: yes for #28303/#33357 (and #26783/#26224), yes-on-Windows for #31816/#31971, and for #37180 only when timestamp_parsers is set explicitly (not the default).

…n parsing CSV date and time columns
CSV columns explicitly typed as date32, date64, time32 or time64 could
only be parsed from strict ISO-8601 strings; ConvertOptions::timestamp_parsers
was consulted only for timestamp columns.
Make the user-defined timestamp parsers act as a fallback for these
column types: the built-in ISO-8601 parser is tried first (preserving
existing behavior), then each configured parser in order. A timestamp
produced by a fallback parser is floored to the day boundary for dates
and reduced to the time of day for times, consistent with casting a
timestamp to a date or time type.
Type inference of date and time columns is deliberately unaffected:
inference keeps using strict ISO-8601 parsing, otherwise a value with a
time-of-day part could be inferred as a date and silently truncated.
Also provide C-locale name tables to the vendored musl strptime used on
Windows, where nl_langinfo() is unavailable: this makes %a/%A/%b/%B/%h/
%p/%c/%r/%x/%X work on Windows (matching musl's C locale), so that the
month-name formats from the original issue reports parse on all
platforms.
ClosesapacheGH-28303.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@pearu
pearuforce-pushed the pearu/fix-csv-date-time-parsers branch from fcf4f95 to 808f110CompareAugust 10, 2026 13:25
@pearu

Copy link
Copy Markdown
ContributorAuthor

Rebased onto current main and re-ran the CSV suite locally — arrow-csv-test passes 278/278, including the four new Date32/Date64/Time32/Time64Conversion.UserDefinedParsers cases.

Status recap for whoever picks this up:

  • Both automated-reviewer comments were addressed in June — std::ranges::transform replaced with a plain index loop, and the %b locale question answered with a reproducer showing the parsing is locale-independent as written.
  • The two failing jobs in the last CI run (AMD64 macOS 15-intel C++ and Python 3) were the same Homebrew infrastructure flake — brew install --formula aws-sdk-cpp hit a /usr/local/Cellar/cmake lock — unrelated to this change. The fresh run on the rebase should clear them.

@pitrou — you routed this to @jorisvandenbossche back in June and it has been quiet since. Is there anything I can do to make this easier to review? If reviewing the CSV converter change together with the vendored strptime C-locale tables is the sticking point, I'm happy to split the strptime part into its own PR.


🤖 Drafted by Claude Code (an AI agent) and reviewed & approved by pearu.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow ConvertOptions.timestamp_parsers for date types [C++] Unable to read date64 or date32 in specific format from CSV

4 participants

@pearu@pitrou@manyiResearch
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' GH-41488: [C++][Python] Apply timestamp_parsers as fallback when parsing CSV date and time columns by pearu · Pull Request #50146 · apache/arrow · GitHub
Skip to content

GH-41488: [C++][Python] Apply timestamp_parsers as fallback when parsing CSV date and time columns - #50146

Open
pearu wants to merge 1 commit into
apache:mainfrom
pearu:pearu/fix-csv-date-time-parsers
Open

GH-41488: [C++][Python] Apply timestamp_parsers as fallback when parsing CSV date and time columns#50146
pearu wants to merge 1 commit into
apache:mainfrom
pearu:pearu/fix-csv-date-time-parsers

Conversation

@pearu

@pearupearu commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Rationale for this change

CSV columns explicitly typed as date32, date64, time32 or time64 can only be parsed from strict ISO-8601 strings: ConvertOptions::timestamp_parsers is consulted only for timestamp columns. Reading e.g. 15-OCT-15 into a date32 column fails even with timestamp_parsers=["%d-%b-%y"], and 7:55:00 (non-zero-padded hour) fails for time32[s]. Users currently work around this by declaring such columns as timestamp, reading, then casting back to the date/time type.

Effect on the issues collected in #41488:

What changes are included in this PR?

  • A new DateTimeWithParsersValueDecoder in csv/converter.cc, used for date32/date64/time32/time64 columns when timestamp_parsers is non-empty. It tries the built-in ISO-8601 parser first (preserving all existing behavior), then each configured parser in order. A timestamp produced by a fallback parser is floored to the day boundary for dates and reduced to the time of day for times, consistent with casting a timestamp to a date or time type. Values carrying a zone offset are rejected, as for zone-less timestamp columns. When no parsers are configured, the pre-existing decoder is used unchanged.
  • Type inference is deliberately unaffected: the Date/Time inference stages now explicitly use options with timestamp_parsers cleared, so inference keeps strict ISO-8601 semantics (otherwise a value with a time-of-day part could be inferred as a date and silently truncated). The existing test_timestamp_parsers Python test pins this behavior.
  • Documentation of the fallback and flooring semantics in ConvertOptions::timestamp_parsers (C++ and Python docstrings) and a new "Date and time parsing" section in the C++ CSV user guide.
  • C-locale name tables for the vendored musl strptime used on Windows, where nl_langinfo() is unavailable. Previously the %a/%A/%b/%B/%h/%p/%c/%r/%x/%X specifiers were compiled out on Windows, so the month-name formats from the original issue reports (%d-%b-%y) could not work there for any column type. The tables match musl's C locale, and name matching is case-insensitive as on glibc/musl/BSD. The fallback path is compiled and verified on Linux via the ARROW_TEST_FALLBACK_LANGINFO hook.

Are these changes tested?

Yes:

  • New C++ tests (Date32Conversion.UserDefinedParsers, Date64Conversion.UserDefinedParsers, Time32Conversion.UserDefinedParsers, Time64Conversion.UserDefinedParsers) covering custom formats, mixed ISO + custom values in one column (backward compatibility of ISO values when parsers are set), pre-epoch flooring with a time-of-day component (distinguishes floor from truncating division), time-of-day extraction from pre-epoch timestamps, zone-offset rejection, and error cases.
  • New Python tests with the reproducers from [C++] Unable to read date64 or date32 in specific format from CSV #28303 and CSV reader cannot parse dates or times #41488, plus an inference-unchanged guard.

Are there any user-facing changes?

Yes: ConvertOptions::timestamp_parsers now also applies, as a fallback after ISO-8601, to columns explicitly typed as date32/date64/time32/time64 (previously such values always errored). No breaking changes: behavior without timestamp_parsers is untouched, ISO values keep parsing when parsers are set, and type inference is unchanged. All language bindings gain the behavior without API changes.

AI usage disclosure

This PR was developed with AI assistance (Claude Code): the decoder, tests and documentation were AI-generated under my direction, then reviewed line-by-line and iterated on by me (design decisions: fallback-after-ISO semantics, silent flooring, inference isolation, and several implementation details adjusted during review). I own and can debug these changes.

🤖 Generated with Claude Code

@pearu

Copy link
Copy Markdown
ContributorAuthor

Two out-of-scope discoveries made while working on this, recorded here rather than folded into the PR to keep it minimal:

  1. MultipleParsersTimestampValueDecoder::Decode (pre-existing, csv/converter.cc) declares its zone_offset_present flag once outside the parser loop. The built-in parsers happen to write the out-parameter on every call (the strptime parser unconditionally, the ISO-8601 parser resets it to false before scanning), so this is currently harmless — but TimestampParser is a public interface, and a user-implemented parser that only writes the flag when an offset is found could observe a stale value from a previous loop iteration. The new decoder in this PR declares the flag per-iteration; the timestamp decoder could get the same two-line treatment as a MINOR follow-up.

  2. The "Timestamp inference/parsing" section of the C++ CSV user guide (docs/source/cpp/csv.rst) does not mention ConvertOptions::timestamp_parsers at all — custom timestamp parsing was undocumented in the user guide before the date/time subsection added here. A short paragraph there could be a docs follow-up.

@github-actionsgithub-actionsBot added the awaiting review Awaiting review label Jun 10, 2026
@github-actions

Copy link
Copy Markdown

⚠️ GitHub issue #41488has been automatically assigned in GitHub to PR creator.

@github-actions

Copy link
Copy Markdown

⚠️ GitHub issue #41488has been automatically assigned in GitHub to PR creator.

@pearu
pearuforce-pushed the pearu/fix-csv-date-time-parsers branch from 78ca3cb to e0af29eCompareJune 10, 2026 10:08
@pearu

pearu commented Jun 10, 2026

Copy link
Copy Markdown
ContributorAuthor

CI triage of the first run (3 failing jobs, all Windows): all three shared one root cause — the tests used the %d-%b-%y format from the original issue reproducer, but month names (%b) are not supported by the vendored musl strptime used on Windows: cpp/src/arrow/vendored/musl/strptime.c force-undefines HAVE_LANGINFO on _WIN32, which compiles out the %a/%A/%b/%B/%h/%c/%p/%r/%x/%X cases entirely. The feature code is unaffected; numeric-format and time tests passed on Windows.

Fixed (amended) by making numeric formats the primary test coverage and keeping the month-name reproducer guarded to non-Windows (#ifndef _WIN32 in C++, sys.platform != "win32" in Python, following the existing kStrptimeSupportsZone / test_strftime precedents).
UPDATE: sorry for this noise, Claude was too eager to post comments, it is better restrained now.

A third discovery for the list above: this %b limitation is pre-existing and applies equally to timestamp columns with timestamp_parsers on Windows — it just had no CI coverage because the existing timestamp tests only use numeric formats. Could deserve its own issue (either implementing C-locale month names in the vendored strptime, or documenting the limitation in timestamp_parsers docs).

@pearu
pearuforce-pushed the pearu/fix-csv-date-time-parsers branch from e0af29e to e75ececCompareJune 10, 2026 13:19
@github-actions

Copy link
Copy Markdown

⚠️ GitHub issue #41488has been automatically assigned in GitHub to PR creator.

@pearu
pearu marked this pull request as ready for review June 10, 2026 14:13
@pearu
pearu requested review from AlenkaF, raulcd and rok as code ownersJune 10, 2026 14:13
@pearu

Copy link
Copy Markdown
ContributorAuthor

The single CI failure (AMD64 Conda C++ AVX2) is unrelated to this PR: Gandiva's TestTime.TestCastTimestampWithTZ fails identically on main since this morning (passing at 4e25461, failing from ca47cd1 on — see e.g. this main run). castTIMESTAMP_utf8 returns 0 for the Canada/Pacific tz name, pointing at tz-database resolution in the CI conda environment — code this PR does not touch.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR extends the CSV reader’s ConvertOptions::timestamp_parsers behavior so that, when columns are explicitly typed as date32/date64/time32/time64, the reader first attempts the existing ISO-8601 parsing and then falls back to the user-provided timestamp parsers (with flooring/extracting semantics consistent with casting). It also keeps type inference strict (ISO-only) to avoid silent truncation.

Changes:

  • Add a new C++ CSV date/time value decoder that falls back to timestamp_parsers after ISO parsing and applies flooring/time-of-day extraction.
  • Ensure CSV type inference for date/time remains ISO-only even when timestamp_parsers are configured.
  • Update C++/Python docs and add C++/Python tests; improve vendored Windows strptime support for C-locale day/month names.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Show a summary per file
FileDescription
python/pyarrow/tests/test_csv.pyAdds Python coverage for date/time typed columns using timestamp_parsers fallback and inference guard.
python/pyarrow/_csv.pyxDocuments the new fallback behavior in Python ConvertOptions docstring.
docs/source/cpp/csv.rstAdds a “Date and time parsing” section documenting fallback + semantics.
cpp/src/arrow/vendored/musl/strptime.cAdds a C-locale nl_langinfo fallback table for Windows/testing to support %b/%B/%p/....
cpp/src/arrow/csv/options.hDocuments fallback semantics for timestamp_parsers in C++ API docs.
cpp/src/arrow/csv/inference_internal.hEnsures date/time inference ignores configured timestamp_parsers.
cpp/src/arrow/csv/converter.ccImplements fallback decoder + converter factory changes for date/time types.
cpp/src/arrow/csv/converter_test.ccAdds C++ tests for date/time fallback parsing behavior and edge cases.

Comment threadcpp/src/arrow/csv/converter.cc
Comment on lines +1188 to +1195
# Month names are parsed case-insensitively
rows = b"a\n15-OCT-15\n18-Jun-90\n"
opts = ConvertOptions(column_types={'a': pa.date32()},
timestamp_parsers=['%d-%b-%y'])
table = self.read_bytes(rows, convert_options=opts)
assert table.to_pydict() == {
'a': [date(2015, 10, 15), date(1990, 6, 18)],
}

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — I checked this and the tests are deterministic as written, so I've left them unchanged.

Neither the test binary nor pyarrow adopts the environment's LC_TIME: there is no setlocale(LC_ALL/LC_TIME, "") anywhere in Arrow's C++ (outside vendored code) or in pyarrow, and CPython coerces only LC_CTYPE at startup, never LC_TIME. So a process started with a non-English LC_TIME in the environment still runs strptime in the C locale. On glibc there is a second reason: strptime's %b/%B keeps the C-locale (English) month names as a fallback even under a non-English locale, so English abbreviations parse regardless (setlocale(LC_TIME, "fr_FR.UTF-8") followed by parsing "15-JUL-15" still succeeds).

Minimal check: CSV %b parsing is independent of the environment locale

Each child process below is started with a non-English LC_ALL/LC_TIME in its environment — exactly the scenario in the comment — and still parses the English month abbreviation correctly:

importosimportsubprocessimportsysCHILD=r"""import osimport pyarrow as pafrom pyarrow import csvfrom datetime import datedata = b"a\n15-JUL-15\n" # English abbreviated month name "JUL"opts = csv.ConvertOptions(column_types={"a": pa.date32()}, timestamp_parsers=["%d-%b-%y"])got = csv.read_csv(pa.py_buffer(data), convert_options=opts).to_pydict()assert got == {"a": [date(2015, 7, 15)]}, gotprint("OK with LC_ALL=%-14r ->" % os.environ.get("LC_ALL"), got)"""forlcin ("C", "fr_FR.UTF-8", "de_DE.UTF-8"):
env=dict(os.environ, LC_ALL=lc, LC_TIME=lc)
subprocess.run([sys.executable, "-c", CHILD], env=env, check=True)

Output (identical whether or not those locales are actually installed):

OK with LC_ALL='C' -> {'a': [datetime.date(2015, 7, 15)]}
OK with LC_ALL='fr_FR.UTF-8' -> {'a': [datetime.date(2015, 7, 15)]}
OK with LC_ALL='de_DE.UTF-8' -> {'a': [datetime.date(2015, 7, 15)]}

A genuinely locale-dependent case does remain — an application that itself calls setlocale(LC_ALL, "") under a non-English locale, on a libc whose strptime lacks that English fallback — but that is pre-existing (it affects timestamp columns too) and out of scope here. Happy to file a follow-up issue if that is worth tracking.

@github-actionsgithub-actionsBot added awaiting committer review Awaiting committer review and removed awaiting review Awaiting review labels Jun 22, 2026
@pearu
pearuforce-pushed the pearu/fix-csv-date-time-parsers branch from e75ecec to fcf4f95CompareJune 22, 2026 18:17
@pearu
pearu requested a review from pitrou as a code ownerJune 22, 2026 18:17
@pitrou

Copy link
Copy Markdown
Member

@jorisvandenbossche Do you want to take a look at this PR?

@manyiResearch

Copy link
Copy Markdown

Hi, a small quick question — I noticed that you mentioned multiple issues in the PR description. I was wondering, does this PR fix all of them? Thanks!

@pearu

pearu commented Jul 2, 2026

Copy link
Copy Markdown
ContributorAuthor

@manyifire Good question — mostly, but not every one in the same way:

So: yes for #28303/#33357 (and #26783/#26224), yes-on-Windows for #31816/#31971, and for #37180 only when timestamp_parsers is set explicitly (not the default).

…n parsing CSV date and time columns
CSV columns explicitly typed as date32, date64, time32 or time64 could
only be parsed from strict ISO-8601 strings; ConvertOptions::timestamp_parsers
was consulted only for timestamp columns.
Make the user-defined timestamp parsers act as a fallback for these
column types: the built-in ISO-8601 parser is tried first (preserving
existing behavior), then each configured parser in order. A timestamp
produced by a fallback parser is floored to the day boundary for dates
and reduced to the time of day for times, consistent with casting a
timestamp to a date or time type.
Type inference of date and time columns is deliberately unaffected:
inference keeps using strict ISO-8601 parsing, otherwise a value with a
time-of-day part could be inferred as a date and silently truncated.
Also provide C-locale name tables to the vendored musl strptime used on
Windows, where nl_langinfo() is unavailable: this makes %a/%A/%b/%B/%h/
%p/%c/%r/%x/%X work on Windows (matching musl's C locale), so that the
month-name formats from the original issue reports parse on all
platforms.
ClosesapacheGH-28303.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@pearu
pearuforce-pushed the pearu/fix-csv-date-time-parsers branch from fcf4f95 to 808f110CompareAugust 10, 2026 13:25
@pearu

Copy link
Copy Markdown
ContributorAuthor

Rebased onto current main and re-ran the CSV suite locally — arrow-csv-test passes 278/278, including the four new Date32/Date64/Time32/Time64Conversion.UserDefinedParsers cases.

Status recap for whoever picks this up:

  • Both automated-reviewer comments were addressed in June — std::ranges::transform replaced with a plain index loop, and the %b locale question answered with a reproducer showing the parsing is locale-independent as written.
  • The two failing jobs in the last CI run (AMD64 macOS 15-intel C++ and Python 3) were the same Homebrew infrastructure flake — brew install --formula aws-sdk-cpp hit a /usr/local/Cellar/cmake lock — unrelated to this change. The fresh run on the rebase should clear them.

@pitrou — you routed this to @jorisvandenbossche back in June and it has been quiet since. Is there anything I can do to make this easier to review? If reviewing the CSV converter change together with the vendored strptime C-locale tables is the sticking point, I'm happy to split the strptime part into its own PR.


🤖 Drafted by Claude Code (an AI agent) and reviewed & approved by pearu.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow ConvertOptions.timestamp_parsers for date types [C++] Unable to read date64 or date32 in specific format from CSV

4 participants

@pearu@pitrou@manyiResearch
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GH-41488: [C++][Python] Apply timestamp_parsers as fallback when parsing CSV date and time columns by pearu · Pull Request #50146 · apache/arrow · GitHub
Skip to content

GH-41488: [C++][Python] Apply timestamp_parsers as fallback when parsing CSV date and time columns - #50146

Open
pearu wants to merge 1 commit into
apache:mainfrom
pearu:pearu/fix-csv-date-time-parsers
Open

GH-41488: [C++][Python] Apply timestamp_parsers as fallback when parsing CSV date and time columns#50146
pearu wants to merge 1 commit into
apache:mainfrom
pearu:pearu/fix-csv-date-time-parsers

Conversation

@pearu

@pearupearu commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Rationale for this change

CSV columns explicitly typed as date32, date64, time32 or time64 can only be parsed from strict ISO-8601 strings: ConvertOptions::timestamp_parsers is consulted only for timestamp columns. Reading e.g. 15-OCT-15 into a date32 column fails even with timestamp_parsers=["%d-%b-%y"], and 7:55:00 (non-zero-padded hour) fails for time32[s]. Users currently work around this by declaring such columns as timestamp, reading, then casting back to the date/time type.

Effect on the issues collected in #41488:

What changes are included in this PR?

  • A new DateTimeWithParsersValueDecoder in csv/converter.cc, used for date32/date64/time32/time64 columns when timestamp_parsers is non-empty. It tries the built-in ISO-8601 parser first (preserving all existing behavior), then each configured parser in order. A timestamp produced by a fallback parser is floored to the day boundary for dates and reduced to the time of day for times, consistent with casting a timestamp to a date or time type. Values carrying a zone offset are rejected, as for zone-less timestamp columns. When no parsers are configured, the pre-existing decoder is used unchanged.
  • Type inference is deliberately unaffected: the Date/Time inference stages now explicitly use options with timestamp_parsers cleared, so inference keeps strict ISO-8601 semantics (otherwise a value with a time-of-day part could be inferred as a date and silently truncated). The existing test_timestamp_parsers Python test pins this behavior.
  • Documentation of the fallback and flooring semantics in ConvertOptions::timestamp_parsers (C++ and Python docstrings) and a new "Date and time parsing" section in the C++ CSV user guide.
  • C-locale name tables for the vendored musl strptime used on Windows, where nl_langinfo() is unavailable. Previously the %a/%A/%b/%B/%h/%p/%c/%r/%x/%X specifiers were compiled out on Windows, so the month-name formats from the original issue reports (%d-%b-%y) could not work there for any column type. The tables match musl's C locale, and name matching is case-insensitive as on glibc/musl/BSD. The fallback path is compiled and verified on Linux via the ARROW_TEST_FALLBACK_LANGINFO hook.

Are these changes tested?

Yes:

  • New C++ tests (Date32Conversion.UserDefinedParsers, Date64Conversion.UserDefinedParsers, Time32Conversion.UserDefinedParsers, Time64Conversion.UserDefinedParsers) covering custom formats, mixed ISO + custom values in one column (backward compatibility of ISO values when parsers are set), pre-epoch flooring with a time-of-day component (distinguishes floor from truncating division), time-of-day extraction from pre-epoch timestamps, zone-offset rejection, and error cases.
  • New Python tests with the reproducers from [C++] Unable to read date64 or date32 in specific format from CSV #28303 and CSV reader cannot parse dates or times #41488, plus an inference-unchanged guard.

Are there any user-facing changes?

Yes: ConvertOptions::timestamp_parsers now also applies, as a fallback after ISO-8601, to columns explicitly typed as date32/date64/time32/time64 (previously such values always errored). No breaking changes: behavior without timestamp_parsers is untouched, ISO values keep parsing when parsers are set, and type inference is unchanged. All language bindings gain the behavior without API changes.

AI usage disclosure

This PR was developed with AI assistance (Claude Code): the decoder, tests and documentation were AI-generated under my direction, then reviewed line-by-line and iterated on by me (design decisions: fallback-after-ISO semantics, silent flooring, inference isolation, and several implementation details adjusted during review). I own and can debug these changes.

🤖 Generated with Claude Code

@pearu

Copy link
Copy Markdown
ContributorAuthor

Two out-of-scope discoveries made while working on this, recorded here rather than folded into the PR to keep it minimal:

  1. MultipleParsersTimestampValueDecoder::Decode (pre-existing, csv/converter.cc) declares its zone_offset_present flag once outside the parser loop. The built-in parsers happen to write the out-parameter on every call (the strptime parser unconditionally, the ISO-8601 parser resets it to false before scanning), so this is currently harmless — but TimestampParser is a public interface, and a user-implemented parser that only writes the flag when an offset is found could observe a stale value from a previous loop iteration. The new decoder in this PR declares the flag per-iteration; the timestamp decoder could get the same two-line treatment as a MINOR follow-up.

  2. The "Timestamp inference/parsing" section of the C++ CSV user guide (docs/source/cpp/csv.rst) does not mention ConvertOptions::timestamp_parsers at all — custom timestamp parsing was undocumented in the user guide before the date/time subsection added here. A short paragraph there could be a docs follow-up.

@github-actionsgithub-actionsBot added the awaiting review Awaiting review label Jun 10, 2026
@github-actions

Copy link
Copy Markdown

⚠️ GitHub issue #41488has been automatically assigned in GitHub to PR creator.

@github-actions

Copy link
Copy Markdown

⚠️ GitHub issue #41488has been automatically assigned in GitHub to PR creator.

@pearu
pearuforce-pushed the pearu/fix-csv-date-time-parsers branch from 78ca3cb to e0af29eCompareJune 10, 2026 10:08
@pearu

pearu commented Jun 10, 2026

Copy link
Copy Markdown
ContributorAuthor

CI triage of the first run (3 failing jobs, all Windows): all three shared one root cause — the tests used the %d-%b-%y format from the original issue reproducer, but month names (%b) are not supported by the vendored musl strptime used on Windows: cpp/src/arrow/vendored/musl/strptime.c force-undefines HAVE_LANGINFO on _WIN32, which compiles out the %a/%A/%b/%B/%h/%c/%p/%r/%x/%X cases entirely. The feature code is unaffected; numeric-format and time tests passed on Windows.

Fixed (amended) by making numeric formats the primary test coverage and keeping the month-name reproducer guarded to non-Windows (#ifndef _WIN32 in C++, sys.platform != "win32" in Python, following the existing kStrptimeSupportsZone / test_strftime precedents).
UPDATE: sorry for this noise, Claude was too eager to post comments, it is better restrained now.

A third discovery for the list above: this %b limitation is pre-existing and applies equally to timestamp columns with timestamp_parsers on Windows — it just had no CI coverage because the existing timestamp tests only use numeric formats. Could deserve its own issue (either implementing C-locale month names in the vendored strptime, or documenting the limitation in timestamp_parsers docs).

@pearu
pearuforce-pushed the pearu/fix-csv-date-time-parsers branch from e0af29e to e75ececCompareJune 10, 2026 13:19
@github-actions

Copy link
Copy Markdown

⚠️ GitHub issue #41488has been automatically assigned in GitHub to PR creator.

@pearu
pearu marked this pull request as ready for review June 10, 2026 14:13
@pearu
pearu requested review from AlenkaF, raulcd and rok as code ownersJune 10, 2026 14:13
@pearu

Copy link
Copy Markdown
ContributorAuthor

The single CI failure (AMD64 Conda C++ AVX2) is unrelated to this PR: Gandiva's TestTime.TestCastTimestampWithTZ fails identically on main since this morning (passing at 4e25461, failing from ca47cd1 on — see e.g. this main run). castTIMESTAMP_utf8 returns 0 for the Canada/Pacific tz name, pointing at tz-database resolution in the CI conda environment — code this PR does not touch.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR extends the CSV reader’s ConvertOptions::timestamp_parsers behavior so that, when columns are explicitly typed as date32/date64/time32/time64, the reader first attempts the existing ISO-8601 parsing and then falls back to the user-provided timestamp parsers (with flooring/extracting semantics consistent with casting). It also keeps type inference strict (ISO-only) to avoid silent truncation.

Changes:

  • Add a new C++ CSV date/time value decoder that falls back to timestamp_parsers after ISO parsing and applies flooring/time-of-day extraction.
  • Ensure CSV type inference for date/time remains ISO-only even when timestamp_parsers are configured.
  • Update C++/Python docs and add C++/Python tests; improve vendored Windows strptime support for C-locale day/month names.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Show a summary per file
FileDescription
python/pyarrow/tests/test_csv.pyAdds Python coverage for date/time typed columns using timestamp_parsers fallback and inference guard.
python/pyarrow/_csv.pyxDocuments the new fallback behavior in Python ConvertOptions docstring.
docs/source/cpp/csv.rstAdds a “Date and time parsing” section documenting fallback + semantics.
cpp/src/arrow/vendored/musl/strptime.cAdds a C-locale nl_langinfo fallback table for Windows/testing to support %b/%B/%p/....
cpp/src/arrow/csv/options.hDocuments fallback semantics for timestamp_parsers in C++ API docs.
cpp/src/arrow/csv/inference_internal.hEnsures date/time inference ignores configured timestamp_parsers.
cpp/src/arrow/csv/converter.ccImplements fallback decoder + converter factory changes for date/time types.
cpp/src/arrow/csv/converter_test.ccAdds C++ tests for date/time fallback parsing behavior and edge cases.

Comment threadcpp/src/arrow/csv/converter.cc
Comment on lines +1188 to +1195
# Month names are parsed case-insensitively
rows = b"a\n15-OCT-15\n18-Jun-90\n"
opts = ConvertOptions(column_types={'a': pa.date32()},
timestamp_parsers=['%d-%b-%y'])
table = self.read_bytes(rows, convert_options=opts)
assert table.to_pydict() == {
'a': [date(2015, 10, 15), date(1990, 6, 18)],
}

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — I checked this and the tests are deterministic as written, so I've left them unchanged.

Neither the test binary nor pyarrow adopts the environment's LC_TIME: there is no setlocale(LC_ALL/LC_TIME, "") anywhere in Arrow's C++ (outside vendored code) or in pyarrow, and CPython coerces only LC_CTYPE at startup, never LC_TIME. So a process started with a non-English LC_TIME in the environment still runs strptime in the C locale. On glibc there is a second reason: strptime's %b/%B keeps the C-locale (English) month names as a fallback even under a non-English locale, so English abbreviations parse regardless (setlocale(LC_TIME, "fr_FR.UTF-8") followed by parsing "15-JUL-15" still succeeds).

Minimal check: CSV %b parsing is independent of the environment locale

Each child process below is started with a non-English LC_ALL/LC_TIME in its environment — exactly the scenario in the comment — and still parses the English month abbreviation correctly:

importosimportsubprocessimportsysCHILD=r"""import osimport pyarrow as pafrom pyarrow import csvfrom datetime import datedata = b"a\n15-JUL-15\n" # English abbreviated month name "JUL"opts = csv.ConvertOptions(column_types={"a": pa.date32()}, timestamp_parsers=["%d-%b-%y"])got = csv.read_csv(pa.py_buffer(data), convert_options=opts).to_pydict()assert got == {"a": [date(2015, 7, 15)]}, gotprint("OK with LC_ALL=%-14r ->" % os.environ.get("LC_ALL"), got)"""forlcin ("C", "fr_FR.UTF-8", "de_DE.UTF-8"):
env=dict(os.environ, LC_ALL=lc, LC_TIME=lc)
subprocess.run([sys.executable, "-c", CHILD], env=env, check=True)

Output (identical whether or not those locales are actually installed):

OK with LC_ALL='C' -> {'a': [datetime.date(2015, 7, 15)]}
OK with LC_ALL='fr_FR.UTF-8' -> {'a': [datetime.date(2015, 7, 15)]}
OK with LC_ALL='de_DE.UTF-8' -> {'a': [datetime.date(2015, 7, 15)]}

A genuinely locale-dependent case does remain — an application that itself calls setlocale(LC_ALL, "") under a non-English locale, on a libc whose strptime lacks that English fallback — but that is pre-existing (it affects timestamp columns too) and out of scope here. Happy to file a follow-up issue if that is worth tracking.

@github-actionsgithub-actionsBot added awaiting committer review Awaiting committer review and removed awaiting review Awaiting review labels Jun 22, 2026
@pearu
pearuforce-pushed the pearu/fix-csv-date-time-parsers branch from e75ecec to fcf4f95CompareJune 22, 2026 18:17
@pearu
pearu requested a review from pitrou as a code ownerJune 22, 2026 18:17
@pitrou

Copy link
Copy Markdown
Member

@jorisvandenbossche Do you want to take a look at this PR?

@manyiResearch

Copy link
Copy Markdown

Hi, a small quick question — I noticed that you mentioned multiple issues in the PR description. I was wondering, does this PR fix all of them? Thanks!

@pearu

pearu commented Jul 2, 2026

Copy link
Copy Markdown
ContributorAuthor

@manyifire Good question — mostly, but not every one in the same way:

So: yes for #28303/#33357 (and #26783/#26224), yes-on-Windows for #31816/#31971, and for #37180 only when timestamp_parsers is set explicitly (not the default).

…n parsing CSV date and time columns
CSV columns explicitly typed as date32, date64, time32 or time64 could
only be parsed from strict ISO-8601 strings; ConvertOptions::timestamp_parsers
was consulted only for timestamp columns.
Make the user-defined timestamp parsers act as a fallback for these
column types: the built-in ISO-8601 parser is tried first (preserving
existing behavior), then each configured parser in order. A timestamp
produced by a fallback parser is floored to the day boundary for dates
and reduced to the time of day for times, consistent with casting a
timestamp to a date or time type.
Type inference of date and time columns is deliberately unaffected:
inference keeps using strict ISO-8601 parsing, otherwise a value with a
time-of-day part could be inferred as a date and silently truncated.
Also provide C-locale name tables to the vendored musl strptime used on
Windows, where nl_langinfo() is unavailable: this makes %a/%A/%b/%B/%h/
%p/%c/%r/%x/%X work on Windows (matching musl's C locale), so that the
month-name formats from the original issue reports parse on all
platforms.
ClosesapacheGH-28303.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@pearu
pearuforce-pushed the pearu/fix-csv-date-time-parsers branch from fcf4f95 to 808f110CompareAugust 10, 2026 13:25
@pearu

Copy link
Copy Markdown
ContributorAuthor

Rebased onto current main and re-ran the CSV suite locally — arrow-csv-test passes 278/278, including the four new Date32/Date64/Time32/Time64Conversion.UserDefinedParsers cases.

Status recap for whoever picks this up:

  • Both automated-reviewer comments were addressed in June — std::ranges::transform replaced with a plain index loop, and the %b locale question answered with a reproducer showing the parsing is locale-independent as written.
  • The two failing jobs in the last CI run (AMD64 macOS 15-intel C++ and Python 3) were the same Homebrew infrastructure flake — brew install --formula aws-sdk-cpp hit a /usr/local/Cellar/cmake lock — unrelated to this change. The fresh run on the rebase should clear them.

@pitrou — you routed this to @jorisvandenbossche back in June and it has been quiet since. Is there anything I can do to make this easier to review? If reviewing the CSV converter change together with the vendored strptime C-locale tables is the sticking point, I'm happy to split the strptime part into its own PR.


🤖 Drafted by Claude Code (an AI agent) and reviewed & approved by pearu.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow ConvertOptions.timestamp_parsers for date types [C++] Unable to read date64 or date32 in specific format from CSV

4 participants

@pearu@pitrou@manyiResearch
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GH-41488: [C++][Python] Apply timestamp_parsers as fallback when parsing CSV date and time columns by pearu · Pull Request #50146 · apache/arrow · GitHub
Skip to content

GH-41488: [C++][Python] Apply timestamp_parsers as fallback when parsing CSV date and time columns - #50146

Open
pearu wants to merge 1 commit into
apache:mainfrom
pearu:pearu/fix-csv-date-time-parsers
Open

GH-41488: [C++][Python] Apply timestamp_parsers as fallback when parsing CSV date and time columns#50146
pearu wants to merge 1 commit into
apache:mainfrom
pearu:pearu/fix-csv-date-time-parsers

Conversation

@pearu

@pearupearu commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Rationale for this change

CSV columns explicitly typed as date32, date64, time32 or time64 can only be parsed from strict ISO-8601 strings: ConvertOptions::timestamp_parsers is consulted only for timestamp columns. Reading e.g. 15-OCT-15 into a date32 column fails even with timestamp_parsers=["%d-%b-%y"], and 7:55:00 (non-zero-padded hour) fails for time32[s]. Users currently work around this by declaring such columns as timestamp, reading, then casting back to the date/time type.

Effect on the issues collected in #41488:

What changes are included in this PR?

  • A new DateTimeWithParsersValueDecoder in csv/converter.cc, used for date32/date64/time32/time64 columns when timestamp_parsers is non-empty. It tries the built-in ISO-8601 parser first (preserving all existing behavior), then each configured parser in order. A timestamp produced by a fallback parser is floored to the day boundary for dates and reduced to the time of day for times, consistent with casting a timestamp to a date or time type. Values carrying a zone offset are rejected, as for zone-less timestamp columns. When no parsers are configured, the pre-existing decoder is used unchanged.
  • Type inference is deliberately unaffected: the Date/Time inference stages now explicitly use options with timestamp_parsers cleared, so inference keeps strict ISO-8601 semantics (otherwise a value with a time-of-day part could be inferred as a date and silently truncated). The existing test_timestamp_parsers Python test pins this behavior.
  • Documentation of the fallback and flooring semantics in ConvertOptions::timestamp_parsers (C++ and Python docstrings) and a new "Date and time parsing" section in the C++ CSV user guide.
  • C-locale name tables for the vendored musl strptime used on Windows, where nl_langinfo() is unavailable. Previously the %a/%A/%b/%B/%h/%p/%c/%r/%x/%X specifiers were compiled out on Windows, so the month-name formats from the original issue reports (%d-%b-%y) could not work there for any column type. The tables match musl's C locale, and name matching is case-insensitive as on glibc/musl/BSD. The fallback path is compiled and verified on Linux via the ARROW_TEST_FALLBACK_LANGINFO hook.

Are these changes tested?

Yes:

  • New C++ tests (Date32Conversion.UserDefinedParsers, Date64Conversion.UserDefinedParsers, Time32Conversion.UserDefinedParsers, Time64Conversion.UserDefinedParsers) covering custom formats, mixed ISO + custom values in one column (backward compatibility of ISO values when parsers are set), pre-epoch flooring with a time-of-day component (distinguishes floor from truncating division), time-of-day extraction from pre-epoch timestamps, zone-offset rejection, and error cases.
  • New Python tests with the reproducers from [C++] Unable to read date64 or date32 in specific format from CSV #28303 and CSV reader cannot parse dates or times #41488, plus an inference-unchanged guard.

Are there any user-facing changes?

Yes: ConvertOptions::timestamp_parsers now also applies, as a fallback after ISO-8601, to columns explicitly typed as date32/date64/time32/time64 (previously such values always errored). No breaking changes: behavior without timestamp_parsers is untouched, ISO values keep parsing when parsers are set, and type inference is unchanged. All language bindings gain the behavior without API changes.

AI usage disclosure

This PR was developed with AI assistance (Claude Code): the decoder, tests and documentation were AI-generated under my direction, then reviewed line-by-line and iterated on by me (design decisions: fallback-after-ISO semantics, silent flooring, inference isolation, and several implementation details adjusted during review). I own and can debug these changes.

🤖 Generated with Claude Code

@pearu

Copy link
Copy Markdown
ContributorAuthor

Two out-of-scope discoveries made while working on this, recorded here rather than folded into the PR to keep it minimal:

  1. MultipleParsersTimestampValueDecoder::Decode (pre-existing, csv/converter.cc) declares its zone_offset_present flag once outside the parser loop. The built-in parsers happen to write the out-parameter on every call (the strptime parser unconditionally, the ISO-8601 parser resets it to false before scanning), so this is currently harmless — but TimestampParser is a public interface, and a user-implemented parser that only writes the flag when an offset is found could observe a stale value from a previous loop iteration. The new decoder in this PR declares the flag per-iteration; the timestamp decoder could get the same two-line treatment as a MINOR follow-up.

  2. The "Timestamp inference/parsing" section of the C++ CSV user guide (docs/source/cpp/csv.rst) does not mention ConvertOptions::timestamp_parsers at all — custom timestamp parsing was undocumented in the user guide before the date/time subsection added here. A short paragraph there could be a docs follow-up.

@github-actionsgithub-actionsBot added the awaiting review Awaiting review label Jun 10, 2026
@github-actions

Copy link
Copy Markdown

⚠️ GitHub issue #41488has been automatically assigned in GitHub to PR creator.

@github-actions

Copy link
Copy Markdown

⚠️ GitHub issue #41488has been automatically assigned in GitHub to PR creator.

@pearu
pearuforce-pushed the pearu/fix-csv-date-time-parsers branch from 78ca3cb to e0af29eCompareJune 10, 2026 10:08
@pearu

pearu commented Jun 10, 2026

Copy link
Copy Markdown
ContributorAuthor

CI triage of the first run (3 failing jobs, all Windows): all three shared one root cause — the tests used the %d-%b-%y format from the original issue reproducer, but month names (%b) are not supported by the vendored musl strptime used on Windows: cpp/src/arrow/vendored/musl/strptime.c force-undefines HAVE_LANGINFO on _WIN32, which compiles out the %a/%A/%b/%B/%h/%c/%p/%r/%x/%X cases entirely. The feature code is unaffected; numeric-format and time tests passed on Windows.

Fixed (amended) by making numeric formats the primary test coverage and keeping the month-name reproducer guarded to non-Windows (#ifndef _WIN32 in C++, sys.platform != "win32" in Python, following the existing kStrptimeSupportsZone / test_strftime precedents).
UPDATE: sorry for this noise, Claude was too eager to post comments, it is better restrained now.

A third discovery for the list above: this %b limitation is pre-existing and applies equally to timestamp columns with timestamp_parsers on Windows — it just had no CI coverage because the existing timestamp tests only use numeric formats. Could deserve its own issue (either implementing C-locale month names in the vendored strptime, or documenting the limitation in timestamp_parsers docs).

@pearu
pearuforce-pushed the pearu/fix-csv-date-time-parsers branch from e0af29e to e75ececCompareJune 10, 2026 13:19
@github-actions

Copy link
Copy Markdown

⚠️ GitHub issue #41488has been automatically assigned in GitHub to PR creator.

@pearu
pearu marked this pull request as ready for review June 10, 2026 14:13
@pearu
pearu requested review from AlenkaF, raulcd and rok as code ownersJune 10, 2026 14:13
@pearu

Copy link
Copy Markdown
ContributorAuthor

The single CI failure (AMD64 Conda C++ AVX2) is unrelated to this PR: Gandiva's TestTime.TestCastTimestampWithTZ fails identically on main since this morning (passing at 4e25461, failing from ca47cd1 on — see e.g. this main run). castTIMESTAMP_utf8 returns 0 for the Canada/Pacific tz name, pointing at tz-database resolution in the CI conda environment — code this PR does not touch.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR extends the CSV reader’s ConvertOptions::timestamp_parsers behavior so that, when columns are explicitly typed as date32/date64/time32/time64, the reader first attempts the existing ISO-8601 parsing and then falls back to the user-provided timestamp parsers (with flooring/extracting semantics consistent with casting). It also keeps type inference strict (ISO-only) to avoid silent truncation.

Changes:

  • Add a new C++ CSV date/time value decoder that falls back to timestamp_parsers after ISO parsing and applies flooring/time-of-day extraction.
  • Ensure CSV type inference for date/time remains ISO-only even when timestamp_parsers are configured.
  • Update C++/Python docs and add C++/Python tests; improve vendored Windows strptime support for C-locale day/month names.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Show a summary per file
FileDescription
python/pyarrow/tests/test_csv.pyAdds Python coverage for date/time typed columns using timestamp_parsers fallback and inference guard.
python/pyarrow/_csv.pyxDocuments the new fallback behavior in Python ConvertOptions docstring.
docs/source/cpp/csv.rstAdds a “Date and time parsing” section documenting fallback + semantics.
cpp/src/arrow/vendored/musl/strptime.cAdds a C-locale nl_langinfo fallback table for Windows/testing to support %b/%B/%p/....
cpp/src/arrow/csv/options.hDocuments fallback semantics for timestamp_parsers in C++ API docs.
cpp/src/arrow/csv/inference_internal.hEnsures date/time inference ignores configured timestamp_parsers.
cpp/src/arrow/csv/converter.ccImplements fallback decoder + converter factory changes for date/time types.
cpp/src/arrow/csv/converter_test.ccAdds C++ tests for date/time fallback parsing behavior and edge cases.

Comment threadcpp/src/arrow/csv/converter.cc
Comment on lines +1188 to +1195
# Month names are parsed case-insensitively
rows = b"a\n15-OCT-15\n18-Jun-90\n"
opts = ConvertOptions(column_types={'a': pa.date32()},
timestamp_parsers=['%d-%b-%y'])
table = self.read_bytes(rows, convert_options=opts)
assert table.to_pydict() == {
'a': [date(2015, 10, 15), date(1990, 6, 18)],
}

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — I checked this and the tests are deterministic as written, so I've left them unchanged.

Neither the test binary nor pyarrow adopts the environment's LC_TIME: there is no setlocale(LC_ALL/LC_TIME, "") anywhere in Arrow's C++ (outside vendored code) or in pyarrow, and CPython coerces only LC_CTYPE at startup, never LC_TIME. So a process started with a non-English LC_TIME in the environment still runs strptime in the C locale. On glibc there is a second reason: strptime's %b/%B keeps the C-locale (English) month names as a fallback even under a non-English locale, so English abbreviations parse regardless (setlocale(LC_TIME, "fr_FR.UTF-8") followed by parsing "15-JUL-15" still succeeds).

Minimal check: CSV %b parsing is independent of the environment locale

Each child process below is started with a non-English LC_ALL/LC_TIME in its environment — exactly the scenario in the comment — and still parses the English month abbreviation correctly:

importosimportsubprocessimportsysCHILD=r"""import osimport pyarrow as pafrom pyarrow import csvfrom datetime import datedata = b"a\n15-JUL-15\n" # English abbreviated month name "JUL"opts = csv.ConvertOptions(column_types={"a": pa.date32()}, timestamp_parsers=["%d-%b-%y"])got = csv.read_csv(pa.py_buffer(data), convert_options=opts).to_pydict()assert got == {"a": [date(2015, 7, 15)]}, gotprint("OK with LC_ALL=%-14r ->" % os.environ.get("LC_ALL"), got)"""forlcin ("C", "fr_FR.UTF-8", "de_DE.UTF-8"):
env=dict(os.environ, LC_ALL=lc, LC_TIME=lc)
subprocess.run([sys.executable, "-c", CHILD], env=env, check=True)

Output (identical whether or not those locales are actually installed):

OK with LC_ALL='C' -> {'a': [datetime.date(2015, 7, 15)]}
OK with LC_ALL='fr_FR.UTF-8' -> {'a': [datetime.date(2015, 7, 15)]}
OK with LC_ALL='de_DE.UTF-8' -> {'a': [datetime.date(2015, 7, 15)]}

A genuinely locale-dependent case does remain — an application that itself calls setlocale(LC_ALL, "") under a non-English locale, on a libc whose strptime lacks that English fallback — but that is pre-existing (it affects timestamp columns too) and out of scope here. Happy to file a follow-up issue if that is worth tracking.

@github-actionsgithub-actionsBot added awaiting committer review Awaiting committer review and removed awaiting review Awaiting review labels Jun 22, 2026
@pearu
pearuforce-pushed the pearu/fix-csv-date-time-parsers branch from e75ecec to fcf4f95CompareJune 22, 2026 18:17
@pearu
pearu requested a review from pitrou as a code ownerJune 22, 2026 18:17
@pitrou

Copy link
Copy Markdown
Member

@jorisvandenbossche Do you want to take a look at this PR?

@manyiResearch

Copy link
Copy Markdown

Hi, a small quick question — I noticed that you mentioned multiple issues in the PR description. I was wondering, does this PR fix all of them? Thanks!

@pearu

pearu commented Jul 2, 2026

Copy link
Copy Markdown
ContributorAuthor

@manyifire Good question — mostly, but not every one in the same way:

So: yes for #28303/#33357 (and #26783/#26224), yes-on-Windows for #31816/#31971, and for #37180 only when timestamp_parsers is set explicitly (not the default).

…n parsing CSV date and time columns
CSV columns explicitly typed as date32, date64, time32 or time64 could
only be parsed from strict ISO-8601 strings; ConvertOptions::timestamp_parsers
was consulted only for timestamp columns.
Make the user-defined timestamp parsers act as a fallback for these
column types: the built-in ISO-8601 parser is tried first (preserving
existing behavior), then each configured parser in order. A timestamp
produced by a fallback parser is floored to the day boundary for dates
and reduced to the time of day for times, consistent with casting a
timestamp to a date or time type.
Type inference of date and time columns is deliberately unaffected:
inference keeps using strict ISO-8601 parsing, otherwise a value with a
time-of-day part could be inferred as a date and silently truncated.
Also provide C-locale name tables to the vendored musl strptime used on
Windows, where nl_langinfo() is unavailable: this makes %a/%A/%b/%B/%h/
%p/%c/%r/%x/%X work on Windows (matching musl's C locale), so that the
month-name formats from the original issue reports parse on all
platforms.
ClosesapacheGH-28303.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@pearu
pearuforce-pushed the pearu/fix-csv-date-time-parsers branch from fcf4f95 to 808f110CompareAugust 10, 2026 13:25
@pearu

Copy link
Copy Markdown
ContributorAuthor

Rebased onto current main and re-ran the CSV suite locally — arrow-csv-test passes 278/278, including the four new Date32/Date64/Time32/Time64Conversion.UserDefinedParsers cases.

Status recap for whoever picks this up:

  • Both automated-reviewer comments were addressed in June — std::ranges::transform replaced with a plain index loop, and the %b locale question answered with a reproducer showing the parsing is locale-independent as written.
  • The two failing jobs in the last CI run (AMD64 macOS 15-intel C++ and Python 3) were the same Homebrew infrastructure flake — brew install --formula aws-sdk-cpp hit a /usr/local/Cellar/cmake lock — unrelated to this change. The fresh run on the rebase should clear them.

@pitrou — you routed this to @jorisvandenbossche back in June and it has been quiet since. Is there anything I can do to make this easier to review? If reviewing the CSV converter change together with the vendored strptime C-locale tables is the sticking point, I'm happy to split the strptime part into its own PR.


🤖 Drafted by Claude Code (an AI agent) and reviewed & approved by pearu.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow ConvertOptions.timestamp_parsers for date types [C++] Unable to read date64 or date32 in specific format from CSV

4 participants

@pearu@pitrou@manyiResearch
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); GH-41488: [C++][Python] Apply timestamp_parsers as fallback when parsing CSV date and time columns by pearu · Pull Request #50146 · apache/arrow · GitHub
Skip to content

GH-41488: [C++][Python] Apply timestamp_parsers as fallback when parsing CSV date and time columns - #50146

Open
pearu wants to merge 1 commit into
apache:mainfrom
pearu:pearu/fix-csv-date-time-parsers
Open

GH-41488: [C++][Python] Apply timestamp_parsers as fallback when parsing CSV date and time columns#50146
pearu wants to merge 1 commit into
apache:mainfrom
pearu:pearu/fix-csv-date-time-parsers

Conversation

@pearu

@pearupearu commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Rationale for this change

CSV columns explicitly typed as date32, date64, time32 or time64 can only be parsed from strict ISO-8601 strings: ConvertOptions::timestamp_parsers is consulted only for timestamp columns. Reading e.g. 15-OCT-15 into a date32 column fails even with timestamp_parsers=["%d-%b-%y"], and 7:55:00 (non-zero-padded hour) fails for time32[s]. Users currently work around this by declaring such columns as timestamp, reading, then casting back to the date/time type.

Effect on the issues collected in #41488:

What changes are included in this PR?

  • A new DateTimeWithParsersValueDecoder in csv/converter.cc, used for date32/date64/time32/time64 columns when timestamp_parsers is non-empty. It tries the built-in ISO-8601 parser first (preserving all existing behavior), then each configured parser in order. A timestamp produced by a fallback parser is floored to the day boundary for dates and reduced to the time of day for times, consistent with casting a timestamp to a date or time type. Values carrying a zone offset are rejected, as for zone-less timestamp columns. When no parsers are configured, the pre-existing decoder is used unchanged.
  • Type inference is deliberately unaffected: the Date/Time inference stages now explicitly use options with timestamp_parsers cleared, so inference keeps strict ISO-8601 semantics (otherwise a value with a time-of-day part could be inferred as a date and silently truncated). The existing test_timestamp_parsers Python test pins this behavior.
  • Documentation of the fallback and flooring semantics in ConvertOptions::timestamp_parsers (C++ and Python docstrings) and a new "Date and time parsing" section in the C++ CSV user guide.
  • C-locale name tables for the vendored musl strptime used on Windows, where nl_langinfo() is unavailable. Previously the %a/%A/%b/%B/%h/%p/%c/%r/%x/%X specifiers were compiled out on Windows, so the month-name formats from the original issue reports (%d-%b-%y) could not work there for any column type. The tables match musl's C locale, and name matching is case-insensitive as on glibc/musl/BSD. The fallback path is compiled and verified on Linux via the ARROW_TEST_FALLBACK_LANGINFO hook.

Are these changes tested?

Yes:

  • New C++ tests (Date32Conversion.UserDefinedParsers, Date64Conversion.UserDefinedParsers, Time32Conversion.UserDefinedParsers, Time64Conversion.UserDefinedParsers) covering custom formats, mixed ISO + custom values in one column (backward compatibility of ISO values when parsers are set), pre-epoch flooring with a time-of-day component (distinguishes floor from truncating division), time-of-day extraction from pre-epoch timestamps, zone-offset rejection, and error cases.
  • New Python tests with the reproducers from [C++] Unable to read date64 or date32 in specific format from CSV #28303 and CSV reader cannot parse dates or times #41488, plus an inference-unchanged guard.

Are there any user-facing changes?

Yes: ConvertOptions::timestamp_parsers now also applies, as a fallback after ISO-8601, to columns explicitly typed as date32/date64/time32/time64 (previously such values always errored). No breaking changes: behavior without timestamp_parsers is untouched, ISO values keep parsing when parsers are set, and type inference is unchanged. All language bindings gain the behavior without API changes.

AI usage disclosure

This PR was developed with AI assistance (Claude Code): the decoder, tests and documentation were AI-generated under my direction, then reviewed line-by-line and iterated on by me (design decisions: fallback-after-ISO semantics, silent flooring, inference isolation, and several implementation details adjusted during review). I own and can debug these changes.

🤖 Generated with Claude Code

@pearu

Copy link
Copy Markdown
ContributorAuthor

Two out-of-scope discoveries made while working on this, recorded here rather than folded into the PR to keep it minimal:

  1. MultipleParsersTimestampValueDecoder::Decode (pre-existing, csv/converter.cc) declares its zone_offset_present flag once outside the parser loop. The built-in parsers happen to write the out-parameter on every call (the strptime parser unconditionally, the ISO-8601 parser resets it to false before scanning), so this is currently harmless — but TimestampParser is a public interface, and a user-implemented parser that only writes the flag when an offset is found could observe a stale value from a previous loop iteration. The new decoder in this PR declares the flag per-iteration; the timestamp decoder could get the same two-line treatment as a MINOR follow-up.

  2. The "Timestamp inference/parsing" section of the C++ CSV user guide (docs/source/cpp/csv.rst) does not mention ConvertOptions::timestamp_parsers at all — custom timestamp parsing was undocumented in the user guide before the date/time subsection added here. A short paragraph there could be a docs follow-up.

@github-actionsgithub-actionsBot added the awaiting review Awaiting review label Jun 10, 2026
@github-actions

Copy link
Copy Markdown

⚠️ GitHub issue #41488has been automatically assigned in GitHub to PR creator.

@github-actions

Copy link
Copy Markdown

⚠️ GitHub issue #41488has been automatically assigned in GitHub to PR creator.

@pearu
pearuforce-pushed the pearu/fix-csv-date-time-parsers branch from 78ca3cb to e0af29eCompareJune 10, 2026 10:08
@pearu

pearu commented Jun 10, 2026

Copy link
Copy Markdown
ContributorAuthor

CI triage of the first run (3 failing jobs, all Windows): all three shared one root cause — the tests used the %d-%b-%y format from the original issue reproducer, but month names (%b) are not supported by the vendored musl strptime used on Windows: cpp/src/arrow/vendored/musl/strptime.c force-undefines HAVE_LANGINFO on _WIN32, which compiles out the %a/%A/%b/%B/%h/%c/%p/%r/%x/%X cases entirely. The feature code is unaffected; numeric-format and time tests passed on Windows.

Fixed (amended) by making numeric formats the primary test coverage and keeping the month-name reproducer guarded to non-Windows (#ifndef _WIN32 in C++, sys.platform != "win32" in Python, following the existing kStrptimeSupportsZone / test_strftime precedents).
UPDATE: sorry for this noise, Claude was too eager to post comments, it is better restrained now.

A third discovery for the list above: this %b limitation is pre-existing and applies equally to timestamp columns with timestamp_parsers on Windows — it just had no CI coverage because the existing timestamp tests only use numeric formats. Could deserve its own issue (either implementing C-locale month names in the vendored strptime, or documenting the limitation in timestamp_parsers docs).

@pearu
pearuforce-pushed the pearu/fix-csv-date-time-parsers branch from e0af29e to e75ececCompareJune 10, 2026 13:19
@github-actions

Copy link
Copy Markdown

⚠️ GitHub issue #41488has been automatically assigned in GitHub to PR creator.

@pearu
pearu marked this pull request as ready for review June 10, 2026 14:13
@pearu
pearu requested review from AlenkaF, raulcd and rok as code ownersJune 10, 2026 14:13
@pearu

Copy link
Copy Markdown
ContributorAuthor

The single CI failure (AMD64 Conda C++ AVX2) is unrelated to this PR: Gandiva's TestTime.TestCastTimestampWithTZ fails identically on main since this morning (passing at 4e25461, failing from ca47cd1 on — see e.g. this main run). castTIMESTAMP_utf8 returns 0 for the Canada/Pacific tz name, pointing at tz-database resolution in the CI conda environment — code this PR does not touch.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR extends the CSV reader’s ConvertOptions::timestamp_parsers behavior so that, when columns are explicitly typed as date32/date64/time32/time64, the reader first attempts the existing ISO-8601 parsing and then falls back to the user-provided timestamp parsers (with flooring/extracting semantics consistent with casting). It also keeps type inference strict (ISO-only) to avoid silent truncation.

Changes:

  • Add a new C++ CSV date/time value decoder that falls back to timestamp_parsers after ISO parsing and applies flooring/time-of-day extraction.
  • Ensure CSV type inference for date/time remains ISO-only even when timestamp_parsers are configured.
  • Update C++/Python docs and add C++/Python tests; improve vendored Windows strptime support for C-locale day/month names.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Show a summary per file
FileDescription
python/pyarrow/tests/test_csv.pyAdds Python coverage for date/time typed columns using timestamp_parsers fallback and inference guard.
python/pyarrow/_csv.pyxDocuments the new fallback behavior in Python ConvertOptions docstring.
docs/source/cpp/csv.rstAdds a “Date and time parsing” section documenting fallback + semantics.
cpp/src/arrow/vendored/musl/strptime.cAdds a C-locale nl_langinfo fallback table for Windows/testing to support %b/%B/%p/....
cpp/src/arrow/csv/options.hDocuments fallback semantics for timestamp_parsers in C++ API docs.
cpp/src/arrow/csv/inference_internal.hEnsures date/time inference ignores configured timestamp_parsers.
cpp/src/arrow/csv/converter.ccImplements fallback decoder + converter factory changes for date/time types.
cpp/src/arrow/csv/converter_test.ccAdds C++ tests for date/time fallback parsing behavior and edge cases.

Comment threadcpp/src/arrow/csv/converter.cc
Comment on lines +1188 to +1195
# Month names are parsed case-insensitively
rows = b"a\n15-OCT-15\n18-Jun-90\n"
opts = ConvertOptions(column_types={'a': pa.date32()},
timestamp_parsers=['%d-%b-%y'])
table = self.read_bytes(rows, convert_options=opts)
assert table.to_pydict() == {
'a': [date(2015, 10, 15), date(1990, 6, 18)],
}

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — I checked this and the tests are deterministic as written, so I've left them unchanged.

Neither the test binary nor pyarrow adopts the environment's LC_TIME: there is no setlocale(LC_ALL/LC_TIME, "") anywhere in Arrow's C++ (outside vendored code) or in pyarrow, and CPython coerces only LC_CTYPE at startup, never LC_TIME. So a process started with a non-English LC_TIME in the environment still runs strptime in the C locale. On glibc there is a second reason: strptime's %b/%B keeps the C-locale (English) month names as a fallback even under a non-English locale, so English abbreviations parse regardless (setlocale(LC_TIME, "fr_FR.UTF-8") followed by parsing "15-JUL-15" still succeeds).

Minimal check: CSV %b parsing is independent of the environment locale

Each child process below is started with a non-English LC_ALL/LC_TIME in its environment — exactly the scenario in the comment — and still parses the English month abbreviation correctly:

importosimportsubprocessimportsysCHILD=r"""import osimport pyarrow as pafrom pyarrow import csvfrom datetime import datedata = b"a\n15-JUL-15\n" # English abbreviated month name "JUL"opts = csv.ConvertOptions(column_types={"a": pa.date32()}, timestamp_parsers=["%d-%b-%y"])got = csv.read_csv(pa.py_buffer(data), convert_options=opts).to_pydict()assert got == {"a": [date(2015, 7, 15)]}, gotprint("OK with LC_ALL=%-14r ->" % os.environ.get("LC_ALL"), got)"""forlcin ("C", "fr_FR.UTF-8", "de_DE.UTF-8"):
env=dict(os.environ, LC_ALL=lc, LC_TIME=lc)
subprocess.run([sys.executable, "-c", CHILD], env=env, check=True)

Output (identical whether or not those locales are actually installed):

OK with LC_ALL='C' -> {'a': [datetime.date(2015, 7, 15)]}
OK with LC_ALL='fr_FR.UTF-8' -> {'a': [datetime.date(2015, 7, 15)]}
OK with LC_ALL='de_DE.UTF-8' -> {'a': [datetime.date(2015, 7, 15)]}

A genuinely locale-dependent case does remain — an application that itself calls setlocale(LC_ALL, "") under a non-English locale, on a libc whose strptime lacks that English fallback — but that is pre-existing (it affects timestamp columns too) and out of scope here. Happy to file a follow-up issue if that is worth tracking.

@github-actionsgithub-actionsBot added awaiting committer review Awaiting committer review and removed awaiting review Awaiting review labels Jun 22, 2026
@pearu
pearuforce-pushed the pearu/fix-csv-date-time-parsers branch from e75ecec to fcf4f95CompareJune 22, 2026 18:17
@pearu
pearu requested a review from pitrou as a code ownerJune 22, 2026 18:17
@pitrou

Copy link
Copy Markdown
Member

@jorisvandenbossche Do you want to take a look at this PR?

@manyiResearch

Copy link
Copy Markdown

Hi, a small quick question — I noticed that you mentioned multiple issues in the PR description. I was wondering, does this PR fix all of them? Thanks!

@pearu

pearu commented Jul 2, 2026

Copy link
Copy Markdown
ContributorAuthor

@manyifire Good question — mostly, but not every one in the same way:

So: yes for #28303/#33357 (and #26783/#26224), yes-on-Windows for #31816/#31971, and for #37180 only when timestamp_parsers is set explicitly (not the default).

…n parsing CSV date and time columns
CSV columns explicitly typed as date32, date64, time32 or time64 could
only be parsed from strict ISO-8601 strings; ConvertOptions::timestamp_parsers
was consulted only for timestamp columns.
Make the user-defined timestamp parsers act as a fallback for these
column types: the built-in ISO-8601 parser is tried first (preserving
existing behavior), then each configured parser in order. A timestamp
produced by a fallback parser is floored to the day boundary for dates
and reduced to the time of day for times, consistent with casting a
timestamp to a date or time type.
Type inference of date and time columns is deliberately unaffected:
inference keeps using strict ISO-8601 parsing, otherwise a value with a
time-of-day part could be inferred as a date and silently truncated.
Also provide C-locale name tables to the vendored musl strptime used on
Windows, where nl_langinfo() is unavailable: this makes %a/%A/%b/%B/%h/
%p/%c/%r/%x/%X work on Windows (matching musl's C locale), so that the
month-name formats from the original issue reports parse on all
platforms.
ClosesapacheGH-28303.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@pearu
pearuforce-pushed the pearu/fix-csv-date-time-parsers branch from fcf4f95 to 808f110CompareAugust 10, 2026 13:25
@pearu

Copy link
Copy Markdown
ContributorAuthor

Rebased onto current main and re-ran the CSV suite locally — arrow-csv-test passes 278/278, including the four new Date32/Date64/Time32/Time64Conversion.UserDefinedParsers cases.

Status recap for whoever picks this up:

  • Both automated-reviewer comments were addressed in June — std::ranges::transform replaced with a plain index loop, and the %b locale question answered with a reproducer showing the parsing is locale-independent as written.
  • The two failing jobs in the last CI run (AMD64 macOS 15-intel C++ and Python 3) were the same Homebrew infrastructure flake — brew install --formula aws-sdk-cpp hit a /usr/local/Cellar/cmake lock — unrelated to this change. The fresh run on the rebase should clear them.

@pitrou — you routed this to @jorisvandenbossche back in June and it has been quiet since. Is there anything I can do to make this easier to review? If reviewing the CSV converter change together with the vendored strptime C-locale tables is the sticking point, I'm happy to split the strptime part into its own PR.


🤖 Drafted by Claude Code (an AI agent) and reviewed & approved by pearu.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow ConvertOptions.timestamp_parsers for date types [C++] Unable to read date64 or date32 in specific format from CSV

4 participants

@pearu@pitrou@manyiResearch