diff --git a/pandas/_libs/tslibs/frequencies.pxd b/pandas/_libs/tslibs/frequencies.pxd index 974eb4ab45df0..98d600c540ace 100644 --- a/pandas/_libs/tslibs/frequencies.pxd +++ b/pandas/_libs/tslibs/frequencies.pxd @@ -1,4 +1,10 @@ # -*- coding: utf-8 -*- # cython: profile=False +cpdef object get_rule_month(object source, object default=*) + cpdef get_freq_code(freqstr) +cpdef object get_freq(object freq) +cpdef str get_base_alias(freqstr) +cpdef int get_to_timestamp_base(int base) +cpdef str get_freq_str(base, mult=*) diff --git a/pandas/_libs/tslibs/frequencies.pyx b/pandas/_libs/tslibs/frequencies.pyx index 2a700d52eaaf3..cce3600371300 100644 --- a/pandas/_libs/tslibs/frequencies.pyx +++ b/pandas/_libs/tslibs/frequencies.pyx @@ -6,9 +6,12 @@ cimport cython import numpy as np cimport numpy as np +from numpy cimport int64_t np.import_array() -from util cimport is_integer_object +from util cimport is_integer_object, is_string_object + +from ccalendar import MONTH_NUMBERS # ---------------------------------------------------------------------- # Constants @@ -23,6 +26,22 @@ _INVALID_FREQ_ERROR = "Invalid frequency: {0}" # --------------------------------------------------------------------- # Period codes + +class FreqGroup(object): + FR_ANN = 1000 + FR_QTR = 2000 + FR_MTH = 3000 + FR_WK = 4000 + FR_BUS = 5000 + FR_DAY = 6000 + FR_HR = 7000 + FR_MIN = 8000 + FR_SEC = 9000 + FR_MS = 10000 + FR_US = 11000 + FR_NS = 12000 + + # period frequency constants corresponding to scikits timeseries # originals _period_code_map = { @@ -125,8 +144,8 @@ cpdef get_freq_code(freqstr): ------- return : tuple of base frequency code and stride (mult) - Example - ------- + Examples + -------- >>> get_freq_code('3D') (6000, 3) @@ -203,3 +222,292 @@ cpdef _period_str_to_code(freqstr): return _period_code_map[freqstr] except KeyError: raise ValueError(_INVALID_FREQ_ERROR.format(freqstr)) + + +cpdef str get_freq_str(base, mult=1): + """ + Return the summary string associated with this offset code, possibly + adjusted by a multiplier. + + Parameters + ---------- + base : int (member of FreqGroup) + + Returns + ------- + freq_str : str + + Examples + -------- + >>> get_freq_str(1000) + 'A-DEC' + + >>> get_freq_str(2000, 2) + '2Q-DEC' + + >>> get_freq_str("foo") + """ + code = _reverse_period_code_map.get(base) + if mult == 1: + return code + return str(mult) + code + + +cpdef str get_base_alias(freqstr): + """ + Returns the base frequency alias, e.g., '5D' -> 'D' + + Parameters + ---------- + freqstr : str + + Returns + ------- + base_alias : str + """ + return _base_and_stride(freqstr)[0] + + +cpdef int get_to_timestamp_base(int base): + """ + Return frequency code group used for base of to_timestamp against + frequency code. + + Parameters + ---------- + base : int (member of FreqGroup) + + Returns + ------- + base : int + + Examples + -------- + # Return day freq code against longer freq than day + >>> get_to_timestamp_base(get_freq_code('D')[0]) + 6000 + >>> get_to_timestamp_base(get_freq_code('W')[0]) + 6000 + >>> get_to_timestamp_base(get_freq_code('M')[0]) + 6000 + + # Return second freq code against hour between second + >>> get_to_timestamp_base(get_freq_code('H')[0]) + 9000 + >>> get_to_timestamp_base(get_freq_code('S')[0]) + 9000 + """ + if base < FreqGroup.FR_BUS: + return FreqGroup.FR_DAY + elif FreqGroup.FR_HR <= base <= FreqGroup.FR_SEC: + return FreqGroup.FR_SEC + return base + + +cpdef object get_freq(object freq): + """ + Return frequency code of given frequency str. + If input is not string, return input as it is. + + Examples + -------- + >>> get_freq('A') + 1000 + + >>> get_freq('3A') + 1000 + """ + if is_string_object(freq): + base, mult = get_freq_code(freq) + freq = base + return freq + + +# ---------------------------------------------------------------------- +# Frequency comparison + +cpdef bint is_subperiod(source, target): + """ + 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'} + + +cpdef bint is_superperiod(source, target): + """ + 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 getattr(code, '_typ', None) == 'dateoffset': + # i.e. isinstance(code, ABCDateOffset): + code = code.rule_code + return code.upper() + + +cdef bint _quarter_months_conform(str source, str target): + snum = MONTH_NUMBERS[source] + tnum = 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-') + + +# ---------------------------------------------------------------------- + +cpdef object get_rule_month(object source, object default='DEC'): + """ + Return starting month of given freq, default is December. + + Parameters + ---------- + source : object + default : object (default "DEC") + + Returns + ------- + rule_month: object (usually string) + + Examples + -------- + >>> get_rule_month('D') + 'DEC' + + >>> get_rule_month('A-JAN') + 'JAN' + """ + if hasattr(source, 'freqstr'): + source = source.freqstr + source = source.upper() + if '-' not in source: + return default + else: + return source.split('-')[1] diff --git a/pandas/_libs/tslibs/period.pyx b/pandas/_libs/tslibs/period.pyx index 42570e355e2bf..46365035a0b9a 100644 --- a/pandas/_libs/tslibs/period.pyx +++ b/pandas/_libs/tslibs/period.pyx @@ -28,14 +28,16 @@ cimport util from util cimport is_period_object, is_string_object, INT32_MIN from pandas._libs.missing cimport is_null_datetimelike + from timestamps import Timestamp from timezones cimport is_utc, is_tzlocal, get_utcoffset, get_dst_info from timedeltas cimport delta_to_nanoseconds from ccalendar import MONTH_NUMBERS -from parsing import (parse_time_string, NAT_SENTINEL, - _get_rule_month) -from frequencies cimport get_freq_code +from frequencies cimport (get_freq_code, get_base_alias, + get_to_timestamp_base, get_freq_str, + get_rule_month) +from parsing import parse_time_string, NAT_SENTINEL from resolution import resolution, Resolution from nattype import nat_strings, NaT, iNaT from nattype cimport _nat_scalar_rules, NPY_NAT @@ -572,7 +574,7 @@ cdef class _Period(object): if isinstance(freq, (int, tuple)): code, stride = get_freq_code(freq) - freq = frequencies._get_freq_str(code, stride) + freq = get_freq_str(code, stride) freq = frequencies.to_offset(freq) @@ -630,7 +632,7 @@ cdef class _Period(object): raise IncompatibleFrequency(msg.format(self.freqstr)) elif isinstance(other, offsets.DateOffset): freqstr = other.rule_code - base = frequencies.get_base_alias(freqstr) + base = get_base_alias(freqstr) if base == self.freq.rule_code: ordinal = self.ordinal + other.n return Period(ordinal=ordinal, freq=self.freq) @@ -756,7 +758,7 @@ cdef class _Period(object): if freq is None: base, mult = get_freq_code(self.freq) - freq = frequencies.get_to_timestamp_base(base) + freq = get_to_timestamp_base(base) base, mult = get_freq_code(freq) val = self.asfreq(freq, how) @@ -1149,7 +1151,7 @@ def _quarter_to_myear(year, quarter, freq): if quarter <= 0 or quarter > 4: raise ValueError('Quarter must be 1 <= q <= 4') - mnum = MONTH_NUMBERS[_get_rule_month(freq)] + 1 + mnum = MONTH_NUMBERS[get_rule_month(freq)] + 1 month = (mnum + (quarter - 1) * 3) % 12 + 1 if month > mnum: year -= 1 diff --git a/pandas/_libs/tslibs/resolution.pyx b/pandas/_libs/tslibs/resolution.pyx index 9cb2c450524fb..6eb867377bf54 100644 --- a/pandas/_libs/tslibs/resolution.pyx +++ b/pandas/_libs/tslibs/resolution.pyx @@ -23,7 +23,7 @@ from timezones cimport (is_utc, is_tzlocal, maybe_get_tz, get_dst_info, get_utcoffset) from fields import build_field_sarray from conversion import tz_convert -from ccalendar import DAYS, MONTH_ALIASES, int_to_weekday +from ccalendar import MONTH_ALIASES, int_to_weekday from pandas._libs.properties import cache_readonly from pandas._libs.tslib import Timestamp diff --git a/pandas/tests/indexes/datetimes/test_ops.py b/pandas/tests/indexes/datetimes/test_ops.py index 41d0dd38cd5f6..a7a6e3caab727 100644 --- a/pandas/tests/indexes/datetimes/test_ops.py +++ b/pandas/tests/indexes/datetimes/test_ops.py @@ -161,7 +161,7 @@ def test_round(self): tm.assert_index_equal(rng.round(freq='H'), expected_rng) assert elt.round(freq='H') == expected_elt - msg = pd.tseries.frequencies._INVALID_FREQ_ERROR + msg = pd._libs.tslibs.frequencies._INVALID_FREQ_ERROR with tm.assert_raises_regex(ValueError, msg): rng.round(freq='foo') with tm.assert_raises_regex(ValueError, msg): diff --git a/pandas/tests/indexes/period/test_tools.py b/pandas/tests/indexes/period/test_tools.py index 9df23948ae627..0e72cadb5d494 100644 --- a/pandas/tests/indexes/period/test_tools.py +++ b/pandas/tests/indexes/period/test_tools.py @@ -5,10 +5,11 @@ import pandas.util.testing as tm import pandas.core.indexes.period as period from pandas.compat import lrange -from pandas.tseries.frequencies import get_freq -from pandas._libs.tslibs.ccalendar import MONTHS +from pandas._libs.tslibs.frequencies import get_freq from pandas._libs.tslibs.period import period_ordinal, period_asfreq +from pandas._libs.tslibs.ccalendar import MONTHS + from pandas import (PeriodIndex, Period, DatetimeIndex, Timestamp, Series, date_range, to_datetime, period_range) @@ -369,7 +370,7 @@ def test_to_period_monthish(self): prng = rng.to_period() assert prng.freq == 'M' - msg = pd.tseries.frequencies._INVALID_FREQ_ERROR + msg = pd._libs.tslibs.frequencies._INVALID_FREQ_ERROR with tm.assert_raises_regex(ValueError, msg): date_range('01-Jan-2012', periods=8, freq='EOM') diff --git a/pandas/tests/indexes/timedeltas/test_ops.py b/pandas/tests/indexes/timedeltas/test_ops.py index fac3745ba4fb4..081e299caa876 100644 --- a/pandas/tests/indexes/timedeltas/test_ops.py +++ b/pandas/tests/indexes/timedeltas/test_ops.py @@ -114,7 +114,7 @@ def test_round(self): tm.assert_index_equal(td.round(freq='H'), expected_rng) assert elt.round(freq='H') == expected_elt - msg = pd.tseries.frequencies._INVALID_FREQ_ERROR + msg = pd._libs.tslibs.frequencies._INVALID_FREQ_ERROR with tm.assert_raises_regex(ValueError, msg): td.round(freq='foo') with tm.assert_raises_regex(ValueError, msg): diff --git a/pandas/tests/scalar/test_period.py b/pandas/tests/scalar/test_period.py index 792eb0d49077f..ce733829c2315 100644 --- a/pandas/tests/scalar/test_period.py +++ b/pandas/tests/scalar/test_period.py @@ -514,7 +514,7 @@ def test_period_deprecated_freq(self): "U": ["MICROSECOND", "MICROSECONDLY", "microsecond"], "N": ["NANOSECOND", "NANOSECONDLY", "nanosecond"]} - msg = pd.tseries.frequencies._INVALID_FREQ_ERROR + msg = pd._libs.tslibs.frequencies._INVALID_FREQ_ERROR for exp, freqs in iteritems(cases): for freq in freqs: with tm.assert_raises_regex(ValueError, msg): @@ -758,7 +758,7 @@ def test_properties_weekly_legacy(self): exp = Period(freq='W', year=2012, month=2, day=1) assert exp.days_in_month == 29 - msg = pd.tseries.frequencies._INVALID_FREQ_ERROR + msg = pd._libs.tslibs.frequencies._INVALID_FREQ_ERROR with tm.assert_raises_regex(ValueError, msg): Period(freq='WK', year=2007, month=1, day=7) diff --git a/pandas/tests/scalar/test_period_asfreq.py b/pandas/tests/scalar/test_period_asfreq.py index 32cea60c333b7..a2819a3478f79 100644 --- a/pandas/tests/scalar/test_period_asfreq.py +++ b/pandas/tests/scalar/test_period_asfreq.py @@ -1,7 +1,7 @@ import pandas as pd from pandas import Period, offsets from pandas.util import testing as tm -from pandas.tseries.frequencies import _period_code_map +from pandas._libs.tslibs.frequencies import _period_code_map class TestFreqConversion(object): @@ -293,13 +293,13 @@ def test_conv_weekly(self): assert ival_W.asfreq('W') == ival_W - msg = pd.tseries.frequencies._INVALID_FREQ_ERROR + msg = pd._libs.tslibs.frequencies._INVALID_FREQ_ERROR with tm.assert_raises_regex(ValueError, msg): ival_W.asfreq('WK') def test_conv_weekly_legacy(self): # frequency conversion tests: from Weekly Frequency - msg = pd.tseries.frequencies._INVALID_FREQ_ERROR + msg = pd._libs.tslibs.frequencies._INVALID_FREQ_ERROR with tm.assert_raises_regex(ValueError, msg): Period(freq='WK', year=2007, month=1, day=1) @@ -706,7 +706,7 @@ def test_asfreq_MS(self): assert initial.asfreq(freq="M", how="S") == Period('2013-01', 'M') - msg = pd.tseries.frequencies._INVALID_FREQ_ERROR + msg = pd._libs.tslibs.frequencies._INVALID_FREQ_ERROR with tm.assert_raises_regex(ValueError, msg): initial.asfreq(freq="MS", how="S") diff --git a/pandas/tests/scalar/test_timestamp.py b/pandas/tests/scalar/test_timestamp.py index 4f4f2648d3834..a3e9a0442ea0b 100644 --- a/pandas/tests/scalar/test_timestamp.py +++ b/pandas/tests/scalar/test_timestamp.py @@ -16,9 +16,12 @@ import pandas.util.testing as tm import pandas.util._test_decorators as td -from pandas.tseries import offsets, frequencies -from pandas._libs.tslibs.timezones import get_timezone, dateutil_gettz as gettz + +from pandas.tseries import offsets + from pandas._libs.tslibs import conversion, period +from pandas._libs.tslibs.timezones import get_timezone, dateutil_gettz as gettz +from pandas._libs.tslibs.frequencies import _INVALID_FREQ_ERROR from pandas.compat import long, PY3 from pandas.util.testing import assert_series_equal @@ -753,8 +756,7 @@ def _check_round(freq, expected): ('S', Timestamp('2000-01-05 05:09:15'))]: _check_round(freq, expected) - msg = frequencies._INVALID_FREQ_ERROR - with tm.assert_raises_regex(ValueError, msg): + with tm.assert_raises_regex(ValueError, _INVALID_FREQ_ERROR): stamp.round('foo') def test_class_ops_pytz(self): diff --git a/pandas/tests/tseries/offsets/test_fiscal.py b/pandas/tests/tseries/offsets/test_fiscal.py index f71480e1f83a5..c084cccbb74ac 100644 --- a/pandas/tests/tseries/offsets/test_fiscal.py +++ b/pandas/tests/tseries/offsets/test_fiscal.py @@ -10,7 +10,8 @@ import pandas.util.testing as tm from pandas import Timestamp -from pandas.tseries.frequencies import get_offset, _INVALID_FREQ_ERROR +from pandas.tseries.frequencies import get_offset +from pandas._libs.tslibs.frequencies import _INVALID_FREQ_ERROR from pandas.tseries.offsets import FY5253Quarter, FY5253 from pandas._libs.tslibs.offsets import WeekDay diff --git a/pandas/tests/tseries/offsets/test_offsets.py b/pandas/tests/tseries/offsets/test_offsets.py index 1a032182319f2..23e627aeba017 100644 --- a/pandas/tests/tseries/offsets/test_offsets.py +++ b/pandas/tests/tseries/offsets/test_offsets.py @@ -11,8 +11,9 @@ from pandas.compat.numpy import np_datetime64_compat from pandas.core.series import Series -from pandas.tseries.frequencies import (_offset_map, get_freq_code, get_offset, - _get_freq_str, _INVALID_FREQ_ERROR) +from pandas._libs.tslibs.frequencies import (get_freq_code, get_freq_str, + _INVALID_FREQ_ERROR) +from pandas.tseries.frequencies import _offset_map, get_offset from pandas.core.indexes.datetimes import ( _to_m8, DatetimeIndex, _daterange_cache) import pandas._libs.tslibs.offsets as liboffsets @@ -2825,7 +2826,7 @@ def test_rule_code(self): code, stride = get_freq_code('3' + k) assert isinstance(code, int) assert stride == 3 - assert k == _get_freq_str(code) + assert k == get_freq_str(code) def test_dateoffset_misc(): diff --git a/pandas/tests/tseries/test_frequencies.py b/pandas/tests/tseries/test_frequencies.py index 2486895086b2f..92d7eb15c929c 100644 --- a/pandas/tests/tseries/test_frequencies.py +++ b/pandas/tests/tseries/test_frequencies.py @@ -7,6 +7,9 @@ from pandas import (Index, DatetimeIndex, Timestamp, Series, date_range, period_range) +from pandas._libs.tslibs.frequencies import (_period_code_map, + _INVALID_FREQ_ERROR) +from pandas._libs.tslibs.ccalendar import MONTHS from pandas._libs.tslibs import resolution import pandas.tseries.frequencies as frequencies from pandas.core.tools.datetimes import to_datetime @@ -284,87 +287,6 @@ def test_rule_aliases(): assert rule == offsets.Micro(10) -def test_get_rule_month(): - result = frequencies._get_rule_month('W') - assert (result == 'DEC') - result = frequencies._get_rule_month(offsets.Week()) - assert (result == 'DEC') - - result = frequencies._get_rule_month('D') - assert (result == 'DEC') - result = frequencies._get_rule_month(offsets.Day()) - assert (result == 'DEC') - - result = frequencies._get_rule_month('Q') - assert (result == 'DEC') - result = frequencies._get_rule_month(offsets.QuarterEnd(startingMonth=12)) - print(result == 'DEC') - - result = frequencies._get_rule_month('Q-JAN') - assert (result == 'JAN') - result = frequencies._get_rule_month(offsets.QuarterEnd(startingMonth=1)) - assert (result == 'JAN') - - result = frequencies._get_rule_month('A-DEC') - assert (result == 'DEC') - result = frequencies._get_rule_month('Y-DEC') - assert (result == 'DEC') - result = frequencies._get_rule_month(offsets.YearEnd()) - assert (result == 'DEC') - - result = frequencies._get_rule_month('A-MAY') - assert (result == 'MAY') - result = frequencies._get_rule_month('Y-MAY') - assert (result == 'MAY') - result = frequencies._get_rule_month(offsets.YearEnd(month=5)) - assert (result == 'MAY') - - -def test_period_str_to_code(): - assert (frequencies._period_str_to_code('A') == 1000) - assert (frequencies._period_str_to_code('A-DEC') == 1000) - assert (frequencies._period_str_to_code('A-JAN') == 1001) - assert (frequencies._period_str_to_code('Y') == 1000) - assert (frequencies._period_str_to_code('Y-DEC') == 1000) - assert (frequencies._period_str_to_code('Y-JAN') == 1001) - - assert (frequencies._period_str_to_code('Q') == 2000) - assert (frequencies._period_str_to_code('Q-DEC') == 2000) - assert (frequencies._period_str_to_code('Q-FEB') == 2002) - - def _assert_depr(freq, expected, aliases): - assert isinstance(aliases, list) - assert (frequencies._period_str_to_code(freq) == expected) - - msg = frequencies._INVALID_FREQ_ERROR - for alias in aliases: - with tm.assert_raises_regex(ValueError, msg): - frequencies._period_str_to_code(alias) - - _assert_depr("M", 3000, ["MTH", "MONTH", "MONTHLY"]) - - assert (frequencies._period_str_to_code('W') == 4000) - assert (frequencies._period_str_to_code('W-SUN') == 4000) - assert (frequencies._period_str_to_code('W-FRI') == 4005) - - _assert_depr("B", 5000, ["BUS", "BUSINESS", "BUSINESSLY", "WEEKDAY"]) - _assert_depr("D", 6000, ["DAY", "DLY", "DAILY"]) - _assert_depr("H", 7000, ["HR", "HOUR", "HRLY", "HOURLY"]) - - _assert_depr("T", 8000, ["minute", "MINUTE", "MINUTELY"]) - assert (frequencies._period_str_to_code('Min') == 8000) - - _assert_depr("S", 9000, ["sec", "SEC", "SECOND", "SECONDLY"]) - _assert_depr("L", 10000, ["MILLISECOND", "MILLISECONDLY"]) - assert (frequencies._period_str_to_code('ms') == 10000) - - _assert_depr("U", 11000, ["MICROSECOND", "MICROSECONDLY"]) - assert (frequencies._period_str_to_code('US') == 11000) - - _assert_depr("N", 12000, ["NANOSECOND", "NANOSECONDLY"]) - assert (frequencies._period_str_to_code('NS') == 12000) - - class TestFrequencyCode(object): def test_freq_code(self): @@ -380,7 +302,7 @@ def test_freq_code(self): assert frequencies.get_freq('W-MON') == 4001 assert frequencies.get_freq('W-FRI') == 4005 - for freqstr, code in compat.iteritems(frequencies._period_code_map): + for freqstr, code in compat.iteritems(_period_code_map): result = frequencies.get_freq(freqstr) assert result == code @@ -875,40 +797,10 @@ def test_legacy_offset_warnings(self): 'WOM@4THU', 'WOM@1FRI', 'WOM@2FRI', 'WOM@3FRI', 'WOM@4FRI'] - msg = frequencies._INVALID_FREQ_ERROR + msg = _INVALID_FREQ_ERROR for freq in freqs: with tm.assert_raises_regex(ValueError, msg): frequencies.get_offset(freq) with tm.assert_raises_regex(ValueError, msg): date_range('2011-01-01', periods=5, freq=freq) - - -MONTHS = ['JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', - 'NOV', 'DEC'] - - -def test_is_superperiod_subperiod(): - - # input validation - assert not (frequencies.is_superperiod(offsets.YearEnd(), None)) - assert not (frequencies.is_subperiod(offsets.MonthEnd(), None)) - assert not (frequencies.is_superperiod(None, offsets.YearEnd())) - assert not (frequencies.is_subperiod(None, offsets.MonthEnd())) - assert not (frequencies.is_superperiod(None, None)) - assert not (frequencies.is_subperiod(None, None)) - - assert (frequencies.is_superperiod(offsets.YearEnd(), offsets.MonthEnd())) - assert (frequencies.is_subperiod(offsets.MonthEnd(), offsets.YearEnd())) - - assert (frequencies.is_superperiod(offsets.Hour(), offsets.Minute())) - assert (frequencies.is_subperiod(offsets.Minute(), offsets.Hour())) - - assert (frequencies.is_superperiod(offsets.Second(), offsets.Milli())) - assert (frequencies.is_subperiod(offsets.Milli(), offsets.Second())) - - assert (frequencies.is_superperiod(offsets.Milli(), offsets.Micro())) - assert (frequencies.is_subperiod(offsets.Micro(), offsets.Milli())) - - assert (frequencies.is_superperiod(offsets.Micro(), offsets.Nano())) - assert (frequencies.is_subperiod(offsets.Nano(), offsets.Micro())) diff --git a/pandas/tests/tseries/test_libfrequencies.py b/pandas/tests/tseries/test_libfrequencies.py new file mode 100644 index 0000000000000..601d542da3095 --- /dev/null +++ b/pandas/tests/tseries/test_libfrequencies.py @@ -0,0 +1,116 @@ +# -*- coding: utf-8 -*- + +import pandas.util.testing as tm + +from pandas.tseries import offsets +from pandas._libs.tslibs.frequencies import (get_rule_month, + _period_str_to_code, + _INVALID_FREQ_ERROR, + is_superperiod, is_subperiod) + + +def assert_aliases_deprecated(freq, expected, aliases): + assert isinstance(aliases, list) + assert (_period_str_to_code(freq) == expected) + + for alias in aliases: + with tm.assert_raises_regex(ValueError, _INVALID_FREQ_ERROR): + _period_str_to_code(alias) + + +def test_get_rule_month(): + result = get_rule_month('W') + assert (result == 'DEC') + result = get_rule_month(offsets.Week()) + assert (result == 'DEC') + + result = get_rule_month('D') + assert (result == 'DEC') + result = get_rule_month(offsets.Day()) + assert (result == 'DEC') + + result = get_rule_month('Q') + assert (result == 'DEC') + result = get_rule_month(offsets.QuarterEnd(startingMonth=12)) + + result = get_rule_month('Q-JAN') + assert (result == 'JAN') + result = get_rule_month(offsets.QuarterEnd(startingMonth=1)) + assert (result == 'JAN') + + result = get_rule_month('A-DEC') + assert (result == 'DEC') + result = get_rule_month('Y-DEC') + assert (result == 'DEC') + result = get_rule_month(offsets.YearEnd()) + assert (result == 'DEC') + + result = get_rule_month('A-MAY') + assert (result == 'MAY') + result = get_rule_month('Y-MAY') + assert (result == 'MAY') + result = get_rule_month(offsets.YearEnd(month=5)) + assert (result == 'MAY') + + +def test_period_str_to_code(): + assert (_period_str_to_code('A') == 1000) + assert (_period_str_to_code('A-DEC') == 1000) + assert (_period_str_to_code('A-JAN') == 1001) + assert (_period_str_to_code('Y') == 1000) + assert (_period_str_to_code('Y-DEC') == 1000) + assert (_period_str_to_code('Y-JAN') == 1001) + + assert (_period_str_to_code('Q') == 2000) + assert (_period_str_to_code('Q-DEC') == 2000) + assert (_period_str_to_code('Q-FEB') == 2002) + + assert_aliases_deprecated("M", 3000, ["MTH", "MONTH", "MONTHLY"]) + + assert (_period_str_to_code('W') == 4000) + assert (_period_str_to_code('W-SUN') == 4000) + assert (_period_str_to_code('W-FRI') == 4005) + + assert_aliases_deprecated("B", 5000, ["BUS", "BUSINESS", + "BUSINESSLY", "WEEKDAY"]) + assert_aliases_deprecated("D", 6000, ["DAY", "DLY", "DAILY"]) + assert_aliases_deprecated("H", 7000, ["HR", "HOUR", "HRLY", "HOURLY"]) + + assert_aliases_deprecated("T", 8000, ["minute", "MINUTE", "MINUTELY"]) + assert (_period_str_to_code('Min') == 8000) + + assert_aliases_deprecated("S", 9000, ["sec", "SEC", "SECOND", "SECONDLY"]) + assert_aliases_deprecated("L", 10000, ["MILLISECOND", "MILLISECONDLY"]) + assert (_period_str_to_code('ms') == 10000) + + assert_aliases_deprecated("U", 11000, ["MICROSECOND", "MICROSECONDLY"]) + assert (_period_str_to_code('US') == 11000) + + assert_aliases_deprecated("N", 12000, ["NANOSECOND", "NANOSECONDLY"]) + assert (_period_str_to_code('NS') == 12000) + + +def test_is_superperiod_subperiod(): + + # input validation + assert not (is_superperiod(offsets.YearEnd(), None)) + assert not (is_subperiod(offsets.MonthEnd(), None)) + assert not (is_superperiod(None, offsets.YearEnd())) + assert not (is_subperiod(None, offsets.MonthEnd())) + assert not (is_superperiod(None, None)) + assert not (is_subperiod(None, None)) + + assert (is_superperiod(offsets.YearEnd(), offsets.MonthEnd())) + assert (is_subperiod(offsets.MonthEnd(), offsets.YearEnd())) + + assert (is_superperiod(offsets.Hour(), offsets.Minute())) + assert (is_subperiod(offsets.Minute(), offsets.Hour())) + + assert (is_superperiod(offsets.Second(), offsets.Milli())) + assert (is_subperiod(offsets.Milli(), offsets.Second())) + + assert (is_superperiod(offsets.Milli(), offsets.Micro())) + assert (is_subperiod(offsets.Micro(), offsets.Milli())) + + assert (is_superperiod(offsets.Micro(), offsets.Nano())) + assert (is_subperiod(offsets.Nano(), offsets.Micro())) diff --git a/pandas/tseries/frequencies.py b/pandas/tseries/frequencies.py index 4d1dd422be946..0cffd818202ed 100644 --- a/pandas/tseries/frequencies.py +++ b/pandas/tseries/frequencies.py @@ -13,37 +13,22 @@ is_datetime64_dtype) from pandas.tseries.offsets import DateOffset -import pandas.tseries.offsets as offsets from pandas._libs.tslib import Timedelta -from pandas._libs.tslibs.frequencies import ( # noqa - get_freq_code, _base_and_stride, _period_str_to_code, - _INVALID_FREQ_ERROR, opattern, _lite_rule_alias, _dont_uppercase, - _period_code_map, _reverse_period_code_map) + +import pandas._libs.tslibs.frequencies as libfreqs +from pandas._libs.tslibs.frequencies import ( # noqa, semi-public API + get_freq, get_base_alias, get_to_timestamp_base, get_freq_code, + FreqGroup, + is_subperiod, is_superperiod) + from pandas._libs.tslibs.resolution import (Resolution, _FrequencyInferer, _TimedeltaFrequencyInferer) -from pandas._libs.tslibs.parsing import _get_rule_month -from pandas._libs.tslibs.ccalendar import MONTH_NUMBERS from pytz import AmbiguousTimeError -class FreqGroup(object): - FR_ANN = 1000 - FR_QTR = 2000 - FR_MTH = 3000 - FR_WK = 4000 - FR_BUS = 5000 - FR_DAY = 6000 - FR_HR = 7000 - FR_MIN = 8000 - FR_SEC = 9000 - FR_MS = 10000 - FR_US = 11000 - FR_NS = 12000 - - RESO_NS = 0 RESO_US = 1 RESO_MS = 2 @@ -52,61 +37,6 @@ class FreqGroup(object): RESO_HR = 5 RESO_DAY = 6 - -def get_to_timestamp_base(base): - """ - Return frequency code group used for base of to_timestamp against - frequency code. - - Example - ------- - # Return day freq code against longer freq than day - >>> get_to_timestamp_base(get_freq_code('D')[0]) - 6000 - >>> get_to_timestamp_base(get_freq_code('W')[0]) - 6000 - >>> get_to_timestamp_base(get_freq_code('M')[0]) - 6000 - - # Return second freq code against hour between second - >>> get_to_timestamp_base(get_freq_code('H')[0]) - 9000 - >>> get_to_timestamp_base(get_freq_code('S')[0]) - 9000 - """ - if base < FreqGroup.FR_BUS: - return FreqGroup.FR_DAY - if FreqGroup.FR_HR <= base <= FreqGroup.FR_SEC: - return FreqGroup.FR_SEC - return base - - -def get_freq(freq): - """ - Return frequency code of given frequency str. - If input is not string, return input as it is. - - Example - ------- - >>> get_freq('A') - 1000 - - >>> get_freq('3A') - 1000 - """ - if isinstance(freq, compat.string_types): - base, mult = get_freq_code(freq) - freq = base - return freq - - -def _get_freq_str(base, mult=1): - code = _reverse_period_code_map.get(base) - if mult == 1: - return code - return str(mult) + code - - # --------------------------------------------------------------------- # Offset names ("time rules") and related functions @@ -195,7 +125,7 @@ def to_offset(freq): stride = freq[1] if isinstance(stride, compat.string_types): name, stride = stride, name - name, _ = _base_and_stride(name) + name, _ = libfreqs._base_and_stride(name) delta = get_offset(name) * stride elif isinstance(freq, timedelta): @@ -212,13 +142,13 @@ def to_offset(freq): else: delta = delta + offset except Exception: - raise ValueError(_INVALID_FREQ_ERROR.format(freq)) + raise ValueError(libfreqs._INVALID_FREQ_ERROR.format(freq)) else: delta = None stride_sign = None try: - splitted = re.split(opattern, freq) + splitted = re.split(libfreqs.opattern, freq) if splitted[-1] != '' and not splitted[-1].isspace(): # the last element must be blank raise ValueError('last element must be blank') @@ -226,7 +156,7 @@ def to_offset(freq): splitted[2::4]): if sep != '' and not sep.isspace(): raise ValueError('separator must be spaces') - prefix = _lite_rule_alias.get(name) or name + prefix = libfreqs._lite_rule_alias.get(name) or name if stride_sign is None: stride_sign = -1 if stride.startswith('-') else 1 if not stride: @@ -243,21 +173,14 @@ def to_offset(freq): else: delta = delta + offset except Exception: - raise ValueError(_INVALID_FREQ_ERROR.format(freq)) + raise ValueError(libfreqs._INVALID_FREQ_ERROR.format(freq)) if delta is None: - raise ValueError(_INVALID_FREQ_ERROR.format(freq)) + raise ValueError(libfreqs._INVALID_FREQ_ERROR.format(freq)) return delta -def get_base_alias(freqstr): - """ - Returns the base frequency alias, e.g., '5D' -> 'D' - """ - return _base_and_stride(freqstr)[0] - - def get_offset(name): """ Return DateOffset object associated with rule name @@ -266,12 +189,12 @@ def get_offset(name): -------- get_offset('EOM') --> BMonthEnd(1) """ - if name not in _dont_uppercase: + if name not in libfreqs._dont_uppercase: name = name.upper() - name = _lite_rule_alias.get(name, name) - name = _lite_rule_alias.get(name.lower(), name) + name = libfreqs._lite_rule_alias.get(name, name) + name = libfreqs._lite_rule_alias.get(name.lower(), name) else: - name = _lite_rule_alias.get(name, name) + name = libfreqs._lite_rule_alias.get(name, name) if name not in _offset_map: try: @@ -282,7 +205,7 @@ def get_offset(name): offset = klass._from_name(*split[1:]) except (ValueError, TypeError, KeyError): # bad prefix or suffix - raise ValueError(_INVALID_FREQ_ERROR.format(name)) + raise ValueError(libfreqs._INVALID_FREQ_ERROR.format(name)) # cache _offset_map[name] = offset # do not return cache because it's mutable @@ -345,158 +268,3 @@ def infer_freq(index, warn=True): inferer = _FrequencyInferer(index, warn=warn) return inferer.get_freq() - - -def _maybe_coerce_freq(code): - """ we might need to coerce a code to a rule_code - and uppercase it - - Parameters - ---------- - source : string - Frequency converting from - - Returns - ------- - string code - """ - - assert code is not None - if isinstance(code, offsets.DateOffset): - code = code.rule_code - return code.upper() - - -def is_subperiod(source, target): - """ - Returns True if downsampling is possible between source and target - frequencies - - Parameters - ---------- - source : string - Frequency converting from - target : string - 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): - """ - 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'] - - -def _is_annual(rule): - rule = rule.upper() - return rule == 'A' or rule.startswith('A-') - - -def _quarter_months_conform(source, target): - snum = MONTH_NUMBERS[source] - tnum = MONTH_NUMBERS[target] - return snum % 3 == tnum % 3 - - -def _is_quarterly(rule): - rule = rule.upper() - return rule == 'Q' or rule.startswith('Q-') or rule.startswith('BQ') - - -def _is_monthly(rule): - rule = rule.upper() - return rule == 'M' or rule == 'BM' - - -def _is_weekly(rule): - rule = rule.upper() - return rule == 'W' or rule.startswith('W-')