Skip to content

GH-54534: [C++] Fix truncation of sub-minute UTC offsets when formatting and parsing zoned timestamps - #54684

Open
ShivanshhhG wants to merge 1 commit into
apache:mainfrom
ShivanshhhG:fix-54534-subminute-offsets
Open

ShivanshhhG wants to merge 1 commit into
apache:mainfrom
ShivanshhhG:fix-54534-subminute-offsets

Conversation

@ShivanshhhG

@ShivanshhhG ShivanshhhG commented Oct 20, 2026 •

Copy link
Copy Markdown

Rationale for this change

When formatting zoned timestamps whose UTC offset includes sub-minute seconds (e.g. historical IANA zones such as Africa/Monrovia prior to 1972 at UTC-00:44:30 or Europe/Paris prior to 1911 at UTC+00:09:21), formatting with %z would truncate the offset to whole minutes, dropping remaining seconds and causing instant drift. To preserve instant fidelity without violating the %z / %Ez formatting contract (whole minutes ±HHMM / ±HH:MM), timestamps with sub-minute offsets are formatted in UTC (...Z) during timestamp-to-string cast. Additionally, ParseTimestampISO8601 is updated to support parsing 6-digit sub-minute offsets ([+-]HHMMSS and [+-]HH:MM:SS), and %Ez is recognized across Strftime and GetZone.

What changes are included in this PR?

  1. cpp/src/arrow/compute/kernels/scalar_cast_string.cc: In ConvertZoned, if a timestamp instant has non-zero sub-minute seconds (offset % 60 != 0), format it in UTC (...Z) to preserve the exact stored instant without violating the %z minute contract. Fixed-offset zones (OffsetZone) and UTC take the fast path without redundant get_info() calls.
  2. cpp/src/arrow/util/value_parsing.h: Add detail::ParseHHMMSS and update ParseTimestampISO8601 to recognize and parse [+-]HHMMSS and [+-]HH:MM:SS zone offsets.
  3. cpp/src/arrow/util/value_parsing.cc & cpp/src/arrow/compute/kernels/scalar_temporal_unary.cc: Unify zone format validation in Strftime::Make and GetZone via FormatHasZone to recognize %Ez alongside %z and %Z, properly rejecting %Ez when the input timestamp has no timezone.
  4. cpp/src/arrow/compute/kernels/scalar_cast_test.cc: Add round-trip tests across all four timestamp units (s, ms, us, ns) for historical sub-minute zones (Africa/Monrovia and Europe/Paris), verifying cast to string and back to UTC matches the exact stored value.
  5. cpp/src/arrow/compute/kernels/scalar_temporal_test.cc & cpp/src/arrow/util/value_parsing_test.cc: Add unit tests for %Ez (with and without timezones) and sub-minute offset parsing/validation bounds.

Are these changes tested?

Yes, covered by unit tests in scalar_cast_test.cc, scalar_temporal_test.cc, and value_parsing_test.cc.

Are there any user-facing changes?

  • Casting zoned timestamps with historical sub-minute UTC offsets to string formats them in UTC (...Z), preventing truncation errors and preserving round-trip instant accuracy.
  • Timestamp parsing now supports ISO8601 strings with 6-digit offsets ([+-]HHMMSS and [+-]HH:MM:SS).
  • strftime now recognizes %Ez and validates timezone presence identically to %z and %Z.

@github-actions

Copy link
Copy Markdown

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

@github-actions

Copy link
Copy Markdown

⚠️ GitHub issue #54534 has no components, please add labels for components.

@ShivanshhhG
ShivanshhhG force-pushed the fix-54534-subminute-offsets branch from cf3dc8c to 48e0b42 Compare October 26, 2026 12:24
@ShivanshhhG

Copy link
Copy Markdown
Author

Thank you for the detailed feedback @dmitry-chirkov-dremio!

I have updated the PR accordingly:

  1. Preserved %z / %Ez formatting contract in date.h: Reverted all changes to vendored date.h so that %z and %Ez continue adhering to their standard contracts (±hhmm and ±hh:mm).
  2. Sub-minute offset fallback to UTC in ConvertZoned: When casting a zoned timestamp to string, if the timestamp instant has a sub-minute offset (offset % 60 != 0), it is formatted in UTC (...Z) as suggested in [C++] Casting a zoned timestamp to string (and the CSV writer) truncates UTC offsets that have seconds, so the written instant is wrong #54534 to preserve the exact stored instant without emitting an inaccurate local wall offset. Updated expected values in scalar_cast_test.cc.
  3. Unified timezone format validation in Strftime::Make and GetZone: Added FormatHasZone helper function recognizing %Ez alongside %z and %Z. If an unzoned timestamp is passed to Strftime with %Ez, it is properly rejected with "Invalid: Timezone not present, cannot convert to string with timezone: ...".
  4. Added Unit Tests: Added test cases for %Ez both with a timezone (e.g. Asia/Kolkata -> +05:30) and without a timezone (verifying rejection) in scalar_temporal_test.cc.

@ShivanshhhG
ShivanshhhG force-pushed the fix-54534-subminute-offsets branch from 48e0b42 to 092de95 Compare October 30, 2026 02:14

@dmitry-chirkov-dremio dmitry-chirkov-dremio 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.

I'm ok with the current state.

Update the description - date.h is unchanged now. Values with sub-minute offsets are written in UTC with Z, not with offset seconds.

Ideally I'd like to see round-trip test for all four timestamp units. Cast to string and back to UTC, and check the stored value. The new historical-zone cases only check formatted output at microsecond precision.

const auto timepoint = sys_time<Duration>(Duration{v});
bool subminute_offset = false;
auto format_zoned_time = [&](auto&& zt) {
if (ARROW_PREDICT_FALSE(zt.get_info().offset.count() % 60 != 0)) {

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.

zt.get_info() formats an abbreviation for fixed-offset zones, and to_stream() calls it again. Can we skip the extra call for OffsetZone, which stores whole minutes?

In local tests, a 1M-row, six-column in-memory CSV export took 0.812 s on parent fb767a7 and 0.954 s on this head (092de95). Skipping only this call for fixed-offset zones brought it back to 0.820 s, with identical CSV output.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Good catch, thanks! I updated ConvertZoned to branch on OffsetZone (and UTC) upfront so fixed-offset zones take the fast path and avoid the extra get_info() call and allocation entirely.

…p string casting and strftime %Ez

When casting zoned timestamps whose UTC offset includes sub-minute seconds (e.g. historical IANA zones like Africa/Monrovia prior to 1972 at UTC-00:44:30 or Europe/Paris prior to 1911), format in UTC (...Z) to preserve the stored instant without violating the %z format contract of whole minutes.

This patch:
1. Updates ConvertZoned in scalar_cast_string.cc to format in UTC when the instant offset contains non-zero seconds (sub-minute offset), while preserving fast-path formatting for OffsetZone and UTC to avoid redundant get_info() formatting calls.
2. Unifies timezone format validation across Strftime and Strptime (GetZone) using FormatHasZone to recognize %Ez alongside %z and %Z, rejecting %Ez when input has no timezone.
3. Updates ParseTimestampISO8601 in value_parsing.h to parse [+-]HHMMSS and [+-]HH:MM:SS zone offsets.
4. Adds tests for %Ez with and without a timezone in scalar_temporal_test.cc, and adds round-trip regression tests across all timestamp units (s, ms, us, ns) in scalar_cast_test.cc and value_parsing_test.cc.

Closes apache#54534.

Signed-off-by: SHIVANSH-ux-ys <singaser78@gmail.com>
@ShivanshhhG
ShivanshhhG force-pushed the fix-54534-subminute-offsets branch from 092de95 to ab97171 Compare November 2, 2026 06:17
@ShivanshhhG

Copy link
Copy Markdown
Author

I have pushed an update addressing the review feedback:

  1. Avoid redundant get_info() on fixed-offset zones: Updated ConvertZoned to branch on OffsetZone (and UTC) upfront so fixed-offset zones take the fast path directly and skip the extra get_info() evaluation/allocation.
  2. Round-trip tests across all 4 timestamp units: Expanded the test suite in scalar_cast_test.cc to test s, ms, us, and ns for historical sub-minute zones (Africa/Monrovia and Europe/Paris), verifying round-trip casting to string and back to UTC preserves the exact stored value.
  3. PR description: Updated to reflect that date.h remains unchanged and sub-minute offsets are formatted in UTC (...Z).

@github-actions

github-actions Bot commented Nov 2, 2026

Copy link
Copy Markdown

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

@dmitry-chirkov-dremio dmitry-chirkov-dremio 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.

LGTM. Thank you addressing the comments.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants