diff --git a/arrow-arith/src/temporal.rs b/arrow-arith/src/temporal.rs index 96a3b43a3811..f55a39fa265f 100644 --- a/arrow-arith/src/temporal.rs +++ b/arrow-arith/src/temporal.rs @@ -63,6 +63,10 @@ pub enum DatePart { DayOfWeekSunday0, /// Day of the week, in range `0..=6`, where Monday is `0` DayOfWeekMonday0, + /// Day of the week, in range `1..=7`, where Sunday is `1` + DayOfWeekSunday1, + /// ISO day of the week, in range `1..=7`, where Monday is `1` + DayOfWeekMonday1, /// Day of year, in range `1..=366` DayOfYear, /// Hour of the day, in range `0..=23` @@ -100,11 +104,6 @@ impl std::fmt::Display for DatePart { /// - `century`, `decade`, `millennium` — no matching [`DatePart`] variant. /// - `timezone`, `timezone_hour`, `timezone_minute` — not modeled by /// [`DatePart`]. -/// - `isodow` — PostgreSQL's `isodow` returns `1..=7` (Mon=1), but the -/// closest variant ([`DatePart::DayOfWeekMonday0`]) returns `0..=6`. -/// Accepting it here would silently shift the value range; callers that -/// need the PostgreSQL semantic should map the alias themselves and -/// add `1` to the extracted value. impl FromStr for DatePart { type Err = ArrowError; @@ -118,6 +117,8 @@ impl FromStr for DatePart { "isoweek" => Self::WeekISO, "d" | "day" | "days" => Self::Day, "dow" | "dayofweek" => Self::DayOfWeekSunday0, + "dayofweek1" => Self::DayOfWeekSunday1, + "isodow" => Self::DayOfWeekMonday1, "doy" | "dayofyear" => Self::DayOfYear, "h" | "hr" | "hrs" | "hour" | "hours" => Self::Hour, "m" | "min" | "mins" | "minute" | "minutes" => Self::Minute, @@ -158,6 +159,8 @@ where DatePart::Day => |d| d.day() as i32, DatePart::DayOfWeekSunday0 => |d| d.num_days_from_sunday(), DatePart::DayOfWeekMonday0 => |d| d.num_days_from_monday(), + DatePart::DayOfWeekSunday1 => |d| d.num_days_from_sunday() + 1, + DatePart::DayOfWeekMonday1 => |d| d.num_days_from_monday() + 1, DatePart::DayOfYear => |d| d.ordinal() as i32, DatePart::Hour => |d| d.hour() as i32, DatePart::Minute => |d| d.minute() as i32, @@ -500,6 +503,8 @@ impl ExtractDatePartExt for PrimitiveArray { | DatePart::Day | DatePart::DayOfWeekSunday0 | DatePart::DayOfWeekMonday0 + | DatePart::DayOfWeekSunday1 + | DatePart::DayOfWeekMonday1 | DatePart::DayOfYear | DatePart::Hour | DatePart::Minute @@ -536,6 +541,8 @@ impl ExtractDatePartExt for PrimitiveArray { | DatePart::Month | DatePart::DayOfWeekSunday0 | DatePart::DayOfWeekMonday0 + | DatePart::DayOfWeekSunday1 + | DatePart::DayOfWeekMonday1 | DatePart::DayOfYear => { return_compute_error_with!(format!("{part} does not support"), self.data_type()) } @@ -578,6 +585,8 @@ impl ExtractDatePartExt for PrimitiveArray { | DatePart::YearISO | DatePart::DayOfWeekSunday0 | DatePart::DayOfWeekMonday0 + | DatePart::DayOfWeekSunday1 + | DatePart::DayOfWeekMonday1 | DatePart::DayOfYear => { return_compute_error_with!(format!("{part} does not support"), self.data_type()) } @@ -610,6 +619,8 @@ impl ExtractDatePartExt for PrimitiveArray { | DatePart::Month | DatePart::DayOfWeekSunday0 | DatePart::DayOfWeekMonday0 + | DatePart::DayOfWeekSunday1 + | DatePart::DayOfWeekMonday1 | DatePart::DayOfYear => { return_compute_error_with!(format!("{part} does not support"), self.data_type()) } @@ -642,6 +653,8 @@ impl ExtractDatePartExt for PrimitiveArray { | DatePart::Month | DatePart::DayOfWeekSunday0 | DatePart::DayOfWeekMonday0 + | DatePart::DayOfWeekSunday1 + | DatePart::DayOfWeekMonday1 | DatePart::DayOfYear => { return_compute_error_with!(format!("{part} does not support"), self.data_type()) } @@ -674,6 +687,8 @@ impl ExtractDatePartExt for PrimitiveArray { | DatePart::Month | DatePart::DayOfWeekSunday0 | DatePart::DayOfWeekMonday0 + | DatePart::DayOfWeekSunday1 + | DatePart::DayOfWeekMonday1 | DatePart::DayOfYear => { return_compute_error_with!(format!("{part} does not support"), self.data_type()) } @@ -706,6 +721,8 @@ impl ExtractDatePartExt for PrimitiveArray { | DatePart::Month | DatePart::DayOfWeekSunday0 | DatePart::DayOfWeekMonday0 + | DatePart::DayOfWeekSunday1 + | DatePart::DayOfWeekMonday1 | DatePart::DayOfYear => { return_compute_error_with!(format!("{part} does not support"), self.data_type()) } @@ -985,6 +1002,46 @@ mod tests { assert_eq!(2, b.value(2)); } + #[test] + fn test_temporal_array_date64_dayofweek1() { + //1514764800000 -> 2018-01-01 (Monday) + //1550636625000 -> 2019-02-20 (Wednesday) + //1483228800000 -> 2017-01-01 (Sunday) + let a: PrimitiveArray = vec![ + Some(1514764800000), + None, + Some(1550636625000), + Some(1483228800000), + ] + .into(); + + let b = date_part_primitive(&a, DatePart::DayOfWeekSunday1).unwrap(); + assert_eq!(2, b.value(0)); + assert!(!b.is_valid(1)); + assert_eq!(4, b.value(2)); + assert_eq!(1, b.value(3)); + } + + #[test] + fn test_temporal_array_date64_isodow() { + //1514764800000 -> 2018-01-01 (Monday) + //1550636625000 -> 2019-02-20 (Wednesday) + //1483228800000 -> 2017-01-01 (Sunday) + let a: PrimitiveArray = vec![ + Some(1514764800000), + None, + Some(1550636625000), + Some(1483228800000), + ] + .into(); + + let b = date_part_primitive(&a, DatePart::DayOfWeekMonday1).unwrap(); + assert_eq!(1, b.value(0)); + assert!(!b.is_valid(1)); + assert_eq!(3, b.value(2)); + assert_eq!(7, b.value(3)); + } + #[test] fn test_temporal_array_date64_weekday0() { //1483228800000 -> 2017-01-01 (Sunday) @@ -1579,11 +1636,15 @@ mod tests { let invalid_parts = [ DatePart::Quarter, DatePart::Year, + DatePart::YearISO, DatePart::Month, DatePart::Week, + DatePart::WeekISO, DatePart::Day, DatePart::DayOfWeekSunday0, DatePart::DayOfWeekMonday0, + DatePart::DayOfWeekSunday1, + DatePart::DayOfWeekMonday1, DatePart::DayOfYear, ]; @@ -1813,8 +1874,12 @@ mod tests { fn ensure_returns_error(array: &dyn Array) { let invalid_parts = [ DatePart::Quarter, + DatePart::YearISO, + DatePart::WeekISO, DatePart::DayOfWeekSunday0, DatePart::DayOfWeekMonday0, + DatePart::DayOfWeekSunday1, + DatePart::DayOfWeekMonday1, DatePart::DayOfYear, ]; @@ -1956,10 +2021,14 @@ mod tests { fn ensure_returns_error(array: &dyn Array) { let invalid_parts = [ DatePart::Year, + DatePart::YearISO, DatePart::Quarter, DatePart::Month, + DatePart::WeekISO, DatePart::DayOfWeekSunday0, DatePart::DayOfWeekMonday0, + DatePart::DayOfWeekSunday1, + DatePart::DayOfWeekMonday1, DatePart::DayOfYear, ]; @@ -2157,6 +2226,10 @@ mod tests { ("day", DatePart::Day), ("dow", DatePart::DayOfWeekSunday0), ("DayOfWeek", DatePart::DayOfWeekSunday0), + ("dayofweek1", DatePart::DayOfWeekSunday1), + ("DAYOFWEEK1", DatePart::DayOfWeekSunday1), + ("isodow", DatePart::DayOfWeekMonday1), + ("ISODOW", DatePart::DayOfWeekMonday1), ("doy", DatePart::DayOfYear), ("DayOfYear", DatePart::DayOfYear), ("h", DatePart::Hour), @@ -2195,8 +2268,6 @@ mod tests { #[test] fn test_date_part_from_str_unknown() { // Names intentionally rejected — see the FromStr doc comment for why. - // `isodow` is here because mapping it to `DayOfWeekMonday0` would - // silently shift the value range vs. PostgreSQL's `isodow` (1..=7). let unknown = [ "epoch", "century", @@ -2205,7 +2276,6 @@ mod tests { "timezone", "timezone_hour", "timezone_minute", - "isodow", // Whitespace is not trimmed — pin this so the behavior doesn't // change silently. Callers should trim before parsing. " year ",