From 527be4f015f8b88bbff1bc173d728d3e11a40e38 Mon Sep 17 00:00:00 2001 From: Marco Gorelli <33491632+MarcoGorelli@users.noreply.github.com> Date: Thu, 6 Jun 2024 16:25:46 +0100 Subject: [PATCH] chore(rust!): move offset_by implementation from polars-plan to polars-time, rename feature from DateOffset to OffsetBy --- crates/polars-lazy/Cargo.toml | 4 +- crates/polars-plan/Cargo.toml | 4 +- crates/polars-plan/src/dsl/dt.rs | 14 +- .../src/dsl/function_expr/datetime.rs | 29 ++-- .../polars-plan/src/dsl/function_expr/mod.rs | 13 +- .../src/dsl/function_expr/schema.rs | 2 - .../src/dsl/function_expr/temporal.rs | 134 +----------------- crates/polars-time/src/lib.rs | 2 + crates/polars-time/src/offset_by.rs | 121 ++++++++++++++++ crates/polars/Cargo.toml | 2 +- crates/polars/src/lib.rs | 2 +- docs/user-guide/installation.md | 2 +- py-polars/Cargo.toml | 2 +- py-polars/src/lazyframe/visitor/expr_nodes.rs | 5 +- .../namespaces/temporal/test_datetime.py | 2 +- 15 files changed, 171 insertions(+), 167 deletions(-) create mode 100644 crates/polars-time/src/offset_by.rs diff --git a/crates/polars-lazy/Cargo.toml b/crates/polars-lazy/Cargo.toml index 054089ff404f..6c122db96da4 100644 --- a/crates/polars-lazy/Cargo.toml +++ b/crates/polars-lazy/Cargo.toml @@ -98,7 +98,7 @@ dtype-u16 = ["polars-plan/dtype-u16", "polars-pipe?/dtype-u16", "polars-expr/dty dtype-u8 = ["polars-plan/dtype-u8", "polars-pipe?/dtype-u8", "polars-expr/dtype-u8"] object = ["polars-plan/object"] -date_offset = ["polars-plan/date_offset"] +offset_by = ["polars-plan/offset_by"] trigonometry = ["polars-plan/trigonometry"] sign = ["polars-plan/sign"] timezones = ["polars-plan/timezones"] @@ -258,7 +258,7 @@ features = [ "cum_agg", "cumulative_eval", "cutqcut", - "date_offset", + "offset_by", "diagonal_concat", "diff", "dot_diagram", diff --git a/crates/polars-plan/Cargo.toml b/crates/polars-plan/Cargo.toml index 92113dc29b04..1b4e0cd15e6d 100644 --- a/crates/polars-plan/Cargo.toml +++ b/crates/polars-plan/Cargo.toml @@ -88,7 +88,7 @@ dtype-array = ["polars-core/dtype-array", "polars-ops/dtype-array"] dtype-categorical = ["polars-core/dtype-categorical"] dtype-struct = ["polars-core/dtype-struct"] object = ["polars-core/object"] -date_offset = ["polars-time", "chrono"] +offset_by = ["polars-time", "chrono"] list_gather = ["polars-ops/list_gather"] list_count = ["polars-ops/list_count"] array_count = ["polars-ops/array_count", "dtype-array"] @@ -199,7 +199,7 @@ features = [ "is_last_distinct", "dtype-time", "array_any_all", - "date_offset", + "offset_by", "parquet", "strings", "row_hash", diff --git a/crates/polars-plan/src/dsl/dt.rs b/crates/polars-plan/src/dsl/dt.rs index 42eb75d8e8e1..d888fea9b025 100644 --- a/crates/polars-plan/src/dsl/dt.rs +++ b/crates/polars-plan/src/dsl/dt.rs @@ -213,14 +213,14 @@ impl DateLikeNameSpace { } /// Roll backward to the first day of the month. - #[cfg(feature = "date_offset")] + #[cfg(feature = "offset_by")] pub fn month_start(self) -> Expr { self.0 .map_private(FunctionExpr::TemporalExpr(TemporalFunction::MonthStart)) } /// Roll forward to the last day of the month. - #[cfg(feature = "date_offset")] + #[cfg(feature = "offset_by")] pub fn month_end(self) -> Expr { self.0 .map_private(FunctionExpr::TemporalExpr(TemporalFunction::MonthEnd)) @@ -252,10 +252,14 @@ impl DateLikeNameSpace { /// Offset this `Date/Datetime` by a given offset [`Duration`]. /// This will take leap years/ months into account. - #[cfg(feature = "date_offset")] + #[cfg(feature = "offset_by")] pub fn offset_by(self, by: Expr) -> Expr { - self.0 - .map_many_private(FunctionExpr::DateOffset, &[by], false, false) + self.0.map_many_private( + FunctionExpr::TemporalExpr(TemporalFunction::OffsetBy), + &[by], + false, + false, + ) } #[cfg(feature = "timezones")] diff --git a/crates/polars-plan/src/dsl/function_expr/datetime.rs b/crates/polars-plan/src/dsl/function_expr/datetime.rs index 3db596f7115b..fe6cd18623b8 100644 --- a/crates/polars-plan/src/dsl/function_expr/datetime.rs +++ b/crates/polars-plan/src/dsl/function_expr/datetime.rs @@ -5,6 +5,8 @@ use chrono_tz::Tz; use polars_time::base_utc_offset as base_utc_offset_fn; #[cfg(feature = "timezones")] use polars_time::dst_offset as dst_offset_fn; +#[cfg(feature = "offset_by")] +use polars_time::impl_offset_by; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; @@ -48,9 +50,11 @@ pub enum TemporalFunction { ConvertTimeZone(TimeZone), TimeStamp(TimeUnit), Truncate, - #[cfg(feature = "date_offset")] + #[cfg(feature = "offset_by")] + OffsetBy, + #[cfg(feature = "offset_by")] MonthStart, - #[cfg(feature = "date_offset")] + #[cfg(feature = "offset_by")] MonthEnd, #[cfg(feature = "timezones")] BaseUtcOffset, @@ -101,9 +105,11 @@ impl TemporalFunction { dtype => polars_bail!(ComputeError: "expected Datetime, got {}", dtype), }), Truncate => mapper.with_same_dtype(), - #[cfg(feature = "date_offset")] + #[cfg(feature = "offset_by")] + OffsetBy => mapper.with_same_dtype(), + #[cfg(feature = "offset_by")] MonthStart => mapper.with_same_dtype(), - #[cfg(feature = "date_offset")] + #[cfg(feature = "offset_by")] MonthEnd => mapper.with_same_dtype(), #[cfg(feature = "timezones")] BaseUtcOffset => mapper.with_dtype(DataType::Duration(TimeUnit::Milliseconds)), @@ -169,9 +175,11 @@ impl Display for TemporalFunction { WithTimeUnit(_) => "with_time_unit", TimeStamp(tu) => return write!(f, "dt.timestamp({tu})"), Truncate => "truncate", - #[cfg(feature = "date_offset")] + #[cfg(feature = "offset_by")] + OffsetBy => "offset_by", + #[cfg(feature = "offset_by")] MonthStart => "month_start", - #[cfg(feature = "date_offset")] + #[cfg(feature = "offset_by")] MonthEnd => "month_end", #[cfg(feature = "timezones")] BaseUtcOffset => "base_utc_offset", @@ -392,7 +400,12 @@ pub(super) fn truncate(s: &[Series]) -> PolarsResult { Ok(out) } -#[cfg(feature = "date_offset")] +#[cfg(feature = "offset_by")] +pub(super) fn offset_by(s: &[Series]) -> PolarsResult { + impl_offset_by(&s[0], &s[1]) +} + +#[cfg(feature = "offset_by")] pub(super) fn month_start(s: &Series) -> PolarsResult { Ok(match s.dtype() { DataType::Datetime(_, tz) => match tz { @@ -409,7 +422,7 @@ pub(super) fn month_start(s: &Series) -> PolarsResult { }) } -#[cfg(feature = "date_offset")] +#[cfg(feature = "offset_by")] pub(super) fn month_end(s: &Series) -> PolarsResult { Ok(match s.dtype() { DataType::Datetime(_, tz) => match tz { diff --git a/crates/polars-plan/src/dsl/function_expr/mod.rs b/crates/polars-plan/src/dsl/function_expr/mod.rs index b46f71a77fa4..6d382f7228b8 100644 --- a/crates/polars-plan/src/dsl/function_expr/mod.rs +++ b/crates/polars-plan/src/dsl/function_expr/mod.rs @@ -62,7 +62,7 @@ mod sign; mod strings; #[cfg(feature = "dtype-struct")] mod struct_; -#[cfg(any(feature = "temporal", feature = "date_offset"))] +#[cfg(any(feature = "temporal", feature = "offset_by"))] mod temporal; #[cfg(feature = "trigonometry")] pub mod trigonometry; @@ -148,8 +148,6 @@ pub enum FunctionExpr { SearchSorted(SearchSortedSide), #[cfg(feature = "range")] Range(RangeFunction), - #[cfg(feature = "date_offset")] - DateOffset, #[cfg(feature = "trigonometry")] Trigonometry(TrigonometricFunction), #[cfg(feature = "trigonometry")] @@ -413,8 +411,6 @@ impl Hash for FunctionExpr { Abs => {}, Negate => {}, NullCount => {}, - #[cfg(feature = "date_offset")] - DateOffset => {}, #[cfg(feature = "arg_where")] ArgWhere => {}, #[cfg(feature = "trigonometry")] @@ -615,8 +611,6 @@ impl Display for FunctionExpr { SearchSorted(_) => "search_sorted", #[cfg(feature = "range")] Range(func) => return write!(f, "{func}"), - #[cfg(feature = "date_offset")] - DateOffset => "dt.offset_by", #[cfg(feature = "trigonometry")] Trigonometry(func) => return write!(f, "{func}"), #[cfg(feature = "trigonometry")] @@ -902,11 +896,6 @@ impl From for SpecialEq> { #[cfg(feature = "range")] Range(func) => func.into(), - #[cfg(feature = "date_offset")] - DateOffset => { - map_as_slice!(temporal::date_offset) - }, - #[cfg(feature = "trigonometry")] Trigonometry(trig_function) => { map!(trigonometry::apply_trigonometric_function, trig_function) diff --git a/crates/polars-plan/src/dsl/function_expr/schema.rs b/crates/polars-plan/src/dsl/function_expr/schema.rs index 65c5a5ef67d6..351fce22b397 100644 --- a/crates/polars-plan/src/dsl/function_expr/schema.rs +++ b/crates/polars-plan/src/dsl/function_expr/schema.rs @@ -51,8 +51,6 @@ impl FunctionExpr { SearchSorted(_) => mapper.with_dtype(IDX_DTYPE), #[cfg(feature = "range")] Range(func) => func.get_field(mapper), - #[cfg(feature = "date_offset")] - DateOffset { .. } => mapper.with_same_dtype(), #[cfg(feature = "trigonometry")] Trigonometry(_) => mapper.map_to_float_dtype(), #[cfg(feature = "trigonometry")] diff --git a/crates/polars-plan/src/dsl/function_expr/temporal.rs b/crates/polars-plan/src/dsl/function_expr/temporal.rs index 86b75d201709..c61c1a17ef3a 100644 --- a/crates/polars-plan/src/dsl/function_expr/temporal.rs +++ b/crates/polars-plan/src/dsl/function_expr/temporal.rs @@ -1,10 +1,3 @@ -#[cfg(feature = "date_offset")] -use arrow::legacy::time_zone::Tz; -#[cfg(feature = "date_offset")] -use polars_core::chunked_array::ops::arity::broadcast_try_binary_elementwise; -#[cfg(feature = "date_offset")] -use polars_time::prelude::*; - use super::*; use crate::{map, map_as_slice}; @@ -49,9 +42,13 @@ impl From for SpecialEq> { Truncate => { map_as_slice!(datetime::truncate) }, - #[cfg(feature = "date_offset")] + #[cfg(feature = "offset_by")] + OffsetBy => { + map_as_slice!(datetime::offset_by) + }, + #[cfg(feature = "offset_by")] MonthStart => map!(datetime::month_start), - #[cfg(feature = "date_offset")] + #[cfg(feature = "offset_by")] MonthEnd => map!(datetime::month_end), #[cfg(feature = "timezones")] BaseUtcOffset => map!(datetime::base_utc_offset), @@ -185,125 +182,6 @@ pub(super) fn datetime( Ok(s) } -#[cfg(feature = "date_offset")] -fn apply_offsets_to_datetime( - datetime: &Logical, - offsets: &StringChunked, - time_zone: Option<&Tz>, -) -> PolarsResult { - match offsets.len() { - 1 => match offsets.get(0) { - Some(offset) => { - let offset = &Duration::parse(offset); - if offset.is_constant_duration(datetime.time_zone().as_deref()) { - // fastpath! - let mut duration = match datetime.time_unit() { - TimeUnit::Milliseconds => offset.duration_ms(), - TimeUnit::Microseconds => offset.duration_us(), - TimeUnit::Nanoseconds => offset.duration_ns(), - }; - if offset.negative() { - duration = -duration; - } - Ok(datetime.0.clone().wrapping_add_scalar(duration)) - } else { - let offset_fn = match datetime.time_unit() { - TimeUnit::Milliseconds => Duration::add_ms, - TimeUnit::Microseconds => Duration::add_us, - TimeUnit::Nanoseconds => Duration::add_ns, - }; - datetime - .0 - .try_apply_nonnull_values_generic(|v| offset_fn(offset, v, time_zone)) - } - }, - _ => Ok(datetime.0.apply(|_| None)), - }, - _ => { - let offset_fn = match datetime.time_unit() { - TimeUnit::Milliseconds => Duration::add_ms, - TimeUnit::Microseconds => Duration::add_us, - TimeUnit::Nanoseconds => Duration::add_ns, - }; - broadcast_try_binary_elementwise(datetime, offsets, |timestamp_opt, offset_opt| match ( - timestamp_opt, - offset_opt, - ) { - (Some(timestamp), Some(offset)) => { - offset_fn(&Duration::parse(offset), timestamp, time_zone).map(Some) - }, - _ => Ok(None), - }) - }, - } -} - -#[cfg(feature = "date_offset")] -pub(super) fn date_offset(s: &[Series]) -> PolarsResult { - let ts = &s[0]; - let offsets = &s[1].str()?; - - let preserve_sortedness: bool; - let out = match ts.dtype() { - DataType::Date => { - let ts = ts - .cast(&DataType::Datetime(TimeUnit::Milliseconds, None)) - .unwrap(); - let datetime = ts.datetime().unwrap(); - let out = apply_offsets_to_datetime(datetime, offsets, None)?; - // sortedness is only guaranteed to be preserved if a constant offset is being added to every datetime - preserve_sortedness = match offsets.len() { - 1 => offsets.get(0).is_some(), - _ => false, - }; - out.cast(&DataType::Datetime(TimeUnit::Milliseconds, None)) - .unwrap() - .cast(&DataType::Date) - }, - DataType::Datetime(tu, tz) => { - let datetime = ts.datetime().unwrap(); - - let out = match tz { - #[cfg(feature = "timezones")] - Some(ref tz) => { - apply_offsets_to_datetime(datetime, offsets, tz.parse::().ok().as_ref())? - }, - _ => apply_offsets_to_datetime(datetime, offsets, None)?, - }; - // Sortedness may not be preserved when crossing daylight savings time boundaries - // for calendar-aware durations. - // Constant durations (e.g. 2 hours) always preserve sortedness. - preserve_sortedness = match offsets.len() { - 1 => match offsets.get(0) { - Some(offset) => { - let offset = Duration::parse(offset); - tz.is_none() - || tz.as_deref() == Some("UTC") - || offset.is_constant_duration(tz.as_deref()) - }, - None => false, - }, - _ => false, - }; - out.cast(&DataType::Datetime(*tu, tz.clone())) - }, - dt => polars_bail!( - ComputeError: "cannot use 'date_offset' on Series of datatype {}", dt, - ), - }; - if preserve_sortedness { - out.map(|mut out| { - out.set_sorted_flag(ts.is_sorted_flag()); - out - }) - } else { - out.map(|mut out| { - out.set_sorted_flag(IsSorted::Not); - out - }) - } -} - pub(super) fn combine(s: &[Series], tu: TimeUnit) -> PolarsResult { let date = &s[0]; let time = &s[1]; diff --git a/crates/polars-time/src/lib.rs b/crates/polars-time/src/lib.rs index ea9f6373eb85..e2c7af012852 100644 --- a/crates/polars-time/src/lib.rs +++ b/crates/polars-time/src/lib.rs @@ -6,6 +6,7 @@ mod dst_offset; mod group_by; mod month_end; mod month_start; +mod offset_by; pub mod prelude; mod round; pub mod series; @@ -23,6 +24,7 @@ pub use dst_offset::*; pub use group_by::dynamic::*; pub use month_end::*; pub use month_start::*; +pub use offset_by::*; pub use round::*; pub use truncate::*; pub use upsample::*; diff --git a/crates/polars-time/src/offset_by.rs b/crates/polars-time/src/offset_by.rs new file mode 100644 index 000000000000..00485a59d50d --- /dev/null +++ b/crates/polars-time/src/offset_by.rs @@ -0,0 +1,121 @@ +use arrow::legacy::time_zone::Tz; +use polars_core::prelude::arity::broadcast_try_binary_elementwise; +use polars_core::prelude::*; +use polars_core::series::IsSorted; + +use crate::Duration; + +fn apply_offsets_to_datetime( + datetime: &Logical, + offsets: &StringChunked, + time_zone: Option<&Tz>, +) -> PolarsResult { + match offsets.len() { + 1 => match offsets.get(0) { + Some(offset) => { + let offset = &Duration::parse(offset); + if offset.is_constant_duration(datetime.time_zone().as_deref()) { + // fastpath! + let mut duration = match datetime.time_unit() { + TimeUnit::Milliseconds => offset.duration_ms(), + TimeUnit::Microseconds => offset.duration_us(), + TimeUnit::Nanoseconds => offset.duration_ns(), + }; + if offset.negative() { + duration = -duration; + } + Ok(datetime.0.clone().wrapping_add_scalar(duration)) + } else { + let offset_fn = match datetime.time_unit() { + TimeUnit::Milliseconds => Duration::add_ms, + TimeUnit::Microseconds => Duration::add_us, + TimeUnit::Nanoseconds => Duration::add_ns, + }; + datetime + .0 + .try_apply_nonnull_values_generic(|v| offset_fn(offset, v, time_zone)) + } + }, + _ => Ok(datetime.0.apply(|_| None)), + }, + _ => { + let offset_fn = match datetime.time_unit() { + TimeUnit::Milliseconds => Duration::add_ms, + TimeUnit::Microseconds => Duration::add_us, + TimeUnit::Nanoseconds => Duration::add_ns, + }; + broadcast_try_binary_elementwise(datetime, offsets, |timestamp_opt, offset_opt| match ( + timestamp_opt, + offset_opt, + ) { + (Some(timestamp), Some(offset)) => { + offset_fn(&Duration::parse(offset), timestamp, time_zone).map(Some) + }, + _ => Ok(None), + }) + }, + } +} + +pub fn impl_offset_by(ts: &Series, offsets: &Series) -> PolarsResult { + let preserve_sortedness: bool; + let offsets = offsets.str()?; + let out = match ts.dtype() { + DataType::Date => { + let ts = ts + .cast(&DataType::Datetime(TimeUnit::Milliseconds, None)) + .unwrap(); + let datetime = ts.datetime().unwrap(); + let out = apply_offsets_to_datetime(datetime, offsets, None)?; + // sortedness is only guaranteed to be preserved if a constant offset is being added to every datetime + preserve_sortedness = match offsets.len() { + 1 => offsets.get(0).is_some(), + _ => false, + }; + out.cast(&DataType::Datetime(TimeUnit::Milliseconds, None)) + .unwrap() + .cast(&DataType::Date) + }, + DataType::Datetime(tu, tz) => { + let datetime = ts.datetime().unwrap(); + + let out = match tz { + #[cfg(feature = "timezones")] + Some(ref tz) => { + apply_offsets_to_datetime(datetime, offsets, tz.parse::().ok().as_ref())? + }, + _ => apply_offsets_to_datetime(datetime, offsets, None)?, + }; + // Sortedness may not be preserved when crossing daylight savings time boundaries + // for calendar-aware durations. + // Constant durations (e.g. 2 hours) always preserve sortedness. + preserve_sortedness = match offsets.len() { + 1 => match offsets.get(0) { + Some(offset) => { + let offset = Duration::parse(offset); + tz.is_none() + || tz.as_deref() == Some("UTC") + || offset.is_constant_duration(tz.as_deref()) + }, + None => false, + }, + _ => false, + }; + out.cast(&DataType::Datetime(*tu, tz.clone())) + }, + dt => polars_bail!( + ComputeError: "cannot use 'offset_by' on Series of datatype {}", dt, + ), + }; + if preserve_sortedness { + out.map(|mut out| { + out.set_sorted_flag(ts.is_sorted_flag()); + out + }) + } else { + out.map(|mut out| { + out.set_sorted_flag(IsSorted::Not); + out + }) + } +} diff --git a/crates/polars/Cargo.toml b/crates/polars/Cargo.toml index 7f88b92ff9e9..cf4fcd34febe 100644 --- a/crates/polars/Cargo.toml +++ b/crates/polars/Cargo.toml @@ -143,7 +143,7 @@ cum_agg = ["polars-ops/cum_agg", "polars-lazy?/cum_agg"] cumulative_eval = ["polars-lazy?/cumulative_eval"] cutqcut = ["polars-lazy?/cutqcut"] dataframe_arithmetic = ["polars-core/dataframe_arithmetic"] -date_offset = ["polars-lazy?/date_offset"] +offset_by = ["polars-lazy?/offset_by"] decompress = ["polars-io/decompress"] decompress-fast = ["polars-io/decompress-fast"] describe = ["polars-core/describe"] diff --git a/crates/polars/src/lib.rs b/crates/polars/src/lib.rs index fd01e7301f00..00086736c6e5 100644 --- a/crates/polars/src/lib.rs +++ b/crates/polars/src/lib.rs @@ -266,7 +266,7 @@ //! - `cumulative_eval` - Apply expressions over cumulatively increasing windows. //! - `arg_where` - Get indices where condition holds. //! - `search_sorted` - Find indices where elements should be inserted to maintain order. -//! - `date_offset` - Add an offset to dates that take months and leap years into account. +//! - `offset_by` - Add an offset to dates that take months and leap years into account. //! - `trigonometry` - Trigonometric functions. //! - `sign` - Compute the element-wise sign of a [`Series`]. //! - `propagate_nans` - NaN propagating min/max aggregations. diff --git a/docs/user-guide/installation.md b/docs/user-guide/installation.md index 03ac7f534bfc..e59ba05fc080 100644 --- a/docs/user-guide/installation.md +++ b/docs/user-guide/installation.md @@ -197,7 +197,7 @@ The opt-in features are: - `cumulative_eval` - Apply expressions over cumulatively increasing windows. - `arg_where` - Get indices where condition holds. - `search_sorted` - Find indices where elements should be inserted to maintain order. - - `date_offset` Add an offset to dates that take months and leap years into account. + - `offset_by` Add an offset to dates that take months and leap years into account. - `trigonometry` Trigonometric functions. - `sign` Compute the element-wise sign of a Series. - `propagate_nans` NaN propagating min/max aggregations. diff --git a/py-polars/Cargo.toml b/py-polars/Cargo.toml index 426b79a16fa8..9c2f93a54bf1 100644 --- a/py-polars/Cargo.toml +++ b/py-polars/Cargo.toml @@ -44,7 +44,7 @@ features = [ "cum_agg", "cumulative_eval", "dataframe_arithmetic", - "date_offset", + "offset_by", "diagonal_concat", "diff", "dot_diagram", diff --git a/py-polars/src/lazyframe/visitor/expr_nodes.rs b/py-polars/src/lazyframe/visitor/expr_nodes.rs index fc807d64402c..e7adae155844 100644 --- a/py-polars/src/lazyframe/visitor/expr_nodes.rs +++ b/py-polars/src/lazyframe/visitor/expr_nodes.rs @@ -230,6 +230,7 @@ pub enum PyTemporalFunction { ConvertTimeZone, TimeStamp, Truncate, + OffsetBy, MonthStart, MonthEnd, BaseUtcOffset, @@ -913,6 +914,7 @@ pub(crate) fn into_py(py: Python<'_>, expr: &AExpr) -> PyResult { (PyTemporalFunction::TimeStamp, Wrap(*time_unit)).into_py(py) }, TemporalFunction::Truncate => (PyTemporalFunction::Truncate).into_py(py), + TemporalFunction::OffsetBy => (PyTemporalFunction::OffsetBy,).into_py(py), TemporalFunction::MonthStart => (PyTemporalFunction::MonthStart,).into_py(py), TemporalFunction::MonthEnd => (PyTemporalFunction::MonthEnd,).into_py(py), TemporalFunction::BaseUtcOffset => { @@ -995,9 +997,6 @@ pub(crate) fn into_py(py: Python<'_>, expr: &AExpr) -> PyResult { return Err(PyNotImplementedError::new_err("search sorted")) }, FunctionExpr::Range(_) => return Err(PyNotImplementedError::new_err("range")), - FunctionExpr::DateOffset => { - return Err(PyNotImplementedError::new_err("date offset")) - }, FunctionExpr::Trigonometry(trigfun) => match trigfun { TrigonometricFunction::Cos => ("cos",), TrigonometricFunction::Cot => ("cot",), diff --git a/py-polars/tests/unit/operations/namespaces/temporal/test_datetime.py b/py-polars/tests/unit/operations/namespaces/temporal/test_datetime.py index f2eed19af83e..a2c3aedbadd1 100644 --- a/py-polars/tests/unit/operations/namespaces/temporal/test_datetime.py +++ b/py-polars/tests/unit/operations/namespaces/temporal/test_datetime.py @@ -771,7 +771,7 @@ def test_quarter() -> None: ).dt.quarter().to_list() == [1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4] -def test_date_offset() -> None: +def test_offset_by() -> None: df = pl.DataFrame( { "dates": pl.datetime_range(