From 91a7c7eb53cecafc51d94d458491ffa197f48841 Mon Sep 17 00:00:00 2001 From: OlivierLuG Date: Thu, 11 Jun 2020 23:17:32 +0200 Subject: [PATCH 01/10] BUG #34621 added nanosecond support to class Period --- pandas/_libs/tslibs/period.pyx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/pandas/_libs/tslibs/period.pyx b/pandas/_libs/tslibs/period.pyx index 47ebf139ed496..c911566bd5510 100644 --- a/pandas/_libs/tslibs/period.pyx +++ b/pandas/_libs/tslibs/period.pyx @@ -2355,6 +2355,7 @@ class Period(_Period): if freq is not None: freq = cls._maybe_convert_freq(freq) + nanosecond = 0 if ordinal is not None and value is not None: raise ValueError("Only value or ordinal but not both should be " @@ -2404,6 +2405,13 @@ class Period(_Period): value = str(value) value = value.upper() dt, reso = parse_time_string(value, freq) + try: + ts = Timestamp(value, freq=freq) + nanosecond = ts.nanosecond + if nanosecond != 0: + reso = 'nanosecond' + except: + pass if dt is NaT: ordinal = NPY_NAT @@ -2435,7 +2443,7 @@ class Period(_Period): base = freq_to_dtype_code(freq) ordinal = period_ordinal(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, - dt.microsecond, 0, base) + dt.microsecond, nanosecond, base) return cls._from_ordinal(ordinal, freq) From 73a6eab7b5c9953929a95b696b7d025d40d3fee4 Mon Sep 17 00:00:00 2001 From: OlivierLuG Date: Fri, 12 Jun 2020 23:43:05 +0200 Subject: [PATCH 02/10] BUG #34621 added tests --- pandas/_libs/tslibs/period.pyx | 7 ++++--- pandas/tests/scalar/period/test_period.py | 17 +++++++++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/pandas/_libs/tslibs/period.pyx b/pandas/_libs/tslibs/period.pyx index c911566bd5510..80a2839c1dffe 100644 --- a/pandas/_libs/tslibs/period.pyx +++ b/pandas/_libs/tslibs/period.pyx @@ -32,6 +32,7 @@ from pandas._libs.tslibs.np_datetime cimport ( NPY_FR_D, NPY_FR_us, ) +from pandas._libs.tslibs.np_datetime import OutOfBoundsDatetime cdef extern from "src/datetime/np_datetime.h": int64_t npy_datetimestruct_to_datetime(NPY_DATETIMEUNIT fr, @@ -2410,8 +2411,8 @@ class Period(_Period): nanosecond = ts.nanosecond if nanosecond != 0: reso = 'nanosecond' - except: - pass + except OutOfBoundsDatetime: + nanosecond = 0 if dt is NaT: ordinal = NPY_NAT @@ -2443,7 +2444,7 @@ class Period(_Period): base = freq_to_dtype_code(freq) ordinal = period_ordinal(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, - dt.microsecond, nanosecond, base) + dt.microsecond, 1000*nanosecond, base) return cls._from_ordinal(ordinal, freq) diff --git a/pandas/tests/scalar/period/test_period.py b/pandas/tests/scalar/period/test_period.py index 702899f163e06..2bad9b6c7bedb 100644 --- a/pandas/tests/scalar/period/test_period.py +++ b/pandas/tests/scalar/period/test_period.py @@ -484,6 +484,23 @@ def test_period_cons_combined(self): with pytest.raises(ValueError, match=msg): Period("2011-01", freq="1D1W") + #@pytest.mark.xfail + @pytest.mark.parametrize("day_", ["1970/01/01 ", "2020-12-31 ", "1981/09/13 "]) + @pytest.mark.parametrize("hour_", ["00:00:00", "00:00:01", "23:59:59", "12:00:59"]) + @pytest.mark.parametrize( + "floating_sec_, expected", + [ + (".000000001", 1), + (".000000999", 999), + (".123456789", 789), + (".999999999", 999), + ], + ) + def test_period_constructor_nanosecond(self, day_, hour_, floating_sec_, expected): + # GH 34621 + result = Period(day_ + hour_ + floating_sec_).start_time.nanosecond + assert result == expected + class TestPeriodMethods: def test_round_trip(self): From ea9f69ce144a815c151a2f60ac458d3d5f2dd297 Mon Sep 17 00:00:00 2001 From: OlivierLuG Date: Fri, 12 Jun 2020 23:52:37 +0200 Subject: [PATCH 03/10] BUG #34621 added tests (black pandas passed) --- pandas/tests/scalar/period/test_period.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pandas/tests/scalar/period/test_period.py b/pandas/tests/scalar/period/test_period.py index 2bad9b6c7bedb..52f46a2767b45 100644 --- a/pandas/tests/scalar/period/test_period.py +++ b/pandas/tests/scalar/period/test_period.py @@ -484,7 +484,6 @@ def test_period_cons_combined(self): with pytest.raises(ValueError, match=msg): Period("2011-01", freq="1D1W") - #@pytest.mark.xfail @pytest.mark.parametrize("day_", ["1970/01/01 ", "2020-12-31 ", "1981/09/13 "]) @pytest.mark.parametrize("hour_", ["00:00:00", "00:00:01", "23:59:59", "12:00:59"]) @pytest.mark.parametrize( From ab5640f6ef9160ba86028ccd25e96d076d80f528 Mon Sep 17 00:00:00 2001 From: OlivierLuG Date: Sat, 13 Jun 2020 15:23:58 +0200 Subject: [PATCH 04/10] #34131 fix one test failure --- pandas/_libs/tslibs/period.pyx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/_libs/tslibs/period.pyx b/pandas/_libs/tslibs/period.pyx index 80a2839c1dffe..6b9fcbfbdd2f4 100644 --- a/pandas/_libs/tslibs/period.pyx +++ b/pandas/_libs/tslibs/period.pyx @@ -2411,7 +2411,7 @@ class Period(_Period): nanosecond = ts.nanosecond if nanosecond != 0: reso = 'nanosecond' - except OutOfBoundsDatetime: + except (ValueError, OutOfBoundsDatetime): nanosecond = 0 if dt is NaT: ordinal = NPY_NAT From 95d740e5fd9e2be58b6004baf22ab9e154a2678e Mon Sep 17 00:00:00 2001 From: OlivierLuG Date: Mon, 15 Jun 2020 21:58:33 +0200 Subject: [PATCH 05/10] #34621 review taken into account --- pandas/_libs/tslibs/period.pyx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pandas/_libs/tslibs/period.pyx b/pandas/_libs/tslibs/period.pyx index 6b9fcbfbdd2f4..c4be74e5e2bcd 100644 --- a/pandas/_libs/tslibs/period.pyx +++ b/pandas/_libs/tslibs/period.pyx @@ -2407,11 +2407,11 @@ class Period(_Period): value = value.upper() dt, reso = parse_time_string(value, freq) try: - ts = Timestamp(value, freq=freq) + ts = Timestamp(value) nanosecond = ts.nanosecond if nanosecond != 0: reso = 'nanosecond' - except (ValueError, OutOfBoundsDatetime): + except ValueError: nanosecond = 0 if dt is NaT: ordinal = NPY_NAT @@ -2498,7 +2498,7 @@ def quarter_to_myear(year: int, quarter: int, freq): mnum = c_MONTH_NUMBERS[get_rule_month(freq)] + 1 month = (mnum + (quarter - 1) * 3) % 12 + 1 if month > mnum: - year -= 1 + year -= 1ough to only use ValueError, as that would already cover the OutOfBoundsDatet return year, month # TODO: This whole func is really similar to parsing.pyx L434-L450 From abb09363c9ff6e2741ad31aac81cb781499d484e Mon Sep 17 00:00:00 2001 From: OlivierLuG Date: Mon, 15 Jun 2020 22:09:30 +0200 Subject: [PATCH 06/10] #34621 review taken into account, and suppress a bad copy/paste on line 2501 --- pandas/_libs/tslibs/period.pyx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/_libs/tslibs/period.pyx b/pandas/_libs/tslibs/period.pyx index c4be74e5e2bcd..0c89de5a071a6 100644 --- a/pandas/_libs/tslibs/period.pyx +++ b/pandas/_libs/tslibs/period.pyx @@ -2498,7 +2498,7 @@ def quarter_to_myear(year: int, quarter: int, freq): mnum = c_MONTH_NUMBERS[get_rule_month(freq)] + 1 month = (mnum + (quarter - 1) * 3) % 12 + 1 if month > mnum: - year -= 1ough to only use ValueError, as that would already cover the OutOfBoundsDatet + year -= 1 return year, month # TODO: This whole func is really similar to parsing.pyx L434-L450 From 8a559cb07f2c731b0ab83fe08ccc20c59b5157b9 Mon Sep 17 00:00:00 2001 From: OlivierLuG Date: Tue, 16 Jun 2020 20:42:49 +0200 Subject: [PATCH 07/10] #34621 Added nanosecond support to Period constructor --- pandas/_libs/tslibs/period.pyx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pandas/_libs/tslibs/period.pyx b/pandas/_libs/tslibs/period.pyx index 0c89de5a071a6..cb0fd7d1aa380 100644 --- a/pandas/_libs/tslibs/period.pyx +++ b/pandas/_libs/tslibs/period.pyx @@ -32,7 +32,6 @@ from pandas._libs.tslibs.np_datetime cimport ( NPY_FR_D, NPY_FR_us, ) -from pandas._libs.tslibs.np_datetime import OutOfBoundsDatetime cdef extern from "src/datetime/np_datetime.h": int64_t npy_datetimestruct_to_datetime(NPY_DATETIMEUNIT fr, @@ -2408,11 +2407,12 @@ class Period(_Period): dt, reso = parse_time_string(value, freq) try: ts = Timestamp(value) + except ValueError: + nanosecond = 0 + else: nanosecond = ts.nanosecond if nanosecond != 0: reso = 'nanosecond' - except ValueError: - nanosecond = 0 if dt is NaT: ordinal = NPY_NAT From 63e72d8ffb1c70fb9a9d6ae4bc8aebe2c6f34a27 Mon Sep 17 00:00:00 2001 From: arw2019 Date: Mon, 30 Nov 2020 05:42:18 +0000 Subject: [PATCH 08/10] rewrite test --- pandas/tests/scalar/period/test_period.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/pandas/tests/scalar/period/test_period.py b/pandas/tests/scalar/period/test_period.py index d635ec0e227ea..9b87e32510b41 100644 --- a/pandas/tests/scalar/period/test_period.py +++ b/pandas/tests/scalar/period/test_period.py @@ -487,10 +487,10 @@ def test_period_cons_combined(self): with pytest.raises(ValueError, match=msg): Period("2011-01", freq="1D1W") - @pytest.mark.parametrize("day_", ["1970/01/01 ", "2020-12-31 ", "1981/09/13 "]) - @pytest.mark.parametrize("hour_", ["00:00:00", "00:00:01", "23:59:59", "12:00:59"]) + @pytest.mark.parametrize("day", ["1970/01/01 ", "2020-12-31 ", "1981/09/13 "]) + @pytest.mark.parametrize("hour", ["00:00:00", "00:00:01", "23:59:59", "12:00:59"]) @pytest.mark.parametrize( - "floating_sec_, expected", + "sec_float, expected", [ (".000000001", 1), (".000000999", 999), @@ -498,10 +498,11 @@ def test_period_cons_combined(self): (".999999999", 999), ], ) - def test_period_constructor_nanosecond(self, day_, hour_, floating_sec_, expected): + def test_period_constructor_nanosecond(self, day, hour, sec_float, expected): # GH 34621 - result = Period(day_ + hour_ + floating_sec_).start_time.nanosecond - assert result == expected + + assert Period(day + hour + sec_float).start_time.nanosecond == expected + @pytest.mark.parametrize("hour", range(24)) def test_period_large_ordinal(self, hour): # Issue #36430 From 55cf0acb75a64519728dfad7e7e10aad316b6692 Mon Sep 17 00:00:00 2001 From: Andrew Wieteska Date: Tue, 1 Dec 2020 23:39:02 -0500 Subject: [PATCH 09/10] whatsnew --- doc/source/whatsnew/v1.2.0.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/source/whatsnew/v1.2.0.rst b/doc/source/whatsnew/v1.2.0.rst index 9168041a4f474..c8a45bce7fc8a 100644 --- a/doc/source/whatsnew/v1.2.0.rst +++ b/doc/source/whatsnew/v1.2.0.rst @@ -575,6 +575,7 @@ Datetimelike - Bug in :meth:`Series.isin` with ``datetime64[ns]`` dtype and :meth:`.DatetimeIndex.isin` incorrectly casting integers to datetimes (:issue:`36621`) - Bug in :meth:`Series.isin` with ``datetime64[ns]`` dtype and :meth:`.DatetimeIndex.isin` failing to consider timezone-aware and timezone-naive datetimes as always different (:issue:`35728`) - Bug in :meth:`Series.isin` with ``PeriodDtype`` dtype and :meth:`PeriodIndex.isin` failing to consider arguments with different ``PeriodDtype`` as always different (:issue:`37528`) +- Bug in :class:`Period` constructor now correctly handles nanoseconds in the ``value`` argument (:issue:`34621` and :issue:`17053`) Timedelta ^^^^^^^^^ From 737ce754c5db6ffd0ba8d1218fb9c784b54b464a Mon Sep 17 00:00:00 2001 From: Andrew Wieteska Date: Tue, 1 Dec 2020 23:41:04 -0500 Subject: [PATCH 10/10] merge conflict --- pandas/tests/frame/test_constructors.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/pandas/tests/frame/test_constructors.py b/pandas/tests/frame/test_constructors.py index a98723e9e31f8..2300a8937991e 100644 --- a/pandas/tests/frame/test_constructors.py +++ b/pandas/tests/frame/test_constructors.py @@ -1238,32 +1238,34 @@ def test_constructor_single_row(self): ) tm.assert_frame_equal(result, expected) - def test_constructor_ordered_dict_preserve_order(self): + @pytest.mark.parametrize("dict_type", [dict, OrderedDict]) + def test_constructor_ordered_dict_preserve_order(self, dict_type): # see gh-13304 expected = DataFrame([[2, 1]], columns=["b", "a"]) - data = OrderedDict() + data = dict_type() data["b"] = [2] data["a"] = [1] result = DataFrame(data) tm.assert_frame_equal(result, expected) - data = OrderedDict() + data = dict_type() data["b"] = 2 data["a"] = 1 result = DataFrame([data]) tm.assert_frame_equal(result, expected) - def test_constructor_ordered_dict_conflicting_orders(self): + @pytest.mark.parametrize("dict_type", [dict, OrderedDict]) + def test_constructor_ordered_dict_conflicting_orders(self, dict_type): # the first dict element sets the ordering for the DataFrame, # even if there are conflicting orders from subsequent ones - row_one = OrderedDict() + row_one = dict_type() row_one["b"] = 2 row_one["a"] = 1 - row_two = OrderedDict() + row_two = dict_type() row_two["a"] = 1 row_two["b"] = 2