22
33from typing import TYPE_CHECKING , Any
44
5+ import pandas as pd
6+
57from narwhals ._compliant .any_namespace import DateTimeNamespace
68from narwhals ._constants import (
79 EPOCH_YEAR ,
1012 SECONDS_PER_DAY ,
1113 US_PER_SECOND ,
1214)
13- from narwhals ._duration import parse_interval_string
15+ from narwhals ._duration import Interval
1416from narwhals ._pandas_like .utils import (
15- UNIT_DICT ,
17+ ALIAS_DICT ,
18+ UNITS_DICT ,
1619 PandasLikeSeriesNamespace ,
1720 calculate_timestamp_date ,
1821 calculate_timestamp_datetime ,
@@ -203,13 +206,14 @@ def timestamp(self, time_unit: TimeUnit) -> PandasLikeSeries:
203206 return self .with_native (result )
204207
205208 def truncate (self , every : str ) -> PandasLikeSeries :
206- multiple , unit = parse_interval_string (every )
209+ interval = Interval .parse (every )
210+ multiple , unit = interval .multiple , interval .unit
207211 native = self .native
208212 if self .implementation .is_cudf ():
209213 if multiple != 1 :
210214 msg = f"Only multiple `1` is supported for cuDF, got: { multiple } ."
211215 raise NotImplementedError (msg )
212- return self .with_native (self .native .dt .floor (UNIT_DICT .get (unit , unit )))
216+ return self .with_native (self .native .dt .floor (ALIAS_DICT .get (unit , unit )))
213217 dtype_backend = get_dtype_backend (native .dtype , self .compliant ._implementation )
214218 if unit in {"mo" , "q" , "y" }:
215219 if self .implementation .is_cudf ():
@@ -218,8 +222,6 @@ def truncate(self, every: str) -> PandasLikeSeries:
218222 if dtype_backend == "pyarrow" :
219223 import pyarrow .compute as pc # ignore-banned-import
220224
221- from narwhals ._arrow .utils import UNITS_DICT
222-
223225 ca = native .array ._pa_array
224226 result_arr = pc .floor_temporal (ca , multiple , UNITS_DICT [unit ])
225227 else :
@@ -230,7 +232,7 @@ def truncate(self, every: str) -> PandasLikeSeries:
230232 np_unit = "M"
231233 else :
232234 np_unit = "Y"
233- arr = native .values
235+ arr = native .values # noqa: PD011
234236 arr_dtype = arr .dtype
235237 result_arr = arr .astype (f"datetime64[{ multiple } { np_unit } ]" ).astype (
236238 arr_dtype
@@ -240,5 +242,48 @@ def truncate(self, every: str) -> PandasLikeSeries:
240242 )
241243 return self .with_native (result_native )
242244 return self .with_native (
243- self .native .dt .floor (f"{ multiple } { UNIT_DICT .get (unit , unit )} " )
245+ self .native .dt .floor (f"{ multiple } { ALIAS_DICT .get (unit , unit )} " )
244246 )
247+
248+ def offset_by (self , by : str ) -> PandasLikeSeries :
249+ if self .implementation .is_cudf ():
250+ msg = "Not implemented for cuDF."
251+ raise NotImplementedError (msg )
252+ native = self .native
253+ if self ._is_pyarrow ():
254+ import pyarrow as pa # ignore-banned-import
255+
256+ compliant = self .compliant
257+ ca = pa .chunked_array ([compliant .to_arrow ()]) # type: ignore[arg-type]
258+ result = (
259+ compliant ._version .namespace .from_backend ("pyarrow" )
260+ .compliant .from_native (ca )
261+ .dt .offset_by (by )
262+ .native
263+ )
264+ result_pd = native .__class__ (
265+ result , dtype = native .dtype , index = native .index , name = native .name
266+ )
267+ else :
268+ interval = Interval .parse_no_constraints (by )
269+ multiple , unit = interval .multiple , interval .unit
270+ if unit == "q" :
271+ multiple *= 3
272+ unit = "mo"
273+ offset : pd .DateOffset | pd .Timedelta
274+ if unit == "y" :
275+ offset = pd .DateOffset (years = multiple )
276+ elif unit == "mo" :
277+ offset = pd .DateOffset (months = multiple )
278+ else :
279+ offset = pd .Timedelta (multiple , unit = UNITS_DICT [unit ]) # type: ignore[arg-type]
280+ if unit == "d" :
281+ original_timezone = native .dt .tz
282+ native_without_timezone = native .dt .tz_localize (None )
283+ result_pd = native_without_timezone + offset
284+ if original_timezone is not None :
285+ result_pd = result_pd .dt .tz_localize (original_timezone )
286+ else :
287+ result_pd = native + offset
288+
289+ return self .with_native (result_pd )
0 commit comments