mbutrovich commented on code in PR #4917:
URL: https://github.com/apache/datafusion-comet/pull/4917#discussion_r3591113183


##########
native/spark-expr/src/conversion_funcs/string.rs:
##########
@@ -1161,6 +1160,25 @@ fn days_from_civil(y: i64, m: i64, d: i64) -> i64 {
     era * 146097 + doe - 719468
 }
 
+fn is_leap_year(year: i64) -> bool {
+    year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)
+}
+
+/// Days since 1970-01-01 for a proleptic Gregorian year/month/day, or `None` 
when the
+/// combination is not a real calendar date. Unlike `NaiveDate::from_ymd_opt`, 
this accepts
+/// any year that fits in `i64`.
+fn ymd_to_epoch_day(year: i64, month: i64, day: i64) -> Option<i64> {

Review Comment:
   `date_parser_test` (`native/spark-expr/src/conversion_funcs/string.rs:2712`) 
never feeds a canonical-shape `dddd-dd-dd` string that is an invalid calendar 
date, so the branch where the fast path calls `ymd_to_epoch_day` and gets 
`None` is untested. The existing invalid-date cases (`2020-010-01`, `202`, 
`2020-\r8`) all fall through to the general parser because they are not 10-char 
`dddd-dd-dd`. A regression that made the fast path skip calendar validation 
would pass the current suite.
   
   Suggested change: add to the invalid list in `date_parser_test` (which 
asserts `None` for Legacy/Try and `is_err()` for Ansi is wrong here, see 
finding 2, so put these in a Legacy/Try-only null assertion) canonical invalid 
dates and one valid boundary:
   
   ```rust
   // invalid calendar dates in canonical yyyy-mm-dd form (exercise the fast 
path)
   for date in &["2020-02-30", "2021-02-29", "2020-13-01", "2020-00-15", 
"2020-04-31", "2020-01-00"] {
       for eval_mode in &[EvalMode::Legacy, EvalMode::Try, EvalMode::Ansi] {
           assert_eq!(date_parser(date, *eval_mode).unwrap(), None);
       }
   }
   // valid leap day through the fast path
   assert_eq!(date_parser("2020-02-29", EvalMode::Legacy).unwrap(), 
Some(18321));
   ```



##########
native/spark-expr/src/conversion_funcs/string.rs:
##########
@@ -1908,6 +1892,29 @@ fn date_parser(date_str: &str, eval_mode: EvalMode) -> 
SparkResult<Option<i32>>
         return return_result(date_str, eval_mode);
     }
 
+    // Fast path for the canonical `yyyy-mm-dd` form. A 4-digit year is always 
inside the
+    // range checked below, so the only way this can fail is an invalid 
calendar date, which
+    // is a null in every eval mode rather than an ANSI error. Any other shape 
(including a

Review Comment:
   The comment at the fast path 
(`native/spark-expr/src/conversion_funcs/string.rs:1897` in the diff) says an 
invalid calendar date "is a null in every eval mode rather than an ANSI error." 
That is not what Spark does. `stringToDate` returns `None` for an invalid 
calendar date, and `stringToDateAnsi` (SparkDateTimeUtils.scala:398) turns any 
`None` into `invalidInputInCastToDatetimeError`, so Spark ANSI throws on 
`2020-02-30`. Comet's existing code already returns `Ok(None)` here (the caller 
at string.rs:716-720 treats `Ok(None)` as null in every mode), so this PR 
introduces no regression, but the comment states a false fact about Spark and 
will mislead the next reader.
   
   Suggested change: describe Comet's actual behavior, not an incorrect Spark 
claim:
   
   ```rust
   // Fast path for the canonical `yyyy-mm-dd` form. A 4-digit year is always 
inside the
   // range checked below, so the only way this fails is an invalid calendar 
date. That
   // path already returns null in Comet for every eval mode (the caller maps 
Ok(None) to
   // null), matching the general parser here. Any other shape, including a 
leading sign
   // that makes the first byte a non-digit, falls through to the general 
parser.
   ```
   



##########
native/spark-expr/src/conversion_funcs/string.rs:
##########
@@ -1960,15 +1967,12 @@ fn date_parser(date_str: &str, eval_mode: EvalMode) -> 
SparkResult<Option<i32>>
         return Ok(None);
     }
 
-    match NaiveDate::from_ymd_opt(year, date_segments[1] as u32, 
date_segments[2] as u32) {
-        Some(date) => {
-            let duration_since_epoch = date
-                .signed_duration_since(DateTime::UNIX_EPOCH.naive_utc().date())
-                .num_days();
-            Ok(Some(duration_since_epoch.to_i32().unwrap()))
-        }
-        None => Ok(None),
-    }
+    Ok(ymd_to_epoch_day(
+        year as i64,
+        date_segments[1] as i64,
+        date_segments[2] as i64,
+    )
+    .map(|days| days as i32))

Review Comment:
   The general path now ends with `.map(|days| days as i32)` 
(`native/spark-expr/src/conversion_funcs/string.rs:1975` in the diff) replacing 
`duration_since_epoch.to_i32().unwrap()`. The year range check at 
string.rs:1966 guarantees the value fits in i32, so both are correct today, but 
`as i32` would silently wrap if that invariant were ever weakened, whereas 
`.unwrap()` failed loudly. Since the invariant is enforced two lines up, prefer 
a `debug_assert!` over reintroducing the checked conversion so the fast path 
stays branch-free:
   
   ```rust
   Ok(ymd_to_epoch_day(year as i64, date_segments[1] as i64, date_segments[2] 
as i64).map(|days| {
       debug_assert!(i32::try_from(days).is_ok(), "epoch day {days} out of i32 
range for year {year}");
       days as i32
   }))
   ```
   
   The fast path can keep the bare `days as i32` since a 4-digit year can never 
overflow.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to