This is an automated email from the ASF dual-hosted git repository. github-merge-queue[bot] pushed a commit to branch gh-readonly-queue/main/pr-11179-16310a9f7b94463e583dc7b70c812333e2cba75d in repository https://gitbox.apache.org/repos/asf/arrow-rs.git
commit fafa22b7c57b8e802dccd6e7fcb65afcc654fc4d Author: Amit Vijapur <[email protected]> AuthorDate: Tue Oct 27 00:07:51 2026 +0000 fix(arrow-cast): parse ISO 8601 timestamps without seconds (#11179) # Which issue does this PR close? Part of #8219. This covers omission of seconds only; hour-only and other ISO 8601 forms remain separate follow-ups. # Rationale for this change `string_to_datetime` documents RFC 3339 / ISO 8601 support, but `TimestampParser::time` only accepts `HH:MM:SS` and `HHMMSS`, so an ISO 8601 time without seconds such as `2025-08-14T00:00Z` fails with `error parsing time`. @alamb agreed on the issue that this form should parse, provided performance is not hurt; the new arm sits after the existing `HH:MM:SS` and `HHMMSS` arms, so those forms match before its guard runs. @klion26 offered to fix it on 2025-09-08 and no PR followed in the year since, so I picked it up. # What changes are included in this PR? One more arm in `TimestampParser::time` accepts `HH:MM` when byte 16 is neither a digit nor `:` nor `.`, so `HH:MM:SS`, `HH:MM:` and `HH:MM.f` keep their current handling and errors. Seconds and nanoseconds are zero and the timezone suffix is parsed as before. The doc list of accepted inputs gains the new form. One consequence: `Date32Type::parse("2020-09-08 01:02")` now succeeds, since the string is a valid timestamp. The `parse_date32` test moves that input from the error cases to the parsed ones. # Are these changes tested? `string_to_timestamp_without_seconds` checks four inputs (with `Z`, no suffix, `+05:30`, and a lowercase `t` with `-08:00`) against their `:00` forms. `string_to_timestamp_invalid` gains `09:2Z`, `09:26:Z`, `09:26.5Z` and `09:61Z`, all still `error parsing time`. `cargo test -p arrow-cast` passes. # Are there any user-facing changes? Timestamps whose time has no seconds now parse in `string_to_datetime`, and through it in the string to timestamp and date casts. Codex prepared the documentation follow-up and these review explanations. The documentation-only follow-up passed all 387 arrow-cast library tests and workspace formatting on October 15. Codex ran the mask/guard mutation checks on October 12 and reverted both mutations. No performance benchmark was rerun for the documentation follow-up. --------- Signed-off-by: Amit Vijapur <[email protected]> --- arrow-cast/src/parse.rs | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/arrow-cast/src/parse.rs b/arrow-cast/src/parse.rs index a571368d63..78b1d7dc36 100644 --- a/arrow-cast/src/parse.rs +++ b/arrow-cast/src/parse.rs @@ -83,6 +83,7 @@ impl TimestampParser { } /// Parses a time of any of forms + /// - `09:26` /// - `09:26:56` /// - `09:26:56.123` /// - `09:26:56.123456` @@ -135,6 +136,16 @@ impl TimestampParser { let time = time(hour, minute, second, 0)?; Some((time, 17)) } + // 09:26 + m if m & 0b111111 == 0b011011 + && self.test(13, b':') + && !self.test(16, b':') + && !self.test(16, b'.') => + { + let hour = self.digits[11] * 10 + self.digits[12]; + let minute = self.digits[14] * 10 + self.digits[15]; + Some((time(hour, minute, 0, 0)?, 16)) + } _ => None, } } @@ -149,6 +160,7 @@ impl TimestampParser { /// Examples of accepted inputs: /// * `1997-01-31T09:26:56.123Z` # RCF3339 /// * `1997-01-31T09:26:56.123-05:00` # RCF3339 +/// * `1997-01-31T09:26Z` # ISO8601 without seconds /// * `1997-01-31 09:26:56.123-05:00` # close to RCF3339 but with a space rather than T /// * `2023-01-01 04:05:06.789 -08` # close to RCF3339, no fractional seconds or time separator /// * `1997-01-31T09:26:56.123` # close to RCF3339 but no timezone offset specified @@ -1945,6 +1957,20 @@ mod tests { } } + #[test] + fn string_to_timestamp_without_seconds() { + let cases = [ + ("2025-08-14T00:00Z", (2025, 8, 14, 0, 0)), + ("2025-08-14T09:26", (2025, 8, 14, 9, 26)), + ("2025-08-14 09:26+05:30", (2025, 8, 14, 3, 56)), + ("2025-08-14t23:59-08:00", (2025, 8, 15, 7, 59)), + ]; + for (case, (y, m, d, h, min)) in cases { + let expected = Utc.with_ymd_and_hms(y, m, d, h, min, 0).unwrap(); + assert_eq!(string_to_datetime(&Utc, case).unwrap(), expected) + } + } + #[test] fn string_to_timestamp_invalid() { // Test parsing invalid formats @@ -1968,6 +1994,10 @@ mod tests { ("1997-01-31T092656.123Z", "error parsing time"), ("1997-01-10T12:00:06.", "error parsing time"), ("1997-01-10T12:00:06. ", "error parsing time"), + ("1997-01-10T09:2Z", "error parsing time"), + ("1997-01-10T09:26:Z", "error parsing time"), + ("1997-01-10T09:26.5Z", "error parsing time"), + ("1997-01-10T09:61Z", "error parsing time"), ]; for (s, ctx) in cases { @@ -2060,11 +2090,13 @@ mod tests { "2020-12-1", "1690-2-5", "2020-09-08 01:02:03", + "2020-09-08 01:02", ]; for case in cases { let v = date32_to_datetime(Date32Type::parse(case).unwrap()).unwrap(); let expected = NaiveDate::parse_from_str(case, "%Y-%m-%d") .or_else(|_| NaiveDate::parse_from_str(case, "%Y-%m-%d %H:%M:%S")) + .or_else(|_| NaiveDate::parse_from_str(case, "%Y-%m-%d %H:%M")) .unwrap(); assert_eq!(v.date(), expected); } @@ -2078,7 +2110,6 @@ mod tests { "2020--04-03", "2020--", "2020-09-08 01", - "2020-09-08 01:02", "2020-09-08 01-02-03", "2020-9-8 01:02:03", "2020-09-08 1:2:3",
