Path: blob/main/py-polars/tests/unit/operations/namespaces/temporal/test_round.py
8424 views
from __future__ import annotations12from datetime import date, datetime, timedelta3from typing import TYPE_CHECKING4from zoneinfo import ZoneInfo56import hypothesis.strategies as st7import pytest8from hypothesis import given910import polars as pl11from polars._utils.convert import parse_as_duration_string12from polars.testing import assert_series_equal1314if TYPE_CHECKING:15from polars._typing import RoundMode16from polars.type_aliases import TimeUnit171819@pytest.mark.parametrize("time_zone", [None, "Asia/Kathmandu"])20def test_round_by_day_datetime(time_zone: str | None) -> None:21ser = pl.Series([datetime(2021, 11, 7, 3)]).dt.replace_time_zone(time_zone)22result = ser.dt.round("1d")23expected = pl.Series([datetime(2021, 11, 7)]).dt.replace_time_zone(time_zone)24assert_series_equal(result, expected)252627def test_round_ambiguous() -> None:28t = (29pl.datetime_range(30date(2020, 10, 25),31datetime(2020, 10, 25, 2),32"30m",33eager=True,34time_zone="Europe/London",35)36.alias("datetime")37.dt.offset_by("15m")38)39result = t.dt.round("30m")40expected = (41pl.Series(42[43"2020-10-25T00:30:00+0100",44"2020-10-25T01:00:00+0100",45"2020-10-25T01:30:00+0100",46"2020-10-25T01:00:00+0000",47"2020-10-25T01:30:00+0000",48"2020-10-25T02:00:00+0000",49"2020-10-25T02:30:00+0000",50]51)52.str.to_datetime()53.dt.convert_time_zone("Europe/London")54.rename("datetime")55)56assert_series_equal(result, expected)5758df = pl.DataFrame(59{60"date": pl.datetime_range(61date(2020, 10, 25),62datetime(2020, 10, 25, 2),63"30m",64eager=True,65time_zone="Europe/London",66).dt.offset_by("15m")67}68)6970df = df.select(pl.col("date").dt.round("30m"))71assert df.to_dict(as_series=False) == {72"date": [73datetime(2020, 10, 25, 0, 30, tzinfo=ZoneInfo("Europe/London")),74datetime(2020, 10, 25, 1, tzinfo=ZoneInfo("Europe/London")),75datetime(2020, 10, 25, 1, 30, tzinfo=ZoneInfo("Europe/London")),76datetime(2020, 10, 25, 1, tzinfo=ZoneInfo("Europe/London")),77datetime(2020, 10, 25, 1, 30, tzinfo=ZoneInfo("Europe/London")),78datetime(2020, 10, 25, 2, tzinfo=ZoneInfo("Europe/London")),79datetime(2020, 10, 25, 2, 30, tzinfo=ZoneInfo("Europe/London")),80]81}828384def test_round_by_week() -> None:85df = pl.DataFrame(86{87"date": pl.Series(88[89# Sunday and Monday90"1998-04-12",91"2022-11-28",92]93).str.strptime(pl.Date, "%Y-%m-%d")94}95)9697assert (98df.select(99pl.col("date").dt.round("7d").alias("7d"),100pl.col("date").dt.round("1w").alias("1w"),101)102).to_dict(as_series=False) == {103"7d": [date(1998, 4, 9), date(2022, 12, 1)],104"1w": [date(1998, 4, 13), date(2022, 11, 28)],105}106107108@given(109datetimes=st.lists(110st.datetimes(min_value=datetime(1960, 1, 1), max_value=datetime(1980, 1, 1)),111min_size=1,112max_size=3,113),114every=st.timedeltas(115min_value=timedelta(microseconds=1), max_value=timedelta(days=1)116).map(parse_as_duration_string),117)118def test_dt_round_fast_path_vs_slow_path(datetimes: list[datetime], every: str) -> None:119s = pl.Series(datetimes)120# Might use fastpath:121result = s.dt.round(every)122# Definitely uses slowpath:123expected = s.dt.round(pl.Series([every] * len(datetimes)))124assert_series_equal(result, expected)125126127def test_round_date() -> None:128# n vs n129df = pl.DataFrame(130{"a": [date(2020, 1, 1), None, date(2020, 1, 19)], "b": [None, "1mo", "1mo"]}131)132result = df.select(pl.col("a").dt.round(pl.col("b")))["a"]133expected = pl.Series("a", [None, None, date(2020, 2, 1)])134assert_series_equal(result, expected)135136# n vs 1137df = pl.DataFrame(138{"a": [date(2020, 1, 1), None, date(2020, 1, 3)], "b": [None, "1mo", "1mo"]}139)140result = df.select(pl.col("a").dt.round("1mo"))["a"]141expected = pl.Series("a", [date(2020, 1, 1), None, date(2020, 1, 1)])142assert_series_equal(result, expected)143144# n vs missing145df = pl.DataFrame(146{"a": [date(2020, 1, 1), None, date(2020, 1, 3)], "b": [None, "1mo", "1mo"]}147)148result = df.select(pl.col("a").dt.round(pl.lit(None, dtype=pl.String)))["a"]149expected = pl.Series("a", [None, None, None], dtype=pl.Date)150assert_series_equal(result, expected)151152# 1 vs n153df = pl.DataFrame(154{"a": [date(2020, 1, 1), None, date(2020, 1, 3)], "b": [None, "1mo", "1mo"]}155)156result = df.select(a=pl.date(2020, 1, 1).dt.round(pl.col("b")))["a"]157expected = pl.Series("a", [None, date(2020, 1, 1), date(2020, 1, 1)])158assert_series_equal(result, expected)159160# missing vs n161df = pl.DataFrame(162{"a": [date(2020, 1, 1), None, date(2020, 1, 3)], "b": [None, "1mo", "1mo"]}163)164result = df.select(a=pl.lit(None, dtype=pl.Date).dt.round(pl.col("b")))["a"]165expected = pl.Series("a", [None, None, None], dtype=pl.Date)166assert_series_equal(result, expected)167168169@pytest.mark.parametrize("time_unit", ["ms", "us", "ns"])170def test_round_datetime_simple(time_unit: TimeUnit) -> None:171s = pl.Series([datetime(2020, 1, 2, 6)], dtype=pl.Datetime(time_unit))172result = s.dt.round("1mo").item()173assert result == datetime(2020, 1, 1)174result = s.dt.round("1d").item()175assert result == datetime(2020, 1, 2)176177178@pytest.mark.parametrize("time_unit", ["ms", "us", "ns"])179def test_round_datetime_w_expression(time_unit: TimeUnit) -> None:180df = pl.DataFrame(181{"a": [datetime(2020, 1, 2, 6), datetime(2020, 1, 20, 21)], "b": ["1mo", "1d"]},182schema_overrides={"a": pl.Datetime(time_unit)},183)184result = df.select(pl.col("a").dt.round(pl.col("b")))["a"]185assert result[0] == datetime(2020, 1, 1)186assert result[1] == datetime(2020, 1, 21)187188189@pytest.mark.parametrize(190("time_unit", "expected"),191[192("ms", 0),193("us", 0),194("ns", 0),195],196)197def test_round_negative_towards_epoch_18239(time_unit: TimeUnit, expected: int) -> None:198s = pl.Series([datetime(1970, 1, 1)], dtype=pl.Datetime(time_unit))199s = s.dt.offset_by(f"-1{time_unit}")200result = s.dt.round(f"2{time_unit}").dt.timestamp(time_unit="ns").item()201assert result == expected202result = (203s.dt.replace_time_zone("Europe/London")204.dt.round(f"2{time_unit}")205.dt.replace_time_zone(None)206.dt.timestamp(time_unit="ns")207.item()208)209assert result == expected210211212@pytest.mark.parametrize(213("time_unit", "expected"),214[215("ms", 2_000_000),216("us", 2_000),217("ns", 2),218],219)220def test_round_positive_away_from_epoch_18239(221time_unit: TimeUnit, expected: int222) -> None:223s = pl.Series([datetime(1970, 1, 1)], dtype=pl.Datetime(time_unit))224s = s.dt.offset_by(f"1{time_unit}")225result = s.dt.round(f"2{time_unit}").dt.timestamp(time_unit="ns").item()226assert result == expected227result = (228s.dt.replace_time_zone("Europe/London")229.dt.round(f"2{time_unit}")230.dt.replace_time_zone(None)231.dt.timestamp(time_unit="ns")232.item()233)234assert result == expected235236237@pytest.mark.parametrize("as_date", [False, True])238def test_round_unequal_length_22018(as_date: bool) -> None:239start = datetime(2001, 1, 1)240stop = datetime(2001, 1, 1, 1)241s = pl.datetime_range(start, stop, "10m", eager=True).alias("datetime")242if as_date:243s = s.dt.date()244245with pytest.raises(pl.exceptions.ShapeError):246s.dt.round(pl.Series(["30m", "20m"]))247248249@pytest.mark.parametrize("mode", ["half_to_even", "half_away_from_zero"])250def test_round_small(mode: RoundMode) -> None:251small = 1.234e-320252small_s = pl.Series([small])253assert small_s.round(mode=mode).item() == 0.0254assert small_s.round(320, mode=mode).item() == 1e-320255assert small_s.round(321, mode=mode).item() == 1.2e-320256assert small_s.round(322, mode=mode).item() == 1.23e-320257assert small_s.round(323, mode=mode).item() == 1.234e-320258assert small_s.round(324, mode=mode).item() == small259assert small_s.round(1000, mode=mode).item() == small260261assert small_s.round_sig_figs(1).item() == 1e-320262assert small_s.round_sig_figs(2).item() == 1.2e-320263assert small_s.round_sig_figs(3).item() == 1.23e-320264assert small_s.round_sig_figs(4).item() == 1.234e-320265assert small_s.round_sig_figs(5).item() == small266assert small_s.round_sig_figs(1000).item() == small267268269@pytest.mark.parametrize("mode", ["half_to_even", "half_away_from_zero"])270def test_round_big(mode: RoundMode) -> None:271big = 1.234e308272max_err = big / 10**10273big_s = pl.Series([big])274assert big_s.round(mode=mode).item() == big275assert big_s.round(1, mode=mode).item() == big276assert big_s.round(100, mode=mode).item() == big277278assert abs(big_s.round_sig_figs(1).item() - 1e308) <= max_err279assert abs(big_s.round_sig_figs(2).item() - 1.2e308) <= max_err280assert abs(big_s.round_sig_figs(3).item() - 1.23e308) <= max_err281assert abs(big_s.round_sig_figs(4).item() - 1.234e308) <= max_err282assert abs(big_s.round_sig_figs(4).item() - big) <= max_err283assert big_s.round_sig_figs(100).item() == big284285286