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-11299-1b68589d69848552f9b32866f71633fe92114596
in repository https://gitbox.apache.org/repos/asf/arrow-rs.git

commit aa36532f09f1eee3576c1612ca10472bfb472730
Author: Wilhelm Ă…gren <[email protected]>
AuthorDate: Fri Oct 2 04:51:00 2026 +0000

    fix(arrow-cast): allow whitespace around temporal values (#11225) (#11299)
    
    # Which issue does this PR close?
    
    Closes #11225.
    
    # Rationale for this change
    
    Integer and floating-point (numeric) parsers already accept surrounding
    ASCII whitespace but the current temporal parsers do not. Expected
    behaviour in a lot of other dbms's is to just parse the temporal values
    surrounded with ASCII whitespace normally. And for this crate to handle
    ASCII whitespace for some dtypes, but not for others, seems
    counter-intuitive.
    
    # What changes are included in this PR?
    
    Temporal parsers now retry parsing after trimming surrounding ASCII
    whitespace if parsing the original input failed. This preserves the
    existing fast path for **correct** (non-padded) values.
    
    # Are these changes tested?
    
    New tests added to arrow-cast/src/parse.rs, and regression test added
    arrow-cast/src/cast/mod.rs.
    
    All tests (and clippy) pass.
    
    `cargo test -p arrow-cast`
    
    # Are there any user-facing changes?
    
    <!--
    If there are user-facing changes then we may require documentation to be
    updated before approving the PR.
    
    If there are any breaking changes to public APIs, please call them out.
    -->
    
    Yes, string casts to temporal types now handle surrounding ASCII
    whitespace, no API changes but behavioural changes that might need
    changes to docs.
    
    Co-authored-by: Jeffrey Vo <[email protected]>
---
 arrow-cast/src/cast/mod.rs |  11 +++
 arrow-cast/src/parse.rs    | 178 ++++++++++++++++++++++++++++++++++++++++++---
 2 files changed, 177 insertions(+), 12 deletions(-)

diff --git a/arrow-cast/src/cast/mod.rs b/arrow-cast/src/cast/mod.rs
index 3e562e508d..cb8a7ebb50 100644
--- a/arrow-cast/src/cast/mod.rs
+++ b/arrow-cast/src/cast/mod.rs
@@ -5627,6 +5627,17 @@ mod tests {
         assert_eq!(result.values(), &[247112596800]);
     }
 
+    #[test]
+    fn test_cast_string_to_date32_with_surrounding_whitespace() {
+        let array = StringArray::from(vec![" 2026-09-29 ", "2026-09-29"]);
+
+        let result = cast(&array, &DataType::Date32).unwrap();
+        let result = result.as_primitive::<Date32Type>();
+
+        assert!(!result.is_null(0));
+        assert_eq!(result.value(0), result.value(1));
+    }
+
     #[test]
     fn test_cast_string_to_date32() {
         let a0 = Arc::new(StringViewArray::from(vec![
diff --git a/arrow-cast/src/parse.rs b/arrow-cast/src/parse.rs
index ffc6738aa9..384eba2007 100644
--- a/arrow-cast/src/parse.rs
+++ b/arrow-cast/src/parse.rs
@@ -530,35 +530,65 @@ parser_primitive!(DurationSecondType);
 
 impl Parser for TimestampNanosecondType {
     fn parse(string: &str) -> Option<i64> {
-        string_to_timestamp_nanos(string).ok()
+        if let Ok(nanos) = string_to_timestamp_nanos(string) {
+            return Some(nanos);
+        }
+
+        let trimmed = trim_pre_and_post_whitespace(string);
+        string_to_timestamp_nanos(trimmed).ok()
     }
 }
 
 impl Parser for TimestampMicrosecondType {
     fn parse(string: &str) -> Option<i64> {
-        let nanos = string_to_timestamp_nanos(string).ok();
-        nanos.map(|x| x / 1000)
+        if let Ok(nanos) = string_to_timestamp_nanos(string) {
+            return Some(nanos / 1_000);
+        }
+
+        let trimmed = trim_pre_and_post_whitespace(string);
+        string_to_timestamp_nanos(trimmed).ok().map(|x| x / 1_000)
     }
 }
 
 impl Parser for TimestampMillisecondType {
     fn parse(string: &str) -> Option<i64> {
-        let nanos = string_to_timestamp_nanos(string).ok();
-        nanos.map(|x| x / 1_000_000)
+        if let Ok(nanos) = string_to_timestamp_nanos(string) {
+            return Some(nanos / 1_000_000);
+        }
+
+        let trimmed = trim_pre_and_post_whitespace(string);
+        string_to_timestamp_nanos(trimmed)
+            .ok()
+            .map(|x| x / 1_000_000)
     }
 }
 
 impl Parser for TimestampSecondType {
     fn parse(string: &str) -> Option<i64> {
-        let nanos = string_to_timestamp_nanos(string).ok();
-        nanos.map(|x| x / 1_000_000_000)
+        if let Ok(nanos) = string_to_timestamp_nanos(string) {
+            return Some(nanos / 1_000_000_000);
+        }
+
+        let trimmed = trim_pre_and_post_whitespace(string);
+        string_to_timestamp_nanos(trimmed)
+            .ok()
+            .map(|x| x / 1_000_000_000)
     }
 }
 
 impl Parser for Time64NanosecondType {
     // Will truncate any fractions of a nanosecond
     fn parse(string: &str) -> Option<Self::Native> {
-        string_to_time_nanoseconds(string)
+        let value = string_to_time_nanoseconds(string)
+            .ok()
+            .or_else(|| string.parse::<Self::Native>().ok());
+
+        if value.is_some() {
+            return value;
+        }
+
+        let trimmed = trim_pre_and_post_whitespace(string);
+        string_to_time_nanoseconds(trimmed)
             .ok()
             .or_else(|| string.parse::<Self::Native>().ok())
     }
@@ -572,9 +602,19 @@ impl Parser for Time64NanosecondType {
 impl Parser for Time64MicrosecondType {
     // Will truncate any fractions of a microsecond
     fn parse(string: &str) -> Option<Self::Native> {
-        string_to_time_nanoseconds(string)
+        let value = string_to_time_nanoseconds(string)
             .ok()
             .map(|nanos| nanos / 1_000)
+            .or_else(|| string.parse::<Self::Native>().ok());
+
+        if value.is_some() {
+            return value;
+        }
+
+        let trimmed = trim_pre_and_post_whitespace(string);
+        string_to_time_nanoseconds(trimmed)
+            .ok()
+            .map(|x| x / 1_000)
             .or_else(|| string.parse::<Self::Native>().ok())
     }
 
@@ -587,9 +627,19 @@ impl Parser for Time64MicrosecondType {
 impl Parser for Time32MillisecondType {
     // Will truncate any fractions of a millisecond
     fn parse(string: &str) -> Option<Self::Native> {
-        string_to_time_nanoseconds(string)
+        let value = string_to_time_nanoseconds(string)
             .ok()
             .map(|nanos| (nanos / 1_000_000) as i32)
+            .or_else(|| string.parse::<Self::Native>().ok());
+
+        if value.is_some() {
+            return value;
+        }
+
+        let trimmed = trim_pre_and_post_whitespace(string);
+        string_to_time_nanoseconds(trimmed)
+            .ok()
+            .map(|x| (x / 1_000_000) as i32)
             .or_else(|| string.parse::<Self::Native>().ok())
     }
 
@@ -602,9 +652,19 @@ impl Parser for Time32MillisecondType {
 impl Parser for Time32SecondType {
     // Will truncate any fractions of a second
     fn parse(string: &str) -> Option<Self::Native> {
-        string_to_time_nanoseconds(string)
+        let value = string_to_time_nanoseconds(string)
             .ok()
             .map(|nanos| (nanos / 1_000_000_000) as i32)
+            .or_else(|| string.parse::<Self::Native>().ok());
+
+        if value.is_some() {
+            return value;
+        }
+
+        let trimmed = trim_pre_and_post_whitespace(string);
+        string_to_time_nanoseconds(trimmed)
+            .ok()
+            .map(|x| (x / 1_000_000_000) as i32)
             .or_else(|| string.parse::<Self::Native>().ok())
     }
 
@@ -749,7 +809,12 @@ fn parse_date_to_days(string: &str) -> Option<i32> {
 
 impl Parser for Date32Type {
     fn parse(string: &str) -> Option<i32> {
-        parse_date_to_days(string)
+        if let Some(days) = parse_date_to_days(string) {
+            return Some(days);
+        }
+
+        let trimmed = trim_pre_and_post_whitespace(string);
+        parse_date_to_days(trimmed)
     }
 
     fn parse_formatted(string: &str, format: &str) -> Option<i32> {
@@ -3623,4 +3688,93 @@ mod tests {
         assert_eq!(Int32Type::parse("3j"), None);
         assert_eq!(Int32Type::parse("3"), Some(3));
     }
+
+    #[test]
+    fn test_parse_temporal_with_surrounding_whitespace() {
+        let date = Date32Type::parse("2024-01-05");
+        assert_eq!(Date32Type::parse(" 2024-01-05 "), date);
+        assert_eq!(Date32Type::parse("\t2024-01-05\n"), date);
+
+        let timestamp = "2024-01-05T10:00:00";
+        let padded_timestamp = " 2024-01-05T10:00:00 ";
+
+        assert_eq!(
+            TimestampNanosecondType::parse(padded_timestamp),
+            TimestampNanosecondType::parse(timestamp)
+        );
+        assert_eq!(
+            TimestampMicrosecondType::parse(padded_timestamp),
+            TimestampMicrosecondType::parse(timestamp)
+        );
+        assert_eq!(
+            TimestampMillisecondType::parse(padded_timestamp),
+            TimestampMillisecondType::parse(timestamp)
+        );
+        assert_eq!(
+            TimestampSecondType::parse(padded_timestamp),
+            TimestampSecondType::parse(timestamp)
+        );
+        assert_eq!(
+            TimestampNanosecondType::parse("\t2024-01-05T10:00:00\n"),
+            TimestampNanosecondType::parse(timestamp)
+        );
+
+        // leet :)
+        let time = "13:37:00";
+        let padded_time = " 13:37:00 ";
+
+        assert_eq!(
+            Time64NanosecondType::parse(padded_time),
+            Time64NanosecondType::parse(time)
+        );
+        assert_eq!(
+            Time64MicrosecondType::parse(padded_time),
+            Time64MicrosecondType::parse(time)
+        );
+        assert_eq!(
+            Time64MicrosecondType::parse("\t13:37:00\n"),
+            Time64MicrosecondType::parse(time)
+        );
+    }
+
+    #[test]
+    fn test_parse_time_with_surrounding_ascii_whitespace() {
+        let time = "10:00:00";
+        let padded = " 10:00:00 ";
+        let ascii_whitespace = "\t10:00:00\n";
+
+        assert_eq!(
+            Time64NanosecondType::parse(padded),
+            Time64NanosecondType::parse(time)
+        );
+        assert_eq!(
+            Time64MicrosecondType::parse(padded),
+            Time64MicrosecondType::parse(time)
+        );
+        assert_eq!(
+            Time32MillisecondType::parse(padded),
+            Time32MillisecondType::parse(time)
+        );
+        assert_eq!(
+            Time32SecondType::parse(padded),
+            Time32SecondType::parse(time)
+        );
+
+        assert_eq!(
+            Time64NanosecondType::parse(ascii_whitespace),
+            Time64NanosecondType::parse(time)
+        );
+        assert_eq!(
+            Time64MicrosecondType::parse(ascii_whitespace),
+            Time64MicrosecondType::parse(time)
+        );
+        assert_eq!(
+            Time32MillisecondType::parse(ascii_whitespace),
+            Time32MillisecondType::parse(time)
+        );
+        assert_eq!(
+            Time32SecondType::parse(ascii_whitespace),
+            Time32SecondType::parse(time)
+        );
+    }
 }

Reply via email to