Path: blob/main/py-polars/tests/unit/operations/namespaces/temporal/test_datetime.py
8422 views
from __future__ import annotations12from collections import OrderedDict3from datetime import date, datetime, time, timedelta4from typing import TYPE_CHECKING5from zoneinfo import ZoneInfo67import pytest8from hypothesis import given910import polars as pl11from polars.datatypes import DTYPE_TEMPORAL_UNITS12from polars.exceptions import ComputeError, InvalidOperationError13from polars.testing import assert_frame_equal, assert_series_equal14from polars.testing.parametric import series1516if TYPE_CHECKING:17from collections.abc import Callable1819from polars._typing import PolarsDataType, TemporalLiteral, TimeUnit202122@pytest.fixture23def series_of_int_dates() -> pl.Series:24return pl.Series([8401, 10000, 20000, 30000], dtype=pl.Date)252627@pytest.fixture28def series_of_str_dates() -> pl.Series:29return pl.Series(["2020-01-01 00:00:00.000000000", "2020-02-02 03:20:10.987654321"])303132def test_dt_to_string(series_of_int_dates: pl.Series) -> None:33expected_str_dates = pl.Series(34["1993-01-01", "1997-05-19", "2024-10-04", "2052-02-20"]35)3637assert series_of_int_dates.dtype == pl.Date38assert_series_equal(series_of_int_dates.dt.to_string("%F"), expected_str_dates)3940# Check strftime alias as well41assert_series_equal(series_of_int_dates.dt.strftime("%F"), expected_str_dates)424344@pytest.mark.parametrize(45("unit_attr", "expected"),46[47("millennium", pl.Series(values=[2, 2, 3, 3], dtype=pl.Int32)),48("century", pl.Series(values=[20, 20, 21, 21], dtype=pl.Int32)),49("year", pl.Series(values=[1993, 1997, 2024, 2052], dtype=pl.Int32)),50("iso_year", pl.Series(values=[1992, 1997, 2024, 2052], dtype=pl.Int32)),51("quarter", pl.Series(values=[1, 2, 4, 1], dtype=pl.Int8)),52("month", pl.Series(values=[1, 5, 10, 2], dtype=pl.Int8)),53("week", pl.Series(values=[53, 21, 40, 8], dtype=pl.Int8)),54("day", pl.Series(values=[1, 19, 4, 20], dtype=pl.Int8)),55("weekday", pl.Series(values=[5, 1, 5, 2], dtype=pl.Int8)),56("ordinal_day", pl.Series(values=[1, 139, 278, 51], dtype=pl.Int16)),57],58)59@pytest.mark.parametrize("time_zone", ["Asia/Kathmandu", None])60def test_dt_extract_datetime_component(61unit_attr: str,62expected: pl.Series,63series_of_int_dates: pl.Series,64time_zone: str | None,65) -> None:66assert_series_equal(getattr(series_of_int_dates.dt, unit_attr)(), expected)67assert_series_equal(68getattr(69series_of_int_dates.cast(pl.Datetime).dt.replace_time_zone(time_zone).dt,70unit_attr,71)(),72expected,73)747576@pytest.mark.parametrize(77("unit_attr", "expected"),78[79("hour", pl.Series(values=[0, 3], dtype=pl.Int8)),80("minute", pl.Series(values=[0, 20], dtype=pl.Int8)),81("second", pl.Series(values=[0, 10], dtype=pl.Int8)),82("millisecond", pl.Series(values=[0, 987], dtype=pl.Int32)),83("microsecond", pl.Series(values=[0, 987654], dtype=pl.Int32)),84("nanosecond", pl.Series(values=[0, 987654321], dtype=pl.Int32)),85],86)87def test_strptime_extract_times(88unit_attr: str,89expected: pl.Series,90series_of_int_dates: pl.Series,91series_of_str_dates: pl.Series,92) -> None:93s = series_of_str_dates.str.strptime(pl.Datetime, format="%Y-%m-%d %H:%M:%S.%9f")9495assert_series_equal(getattr(s.dt, unit_attr)(), expected)969798@pytest.mark.parametrize("time_zone", [None, "Asia/Kathmandu"])99@pytest.mark.parametrize(100("attribute", "expected"),101[102("date", date(2022, 1, 1)),103("time", time(23)),104],105)106def test_dt_date_and_time(107attribute: str, time_zone: None | str, expected: date | time108) -> None:109ser = pl.Series([datetime(2022, 1, 1, 23)]).dt.replace_time_zone(time_zone)110result = getattr(ser.dt, attribute)().item()111assert result == expected112113114@pytest.mark.parametrize("time_zone", [None, "Asia/Kathmandu"])115@pytest.mark.parametrize("time_unit", ["us", "ns", "ms"])116def test_dt_replace_time_zone_none(time_zone: str | None, time_unit: TimeUnit) -> None:117ser = (118pl.Series([datetime(2022, 1, 1, 23)])119.dt.cast_time_unit(time_unit)120.dt.replace_time_zone(time_zone)121)122result = ser.dt.replace_time_zone(None)123expected = datetime(2022, 1, 1, 23)124assert result.dtype == pl.Datetime(time_unit, None)125assert result.item() == expected126127128def test_dt_datetime_deprecated() -> None:129s = pl.Series([datetime(2022, 1, 1, 23)]).dt.replace_time_zone("Asia/Kathmandu")130with pytest.deprecated_call():131result = s.dt.datetime()132expected = datetime(2022, 1, 1, 23)133assert result.dtype == pl.Datetime(time_zone=None)134assert result.item() == expected135136137@pytest.mark.parametrize("time_zone", [None, "Asia/Kathmandu", "UTC"])138def test_local_date_sortedness(time_zone: str | None) -> None:139# singleton140ser = (pl.Series([datetime(2022, 1, 1, 23)]).dt.replace_time_zone(time_zone)).sort()141result = ser.dt.date()142assert result.flags["SORTED_ASC"]143144# 2 elements145ser = (146pl.Series([datetime(2022, 1, 1, 23)] * 2).dt.replace_time_zone(time_zone)147).sort()148result = ser.dt.date()149assert result.flags["SORTED_ASC"]150151152@pytest.mark.parametrize("time_zone", [None, "Asia/Kathmandu", "UTC"])153def test_local_time_sortedness(time_zone: str | None) -> None:154# singleton - always sorted155ser = (pl.Series([datetime(2022, 1, 1, 23)]).dt.replace_time_zone(time_zone)).sort()156result = ser.dt.time()157assert result.flags["SORTED_ASC"]158159# three elements - not sorted160ser = (161pl.Series(162[163datetime(2022, 1, 1, 23),164datetime(2022, 1, 2, 21),165datetime(2022, 1, 3, 22),166]167).dt.replace_time_zone(time_zone)168).sort()169result = ser.dt.time()170assert not result.flags["SORTED_ASC"]171assert not result.flags["SORTED_DESC"]172173174@pytest.mark.parametrize("time_unit", ["ms", "us", "ns"])175def test_local_time_before_epoch(time_unit: TimeUnit) -> None:176ser = pl.Series([datetime(1969, 7, 21, 2, 56, 2, 123000)]).dt.cast_time_unit(177time_unit178)179result = ser.dt.time().item()180expected = time(2, 56, 2, 123000)181assert result == expected182183184@pytest.mark.parametrize(185("time_zone", "offset", "expected"),186[187(None, "+1d", True),188("Europe/London", "1d", False),189("UTC", "1d", True),190(None, "1m", True),191("Europe/London", "1m", True),192("UTC", "1m", True),193(None, "1w", True),194("Europe/London", "1w", False),195("UTC", "+1w", True),196(None, "1h", True),197("Europe/London", "1h", True),198("UTC", "1h", True),199],200)201def test_offset_by_sortedness(202time_zone: str | None, offset: str, expected: bool203) -> None:204s = pl.datetime_range(205datetime(2020, 10, 25),206datetime(2020, 10, 25, 3),207"30m",208time_zone=time_zone,209eager=True,210).sort()211assert s.flags["SORTED_ASC"]212assert not s.flags["SORTED_DESC"]213result = s.dt.offset_by(offset)214assert result.flags["SORTED_ASC"] == expected215assert not result.flags["SORTED_DESC"]216217218@pytest.mark.parametrize("offset", ["?", "xx", "P1D", "~10d"])219def test_offset_by_invalid_duration(offset: str) -> None:220with pytest.raises(221InvalidOperationError,222match="expected leading integer in the duration string",223):224pl.Series([datetime(2022, 3, 20, 5, 7)]).dt.offset_by(offset)225226227@pytest.mark.parametrize("offset", ["++1d", "+1d+1m+1s", "--1d", "-1d-1m-1s"])228def test_offset_by_invalid_duration_unary_ops(offset: str) -> None:229op = "+" if "+" in offset else "-"230with pytest.raises(231InvalidOperationError,232match=rf"duration string can only have a single '\{op}' sign",233):234pl.Series([datetime(2025, 10, 3, 11, 42)]).dt.offset_by(offset)235236237@pytest.mark.parametrize("offset", ["1", "1mo23d4", "-2d1", "12時30分45秒"])238def test_offset_by_missing_or_invalid_unit(offset: str) -> None:239with pytest.raises(240InvalidOperationError,241match=f"expected a valid unit to follow integer in the duration string '{offset}'",242):243pl.Series([datetime(2025, 10, 6, 13, 45)]).dt.offset_by(offset)244245246def test_offset_by_missing_unit_in_expr() -> None:247with pytest.raises(248InvalidOperationError,249match="expected a valid unit to follow integer in the duration string '1d2'",250):251pl.DataFrame(252{"a": [datetime(2022, 3, 20, 5, 7)] * 2, "b": ["1d", "1d2"]}253).select(pl.col("a").dt.offset_by(pl.col("b")))254255256def test_dt_datetime_date_time_invalid() -> None:257with pytest.raises(ComputeError, match="expected Datetime or Date"):258pl.Series([time(23)]).dt.date()259with pytest.raises(ComputeError, match="expected Datetime or Date"):260pl.Series([timedelta(1)]).dt.date()261with pytest.raises(ComputeError, match="expected Datetime or Time"):262pl.Series([timedelta(1)]).dt.time()263with pytest.raises(ComputeError, match="expected Datetime or Time"):264pl.Series([date(2020, 1, 1)]).dt.time()265266267@pytest.mark.parametrize("time_unit", ["ms", "us", "ns"])268def test_base_utc_offset(time_unit: TimeUnit) -> None:269ser = pl.datetime_range(270datetime(2011, 12, 29),271datetime(2012, 1, 1),272"2d",273time_zone="Pacific/Apia",274eager=True,275).dt.cast_time_unit(time_unit)276result = ser.dt.base_utc_offset().rename("base_utc_offset")277expected = pl.Series(278"base_utc_offset",279[-11 * 3600 * 1000, 13 * 3600 * 1000],280dtype=pl.Duration("ms"),281)282assert_series_equal(result, expected)283284285def test_base_utc_offset_lazy_schema() -> None:286ser = pl.datetime_range(287datetime(2020, 10, 25),288datetime(2020, 10, 26),289time_zone="Europe/London",290eager=True,291)292df = pl.DataFrame({"ts": ser}).lazy()293result = df.with_columns(294base_utc_offset=pl.col("ts").dt.base_utc_offset()295).collect_schema()296expected = {297"ts": pl.Datetime(time_unit="us", time_zone="Europe/London"),298"base_utc_offset": pl.Duration(time_unit="ms"),299}300assert result == expected301302303def test_base_utc_offset_invalid() -> None:304ser = pl.datetime_range(datetime(2020, 10, 25), datetime(2020, 10, 26), eager=True)305with pytest.raises(306InvalidOperationError,307match=r"`base_utc_offset` operation not supported for dtype `datetime\[μs\]` \(expected: time-zone-aware datetime\)",308):309ser.dt.base_utc_offset().rename("base_utc_offset")310311312@pytest.mark.parametrize("time_unit", ["ms", "us", "ns"])313def test_dst_offset(time_unit: TimeUnit) -> None:314ser = pl.datetime_range(315datetime(2020, 10, 25),316datetime(2020, 10, 26),317time_zone="Europe/London",318eager=True,319).dt.cast_time_unit(time_unit)320result = ser.dt.dst_offset().rename("dst_offset")321expected = pl.Series("dst_offset", [3_600 * 1_000, 0], dtype=pl.Duration("ms"))322assert_series_equal(result, expected)323324325def test_dst_offset_lazy_schema() -> None:326ser = pl.datetime_range(327datetime(2020, 10, 25),328datetime(2020, 10, 26),329time_zone="Europe/London",330eager=True,331)332df = pl.DataFrame({"ts": ser}).lazy()333result = df.with_columns(dst_offset=pl.col("ts").dt.dst_offset()).collect_schema()334expected = {335"ts": pl.Datetime(time_unit="us", time_zone="Europe/London"),336"dst_offset": pl.Duration(time_unit="ms"),337}338assert result == expected339340341def test_dst_offset_invalid() -> None:342ser = pl.datetime_range(datetime(2020, 10, 25), datetime(2020, 10, 26), eager=True)343with pytest.raises(344InvalidOperationError,345match=r"`dst_offset` operation not supported for dtype `datetime\[μs\]` \(expected: time-zone-aware datetime\)",346):347ser.dt.dst_offset().rename("dst_offset")348349350@pytest.mark.parametrize(351("time_unit", "expected"),352[353("d", pl.Series(values=[18262, 18294], dtype=pl.Int32)),354("s", pl.Series(values=[1_577_836_800, 1_580_613_610], dtype=pl.Int64)),355(356"ms",357pl.Series(values=[1_577_836_800_000, 1_580_613_610_987], dtype=pl.Int64),358),359],360)361def test_strptime_epoch(362time_unit: TimeUnit,363expected: pl.Series,364series_of_str_dates: pl.Series,365) -> None:366s = series_of_str_dates.str.strptime(pl.Datetime, format="%Y-%m-%d %H:%M:%S.%9f")367368assert_series_equal(s.dt.epoch(time_unit=time_unit), expected)369370371def test_strptime_fractional_seconds(series_of_str_dates: pl.Series) -> None:372s = series_of_str_dates.str.strptime(pl.Datetime, format="%Y-%m-%d %H:%M:%S.%9f")373374assert_series_equal(375s.dt.second(fractional=True),376pl.Series([0.0, 10.987654321], dtype=pl.Float64),377)378379380@pytest.mark.parametrize(381("unit_attr", "expected"),382[383("total_days", pl.Series([1])),384("total_hours", pl.Series([24])),385("total_minutes", pl.Series([24 * 60])),386("total_seconds", pl.Series([3600 * 24])),387("total_milliseconds", pl.Series([3600 * 24 * int(1e3)])),388("total_microseconds", pl.Series([3600 * 24 * int(1e6)])),389("total_nanoseconds", pl.Series([3600 * 24 * int(1e9)])),390],391)392def test_duration_extract_times(393unit_attr: str,394expected: pl.Series,395) -> None:396duration = pl.Series([datetime(2022, 1, 2)]) - pl.Series([datetime(2022, 1, 1)])397398assert_series_equal(getattr(duration.dt, unit_attr)(), expected)399400401@pytest.mark.parametrize(402("time_unit", "every"),403[404("ms", "1h"),405("us", "1h0m0s"),406("ns", timedelta(hours=1)),407],408ids=["milliseconds", "microseconds", "nanoseconds"],409)410def test_truncate(411time_unit: TimeUnit,412every: str | timedelta,413) -> None:414start, stop = datetime(2022, 1, 1), datetime(2022, 1, 2)415s = pl.datetime_range(416start,417stop,418timedelta(minutes=30),419time_unit=time_unit,420eager=True,421).alias(f"dates[{time_unit}]")422423# can pass strings and time-deltas424out = s.dt.truncate(every)425assert out.dt[0] == start426assert out.dt[1] == start427assert out.dt[2] == start + timedelta(hours=1)428assert out.dt[3] == start + timedelta(hours=1)429# ...430assert out.dt[-3] == stop - timedelta(hours=1)431assert out.dt[-2] == stop - timedelta(hours=1)432assert out.dt[-1] == stop433434435def test_truncate_negative() -> None:436"""Test that truncating to a negative duration gives a helpful error message."""437df = pl.DataFrame(438{439"date": [date(1895, 5, 7), date(1955, 11, 5)],440"datetime": [datetime(1895, 5, 7), datetime(1955, 11, 5)],441"duration": ["-1m", "1m"],442}443)444445with pytest.raises(446ComputeError, match="cannot truncate a Date to a negative duration"447):448df.select(pl.col("date").dt.truncate("-1m"))449450with pytest.raises(451ComputeError, match="cannot truncate a Datetime to a negative duration"452):453df.select(pl.col("datetime").dt.truncate("-1m"))454455with pytest.raises(456ComputeError, match="cannot truncate a Date to a negative duration"457):458df.select(pl.col("date").dt.truncate(pl.col("duration")))459460with pytest.raises(461ComputeError, match="cannot truncate a Datetime to a negative duration"462):463df.select(pl.col("datetime").dt.truncate(pl.col("duration")))464465466@pytest.mark.parametrize(467("time_unit", "every"),468[469("ms", "1h"),470("us", "1h0m0s"),471("ns", timedelta(hours=1)),472],473ids=["milliseconds", "microseconds", "nanoseconds"],474)475def test_round(476time_unit: TimeUnit,477every: str | timedelta,478) -> None:479start, stop = datetime(2022, 1, 1), datetime(2022, 1, 2)480s = pl.datetime_range(481start,482stop,483timedelta(minutes=30),484time_unit=time_unit,485eager=True,486).alias(f"dates[{time_unit}]")487488# can pass strings and time-deltas489out = s.dt.round(every)490assert out.dt[0] == start491assert out.dt[1] == start + timedelta(hours=1)492assert out.dt[2] == start + timedelta(hours=1)493assert out.dt[3] == start + timedelta(hours=2)494# ...495assert out.dt[-3] == stop - timedelta(hours=1)496assert out.dt[-2] == stop497assert out.dt[-1] == stop498499500def test_round_expr() -> None:501df = pl.DataFrame(502{503"date": [504datetime(2022, 11, 14),505datetime(2023, 10, 11),506datetime(2022, 3, 20, 5, 7, 18),507datetime(2022, 4, 3, 13, 30, 32),508None,509datetime(2022, 12, 1),510],511"every": ["1y", "1mo", "1m", "1m", "1mo", None],512}513)514515output = df.select(516all_expr=pl.col("date").dt.round(every=pl.col("every")),517date_lit=pl.lit(datetime(2022, 4, 3, 13, 30, 32)).dt.round(518every=pl.col("every")519),520every_lit=pl.col("date").dt.round("1d"),521)522523expected = pl.DataFrame(524{525"all_expr": [526datetime(2023, 1, 1),527datetime(2023, 10, 1),528datetime(2022, 3, 20, 5, 7),529datetime(2022, 4, 3, 13, 31),530None,531None,532],533"date_lit": [534datetime(2022, 1, 1),535datetime(2022, 4, 1),536datetime(2022, 4, 3, 13, 31),537datetime(2022, 4, 3, 13, 31),538datetime(2022, 4, 1),539None,540],541"every_lit": [542datetime(2022, 11, 14),543datetime(2023, 10, 11),544datetime(2022, 3, 20),545datetime(2022, 4, 4),546None,547datetime(2022, 12, 1),548],549}550)551552assert_frame_equal(output, expected)553554all_lit = pl.select(all_lit=pl.lit(datetime(2022, 3, 20, 5, 7)).dt.round("1h"))555assert all_lit.to_dict(as_series=False) == {"all_lit": [datetime(2022, 3, 20, 5)]}556557558def test_round_negative() -> None:559"""Test that rounding to a negative duration gives a helpful error message."""560with pytest.raises(561ComputeError, match="cannot round a Date to a negative duration"562):563pl.Series([date(1895, 5, 7)]).dt.round("-1m")564565with pytest.raises(566ComputeError, match="cannot round a Datetime to a negative duration"567):568pl.Series([datetime(1895, 5, 7)]).dt.round("-1m")569570571def test_round_invalid_duration() -> None:572with pytest.raises(573InvalidOperationError, match="expected leading integer in the duration string"574):575pl.Series([datetime(2022, 3, 20, 5, 7)]).dt.round("P")576577578@pytest.mark.parametrize(579("time_unit", "date_in_that_unit"),580[581("ns", [978307200000000000, 981022089000000000]),582("us", [978307200000000, 981022089000000]),583("ms", [978307200000, 981022089000]),584],585ids=["nanoseconds", "microseconds", "milliseconds"],586)587def test_cast_time_units(588time_unit: TimeUnit,589date_in_that_unit: list[int],590) -> None:591dates = pl.Series([datetime(2001, 1, 1), datetime(2001, 2, 1, 10, 8, 9)])592593assert dates.dt.cast_time_unit(time_unit).cast(int).to_list() == date_in_that_unit594595596def test_epoch_matches_timestamp() -> None:597dates = pl.Series([datetime(2001, 1, 1), datetime(2001, 2, 1, 10, 8, 9)])598599for unit in DTYPE_TEMPORAL_UNITS:600assert_series_equal(dates.dt.epoch(unit), dates.dt.timestamp(unit))601602assert_series_equal(dates.dt.epoch("s"), dates.dt.timestamp("ms") // 1000)603assert_series_equal(604dates.dt.epoch("d"),605(dates.dt.timestamp("ms") // (1000 * 3600 * 24)).cast(pl.Int32),606)607608609@pytest.mark.parametrize(610("tzinfo", "time_zone"),611[(None, None), (ZoneInfo("Asia/Kathmandu"), "Asia/Kathmandu")],612)613def test_date_time_combine(tzinfo: ZoneInfo | None, time_zone: str | None) -> None:614# Define a DataFrame with columns for datetime, date, and time615df = pl.DataFrame(616{617"dtm": [618datetime(2022, 12, 31, 10, 30, 45),619datetime(2023, 7, 5, 23, 59, 59),620],621"dt": [622date(2022, 10, 10),623date(2022, 7, 5),624],625"tm": [626time(1, 2, 3, 456000),627time(7, 8, 9, 101000),628],629}630)631df = df.with_columns(pl.col("dtm").dt.replace_time_zone(time_zone))632633# Combine datetime/date with time634df = df.select(635pl.col("dtm").dt.combine(pl.col("tm")).alias("d1"), # datetime & time636pl.col("dt").dt.combine(pl.col("tm")).alias("d2"), # date & time637pl.col("dt").dt.combine(time(4, 5, 6)).alias("d3"), # date & specified time638)639640# Assert that the new columns have the expected values and datatypes641expected_dict = {642"d1": [ # Time component should be overwritten by `tm` values643datetime(2022, 12, 31, 1, 2, 3, 456000, tzinfo=tzinfo),644datetime(2023, 7, 5, 7, 8, 9, 101000, tzinfo=tzinfo),645],646"d2": [ # Both date and time components combined "as-is" into new datetime647datetime(2022, 10, 10, 1, 2, 3, 456000),648datetime(2022, 7, 5, 7, 8, 9, 101000),649],650"d3": [ # New datetime should use specified time component651datetime(2022, 10, 10, 4, 5, 6),652datetime(2022, 7, 5, 4, 5, 6),653],654}655assert df.to_dict(as_series=False) == expected_dict656657expected_schema = {658"d1": pl.Datetime("us", time_zone),659"d2": pl.Datetime("us"),660"d3": pl.Datetime("us"),661}662assert df.schema == expected_schema663664665def test_combine_unsupported_types() -> None:666with pytest.raises(ComputeError, match="expected Date or Datetime, got time"):667pl.Series([time(1, 2)]).dt.combine(time(3, 4))668669670@pytest.mark.parametrize("time_unit", ["ms", "us", "ns"])671@pytest.mark.parametrize("time_zone", ["Asia/Kathmandu", None])672def test_combine_lazy_schema_datetime(673time_zone: str | None,674time_unit: TimeUnit,675) -> None:676df = pl.DataFrame({"ts": pl.Series([datetime(2020, 1, 1)])})677df = df.with_columns(pl.col("ts").dt.replace_time_zone(time_zone))678result = df.lazy().select(679pl.col("ts").dt.combine(time(1, 2, 3), time_unit=time_unit)680)681expected_dtypes = [pl.Datetime(time_unit, time_zone)]682assert result.collect_schema().dtypes() == expected_dtypes683684685@pytest.mark.parametrize("time_unit", ["ms", "us", "ns"])686def test_combine_lazy_schema_date(time_unit: TimeUnit) -> None:687df = pl.DataFrame({"ts": pl.Series([date(2020, 1, 1)])})688result = df.lazy().select(689pl.col("ts").dt.combine(time(1, 2, 3), time_unit=time_unit)690)691expected_dtypes = [pl.Datetime(time_unit, None)]692assert result.collect_schema().dtypes() == expected_dtypes693694695@pytest.mark.parametrize(696("range_fn", "value_type", "kwargs"),697[698(pl.datetime_range, datetime, {"time_unit": "ns"}),699(pl.datetime_range, datetime, {"time_unit": "ns", "time_zone": "CET"}),700(pl.datetime_range, datetime, {"time_unit": "us"}),701(pl.datetime_range, datetime, {"time_unit": "us", "time_zone": "CET"}),702(pl.datetime_range, datetime, {"time_unit": "ms"}),703(pl.datetime_range, datetime, {"time_unit": "ms", "time_zone": "CET"}),704(pl.date_range, date, {}),705],706)707def test_iso_year(708range_fn: Callable[..., pl.Series], value_type: type, kwargs: dict[str, str]709) -> None:710assert range_fn(711value_type(1990, 1, 1), value_type(2004, 1, 1), "1y", **kwargs, eager=True712).dt.iso_year().to_list() == [7131990,7141991,7151992,7161992,7171993,7181994,7191996,7201997,7211998,7221998,7231999,7242001,7252002,7262003,7272004,728]729730731@pytest.mark.parametrize(732("range_fn", "value_type", "kwargs"),733[734(pl.datetime_range, datetime, {"time_unit": "ns"}),735(pl.datetime_range, datetime, {"time_unit": "ns", "time_zone": "CET"}),736(pl.datetime_range, datetime, {"time_unit": "us"}),737(pl.datetime_range, datetime, {"time_unit": "us", "time_zone": "CET"}),738(pl.datetime_range, datetime, {"time_unit": "ms"}),739(pl.datetime_range, datetime, {"time_unit": "ms", "time_zone": "CET"}),740(pl.date_range, date, {}),741],742)743def test_is_leap_year(744range_fn: Callable[..., pl.Series], value_type: type, kwargs: dict[str, str]745) -> None:746assert range_fn(747value_type(1990, 1, 1), value_type(2004, 1, 1), "1y", **kwargs, eager=True748).dt.is_leap_year().to_list() == [749False,750False,751True, # 1992752False,753False,754False,755True, # 1996756False,757False,758False,759True, # 2000760False,761False,762False,763True, # 2004764]765766767@pytest.mark.parametrize(768("value_type", "time_unit", "time_zone"),769[770(date, None, None),771(datetime, "ns", None),772(773datetime,774"ns",775"Asia/Kathmandu",776),777(datetime, "us", None),778(779datetime,780"us",781"Asia/Kathmandu",782),783(datetime, "ms", None),784(785datetime,786"ms",787"Asia/Kathmandu",788),789],790)791@pytest.mark.parametrize(792("start_ymd", "end_ymd", "feb_days"),793[794# Non-leap year cases795((1900, 1, 1), (1900, 12, 1), 28), # 1900 can be divided by 100 but not by 400796((2025, 1, 1), (2025, 12, 1), 28), # 2025 cannot be divided by 4797# Leap year cases798((2000, 1, 1), (2000, 12, 1), 29), # 2000 can be divided by 400799((2004, 1, 1), (2004, 12, 1), 29), # 2004 can be divided by 4 but not by 100800],801)802def test_days_in_month(803value_type: type,804time_unit: str | None,805time_zone: str | None,806start_ymd: tuple[int, int, int],807end_ymd: tuple[int, int, int],808feb_days: int,809) -> None:810assert value_type in (date, datetime)811range_fn: Callable[..., pl.Series] = (812pl.date_range if value_type is date else pl.datetime_range813)814kwargs: dict[str, str] = {}815if time_unit is not None:816kwargs["time_unit"] = time_unit817if time_zone is not None:818kwargs["time_zone"] = time_zone819assert range_fn(820value_type(*start_ymd), value_type(*end_ymd), "1mo", **kwargs, eager=True821).dt.days_in_month().to_list() == [82231,823feb_days,82431,82530,82631,82730,82831,82931,83030,83131,83230,83331,834]835836837def test_quarter() -> None:838assert pl.datetime_range(839datetime(2022, 1, 1), datetime(2022, 12, 1), "1mo", eager=True840).dt.quarter().to_list() == [1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4]841842843def test_offset_by() -> None:844df = pl.DataFrame(845{846"dates": pl.datetime_range(847datetime(2000, 1, 1), datetime(2020, 1, 1), "1y", eager=True848)849}850)851852# Add two new columns to the DataFrame using the offset_by() method853df = df.with_columns(854df["dates"].dt.offset_by("1y").alias("date_plus_1y"),855df["dates"].dt.offset_by("-1y2mo").alias("date_min"),856)857858# Assert that the day of the month for all the dates in new columns is 1859assert (df["date_plus_1y"].dt.day() == 1).all()860assert (df["date_min"].dt.day() == 1).all()861862# Assert that the 'date_min' column contains the expected list of dates863expected_dates = [datetime(year, 11, 1, 0, 0) for year in range(1998, 2019)]864assert df["date_min"].to_list() == expected_dates865866867@pytest.mark.parametrize("time_zone", ["America/Chicago", None])868def test_offset_by_crossing_dst(time_zone: str | None) -> None:869ser = pl.Series([datetime(2021, 11, 7)]).dt.replace_time_zone(time_zone)870result = ser.dt.offset_by("1d")871expected = pl.Series([datetime(2021, 11, 8)]).dt.replace_time_zone(time_zone)872assert_series_equal(result, expected)873874875def test_negative_offset_by_err_msg_8464() -> None:876result = pl.Series([datetime(2022, 3, 30)]).dt.offset_by("-1mo")877expected = pl.Series([datetime(2022, 2, 28)])878assert_series_equal(result, expected)879880881def test_offset_by_truncate_sorted_flag() -> None:882s = pl.Series([datetime(2001, 1, 1), datetime(2001, 1, 2)])883s = s.set_sorted()884885assert s.flags["SORTED_ASC"]886s1 = s.dt.offset_by("1d")887assert s1.to_list() == [datetime(2001, 1, 2), datetime(2001, 1, 3)]888assert s1.flags["SORTED_ASC"]889s2 = s1.dt.truncate("1mo")890assert s2.flags["SORTED_ASC"]891892893def test_offset_by_broadcasting() -> None:894# test broadcast lhs895df = pl.DataFrame(896{897"offset": ["1d", "10d", "3d", None],898}899)900result = df.select(901d1=pl.lit(datetime(2020, 10, 25)).dt.offset_by(pl.col("offset")),902d2=pl.lit(datetime(2020, 10, 25))903.dt.cast_time_unit("ms")904.dt.offset_by(pl.col("offset")),905d3=pl.lit(datetime(2020, 10, 25))906.dt.replace_time_zone("Europe/London")907.dt.offset_by(pl.col("offset")),908d4=pl.lit(datetime(2020, 10, 25)).dt.date().dt.offset_by(pl.col("offset")),909d5=pl.lit(None, dtype=pl.Datetime).dt.offset_by(pl.col("offset")),910)911expected_dict = {912"d1": [913datetime(2020, 10, 26),914datetime(2020, 11, 4),915datetime(2020, 10, 28),916None,917],918"d2": [919datetime(2020, 10, 26),920datetime(2020, 11, 4),921datetime(2020, 10, 28),922None,923],924"d3": [925datetime(2020, 10, 26, tzinfo=ZoneInfo("Europe/London")),926datetime(2020, 11, 4, tzinfo=ZoneInfo("Europe/London")),927datetime(2020, 10, 28, tzinfo=ZoneInfo("Europe/London")),928None,929],930"d4": [931datetime(2020, 10, 26).date(),932datetime(2020, 11, 4).date(),933datetime(2020, 10, 28).date(),934None,935],936"d5": [None, None, None, None],937}938assert result.to_dict(as_series=False) == expected_dict939940# test broadcast rhs941df = pl.DataFrame({"dt": [datetime(2020, 10, 25), datetime(2021, 1, 2), None]})942result = df.select(943d1=pl.col("dt").dt.offset_by(pl.lit("1mo3d")),944d2=pl.col("dt").dt.cast_time_unit("ms").dt.offset_by(pl.lit("1y1mo")),945d3=pl.col("dt")946.dt.replace_time_zone("Europe/London")947.dt.offset_by(pl.lit("3d")),948d4=pl.col("dt").dt.date().dt.offset_by(pl.lit("1y1mo1d")),949)950expected_dict = {951"d1": [datetime(2020, 11, 28), datetime(2021, 2, 5), None],952"d2": [datetime(2021, 11, 25), datetime(2022, 2, 2), None],953"d3": [954datetime(2020, 10, 28, tzinfo=ZoneInfo("Europe/London")),955datetime(2021, 1, 5, tzinfo=ZoneInfo("Europe/London")),956None,957],958"d4": [datetime(2021, 11, 26).date(), datetime(2022, 2, 3).date(), None],959}960assert result.to_dict(as_series=False) == expected_dict961962# test all literal963result = df.select(d=pl.lit(datetime(2021, 11, 26)).dt.offset_by("1mo1d"))964assert result.to_dict(as_series=False) == {"d": [datetime(2021, 12, 27)]}965966967def test_offset_by_expressions() -> None:968df = pl.DataFrame(969{970"a": [971datetime(2020, 10, 25),972datetime(2021, 1, 2),973None,974datetime(2021, 1, 4),975None,976],977"b": ["1d", "10d", "3d", None, None],978}979)980df = df.sort("a")981result = df.select(982c=pl.col("a").dt.offset_by(pl.col("b")),983d=pl.col("a").dt.cast_time_unit("ms").dt.offset_by(pl.col("b")),984e=pl.col("a").dt.replace_time_zone("Europe/London").dt.offset_by(pl.col("b")),985f=pl.col("a").dt.date().dt.offset_by(pl.col("b")),986)987988expected = pl.DataFrame(989{990"c": [None, None, datetime(2020, 10, 26), datetime(2021, 1, 12), None],991"d": [None, None, datetime(2020, 10, 26), datetime(2021, 1, 12), None],992"e": [993None,994None,995datetime(2020, 10, 26, tzinfo=ZoneInfo("Europe/London")),996datetime(2021, 1, 12, tzinfo=ZoneInfo("Europe/London")),997None,998],999"f": [None, None, date(2020, 10, 26), date(2021, 1, 12), None],1000},1001schema_overrides={1002"d": pl.Datetime("ms"),1003"e": pl.Datetime(time_zone="Europe/London"),1004},1005)1006assert_frame_equal(result, expected)1007assert result.flags == {1008"c": {"SORTED_ASC": False, "SORTED_DESC": False},1009"d": {"SORTED_ASC": False, "SORTED_DESC": False},1010"e": {"SORTED_ASC": False, "SORTED_DESC": False},1011"f": {"SORTED_ASC": False, "SORTED_DESC": False},1012}10131014# Check single-row cases1015for i in range(df.height):1016df_slice = df[i : i + 1]1017result = df_slice.select(1018c=pl.col("a").dt.offset_by(pl.col("b")),1019d=pl.col("a").dt.cast_time_unit("ms").dt.offset_by(pl.col("b")),1020e=pl.col("a")1021.dt.replace_time_zone("Europe/London")1022.dt.offset_by(pl.col("b")),1023f=pl.col("a").dt.date().dt.offset_by(pl.col("b")),1024)1025assert_frame_equal(result, expected[i : i + 1])1026# single-row Series are always sorted1027assert result.flags == {1028"c": {"SORTED_ASC": True, "SORTED_DESC": False},1029"d": {"SORTED_ASC": True, "SORTED_DESC": False},1030"e": {"SORTED_ASC": True, "SORTED_DESC": False},1031"f": {"SORTED_ASC": True, "SORTED_DESC": False},1032}103310341035@pytest.mark.parametrize(1036("duration", "input_date", "expected"),1037[1038("1mo", date(2018, 1, 31), date(2018, 2, 28)),1039("1y", date(2024, 2, 29), date(2025, 2, 28)),1040("1y1mo", date(2024, 1, 30), date(2025, 2, 28)),1041],1042)1043def test_offset_by_saturating_8217_8474(1044duration: str, input_date: date, expected: date1045) -> None:1046result = pl.Series([input_date]).dt.offset_by(duration).item()1047assert result == expected104810491050def test_year_empty_df() -> None:1051df = pl.DataFrame(pl.Series(name="date", dtype=pl.Date))1052assert df.select(pl.col("date").dt.year()).dtypes == [pl.Int32]105310541055def test_epoch_invalid() -> None:1056with pytest.raises(InvalidOperationError, match="not supported for dtype"):1057pl.Series([timedelta(1)]).dt.epoch()105810591060@pytest.mark.parametrize(1061"time_unit",1062["ms", "us", "ns"],1063ids=["milliseconds", "microseconds", "nanoseconds"],1064)1065def test_weekday(time_unit: TimeUnit) -> None:1066friday = pl.Series([datetime(2023, 2, 17)])10671068assert friday.dt.cast_time_unit(time_unit).dt.weekday()[0] == 51069assert friday.cast(pl.Date).dt.weekday()[0] == 5107010711072@pytest.mark.parametrize(1073("values", "expected_median"),1074[1075([], None),1076([None, None], None),1077([date(2022, 1, 1)], datetime(2022, 1, 1)),1078([date(2022, 1, 1), date(2022, 1, 2), date(2022, 1, 4)], datetime(2022, 1, 2)),1079([date(2022, 1, 1), date(2022, 1, 2), date(2024, 5, 15)], datetime(2022, 1, 2)),1080([datetime(2022, 1, 1)], datetime(2022, 1, 1)),1081(1082[datetime(2022, 1, 1), datetime(2022, 1, 2), datetime(2022, 1, 3)],1083datetime(2022, 1, 2),1084),1085(1086[datetime(2022, 1, 1), datetime(2022, 1, 2), datetime(2024, 5, 15)],1087datetime(2022, 1, 2),1088),1089([timedelta(days=1)], timedelta(days=1)),1090([timedelta(days=1), timedelta(days=2), timedelta(days=3)], timedelta(days=2)),1091([timedelta(days=1), timedelta(days=2), timedelta(days=15)], timedelta(days=2)),1092([time(hour=1)], time(hour=1)),1093([time(hour=1), time(hour=2), time(hour=3)], time(hour=2)),1094([time(hour=1), time(hour=2), time(hour=15)], time(hour=2)),1095],1096ids=[1097"empty",1098"Nones",1099"single_date",1100"spread_even_date",1101"spread_skewed_date",1102"single_datetime",1103"spread_even_datetime",1104"spread_skewed_datetime",1105"single_dur",1106"spread_even_dur",1107"spread_skewed_dur",1108"single_time",1109"spread_even_time",1110"spread_skewed_time",1111],1112)1113def test_median(1114values: list[TemporalLiteral | None], expected_median: TemporalLiteral | None1115) -> None:1116assert pl.Series(values).median() == expected_median111711181119@pytest.mark.parametrize(1120("values", "expected_mean"),1121[1122([], None),1123([None, None], None),1124([date(2022, 1, 1)], datetime(2022, 1, 1)),1125(1126[date(2022, 1, 1), date(2022, 1, 2), date(2022, 1, 4)],1127datetime(2022, 1, 2, 8),1128),1129(1130[date(2022, 1, 1), date(2022, 1, 2), date(2024, 5, 15)],1131datetime(2022, 10, 16, 16, 0),1132),1133([datetime(2022, 1, 1)], datetime(2022, 1, 1)),1134(1135[datetime(2022, 1, 1), datetime(2022, 1, 2), datetime(2022, 1, 3)],1136datetime(2022, 1, 2),1137),1138(1139[datetime(2022, 1, 1), datetime(2022, 1, 2), datetime(2024, 5, 15)],1140datetime(2022, 10, 16, 16, 0, 0),1141),1142([timedelta(days=1)], timedelta(days=1)),1143([timedelta(days=1), timedelta(days=2), timedelta(days=3)], timedelta(days=2)),1144([timedelta(days=1), timedelta(days=2), timedelta(days=15)], timedelta(days=6)),1145([time(hour=1)], time(hour=1)),1146([time(hour=1), time(hour=2), time(hour=3)], time(hour=2)),1147([time(hour=1), time(hour=2), time(hour=15)], time(hour=6)),1148],1149ids=[1150"empty",1151"Nones",1152"single_date",1153"spread_even_date",1154"spread_skewed_date",1155"single_datetime",1156"spread_even_datetime",1157"spread_skewed_datetime",1158"single_duration",1159"spread_even_duration",1160"spread_skewed_duration",1161"single_time",1162"spread_even_time",1163"spread_skewed_time",1164],1165)1166def test_mean(1167values: list[TemporalLiteral | None], expected_mean: TemporalLiteral | None1168) -> None:1169assert pl.Series(values).mean() == expected_mean117011711172@pytest.mark.parametrize(1173("values", "expected_mean"),1174[1175([None], None),1176(1177[datetime(2022, 1, 1), datetime(2022, 1, 2), datetime(2024, 5, 15)],1178datetime(2022, 10, 16, 16, 0, 0),1179),1180],1181ids=["None_dt", "spread_skewed_dt"],1182)1183@pytest.mark.parametrize("time_unit", ["ms", "us", "ns"])1184def test_datetime_mean_with_tu(1185values: list[datetime], expected_mean: datetime, time_unit: TimeUnit1186) -> None:1187assert pl.Series(values, dtype=pl.Duration(time_unit)).mean() == expected_mean118811891190@pytest.mark.parametrize(1191("values", "expected_median"),1192[1193([None], None),1194(1195[datetime(2022, 1, 1), datetime(2022, 1, 2), datetime(2024, 5, 15)],1196datetime(2022, 1, 2),1197),1198],1199ids=["None_dt", "spread_skewed_dt"],1200)1201@pytest.mark.parametrize("time_unit", ["ms", "us", "ns"])1202def test_datetime_median_with_tu(1203values: list[datetime], expected_median: datetime, time_unit: TimeUnit1204) -> None:1205assert pl.Series(values, dtype=pl.Duration(time_unit)).median() == expected_median120612071208def test_date_median_upcast() -> None:1209df = pl.DataFrame({"a": [date(2022, 1, 1), date(2022, 1, 2), date(2024, 5, 15)]})1210result = df.select(pl.col("a").median())1211expected = pl.DataFrame(1212{"a": pl.Series([datetime(2022, 1, 2)], dtype=pl.Datetime("us"))}1213)1214assert_frame_equal(result, expected)121512161217@pytest.mark.parametrize(1218("values", "expected_mean"),1219[1220([None], None),1221(1222[timedelta(days=1), timedelta(days=2), timedelta(days=15)],1223timedelta(days=6),1224),1225],1226ids=["None_dur", "spread_skewed_dur"],1227)1228@pytest.mark.parametrize("time_unit", ["ms", "us", "ns"])1229def test_duration_mean_with_tu(1230values: list[timedelta], expected_mean: timedelta, time_unit: TimeUnit1231) -> None:1232assert pl.Series(values, dtype=pl.Duration(time_unit)).mean() == expected_mean123312341235@pytest.mark.parametrize(1236("values", "expected_median"),1237[1238([None], None),1239(1240[timedelta(days=1), timedelta(days=2), timedelta(days=15)],1241timedelta(days=2),1242),1243],1244ids=["None_dur", "spread_skewed_dur"],1245)1246@pytest.mark.parametrize("time_unit", ["ms", "us", "ns"])1247def test_duration_median_with_tu(1248values: list[timedelta], expected_median: timedelta, time_unit: TimeUnit1249) -> None:1250assert pl.Series(values, dtype=pl.Duration(time_unit)).median() == expected_median125112521253def test_agg_mean_expr() -> None:1254df = pl.DataFrame(1255{1256"date": pl.Series(1257[date(2023, 1, 1), date(2023, 1, 2), date(2023, 1, 4)],1258dtype=pl.Date,1259),1260"datetime_ms": pl.Series(1261[datetime(2023, 1, 1), datetime(2023, 1, 2), datetime(2023, 1, 4)],1262dtype=pl.Datetime("ms"),1263),1264"datetime_us": pl.Series(1265[datetime(2023, 1, 1), datetime(2023, 1, 2), datetime(2023, 1, 4)],1266dtype=pl.Datetime("us"),1267),1268"datetime_ns": pl.Series(1269[datetime(2023, 1, 1), datetime(2023, 1, 2), datetime(2023, 1, 4)],1270dtype=pl.Datetime("ns"),1271),1272"duration_ms": pl.Series(1273[timedelta(days=1), timedelta(days=2), timedelta(days=4)],1274dtype=pl.Duration("ms"),1275),1276"duration_us": pl.Series(1277[timedelta(days=1), timedelta(days=2), timedelta(days=4)],1278dtype=pl.Duration("us"),1279),1280"duration_ns": pl.Series(1281[timedelta(days=1), timedelta(days=2), timedelta(days=4)],1282dtype=pl.Duration("ns"),1283),1284"time": pl.Series(1285[time(hour=1), time(hour=2), time(hour=4)],1286dtype=pl.Time,1287),1288}1289)12901291expected = pl.DataFrame(1292{1293"date": pl.Series([datetime(2023, 1, 2, 8, 0)], dtype=pl.Datetime("us")),1294"datetime_ms": pl.Series(1295[datetime(2023, 1, 2, 8, 0, 0)], dtype=pl.Datetime("ms")1296),1297"datetime_us": pl.Series(1298[datetime(2023, 1, 2, 8, 0, 0)], dtype=pl.Datetime("us")1299),1300"datetime_ns": pl.Series(1301[datetime(2023, 1, 2, 8, 0, 0)], dtype=pl.Datetime("ns")1302),1303"duration_ms": pl.Series(1304[timedelta(days=2, hours=8)], dtype=pl.Duration("ms")1305),1306"duration_us": pl.Series(1307[timedelta(days=2, hours=8)], dtype=pl.Duration("us")1308),1309"duration_ns": pl.Series(1310[timedelta(days=2, hours=8)], dtype=pl.Duration("ns")1311),1312"time": pl.Series([time(hour=2, minute=20)], dtype=pl.Time),1313}1314)13151316assert_frame_equal(df.select(pl.all().mean()), expected)131713181319def test_agg_median_expr() -> None:1320df = pl.DataFrame(1321{1322"date": pl.Series(1323[date(2023, 1, 1), date(2023, 1, 2), date(2023, 1, 4)],1324dtype=pl.Date,1325),1326"datetime_ms": pl.Series(1327[datetime(2023, 1, 1), datetime(2023, 1, 2), datetime(2023, 1, 4)],1328dtype=pl.Datetime("ms"),1329),1330"datetime_us": pl.Series(1331[datetime(2023, 1, 1), datetime(2023, 1, 2), datetime(2023, 1, 4)],1332dtype=pl.Datetime("us"),1333),1334"datetime_ns": pl.Series(1335[datetime(2023, 1, 1), datetime(2023, 1, 2), datetime(2023, 1, 4)],1336dtype=pl.Datetime("ns"),1337),1338"duration_ms": pl.Series(1339[timedelta(days=1), timedelta(days=2), timedelta(days=4)],1340dtype=pl.Duration("ms"),1341),1342"duration_us": pl.Series(1343[timedelta(days=1), timedelta(days=2), timedelta(days=4)],1344dtype=pl.Duration("us"),1345),1346"duration_ns": pl.Series(1347[timedelta(days=1), timedelta(days=2), timedelta(days=4)],1348dtype=pl.Duration("ns"),1349),1350"time": pl.Series(1351[time(hour=1), time(hour=2), time(hour=4)],1352dtype=pl.Time,1353),1354}1355)13561357expected = pl.DataFrame(1358{1359"date": pl.Series([datetime(2023, 1, 2)], dtype=pl.Datetime("us")),1360"datetime_ms": pl.Series([datetime(2023, 1, 2)], dtype=pl.Datetime("ms")),1361"datetime_us": pl.Series([datetime(2023, 1, 2)], dtype=pl.Datetime("us")),1362"datetime_ns": pl.Series([datetime(2023, 1, 2)], dtype=pl.Datetime("ns")),1363"duration_ms": pl.Series([timedelta(days=2)], dtype=pl.Duration("ms")),1364"duration_us": pl.Series([timedelta(days=2)], dtype=pl.Duration("us")),1365"duration_ns": pl.Series([timedelta(days=2)], dtype=pl.Duration("ns")),1366"time": pl.Series([time(hour=2)], dtype=pl.Time),1367}1368)13691370assert_frame_equal(df.select(pl.all().median()), expected)137113721373@given(1374s=series(min_size=1, max_size=10, dtype=pl.Duration),1375)1376@pytest.mark.skip(1377"These functions are currently bugged for large values: "1378"https://github.com/pola-rs/polars/issues/16057"1379)1380def test_series_duration_timeunits(1381s: pl.Series,1382) -> None:1383nanos = s.dt.total_nanoseconds().to_list()1384micros = s.dt.total_microseconds().to_list()1385millis = s.dt.total_milliseconds().to_list()13861387scale = {1388"ns": 1,1389"us": 1_000,1390"ms": 1_000_000,1391}1392assert nanos == [v * scale[s.dtype.time_unit] for v in s.to_physical()] # type: ignore[attr-defined]1393assert micros == [int(v / 1_000) for v in nanos]1394assert millis == [int(v / 1_000) for v in micros]13951396# special handling for ns timeunit (as we may generate a microsecs-based1397# timedelta that results in 64bit overflow on conversion to nanosecs)1398lower_bound, upper_bound = -(2**63), (2**63) - 11399if all(1400(lower_bound <= (us * 1000) <= upper_bound)1401for us in micros1402if isinstance(us, int)1403):1404for ns, us in zip(s.dt.total_nanoseconds(), micros, strict=True):1405assert ns == (us * 1000)140614071408@given(1409s=series(min_size=1, max_size=10, dtype=pl.Datetime, allow_null=False),1410)1411def test_series_datetime_timeunits(1412s: pl.Series,1413) -> None:1414# datetime1415assert s.to_list() == list(s)1416assert list(s.dt.millisecond()) == [v.microsecond // 1000 for v in s]1417assert list(s.dt.nanosecond()) == [v.microsecond * 1000 for v in s]1418assert list(s.dt.microsecond()) == [v.microsecond for v in s]141914201421def test_dt_median_deprecated() -> None:1422values = [date(2022, 1, 1), date(2022, 1, 2), date(2024, 5, 15)]1423s = pl.Series(values)1424with pytest.deprecated_call():1425result = s.dt.median()1426assert result == s.median()142714281429def test_dt_mean_deprecated() -> None:1430values = [date(2022, 1, 1), date(2022, 1, 2), date(2024, 5, 15)]1431s = pl.Series(values)1432with pytest.deprecated_call():1433result = s.dt.mean()1434assert result == s.mean()143514361437@pytest.mark.parametrize(1438"dtype",1439[1440pl.Date,1441pl.Datetime("ms"),1442pl.Datetime("ms", "America/New_York"),1443pl.Datetime("us"),1444pl.Datetime("us", "America/New_York"),1445pl.Datetime("ns"),1446pl.Datetime("ns", "America/New_York"),1447],1448)1449@pytest.mark.parametrize(1450"value",1451[1452date(1677, 9, 22),1453date(1970, 1, 1),1454date(2024, 2, 29),1455],1456)1457def test_literal_from_date(1458value: date,1459dtype: PolarsDataType,1460) -> None:1461out = pl.select(pl.lit(value, dtype=dtype))1462assert out.schema == OrderedDict({"literal": dtype})1463if dtype == pl.Datetime:1464tz = ZoneInfo(dtype.time_zone) if dtype.time_zone is not None else None # type: ignore[union-attr]1465value = datetime(value.year, value.month, value.day, tzinfo=tz)1466assert out.item() == value146714681469@pytest.mark.parametrize(1470"dtype",1471[1472pl.Date,1473pl.Datetime("ms"),1474pl.Datetime("ms", "America/New_York"),1475pl.Datetime("us"),1476pl.Datetime("us", "America/New_York"),1477pl.Datetime("ns"),1478pl.Datetime("ns", "America/New_York"),1479],1480)1481@pytest.mark.parametrize(1482"value",1483[1484datetime(1677, 9, 22),1485datetime(1677, 9, 22, tzinfo=ZoneInfo("America/New_York")),1486datetime(1970, 1, 1),1487datetime(1970, 1, 1, tzinfo=ZoneInfo("America/New_York")),1488datetime(2024, 2, 29),1489datetime(2024, 2, 29, tzinfo=ZoneInfo("America/New_York")),1490],1491)1492def test_literal_from_datetime(1493value: datetime,1494dtype: pl.Date | pl.Datetime,1495) -> None:1496out = pl.select(pl.lit(value, dtype=dtype))1497if dtype == pl.Date:1498value = value.date() # type: ignore[assignment]1499elif dtype.time_zone is None and value.tzinfo is not None: # type: ignore[union-attr]1500# update the dtype with the supplied time zone in the value1501dtype = pl.Datetime(dtype.time_unit, str(value.tzinfo)) # type: ignore[union-attr]1502elif dtype.time_zone is not None and value.tzinfo is None: # type: ignore[union-attr]1503# cast from dt without tz to dtype with tz1504value = value.replace(tzinfo=ZoneInfo(dtype.time_zone)) # type: ignore[union-attr]15051506assert out.schema == OrderedDict({"literal": dtype})1507assert out.item() == value150815091510@pytest.mark.parametrize(1511"value",1512[1513time(0),1514time(hour=1),1515time(hour=16, minute=43, microsecond=500),1516time(hour=23, minute=59, second=59, microsecond=999999),1517],1518)1519def test_literal_from_time(value: time) -> None:1520out = pl.select(pl.lit(value))1521assert out.schema == OrderedDict({"literal": pl.Time})1522assert out.item() == value152315241525@pytest.mark.parametrize(1526"dtype",1527[1528None,1529pl.Duration("ms"),1530pl.Duration("us"),1531pl.Duration("ns"),1532],1533)1534@pytest.mark.parametrize(1535"value",1536[1537timedelta(0),1538timedelta(hours=1),1539timedelta(days=-99999),1540timedelta(days=99999),1541],1542)1543def test_literal_from_timedelta(value: time, dtype: pl.Duration | None) -> None:1544out = pl.select(pl.lit(value, dtype=dtype))1545assert out.schema == OrderedDict({"literal": dtype or pl.Duration("us")})1546assert out.item() == value154715481549def test_out_of_range_date_year_11991() -> None:1550# Out-of-range dates should return null instead of wrong values or panicking1551# Regression test for #11991 where out-of-range dates silently returned1552# the input value1553s = pl.Series([-96_465_659]).cast(pl.Date)1554result = s.dt.year()1555# Should return null, not the input value -964656591556assert result[0] is None15571558# is_leap_year should also return null for out-of-range dates1559result_leap = s.dt.is_leap_year()1560assert result_leap[0] is None156115621563