diff --git a/pandas/_libs/tslibs/frequencies.pyx b/pandas/_libs/tslibs/frequencies.pyx index 0fec4bca96251..13edf3c46152a 100644 --- a/pandas/_libs/tslibs/frequencies.pyx +++ b/pandas/_libs/tslibs/frequencies.pyx @@ -5,9 +5,7 @@ cnp.import_array() from pandas._libs.tslibs.util cimport is_integer_object -from pandas._libs.tslibs.ccalendar cimport c_MONTH_NUMBERS from pandas._libs.tslibs.offsets cimport is_offset_object -from pandas._libs.tslibs.parsing cimport get_rule_month # ---------------------------------------------------------------------- # Constants @@ -333,161 +331,3 @@ cpdef int get_to_timestamp_base(int base): elif FreqGroup.FR_HR <= base <= FreqGroup.FR_SEC: return FreqGroup.FR_SEC return base - - -# ---------------------------------------------------------------------- -# Frequency comparison - -def is_subperiod(source, target) -> bint: - """ - Returns True if downsampling is possible between source and target - frequencies - - Parameters - ---------- - source : string or DateOffset - Frequency converting from - target : string or DateOffset - Frequency converting to - - Returns - ------- - is_subperiod : boolean - """ - - if target is None or source is None: - return False - source = _maybe_coerce_freq(source) - target = _maybe_coerce_freq(target) - - if _is_annual(target): - if _is_quarterly(source): - return _quarter_months_conform(get_rule_month(source), - get_rule_month(target)) - return source in {'D', 'C', 'B', 'M', 'H', 'T', 'S', 'L', 'U', 'N'} - elif _is_quarterly(target): - return source in {'D', 'C', 'B', 'M', 'H', 'T', 'S', 'L', 'U', 'N'} - elif _is_monthly(target): - return source in {'D', 'C', 'B', 'H', 'T', 'S', 'L', 'U', 'N'} - elif _is_weekly(target): - return source in {target, 'D', 'C', 'B', 'H', 'T', 'S', 'L', 'U', 'N'} - elif target == 'B': - return source in {'B', 'H', 'T', 'S', 'L', 'U', 'N'} - elif target == 'C': - return source in {'C', 'H', 'T', 'S', 'L', 'U', 'N'} - elif target == 'D': - return source in {'D', 'H', 'T', 'S', 'L', 'U', 'N'} - elif target == 'H': - return source in {'H', 'T', 'S', 'L', 'U', 'N'} - elif target == 'T': - return source in {'T', 'S', 'L', 'U', 'N'} - elif target == 'S': - return source in {'S', 'L', 'U', 'N'} - elif target == 'L': - return source in {'L', 'U', 'N'} - elif target == 'U': - return source in {'U', 'N'} - elif target == 'N': - return source in {'N'} - - -def is_superperiod(source, target) -> bint: - """ - Returns True if upsampling is possible between source and target - frequencies - - Parameters - ---------- - source : string - Frequency converting from - target : string - Frequency converting to - - Returns - ------- - is_superperiod : boolean - """ - if target is None or source is None: - return False - source = _maybe_coerce_freq(source) - target = _maybe_coerce_freq(target) - - if _is_annual(source): - if _is_annual(target): - return get_rule_month(source) == get_rule_month(target) - - if _is_quarterly(target): - smonth = get_rule_month(source) - tmonth = get_rule_month(target) - return _quarter_months_conform(smonth, tmonth) - return target in {'D', 'C', 'B', 'M', 'H', 'T', 'S', 'L', 'U', 'N'} - elif _is_quarterly(source): - return target in {'D', 'C', 'B', 'M', 'H', 'T', 'S', 'L', 'U', 'N'} - elif _is_monthly(source): - return target in {'D', 'C', 'B', 'H', 'T', 'S', 'L', 'U', 'N'} - elif _is_weekly(source): - return target in {source, 'D', 'C', 'B', 'H', 'T', 'S', 'L', 'U', 'N'} - elif source == 'B': - return target in {'D', 'C', 'B', 'H', 'T', 'S', 'L', 'U', 'N'} - elif source == 'C': - return target in {'D', 'C', 'B', 'H', 'T', 'S', 'L', 'U', 'N'} - elif source == 'D': - return target in {'D', 'C', 'B', 'H', 'T', 'S', 'L', 'U', 'N'} - elif source == 'H': - return target in {'H', 'T', 'S', 'L', 'U', 'N'} - elif source == 'T': - return target in {'T', 'S', 'L', 'U', 'N'} - elif source == 'S': - return target in {'S', 'L', 'U', 'N'} - elif source == 'L': - return target in {'L', 'U', 'N'} - elif source == 'U': - return target in {'U', 'N'} - elif source == 'N': - return target in {'N'} - - -cdef str _maybe_coerce_freq(code): - """ we might need to coerce a code to a rule_code - and uppercase it - - Parameters - ---------- - source : string or DateOffset - Frequency converting from - - Returns - ------- - code : string - """ - assert code is not None - if is_offset_object(code): - # i.e. isinstance(code, DateOffset): - code = code.rule_code - return code.upper() - - -cdef bint _quarter_months_conform(str source, str target): - snum = c_MONTH_NUMBERS[source] - tnum = c_MONTH_NUMBERS[target] - return snum % 3 == tnum % 3 - - -cdef bint _is_annual(str rule): - rule = rule.upper() - return rule == 'A' or rule.startswith('A-') - - -cdef bint _is_quarterly(str rule): - rule = rule.upper() - return rule == 'Q' or rule.startswith('Q-') or rule.startswith('BQ') - - -cdef bint _is_monthly(str rule): - rule = rule.upper() - return rule == 'M' or rule == 'BM' - - -cdef bint _is_weekly(str rule): - rule = rule.upper() - return rule == 'W' or rule.startswith('W-') diff --git a/pandas/core/resample.py b/pandas/core/resample.py index 755059bf0adf1..4a4c9a1d7434b 100644 --- a/pandas/core/resample.py +++ b/pandas/core/resample.py @@ -7,7 +7,6 @@ from pandas._libs import lib from pandas._libs.tslibs import NaT, Period, Timedelta, Timestamp -from pandas._libs.tslibs.frequencies import is_subperiod, is_superperiod from pandas._libs.tslibs.period import IncompatibleFrequency from pandas._typing import TimedeltaConvertibleTypes, TimestampConvertibleTypes from pandas.compat.numpy import function as nv @@ -29,7 +28,7 @@ from pandas.core.indexes.period import PeriodIndex, period_range from pandas.core.indexes.timedeltas import TimedeltaIndex, timedelta_range -from pandas.tseries.frequencies import to_offset +from pandas.tseries.frequencies import is_subperiod, is_superperiod, to_offset from pandas.tseries.offsets import DateOffset, Day, Nano, Tick _shared_docs_kwargs: Dict[str, str] = dict() @@ -1709,7 +1708,7 @@ def _get_timestamp_range_edges( origin = origin.tz_localize(None) first, last = _adjust_dates_anchored( - first, last, freq, closed=closed, origin=origin, offset=offset, + first, last, freq, closed=closed, origin=origin, offset=offset ) if isinstance(freq, Day): first = first.tz_localize(index_tz) @@ -1771,7 +1770,7 @@ def _get_period_range_edges( adjust_last = freq.is_on_offset(last) first, last = _get_timestamp_range_edges( - first, last, freq, closed=closed, origin=origin, offset=offset, + first, last, freq, closed=closed, origin=origin, offset=offset ) first = (first + int(adjust_first) * freq).to_period(freq) diff --git a/pandas/plotting/_matplotlib/timeseries.py b/pandas/plotting/_matplotlib/timeseries.py index f62340ee8f756..e73a109449d62 100644 --- a/pandas/plotting/_matplotlib/timeseries.py +++ b/pandas/plotting/_matplotlib/timeseries.py @@ -5,13 +5,7 @@ import numpy as np -from pandas._libs.tslibs.frequencies import ( - FreqGroup, - base_and_stride, - get_freq_code, - is_subperiod, - is_superperiod, -) +from pandas._libs.tslibs.frequencies import FreqGroup, base_and_stride, get_freq_code from pandas._libs.tslibs.period import Period from pandas.core.dtypes.generic import ( @@ -27,6 +21,7 @@ TimeSeries_TimedeltaFormatter, ) import pandas.tseries.frequencies as frequencies +from pandas.tseries.frequencies import is_subperiod, is_superperiod from pandas.tseries.offsets import DateOffset # --------------------------------------------------------------------- diff --git a/pandas/tests/tslibs/test_libfrequencies.py b/pandas/tests/tslibs/test_libfrequencies.py index 2dba0f51ca217..65d3b15bb3dac 100644 --- a/pandas/tests/tslibs/test_libfrequencies.py +++ b/pandas/tests/tslibs/test_libfrequencies.py @@ -1,14 +1,10 @@ import pytest -from pandas._libs.tslibs.frequencies import ( - INVALID_FREQ_ERR_MSG, - _period_str_to_code, - is_subperiod, - is_superperiod, -) +from pandas._libs.tslibs.frequencies import INVALID_FREQ_ERR_MSG, _period_str_to_code from pandas._libs.tslibs.parsing import get_rule_month from pandas.tseries import offsets +from pandas.tseries.frequencies import is_subperiod, is_superperiod # TODO: move tests @pytest.mark.parametrize( diff --git a/pandas/tseries/frequencies.py b/pandas/tseries/frequencies.py index f20734598bc74..84113afdb0969 100644 --- a/pandas/tseries/frequencies.py +++ b/pandas/tseries/frequencies.py @@ -7,10 +7,11 @@ from pandas._libs.algos import unique_deltas from pandas._libs.tslibs import Timedelta, Timestamp -from pandas._libs.tslibs.ccalendar import MONTH_ALIASES, int_to_weekday +from pandas._libs.tslibs.ccalendar import MONTH_ALIASES, MONTH_NUMBERS, int_to_weekday from pandas._libs.tslibs.fields import build_field_sarray import pandas._libs.tslibs.frequencies as libfreqs from pandas._libs.tslibs.offsets import _offset_to_period_map +from pandas._libs.tslibs.parsing import get_rule_month from pandas._libs.tslibs.resolution import Resolution, month_position_check from pandas._libs.tslibs.timezones import UTC from pandas._libs.tslibs.tzconversion import tz_convert @@ -540,3 +541,166 @@ def _maybe_add_count(base: str, count: float) -> str: return f"{count}{base}" else: return base + + +# ---------------------------------------------------------------------- +# Frequency comparison + + +def is_subperiod(source, target) -> bool: + """ + Returns True if downsampling is possible between source and target + frequencies + + Parameters + ---------- + source : str or DateOffset + Frequency converting from + target : str or DateOffset + Frequency converting to + + Returns + ------- + bool + """ + + if target is None or source is None: + return False + source = _maybe_coerce_freq(source) + target = _maybe_coerce_freq(target) + + if _is_annual(target): + if _is_quarterly(source): + return _quarter_months_conform( + get_rule_month(source), get_rule_month(target) + ) + return source in {"D", "C", "B", "M", "H", "T", "S", "L", "U", "N"} + elif _is_quarterly(target): + return source in {"D", "C", "B", "M", "H", "T", "S", "L", "U", "N"} + elif _is_monthly(target): + return source in {"D", "C", "B", "H", "T", "S", "L", "U", "N"} + elif _is_weekly(target): + return source in {target, "D", "C", "B", "H", "T", "S", "L", "U", "N"} + elif target == "B": + return source in {"B", "H", "T", "S", "L", "U", "N"} + elif target == "C": + return source in {"C", "H", "T", "S", "L", "U", "N"} + elif target == "D": + return source in {"D", "H", "T", "S", "L", "U", "N"} + elif target == "H": + return source in {"H", "T", "S", "L", "U", "N"} + elif target == "T": + return source in {"T", "S", "L", "U", "N"} + elif target == "S": + return source in {"S", "L", "U", "N"} + elif target == "L": + return source in {"L", "U", "N"} + elif target == "U": + return source in {"U", "N"} + elif target == "N": + return source in {"N"} + else: + return False + + +def is_superperiod(source, target) -> bool: + """ + Returns True if upsampling is possible between source and target + frequencies + + Parameters + ---------- + source : str or DateOffset + Frequency converting from + target : str or DateOffset + Frequency converting to + + Returns + ------- + bool + """ + if target is None or source is None: + return False + source = _maybe_coerce_freq(source) + target = _maybe_coerce_freq(target) + + if _is_annual(source): + if _is_annual(target): + return get_rule_month(source) == get_rule_month(target) + + if _is_quarterly(target): + smonth = get_rule_month(source) + tmonth = get_rule_month(target) + return _quarter_months_conform(smonth, tmonth) + return target in {"D", "C", "B", "M", "H", "T", "S", "L", "U", "N"} + elif _is_quarterly(source): + return target in {"D", "C", "B", "M", "H", "T", "S", "L", "U", "N"} + elif _is_monthly(source): + return target in {"D", "C", "B", "H", "T", "S", "L", "U", "N"} + elif _is_weekly(source): + return target in {source, "D", "C", "B", "H", "T", "S", "L", "U", "N"} + elif source == "B": + return target in {"D", "C", "B", "H", "T", "S", "L", "U", "N"} + elif source == "C": + return target in {"D", "C", "B", "H", "T", "S", "L", "U", "N"} + elif source == "D": + return target in {"D", "C", "B", "H", "T", "S", "L", "U", "N"} + elif source == "H": + return target in {"H", "T", "S", "L", "U", "N"} + elif source == "T": + return target in {"T", "S", "L", "U", "N"} + elif source == "S": + return target in {"S", "L", "U", "N"} + elif source == "L": + return target in {"L", "U", "N"} + elif source == "U": + return target in {"U", "N"} + elif source == "N": + return target in {"N"} + else: + return False + + +def _maybe_coerce_freq(code) -> str: + """ we might need to coerce a code to a rule_code + and uppercase it + + Parameters + ---------- + source : string or DateOffset + Frequency converting from + + Returns + ------- + str + """ + assert code is not None + if isinstance(code, DateOffset): + code = code.rule_code + return code.upper() + + +def _quarter_months_conform(source: str, target: str) -> bool: + snum = MONTH_NUMBERS[source] + tnum = MONTH_NUMBERS[target] + return snum % 3 == tnum % 3 + + +def _is_annual(rule: str) -> bool: + rule = rule.upper() + return rule == "A" or rule.startswith("A-") + + +def _is_quarterly(rule: str) -> bool: + rule = rule.upper() + return rule == "Q" or rule.startswith("Q-") or rule.startswith("BQ") + + +def _is_monthly(rule: str) -> bool: + rule = rule.upper() + return rule == "M" or rule == "BM" + + +def _is_weekly(rule: str) -> bool: + rule = rule.upper() + return rule == "W" or rule.startswith("W-")