diff --git a/doc/source/reference/routines.datetime.rst b/doc/source/reference/routines.datetime.rst new file mode 100644 index 000000000000..aab6f1694d4c --- /dev/null +++ b/doc/source/reference/routines.datetime.rst @@ -0,0 +1,20 @@ +.. _routines.datetime: + +Datetime Support Functions +************************** + +.. currentmodule:: numpy + +Business Day Functions +====================== + +.. currentmodule:: numpy + +.. autosummary:: + :toctree: generated/ + + busdaycalendar + is_busday + busday_offset + busday_count + diff --git a/doc/source/reference/routines.rst b/doc/source/reference/routines.rst index 0788d3a0ab85..c97a3d244fc2 100644 --- a/doc/source/reference/routines.rst +++ b/doc/source/reference/routines.rst @@ -30,6 +30,7 @@ indentation. routines.math routines.functional routines.poly + routines.datetime routines.financial routines.set routines.window diff --git a/numpy/add_newdocs.py b/numpy/add_newdocs.py index 20ce6c37804b..58594b15aebf 100644 --- a/numpy/add_newdocs.py +++ b/numpy/add_newdocs.py @@ -5934,6 +5934,272 @@ def luf(lamdaexpr, *args, **kwargs): """)) +############################################################################## +# +# Datetime-related Methods +# +############################################################################## + +add_newdoc('numpy.core.multiarray', 'busdaycalendar', + """ + busdaycalendar(weekmask='1111100', holidays=None) + + A business day calendar object that efficiently stores + information defining business days for the business + day-related functions. + + Parameters + ---------- + weekmask : str or array_like of bool, optional + A seven-element array indicating which of Monday through Sunday may + be valid business days. May be specified as a list or array, like + [1,1,1,1,1,0,0], a length-seven string like '1111100', or a string + of three-letter weekday names, like 'MonTueWedThuFri'. The latter + string representation is most useful when only one day of the + week is important, like 'Mon' if you want to calculate the date + of Easter. + holidays : array_like of datetime64[D], optional + An array of dates which should be blacked out from being considered + as business days. They may be specified in any order, and NaT + (not-a-time) dates are ignored. Internally, this list is normalized + into a form suited for fast business day calculations. + + Returns + ------- + out : busdaycalendar + A business day calendar object containing the specified + weekmask and holidays. + + See Also + -------- + is_busday : Returns a boolean array indicating valid business days. + busday_offset : Applies an offset counted in business days. + busday_count : Counts how many business days are in a half-open date range. + + Attributes + ---------- + weekmask : seven-element array of bool + holidays : sorted array of datetime64[D] + + Examples + -------- + >>> # Some important days in July + ... bdd = np.busdaycalendar( + ... holidays=['2011-07-01', '2011-07-04', '2011-07-17']) + >>> # Default is Monday to Friday weekdays + ... bdd.weekmask + array([ True, True, True, True, True, False, False], dtype='bool') + >>> # Any holidays already on the weekend are removed + ... bdd.holidays + array(['2011-07-01', '2011-07-04'], dtype='datetime64[D]') + """) + +add_newdoc('numpy.core.multiarray', 'busdaycalendar', ('weekmask', + """A copy of the seven-element boolean mask indicating valid business days.""")) + +add_newdoc('numpy.core.multiarray', 'busdaycalendar', ('holidays', + """A copy of the holiday array indicating blacked out business days.""")) + +add_newdoc('numpy.core.multiarray', 'is_busday', + """ + is_busday(dates, weekmask='1111100', holidays=None, busdaycal=None, out=None) + + Calculates which of the given dates are valid business days, and + which are not. + + Parameters + ---------- + dates : array_like of datetime64[D] + The array of dates to process. + weekmask : str or array_like of bool, optional + A seven-element array indicating which of Monday through Sunday may + be valid business days. May be specified as a list or array, like + [1,1,1,1,1,0,0], a length-seven string like '1111100', or a string + of three-letter weekday names, like 'MonTueWedThuFri'. The latter + string representation is most useful when only one day of the + week is important, like 'Mon' if you want to calculate the date + of Easter. + holidays : array_like of datetime64[D], optional + An array of dates which should be blacked out from being considered + as business days. They may be specified in any order, and NaT + (not-a-time) dates are ignored. Internally, this list is normalized + into a form suited for fast business day calculations. + busdaycal : busdaycalendar, optional + A `busdaycalendar` object which specifies the business days. If this + parameter is provided, neither weekmask nor holidays may be + provided. + out : array of bool, optional + If provided, this array is filled with the result. + + Returns + ------- + out : array of bool + An array with the same shape as ``dates``, containing True for + each valid business day, and False for the others. + + See Also + -------- + busdaycalendar: An object for efficiently specifying which are business days. + busday_offset : Applies an offset counted in business days. + busday_count : Counts how many business days are in a half-open date range. + + Examples + -------- + >>> # The weekdays are Friday, Saturday, and Monday + ... np.is_busday(['2011-07-01', '2011-07-02', '2011-07-18'], + ... holidays=['2011-07-01', '2011-07-04', '2011-07-17']) + array([False, False, True], dtype='bool') + """) + +add_newdoc('numpy.core.multiarray', 'busday_offset', + """ + busday_offset(dates, offsets, roll='raise', weekmask='1111100', holidays=None, busdaycal=None, out=None) + + First adjusts the date to fall on a business day according to + the ``roll`` rule, then applies offsets to the given dates + counted in business days. + + Parameters + ---------- + dates : array_like of datetime64[D] + The array of dates to process. + offsets : array_like of int + The array of offsets, which is broadcast with ``dates``. + roll : {'raise', 'nat', 'forward', 'following', 'backward', 'preceding', 'modifiedfollowing', 'modifiedpreceding'}, optional + How to treat dates that do not fall on a business day. The default + is 'raise'. + + * 'raise' means to raise an exception for invalid business days. + * 'nat' means to return a NaT (not-a-time) for invalid business days. + * 'forward' and 'following' mean to take the first business day + later in time. + * 'backward' and 'preceding' mean to take the first business day + earlier in time. + * 'modifiedfollowing' means to take the first business day + later in time unless it is across a Month boundary, in which + case to take the first business day earlier in time. + * 'modifiedpreceding' means to take the first business day + earlier in time unless it is across a Month boundary, in which + case to take the first business day later in time. + weekmask : str or array_like of bool, optional + A seven-element array indicating which of Monday through Sunday may + be valid business days. May be specified as a list or array, like + [1,1,1,1,1,0,0], a length-seven string like '1111100', or a string + of three-letter weekday names, like 'MonTueWedThuFri'. The latter + string representation is most useful when only one day of the + week is important, like 'Mon' if you want to calculate the date + of Easter. + holidays : array_like of datetime64[D], optional + An array of dates which should be blacked out from being considered + as business days. They may be specified in any order, and NaT + (not-a-time) dates are ignored. Internally, this list is normalized + into a form suited for fast business day calculations. + busdaycal : busdaycalendar, optional + A `busdaycalendar` object which specifies the business days. If this + parameter is provided, neither weekmask nor holidays may be + provided. + out : array of datetime64[D], optional + If provided, this array is filled with the result. + + Returns + ------- + out : array of datetime64[D] + An array with a shape from broadcasting ``dates`` and ``offsets`` + together, containing the dates with offsets applied. + + See Also + -------- + busdaycalendar: An object for efficiently specifying which are business days. + is_busday : Returns a boolean array indicating valid business days. + busday_count : Counts how many business days are in a half-open date range. + + Examples + -------- + >>> # First business day in October 2011 (not accounting for holidays) + ... np.busday_offset('2011-10', 0, roll='forward') + numpy.datetime64('2011-10-03','D') + >>> # Last business day in February 2012 (not accounting for holidays) + ... np.busday_offset('2012-03', -1, roll='forward') + numpy.datetime64('2012-02-29','D') + >>> # Third Wednesday in January 2011 + ... np.busday_offset('2011-01', 2, roll='forward', weekmask='Wed') + numpy.datetime64('2011-01-19','D') + >>> # 2012 Mother's Day in Canada and the U.S. + ... np.busday_offset('2012-05', 1, roll='forward', weekmask='Sun') + numpy.datetime64('2012-05-13','D') + + >>> # First business day on or after a date + ... np.busday_offset('2011-03-20', 0, roll='forward') + numpy.datetime64('2011-03-21','D') + >>> np.busday_offset('2011-03-22', 0, roll='forward') + numpy.datetime64('2011-03-22','D') + >>> # First business day after a date + ... np.busday_offset('2011-03-20', 1, roll='backward') + numpy.datetime64('2011-03-21','D') + >>> np.busday_offset('2011-03-22', 1, roll='backward') + numpy.datetime64('2011-03-23','D') + """) + +add_newdoc('numpy.core.multiarray', 'busday_count', + """ + busday_count(begindates, enddates, weekmask='1111100', holidays=[], busdaycal=None, out=None) + + Counts the number of business days between `begindates` and + `enddates`, not including the day of `enddates`. + + Parameters + ---------- + begindates : array_like of datetime64[D] + The array of the first dates for counting. + enddates : array_like of datetime64[D] + The array of the end dates for counting, which are excluded + from the count themselves. + weekmask : str or array_like of bool, optional + A seven-element array indicating which of Monday through Sunday may + be valid business days. May be specified as a list or array, like + [1,1,1,1,1,0,0], a length-seven string like '1111100', or a string + of three-letter weekday names, like 'MonTueWedThuFri'. The latter + string representation is most useful when only one day of the + week is important, like 'Mon' if you want to calculate the date + of Easter. + holidays : array_like of datetime64[D], optional + An array of dates which should be blacked out from being considered + as business days. They may be specified in any order, and NaT + (not-a-time) dates are ignored. Internally, this list is normalized + into a form suited for fast business day calculations. + busdaycal : busdaycalendar, optional + A `busdaycalendar` object which specifies the business days. If this + parameter is provided, neither weekmask nor holidays may be + provided. + out : array of int, optional + If provided, this array is filled with the result. + + Returns + ------- + out : array of int + An array with a shape from broadcasting ``begindates`` and ``enddates`` + together, containing the number of business days between + the begin and end dates. + + See Also + -------- + busdaycalendar: An object for efficiently specifying which are business days. + is_busday : Returns a boolean array indicating valid business days. + busday_offset : Applies an offset counted in business days. + + Examples + -------- + >>> # Number of weekdays in January 2011 + ... np.busday_count('2011-01', '2011-02') + 21 + >>> # Number of weekdays in 2011 + ... np.busday_count('2011', '2012') + 260 + >>> # Number of Saturdays in 2011 + ... np.busday_count('2011', '2012', weekmask='Sat') + 53 + """) + ############################################################################## # # nd_grid instances diff --git a/numpy/core/SConscript b/numpy/core/SConscript index bb19b9828e1b..0baea3d0c8ab 100644 --- a/numpy/core/SConscript +++ b/numpy/core/SConscript @@ -439,6 +439,8 @@ if ENABLE_SEPARATE_COMPILATION: pjoin('src', 'multiarray', 'hashdescr.c'), pjoin('src', 'multiarray', 'arrayobject.c'), pjoin('src', 'multiarray', 'datetime.c'), + pjoin('src', 'multiarray', 'datetime_busday.c'), + pjoin('src', 'multiarray', 'datetime_busdaycal.c'), pjoin('src', 'multiarray', 'numpyos.c'), pjoin('src', 'multiarray', 'flagsobject.c'), pjoin('src', 'multiarray', 'descriptor.c'), diff --git a/numpy/core/arrayprint.py b/numpy/core/arrayprint.py index 8595d6f021a9..556d4da04b22 100644 --- a/numpy/core/arrayprint.py +++ b/numpy/core/arrayprint.py @@ -718,8 +718,5 @@ def __init__(self, data): self.format = '%' + str(max_str_len) + 'd' def __call__(self, x): - if _MININT < x < _MAXINT: - return self.format % x.astype('i8') - else: - return "%s" % x + return self.format % x.astype('i8') diff --git a/numpy/core/code_generators/genapi.py b/numpy/core/code_generators/genapi.py index 3cded28109fa..844aebff0af9 100644 --- a/numpy/core/code_generators/genapi.py +++ b/numpy/core/code_generators/genapi.py @@ -44,6 +44,8 @@ join('multiarray', 'conversion_utils.c'), join('multiarray', 'buffer.c'), join('multiarray', 'datetime.c'), + join('multiarray', 'datetime_busday.c'), + join('multiarray', 'datetime_busdaycal.c'), join('multiarray', 'nditer.c.src'), join('multiarray', 'nditer_pywrap.c'), join('multiarray', 'einsum.c.src'), diff --git a/numpy/core/code_generators/generate_umath.py b/numpy/core/code_generators/generate_umath.py index 599ffddc8bf2..957607f40113 100644 --- a/numpy/core/code_generators/generate_umath.py +++ b/numpy/core/code_generators/generate_umath.py @@ -275,6 +275,7 @@ def english_upper(s): TD(intfltcmplx), [TypeDescription('m', FullTypeDescr, 'mq', 'm'), TypeDescription('m', FullTypeDescr, 'md', 'm'), + TypeDescription('m', FullTypeDescr, 'mm', 'd'), ], TD(O, f='PyNumber_Divide'), ), diff --git a/numpy/core/include/numpy/ndarraytypes.h b/numpy/core/include/numpy/ndarraytypes.h index 5bed8d70cd49..d3e28fe93029 100644 --- a/numpy/core/include/numpy/ndarraytypes.h +++ b/numpy/core/include/numpy/ndarraytypes.h @@ -233,7 +233,6 @@ typedef enum { NPY_FR_Y, /* Years */ NPY_FR_M, /* Months */ NPY_FR_W, /* Weeks */ - NPY_FR_B, /* Business days (weekdays, doesn't account for holidays) */ NPY_FR_D, /* Days */ NPY_FR_h, /* hours */ NPY_FR_m, /* minutes */ @@ -243,16 +242,16 @@ typedef enum { NPY_FR_ns,/* nanoseconds */ NPY_FR_ps,/* picoseconds */ NPY_FR_fs,/* femtoseconds */ - NPY_FR_as /* attoseconds */ + NPY_FR_as,/* attoseconds */ + NPY_FR_GENERIC /* Generic, unbound units, can convert to anything */ } NPY_DATETIMEUNIT; -#define NPY_DATETIME_NUMUNITS (NPY_FR_as + 1) -#define NPY_DATETIME_DEFAULTUNIT NPY_FR_us +#define NPY_DATETIME_NUMUNITS (NPY_FR_GENERIC + 1) +#define NPY_DATETIME_DEFAULTUNIT NPY_FR_GENERIC #define NPY_STR_Y "Y" #define NPY_STR_M "M" #define NPY_STR_W "W" -#define NPY_STR_B "B" #define NPY_STR_D "D" #define NPY_STR_h "h" #define NPY_STR_m "m" @@ -264,6 +263,32 @@ typedef enum { #define NPY_STR_fs "fs" #define NPY_STR_as "as" +/* + * Business day conventions for mapping invalid business + * days to valid business days. + */ +typedef enum { + /* Go forward in time to the following business day. */ + NPY_BUSDAY_FORWARD, + NPY_BUSDAY_FOLLOWING = NPY_BUSDAY_FORWARD, + /* Go backward in time to the preceding business day. */ + NPY_BUSDAY_BACKWARD, + NPY_BUSDAY_PRECEDING = NPY_BUSDAY_BACKWARD, + /* + * Go forward in time to the following business day, unless it + * crosses a month boundary, in which case go backward + */ + NPY_BUSDAY_MODIFIEDFOLLOWING, + /* + * Go backward in time to the preceding business day, unless it + * crosses a month boundary, in which case go forward. + */ + NPY_BUSDAY_MODIFIEDPRECEDING, + /* Produce a NaT for non-business days. */ + NPY_BUSDAY_NAT, + /* Raise an exception for non-business days. */ + NPY_BUSDAY_RAISE +} NPY_BUSDAY_ROLL; /* * This is to typedef npy_intp to the appropriate pointer size for diff --git a/numpy/core/include/numpy/ufuncobject.h b/numpy/core/include/numpy/ufuncobject.h index ae8f06827e6b..b4354b58cdc7 100644 --- a/numpy/core/include/numpy/ufuncobject.h +++ b/numpy/core/include/numpy/ufuncobject.h @@ -25,7 +25,8 @@ struct _tagPyUFuncObject; * type_tup: Either NULL, or the type_tup passed to the ufunc. * out_dtypes: An array which should be populated with new * references to (ufunc->nin + ufunc->nout) new - * dtypes, one for each input and output. + * dtypes, one for each input and output. These + * dtypes should all be in native-endian format. * out_innerloop: Should be populated with the correct ufunc inner * loop for the given type. * out_innerloopdata: Should be populated with the void* data to diff --git a/numpy/core/numerictypes.py b/numpy/core/numerictypes.py index 6d655cb1707e..f1152cb74e33 100644 --- a/numpy/core/numerictypes.py +++ b/numpy/core/numerictypes.py @@ -92,10 +92,12 @@ __all__ = ['sctypeDict', 'sctypeNA', 'typeDict', 'typeNA', 'sctypes', 'ScalarType', 'obj2sctype', 'cast', 'nbytes', 'sctype2char', 'maximum_sctype', 'issctype', 'typecodes', 'find_common_type', - 'issubdtype','datetime_data','datetime_as_string'] + 'issubdtype', 'datetime_data','datetime_as_string', + 'busday_offset', 'busday_count', 'is_busday', 'busdaycalendar'] from numpy.core.multiarray import typeinfo, ndarray, array, \ - empty, dtype, datetime_data, datetime_as_string + empty, dtype, datetime_data, datetime_as_string, \ + busday_offset, busday_count, is_busday, busdaycalendar import types as _types import sys diff --git a/numpy/core/setup.py b/numpy/core/setup.py index 78bf14f502e1..87e4e4f90f0d 100644 --- a/numpy/core/setup.py +++ b/numpy/core/setup.py @@ -747,6 +747,8 @@ def get_mathlib_info(*args): join('src', 'multiarray', 'numpymemoryview.c'), join('src', 'multiarray', 'buffer.c'), join('src', 'multiarray', 'datetime.c'), + join('src', 'multiarray', 'datetime_busday.c'), + join('src', 'multiarray', 'datetime_busdaycal.c'), join('src', 'multiarray', 'numpyos.c'), join('src', 'multiarray', 'conversion_utils.c'), join('src', 'multiarray', 'flagsobject.c'), diff --git a/numpy/core/src/multiarray/_datetime.h b/numpy/core/src/multiarray/_datetime.h index 519a1c545aa5..8a9ffff7b885 100644 --- a/numpy/core/src/multiarray/_datetime.h +++ b/numpy/core/src/multiarray/_datetime.h @@ -4,6 +4,18 @@ NPY_NO_EXPORT void numpy_pydatetime_import(); +/* + * Creates a datetime or timedelta dtype using a copy of the provided metadata. + */ +NPY_NO_EXPORT PyArray_Descr * +create_datetime_dtype(int type_num, PyArray_DatetimeMetaData *meta); + +/* + * Creates a datetime or timedelta dtype using the given unit. + */ +NPY_NO_EXPORT PyArray_Descr * +create_datetime_dtype_with_unit(int type_num, NPY_DATETIMEUNIT unit); + /* * This function returns the a new reference to the * capsule with the datetime metadata. @@ -35,6 +47,13 @@ convert_datetimestruct_to_datetime(PyArray_DatetimeMetaData *meta, const npy_datetimestruct *dts, npy_datetime *out); +/* + * Extracts the month number, within the current year, + * from a 'datetime64[D]' value. January is 1, etc. + */ +NPY_NO_EXPORT int +days_to_month_number(npy_datetime days); + /* * Parses the metadata string into the metadata C structure. * @@ -45,15 +64,6 @@ parse_datetime_metadata_from_metastr(char *metastr, Py_ssize_t len, PyArray_DatetimeMetaData *out_meta); -/* - * This function returns a reference to a capsule - * which contains the datetime metadata parsed from a metadata - * string. 'metastr' should be NULL-terminated, and len should - * contain its string length. - */ -NPY_NO_EXPORT PyObject * -parse_datetime_metacobj_from_metastr(char *metastr, Py_ssize_t len); - /* * Converts a datetype dtype string into a dtype descr object. * The "type" string should be NULL-terminated, and len should @@ -101,10 +111,11 @@ datetime_metadata_divides( * Returns a capsule with the GCD metadata. */ NPY_NO_EXPORT PyObject * -compute_datetime_metadata_greatest_common_divisor( +compute_datetime_metadata_greatest_common_divisor_capsule( PyArray_Descr *type1, PyArray_Descr *type2, - int strict_with_nonlinear_units); + int strict_with_nonlinear_units1, + int strict_with_nonlinear_units2); /* * Computes the conversion factor to convert data with 'src_meta' metadata @@ -175,23 +186,48 @@ append_metastr_to_string(PyArray_DatetimeMetaData *meta, NPY_NO_EXPORT int get_datetime_iso_8601_strlen(int local, NPY_DATETIMEUNIT base); - /* * Parses (almost) standard ISO 8601 date strings. The differences are: * + * + After the date and time, may place a ' ' followed by an event number. * + The date "20100312" is parsed as the year 20100312, not as * equivalent to "2010-03-12". The '-' in the dates are not optional. * + Only seconds may have a decimal point, with up to 18 digits after it * (maximum attoseconds precision). * + Either a 'T' as in ISO 8601 or a ' ' may be used to separate * the date and the time. Both are treated equivalently. + * + Doesn't (yet) handle the "YYYY-DDD" or "YYYY-Www" formats. + * + Doesn't handle leap seconds (seconds value has 60 in these cases). + * + Doesn't handle 24:00:00 as synonym for midnight (00:00:00) tomorrow + * + Accepts special values "NaT" (not a time), "Today", (current + * day according to local time) and "Now" (current time in UTC). * * 'str' must be a NULL-terminated string, and 'len' must be its length. + * 'unit' should contain -1 if the unit is unknown, or the unit + * which will be used if it is. + * + * 'out' gets filled with the parsed date-time. + * 'out_local' gets set to 1 if the parsed time was in local time, + * to 0 otherwise. The values 'now' and 'today' don't get counted + * as local, and neither do UTC +/-#### timezone offsets, because + * they aren't using the computer's local timezone offset. + * 'out_bestunit' gives a suggested unit based on the amount of + * resolution provided in the string, or -1 for NaT. + * 'out_special' gets set to 1 if the parsed time was 'today', + * 'now', or ''/'NaT'. For 'today', the unit recommended is + * 'D', for 'now', the unit recommended is 's', and for 'NaT' + * the unit recommended is 'Y'. + * * * Returns 0 on success, -1 on failure. */ NPY_NO_EXPORT int -parse_iso_8601_date(char *str, int len, npy_datetimestruct *out); +parse_iso_8601_date(char *str, int len, + NPY_DATETIMEUNIT unit, + npy_datetimestruct *out, + npy_bool *out_local, + NPY_DATETIMEUNIT *out_bestunit, + npy_bool *out_special); /* * Converts an npy_datetimestruct to an (almost) ISO 8601 @@ -214,19 +250,26 @@ NPY_NO_EXPORT int make_iso_8601_date(npy_datetimestruct *dts, char *outstr, int outlen, int local, NPY_DATETIMEUNIT base, int tzoffset); - /* * Tests for and converts a Python datetime.datetime or datetime.date * object into a NumPy npy_datetimestruct. * + * 'out_bestunit' gives a suggested unit based on whether the object + * was a datetime.date or datetime.datetime object. + * * Returns -1 on error, 0 on success, and 1 (with no error set) * if obj doesn't have the neeeded date or datetime attributes. */ NPY_NO_EXPORT int -convert_pydatetime_to_datetimestruct(PyObject *obj, npy_datetimestruct *out); +convert_pydatetime_to_datetimestruct(PyObject *obj, npy_datetimestruct *out, + NPY_DATETIMEUNIT *out_bestunit); /* - * Converts a PyObject * into a datetime, in any of the input forms supported. + * Converts a PyObject * into a datetime, in any of the forms supported. + * + * If the units metadata isn't known ahead of time, set meta->base + * to -1, and this function will populate meta with either default + * values or values from the input object. * * Returns -1 on error, 0 on success. */ @@ -237,6 +280,10 @@ convert_pyobject_to_datetime(PyArray_DatetimeMetaData *meta, PyObject *obj, /* * Converts a PyObject * into a timedelta, in any of the forms supported * + * If the units metadata isn't known ahead of time, set meta->base + * to -1, and this function will populate meta with either default + * values or values from the input object. + * * Returns -1 on error, 0 on success. */ NPY_NO_EXPORT int @@ -329,4 +376,26 @@ cast_timedelta_to_timedelta(PyArray_DatetimeMetaData *src_meta, npy_timedelta src_dt, npy_timedelta *dst_dt); +/* + * Returns true if the object is something that is best considered + * a Datetime or Timedelta, false otherwise. + */ +NPY_NO_EXPORT npy_bool +is_any_numpy_datetime_or_timedelta(PyObject *obj); + +/* + * Implements a datetime-specific arange + */ +NPY_NO_EXPORT PyArrayObject * +datetime_arange(PyObject *start, PyObject *stop, PyObject *step, + PyArray_Descr *dtype); + +/* + * Examines all the objects in the given Python object by + * recursively descending the sequence structure. Returns a + * datetime or timedelta type with metadata based on the data. + */ +NPY_NO_EXPORT PyArray_Descr * +find_object_datetime_type(PyObject *obj, int type_num); + #endif diff --git a/numpy/core/src/multiarray/arraytypes.c.src b/numpy/core/src/multiarray/arraytypes.c.src index e55c7c1337ae..e4dab1f7e6b7 100644 --- a/numpy/core/src/multiarray/arraytypes.c.src +++ b/numpy/core/src/multiarray/arraytypes.c.src @@ -381,7 +381,7 @@ UNICODE_setitem(PyObject *op, char *ov, PyArrayObject *ap) memcpy(ov, ptr, MIN(ap->descr->elsize, datalen)); #else if (!PyArray_ISALIGNED(ap)) { - buffer = _pya_malloc(ap->descr->elsize); + buffer = PyArray_malloc(ap->descr->elsize); if (buffer == NULL) { Py_DECREF(temp); PyErr_NoMemory(); @@ -396,7 +396,7 @@ UNICODE_setitem(PyObject *op, char *ov, PyArrayObject *ap) datalen <<= 2; if (!PyArray_ISALIGNED(ap)) { memcpy(ov, buffer, datalen); - _pya_free(buffer); + PyArray_free(buffer); } #endif /* Fill in the rest of the space with 0 */ @@ -2110,7 +2110,7 @@ UNICODE_nonzero (PyArray_UCS4 *ip, PyArrayObject *ap) char *buffer = NULL; if ((!PyArray_ISNOTSWAPPED(ap)) || (!PyArray_ISALIGNED(ap))) { - buffer = _pya_malloc(ap->descr->elsize); + buffer = PyArray_malloc(ap->descr->elsize); if (buffer == NULL) { return nonz; } @@ -2128,7 +2128,7 @@ UNICODE_nonzero (PyArray_UCS4 *ip, PyArrayObject *ap) } ip++; } - _pya_free(buffer); + PyArray_free(buffer); return nonz; } @@ -2486,7 +2486,7 @@ VOID_compare(char *ip1, char *ip2, PyArrayObject *ap) if ((swap) || (new->alignment > 1)) { if ((swap) || (((intp)(nip1) % new->alignment) != 0)) { /* create buffer and copy */ - nip1 = _pya_malloc(new->elsize); + nip1 = PyArray_malloc(new->elsize); if (nip1 == NULL) { goto finish; } @@ -2496,10 +2496,10 @@ VOID_compare(char *ip1, char *ip2, PyArrayObject *ap) } if ((swap) || (((intp)(nip2) % new->alignment) != 0)) { /* copy data to a buffer */ - nip2 = _pya_malloc(new->elsize); + nip2 = PyArray_malloc(new->elsize); if (nip2 == NULL) { if (nip1 != ip1+offset) { - _pya_free(nip1); + PyArray_free(nip1); } goto finish; } @@ -2511,10 +2511,10 @@ VOID_compare(char *ip1, char *ip2, PyArrayObject *ap) res = new->f->compare(nip1, nip2, ap); if ((swap) || (new->alignment > 1)) { if (nip1 != ip1+offset) { - _pya_free(nip1); + PyArray_free(nip1); } if (nip2 != ip2+offset) { - _pya_free(nip2); + PyArray_free(nip2); } } if (res != 0) { @@ -2647,7 +2647,7 @@ static int { intp i; int elsize = aip->descr->elsize; - @type@ *mp = (@type@ *)_pya_malloc(elsize); + @type@ *mp = (@type@ *)PyArray_malloc(elsize); if (mp==NULL) return 0; memcpy(mp, ip, elsize); @@ -2659,7 +2659,7 @@ static int *max_ind=i; } } - _pya_free(mp); + PyArray_free(mp); return 0; } @@ -3433,8 +3433,8 @@ _init_datetime_descr(PyArray_Descr *descr) PyArray_DatetimeMetaData *dt_data; PyObject *cobj; - dt_data = _pya_malloc(sizeof(PyArray_DatetimeMetaData)); - dt_data->base = NPY_FR_us; + dt_data = PyArray_malloc(sizeof(PyArray_DatetimeMetaData)); + dt_data->base = NPY_DATETIME_DEFAULTUNIT; dt_data->num = 1; dt_data->events = 1; @@ -3487,35 +3487,35 @@ PyArray_DescrFromType(int type) { PyArray_Descr *ret = NULL; - if (type < PyArray_NTYPES) { + if (type < NPY_NTYPES) { ret = _builtin_descrs[type]; } - else if (type == PyArray_NOTYPE) { + else if (type == NPY_NOTYPE) { /* * This needs to not raise an error so - * that PyArray_DescrFromType(PyArray_NOTYPE) + * that PyArray_DescrFromType(NPY_NOTYPE) * works for backwards-compatible C-API */ return NULL; } - else if ((type == PyArray_CHAR) || (type == PyArray_CHARLTR)) { - ret = PyArray_DescrNew(_builtin_descrs[PyArray_STRING]); + else if ((type == NPY_CHAR) || (type == NPY_CHARLTR)) { + ret = PyArray_DescrNew(_builtin_descrs[NPY_STRING]); if (ret == NULL) { return NULL; } ret->elsize = 1; - ret->type = PyArray_CHARLTR; + ret->type = NPY_CHARLTR; return ret; } else if (PyTypeNum_ISUSERDEF(type)) { - ret = userdescrs[type - PyArray_USERDEF]; + ret = userdescrs[type - NPY_USERDEF]; } else { - int num = PyArray_NTYPES; + int num = NPY_NTYPES; if (type < _MAX_LETTER) { num = (int) _letter_to_num[type]; } - if (num >= PyArray_NTYPES) { + if (num >= NPY_NTYPES) { ret = NULL; } else { @@ -3606,9 +3606,9 @@ set_typeinfo(PyObject *dict) * CFLOAT, CDOUBLE, CLONGDOUBLE, OBJECT, STRING, UNICODE, VOID, * DATETIME,TIMEDELTA# */ - _letter_to_num[PyArray_@name@LTR] = PyArray_@name@; + _letter_to_num[NPY_@name@LTR] = NPY_@name@; /**end repeat**/ - _letter_to_num[PyArray_STRINGLTR2] = PyArray_STRING; + _letter_to_num[NPY_STRINGLTR2] = NPY_STRING; /**begin repeat * #name = BOOL, BYTE, UBYTE, SHORT, USHORT, INT, UINT, diff --git a/numpy/core/src/multiarray/convert_datatype.c b/numpy/core/src/multiarray/convert_datatype.c index 73d65c12d2c9..9e0b93a56fbf 100644 --- a/numpy/core/src/multiarray/convert_datatype.c +++ b/numpy/core/src/multiarray/convert_datatype.c @@ -565,6 +565,22 @@ promote_types(PyArray_Descr *type1, PyArray_Descr *type2, } +/* + * Returns a new reference to type if it is already NBO, otherwise + * returns a copy converted to NBO. + */ +static PyArray_Descr * +ensure_dtype_nbo(PyArray_Descr *type) +{ + if (PyArray_ISNBO(type->byteorder)) { + Py_INCREF(type); + return type; + } + else { + return PyArray_DescrNewByteorder(type, NPY_NATIVE); + } +} + /*NUMPY_API * Produces the smallest size and lowest kind type to which both * input types can be cast. @@ -594,23 +610,11 @@ PyArray_PromoteTypes(PyArray_Descr *type1, PyArray_Descr *type2) if (PyArray_CanCastTo(type2, type1)) { /* Promoted types are always native byte order */ - if (PyArray_ISNBO(type1->byteorder)) { - Py_INCREF(type1); - return type1; - } - else { - return PyArray_DescrNewByteorder(type1, NPY_NATIVE); - } + return ensure_dtype_nbo(type1); } else if (PyArray_CanCastTo(type1, type2)) { /* Promoted types are always native byte order */ - if (PyArray_ISNBO(type2->byteorder)) { - Py_INCREF(type2); - return type2; - } - else { - return PyArray_DescrNewByteorder(type2, NPY_NATIVE); - } + return ensure_dtype_nbo(type2); } /* Convert the 'kind' char into a scalar kind */ @@ -691,8 +695,7 @@ PyArray_PromoteTypes(PyArray_Descr *type1, PyArray_Descr *type2) /* BOOL can convert to anything except datetime/void */ case NPY_BOOL: if (type_num2 != NPY_DATETIME && type_num2 != NPY_VOID) { - Py_INCREF(type2); - return type2; + return ensure_dtype_nbo(type2); } else { break; @@ -701,18 +704,15 @@ PyArray_PromoteTypes(PyArray_Descr *type1, PyArray_Descr *type2) case NPY_STRING: if (type_num2 == NPY_STRING) { if (type1->elsize > type2->elsize) { - Py_INCREF(type1); - return type1; + return ensure_dtype_nbo(type1); } else { - Py_INCREF(type2); - return type2; + return ensure_dtype_nbo(type2); } } else if (type_num2 == NPY_UNICODE) { if (type2->elsize >= type1->elsize * 4) { - Py_INCREF(type2); - return type2; + return ensure_dtype_nbo(type2); } else { PyArray_Descr *d = PyArray_DescrNewFromType(NPY_UNICODE); @@ -725,24 +725,20 @@ PyArray_PromoteTypes(PyArray_Descr *type1, PyArray_Descr *type2) } /* Allow NUMBER -> STRING */ else if (PyTypeNum_ISNUMBER(type_num2)) { - Py_INCREF(type1); - return type1; + return ensure_dtype_nbo(type1); } case NPY_UNICODE: if (type_num2 == NPY_UNICODE) { if (type1->elsize > type2->elsize) { - Py_INCREF(type1); - return type1; + return ensure_dtype_nbo(type1); } else { - Py_INCREF(type2); - return type2; + return ensure_dtype_nbo(type2); } } else if (type_num2 == NPY_STRING) { if (type1->elsize >= type2->elsize * 4) { - Py_INCREF(type1); - return type1; + return ensure_dtype_nbo(type1); } else { PyArray_Descr *d = PyArray_DescrNewFromType(NPY_UNICODE); @@ -755,8 +751,7 @@ PyArray_PromoteTypes(PyArray_Descr *type1, PyArray_Descr *type2) } /* Allow NUMBER -> UNICODE */ else if (PyTypeNum_ISNUMBER(type_num2)) { - Py_INCREF(type1); - return type1; + return ensure_dtype_nbo(type1); } break; case NPY_DATETIME: @@ -772,8 +767,7 @@ PyArray_PromoteTypes(PyArray_Descr *type1, PyArray_Descr *type2) case NPY_BOOL: if (type_num1 != NPY_DATETIME && type_num1 != NPY_TIMEDELTA && type_num1 != NPY_VOID) { - Py_INCREF(type1); - return type1; + return ensure_dtype_nbo(type1); } else { break; @@ -781,35 +775,25 @@ PyArray_PromoteTypes(PyArray_Descr *type1, PyArray_Descr *type2) case NPY_STRING: /* Allow NUMBER -> STRING */ if (PyTypeNum_ISNUMBER(type_num1)) { - Py_INCREF(type2); - return type2; + return ensure_dtype_nbo(type2); } case NPY_UNICODE: /* Allow NUMBER -> UNICODE */ if (PyTypeNum_ISNUMBER(type_num1)) { - Py_INCREF(type2); - return type2; - } - break; - case NPY_DATETIME: - if (PyTypeNum_ISINTEGER(type_num1)) { - Py_INCREF(type2); - return type2; + return ensure_dtype_nbo(type2); } break; case NPY_TIMEDELTA: if (PyTypeNum_ISINTEGER(type_num1) || PyTypeNum_ISFLOAT(type_num1)) { - Py_INCREF(type2); - return type2; + return ensure_dtype_nbo(type2); } break; } - /* For equivalent types we can return either */ - if (PyArray_EquivTypes(type1, type2)) { - Py_INCREF(type1); - return type1; + /* For types equivalent up to endianness, can return either */ + if (PyArray_CanCastTypeTo(type1, type2, NPY_EQUIV_CASTING)) { + return ensure_dtype_nbo(type1); } /* TODO: Also combine fields, subarrays, strings, etc */ diff --git a/numpy/core/src/multiarray/ctors.c b/numpy/core/src/multiarray/ctors.c index a292a1a82592..8d4a7430b038 100644 --- a/numpy/core/src/multiarray/ctors.c +++ b/numpy/core/src/multiarray/ctors.c @@ -14,16 +14,12 @@ #include "numpy/npy_3kcompat.h" #include "common.h" - #include "ctors.h" - #include "shape.h" - #include "buffer.h" - #include "numpymemoryview.h" - #include "lowlevel_strided_loops.h" +#include "_datetime.h" /* * Reading from a file or a string. @@ -1694,6 +1690,36 @@ PyArray_FromAny(PyObject *op, PyArray_Descr *newtype, int min_depth, } } } + /* + * Treat datetime generic units with the same idea as flexible strings. + * + * Flexible strings, for example the dtype 'str', use size zero as a + * signal indicating that they represent a "generic string type" instead + * of a string type with the size already baked in. The generic unit + * plays the same role, indicating that it's a "generic datetime type", + * and the actual unit should be filled in when needed just like the + * actual string size should be filled in when needed. + */ + else if (newtype != NULL && newtype->type_num == NPY_DATETIME) { + PyArray_DatetimeMetaData *meta = + get_datetime_metadata_from_dtype(newtype); + if (meta == NULL) { + Py_DECREF(newtype); + return NULL; + } + + if (meta->base == NPY_FR_GENERIC) { + /* Detect the unit from the input's data */ + PyArray_Descr *dtype = find_object_datetime_type(op, + newtype->type_num); + if (dtype == NULL) { + Py_DECREF(newtype); + return NULL; + } + Py_DECREF(newtype); + newtype = dtype; + } + } /* If we got dimensions and dtype instead of an array */ if (arr == NULL) { @@ -3083,6 +3109,15 @@ PyArray_ArangeObj(PyObject *start, PyObject *stop, PyObject *step, PyArray_Descr PyArray_Descr *native = NULL; int swap; + /* Datetime arange is handled specially */ + if ((dtype != NULL && (dtype->type_num == NPY_DATETIME || + dtype->type_num == NPY_TIMEDELTA)) || + (dtype == NULL && (is_any_numpy_datetime_or_timedelta(start) || + is_any_numpy_datetime_or_timedelta(stop) || + is_any_numpy_datetime_or_timedelta(step)))) { + return (PyObject *)datetime_arange(start, stop, step, dtype); + } + if (!dtype) { PyArray_Descr *deftype; PyArray_Descr *newtype; diff --git a/numpy/core/src/multiarray/datetime.c b/numpy/core/src/multiarray/datetime.c index a1fcd54fad66..16ac5c4fbf38 100644 --- a/numpy/core/src/multiarray/datetime.c +++ b/numpy/core/src/multiarray/datetime.c @@ -1,3 +1,12 @@ +/* + * This file implements core functionality for NumPy datetime + * + * Written by Mark Wiebe (mwwiebe@gmail.com) + * Copyright (c) 2011 by Enthought, Inc. + * + * See LICENSE.txt for the license. + */ + #define PY_SSIZE_T_CLEAN #include #include @@ -8,7 +17,6 @@ #include #include "npy_config.h" - #include "numpy/npy_3kcompat.h" #include "numpy/arrayscalars.h" @@ -27,16 +35,11 @@ numpy_pydatetime_import() static int is_leapyear(npy_int64 year); - -/* For defaults and errors */ -#define NPY_FR_ERR -1 - /* Exported as DATETIMEUNITS in multiarraymodule.c */ NPY_NO_EXPORT char *_datetime_strings[] = { NPY_STR_Y, NPY_STR_M, NPY_STR_W, - NPY_STR_B, NPY_STR_D, NPY_STR_h, NPY_STR_m, @@ -46,53 +49,16 @@ NPY_NO_EXPORT char *_datetime_strings[] = { NPY_STR_ns, NPY_STR_ps, NPY_STR_fs, - NPY_STR_as + NPY_STR_as, + "generic" }; -/* - ==================================================== - } - == Beginning of section borrowed from mx.DateTime == - ==================================================== -*/ - -/* - * Functions in the following section are borrowed from mx.DateTime version - * 2.0.6, and hence this code is subject to the terms of the egenix public - * license version 1.0.0 - */ - -/* Table of number of days in a month (0-based, without and with leap) */ +/* Days per month, regular year and leap year */ static int days_in_month[2][12] = { { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }, { 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 } }; -/* - * Return the day of the week for the given absolute date. - * Monday is 0 and Sunday is 6 - */ -static int -day_of_week(npy_longlong absdate) -{ - /* Add in four for the Thursday on Jan 1, 1970 (epoch offset)*/ - absdate += 4; - - if (absdate >= 0) { - return absdate % 7; - } - else { - return 6 + (absdate + 1) % 7; - } -} - -/* - ==================================================== - == End of section adapted from mx.DateTime == - ==================================================== -*/ - - static int is_leapyear(npy_int64 year) { @@ -172,7 +138,8 @@ days_to_yearsdays(npy_int64 *days_) { const npy_int64 days_per_400years = (400*365 + 100 - 4 + 1); /* Adjust so it's relative to the year 2000 (divisible by 400) */ - npy_int64 days = (*days_) - (365*30 + 7), year; + npy_int64 days = (*days_) - (365*30 + 7); + npy_int64 year; /* Break down the 400 year cycle to get the year and day within the year */ if (days >= 0) { @@ -205,6 +172,29 @@ days_to_yearsdays(npy_int64 *days_) return year + 2000; } +/* Extracts the month number from a 'datetime64[D]' value */ +NPY_NO_EXPORT int +days_to_month_number(npy_datetime days) +{ + npy_int64 year; + int *month_lengths, i; + + year = days_to_yearsdays(&days); + month_lengths = days_in_month[is_leapyear(year)]; + + for (i = 0; i < 12; ++i) { + if (days < month_lengths[i]) { + return i + 1; + } + else { + days -= month_lengths[i]; + } + } + + /* Should never get here */ + return 1; +} + /* * Fills in the year, month, day in 'dts' based on the days * offset from 1970. @@ -215,8 +205,8 @@ set_datetimestruct_days(npy_int64 days, npy_datetimestruct *dts) int *month_lengths, i; dts->year = days_to_yearsdays(&days); - month_lengths = days_in_month[is_leapyear(dts->year)]; + for (i = 0; i < 12; ++i) { if (days < month_lengths[i]) { dts->month = i + 1; @@ -251,6 +241,14 @@ convert_datetimestruct_to_datetime(PyArray_DatetimeMetaData *meta, return 0; } + /* Cannot instantiate a datetime with generic units */ + if (meta->base == NPY_FR_GENERIC) { + PyErr_SetString(PyExc_ValueError, + "Cannot create a NumPy datetime other than NaT " + "with generic units"); + return -1; + } + if (dts->event < 0 || dts->event >= meta->events) { PyErr_Format(PyExc_ValueError, "NumPy datetime event %d is outside range [0,%d)", @@ -269,7 +267,6 @@ convert_datetimestruct_to_datetime(PyArray_DatetimeMetaData *meta, else { /* Otherwise calculate the number of days to start */ npy_int64 days = get_datetimestruct_days(dts); - int dotw; switch (base) { case NPY_FR_W: @@ -281,26 +278,6 @@ convert_datetimestruct_to_datetime(PyArray_DatetimeMetaData *meta, ret = (days - 6) / 7; } break; - case NPY_FR_B: - /* TODO: this needs work... */ - dotw = day_of_week(days); - - if (dotw > 4) { - /* Invalid business day */ - ret = 0; - } - else { - npy_int64 x; - if (days >= 0) { - /* offset to adjust first week */ - x = days - 4; - } - else { - x = days - 2; - } - ret = 2 + (x / 7) * 5 + x % 7; - } - break; case NPY_FR_D: ret = days; break; @@ -452,15 +429,6 @@ PyArray_TimedeltaStructToTimedelta(NPY_DATETIMEUNIT fr, npy_timedeltastruct *d) ret = (d->day - 6) / 7; } } - else if (fr == NPY_FR_B) { - /* - * What is the meaning of a relative Business day? - * - * This assumes you want to take the day difference and - * convert it to business-day difference (removing 2 every 7). - */ - ret = (d->day / 7) * 5 + d->day % 7; - } else if (fr == NPY_FR_D) { ret = d->day; } @@ -531,7 +499,6 @@ convert_datetime_to_datetimestruct(PyArray_DatetimeMetaData *meta, npy_datetime dt, npy_datetimestruct *out) { - npy_int64 absdays; npy_int64 perday; /* Initialize the output to all zeros */ @@ -545,6 +512,14 @@ convert_datetime_to_datetimestruct(PyArray_DatetimeMetaData *meta, out->year = NPY_DATETIME_NAT; return 0; } + + /* Datetimes can't be in generic units */ + if (meta->base == NPY_FR_GENERIC) { + PyErr_SetString(PyExc_ValueError, + "Cannot convert a NumPy datetime value other than NaT " + "with generic units"); + return -1; + } /* Extract the event number */ if (meta->events > 1) { @@ -584,28 +559,6 @@ convert_datetime_to_datetimestruct(PyArray_DatetimeMetaData *meta, set_datetimestruct_days(dt * 7, out); break; - case NPY_FR_B: - /* TODO: fix up business days */ - /* Number of business days since Thursday, 1-1-70 */ - /* - * A business day is M T W Th F (i.e. all but Sat and Sun.) - * Convert the business day to the number of actual days. - * - * Must convert [0,1,2,3,4,5,6,7,...] to - * [0,1,4,5,6,7,8,11,...] - * and [...,-9,-8,-7,-6,-5,-4,-3,-2,-1,0] to - * [...,-13,-10,-9,-8,-7,-6,-3,-2,-1,0] - */ - if (dt >= 0) { - absdays = 7 * ((dt + 3) / 5) + ((dt + 3) % 5) - 3; - } - else { - /* Recall how C computes / and % with negative numbers */ - absdays = 7 * ((dt - 1) / 5) + ((dt - 1) % 5) + 1; - } - set_datetimestruct_days(absdays, out); - break; - case NPY_FR_D: set_datetimestruct_days(dt, out); break; @@ -815,7 +768,7 @@ PyArray_DatetimeToDatetimeStruct(npy_datetime val, NPY_DATETIMEUNIT fr, /* * FIXME: Overflow is not handled at all - * To convert from Years, Months, and Business Days, + * To convert from Years or Months, * multiplication by the average is done */ @@ -852,10 +805,6 @@ PyArray_TimedeltaToTimedeltaStruct(npy_timedelta val, NPY_DATETIMEUNIT fr, else if (fr == NPY_FR_W) { day = val * 7; } - else if (fr == NPY_FR_B) { - /* Number of business days since Thursday, 1-1-70 */ - day = (val * 7) / 5; - } else if (fr == NPY_FR_D) { day = val; } @@ -959,6 +908,85 @@ PyArray_TimedeltaToTimedeltaStruct(npy_timedelta val, NPY_DATETIMEUNIT fr, return; } +/* + * Creates a datetime or timedelta dtype using a copy of the provided metadata. + */ +NPY_NO_EXPORT PyArray_Descr * +create_datetime_dtype(int type_num, PyArray_DatetimeMetaData *meta) +{ + PyArray_Descr *dtype = NULL; + PyArray_DatetimeMetaData *dt_data; + PyObject *metacobj = NULL; + + /* Create a default datetime or timedelta */ + if (type_num == NPY_DATETIME || type_num == NPY_TIMEDELTA) { + dtype = PyArray_DescrNewFromType(type_num); + } + else { + PyErr_SetString(PyExc_RuntimeError, + "Asked to create a datetime type with a non-datetime " + "type number"); + return NULL; + } + + if (dtype == NULL) { + return NULL; + } + + /* + * Remove any reference to old metadata dictionary + * And create a new one for this new dtype + */ + Py_XDECREF(dtype->metadata); + dtype->metadata = PyDict_New(); + if (dtype->metadata == NULL) { + Py_DECREF(dtype); + return NULL; + } + + /* Create a metadata capsule to copy the provided metadata */ + dt_data = PyArray_malloc(sizeof(PyArray_DatetimeMetaData)); + if (dt_data == NULL) { + Py_DECREF(dtype); + PyErr_NoMemory(); + return NULL; + } + + /* Copy the metadata */ + *dt_data = *meta; + + /* Allocate a capsule for it (this claims ownership of dt_data) */ + metacobj = NpyCapsule_FromVoidPtr((void *)dt_data, simple_capsule_dtor); + if (metacobj == NULL) { + Py_DECREF(dtype); + return NULL; + } + + /* Set the metadata object in the dictionary. */ + if (PyDict_SetItemString(dtype->metadata, NPY_METADATA_DTSTR, + metacobj) < 0) { + Py_DECREF(dtype); + Py_DECREF(metacobj); + return NULL; + } + Py_DECREF(metacobj); + + return dtype; +} + +/* + * Creates a datetime or timedelta dtype using the given unit. + */ +NPY_NO_EXPORT PyArray_Descr * +create_datetime_dtype_with_unit(int type_num, NPY_DATETIMEUNIT unit) +{ + PyArray_DatetimeMetaData meta; + meta.base = unit; + meta.num = 1; + meta.events = 1; + return create_datetime_dtype(type_num, &meta); +} + /* * This function returns the a new reference to the * capsule with the datetime metadata. @@ -1101,6 +1129,15 @@ parse_datetime_metadata_from_metastr(char *metastr, Py_ssize_t len, { char *substr = metastr, *substrend = NULL; + /* Treat the empty string as generic units */ + if (len == 0) { + out_meta->base = NPY_FR_GENERIC; + out_meta->num = 1; + out_meta->events = 1; + + return 0; + } + /* The metadata string must start with a '[' */ if (len < 3 || *substr++ != '[') { goto bad_input; @@ -1156,32 +1193,6 @@ parse_datetime_metadata_from_metastr(char *metastr, Py_ssize_t len, return -1; } -NPY_NO_EXPORT PyObject * -parse_datetime_metacobj_from_metastr(char *metastr, Py_ssize_t len) -{ - PyArray_DatetimeMetaData *dt_data; - - dt_data = PyArray_malloc(sizeof(PyArray_DatetimeMetaData)); - if (dt_data == NULL) { - return PyErr_NoMemory(); - } - - /* If there's no metastr, use the default */ - if (len == 0) { - dt_data->num = 1; - dt_data->base = NPY_DATETIME_DEFAULTUNIT; - dt_data->events = 1; - } - else { - if (parse_datetime_metadata_from_metastr(metastr, len, dt_data) < 0) { - PyArray_free(dt_data); - return NULL; - } - } - - return NpyCapsule_FromVoidPtr((void *)dt_data, simple_capsule_dtor); -} - /* * Converts a datetype dtype string into a dtype descr object. * The "type" string should be NULL-terminated. @@ -1189,11 +1200,10 @@ parse_datetime_metacobj_from_metastr(char *metastr, Py_ssize_t len) NPY_NO_EXPORT PyArray_Descr * parse_dtype_from_datetime_typestr(char *typestr, Py_ssize_t len) { - PyArray_Descr *dtype = NULL; + PyArray_DatetimeMetaData meta; char *metastr = NULL; int is_timedelta = 0; Py_ssize_t metalen = 0; - PyObject *metacobj = NULL; if (len < 2) { PyErr_Format(PyExc_TypeError, @@ -1233,45 +1243,13 @@ parse_dtype_from_datetime_typestr(char *typestr, Py_ssize_t len) return NULL; } - /* Create a default datetime or timedelta */ - if (is_timedelta) { - dtype = PyArray_DescrNewFromType(NPY_TIMEDELTA); - } - else { - dtype = PyArray_DescrNewFromType(NPY_DATETIME); - } - if (dtype == NULL) { - return NULL; - } - - /* - * Remove any reference to old metadata dictionary - * And create a new one for this new dtype - */ - Py_XDECREF(dtype->metadata); - dtype->metadata = PyDict_New(); - if (dtype->metadata == NULL) { - Py_DECREF(dtype); + /* Parse the metadata string into a metadata struct */ + if (parse_datetime_metadata_from_metastr(metastr, metalen, &meta) < 0) { return NULL; } - /* Parse the metadata string into a metadata capsule */ - metacobj = parse_datetime_metacobj_from_metastr(metastr, metalen); - if (metacobj == NULL) { - Py_DECREF(dtype); - return NULL; - } - - /* Set the metadata object in the dictionary. */ - if (PyDict_SetItemString(dtype->metadata, NPY_METADATA_DTSTR, - metacobj) < 0) { - Py_DECREF(dtype); - Py_DECREF(metacobj); - return NULL; - } - Py_DECREF(metacobj); - - return dtype; + return create_datetime_dtype(is_timedelta ? NPY_TIMEDELTA : NPY_DATETIME, + &meta); } static NPY_DATETIMEUNIT _multiples_table[16][4] = { @@ -1279,10 +1257,8 @@ static NPY_DATETIMEUNIT _multiples_table[16][4] = { {NPY_FR_M, NPY_FR_W, NPY_FR_D}, {4, 30, 720}, /* NPY_FR_M */ {NPY_FR_W, NPY_FR_D, NPY_FR_h}, - {5, 7, 168, 10080}, /* NPY_FR_W */ - {NPY_FR_B, NPY_FR_D, NPY_FR_h, NPY_FR_m}, - {24, 1440, 86400}, /* NPY_FR_B */ - {NPY_FR_h, NPY_FR_m, NPY_FR_s}, + {7, 168, 10080}, /* NPY_FR_W */ + {NPY_FR_D, NPY_FR_h, NPY_FR_m}, {24, 1440, 86400}, /* NPY_FR_D */ {NPY_FR_h, NPY_FR_m, NPY_FR_s}, {60, 3600}, /* NPY_FR_h */ @@ -1313,6 +1289,12 @@ convert_datetime_divisor_to_multiple(PyArray_DatetimeMetaData *meta, NPY_DATETIMEUNIT *baseunit; int q, r; + if (meta->base == NPY_FR_GENERIC) { + PyErr_SetString(PyExc_ValueError, + "Can't use 'den' divisor with generic units"); + return -1; + } + ind = ((int)meta->base - (int)NPY_FR_Y)*2; totry = _multiples_table[ind]; baseunit = _multiples_table[ind + 1]; @@ -1330,10 +1312,10 @@ convert_datetime_divisor_to_multiple(PyArray_DatetimeMetaData *meta, baseunit = _multiples_table[ind + 1]; baseunit[0] = meta->base + 1; baseunit[1] = meta->base + 2; - if (meta->base == NPY_DATETIME_NUMUNITS - 2) { + if (meta->base == NPY_FR_as - 1) { num = 1; } - if (meta->base == NPY_DATETIME_NUMUNITS - 1) { + if (meta->base == NPY_FR_as) { num = 0; } } @@ -1366,14 +1348,13 @@ convert_datetime_divisor_to_multiple(PyArray_DatetimeMetaData *meta, /* * Lookup table for factors between datetime units, except - * for years, months, and business days. + * for years and months. */ static npy_uint32 _datetime_factors[] = { 1, /* Years - not used */ 1, /* Months - not used */ 7, /* Weeks -> Days */ - 1, /* Business days - not used */ 24, /* Days -> Hours */ 60, /* Hours -> Minutes */ 60, /* Minutes -> Seconds */ @@ -1383,12 +1364,14 @@ _datetime_factors[] = { 1000, 1000, 1000, - 1 /* Attoseconds are the smallest base unit */ + 1, /* Attoseconds are the smallest base unit */ + 0 /* Generic units don't have a conversion */ }; /* * Returns the scale factor between the units. Does not validate - * that bigbase represents larger units than littlebase. + * that bigbase represents larger units than littlebase, or that + * the units are not generic. * * Returns 0 if there is an overflow. */ @@ -1451,6 +1434,25 @@ get_datetime_conversion_factor(PyArray_DatetimeMetaData *src_meta, int src_base, dst_base, swapped; npy_uint64 num = 1, denom = 1, tmp, gcd; + /* Generic units change to the destination with no conversion factor */ + if (src_meta->base == NPY_FR_GENERIC) { + *out_num = 1; + *out_denom = 1; + return; + } + /* + * Converting to a generic unit from something other than a generic + * unit is an error. + */ + else if (dst_meta->base == NPY_FR_GENERIC) { + PyErr_SetString(PyExc_ValueError, + "Cannot convert from specific units to generic " + "units in NumPy datetimes or timedeltas"); + *out_num = 0; + *out_denom = 0; + return; + } + if (src_meta->base <= dst_meta->base) { src_base = src_meta->base; dst_base = dst_meta->base; @@ -1503,6 +1505,11 @@ get_datetime_conversion_factor(PyArray_DatetimeMetaData *src_meta, /* If something overflowed, make both num and denom 0 */ if (denom == 0) { + PyErr_Format(PyExc_OverflowError, + "Integer overflow while computing the conversion " + "factor between NumPy datetime units %s and %s", + _datetime_strings[src_base], + _datetime_strings[dst_base]); *out_num = 0; *out_denom = 0; return; @@ -1556,6 +1563,15 @@ datetime_metadata_divides( return 0; } + /* Generic units divide into anything */ + if (meta2->base == NPY_FR_GENERIC) { + return 1; + } + /* Non-generic units never divide into generic units */ + else if (meta1->base == NPY_FR_GENERIC) { + return 0; + } + /* Events must match */ if (meta1->events != meta2->events) { return 0; @@ -1567,14 +1583,11 @@ datetime_metadata_divides( /* If the bases are different, factor in a conversion */ if (meta1->base != meta2->base) { /* - * Years, Months, and Business days are incompatible with + * Years and Months are incompatible with * all other units (except years and months are compatible * with each other). */ - if (meta1->base == NPY_FR_B || meta2->base == NPY_FR_B) { - return 0; - } - else if (meta1->base == NPY_FR_Y) { + if (meta1->base == NPY_FR_Y) { if (meta2->base == NPY_FR_M) { num1 *= 12; } @@ -1631,35 +1644,35 @@ datetime_metadata_divides( return (num1 % num2) == 0; } - -NPY_NO_EXPORT PyObject * +/* + * Computes the GCD of the two date-time metadata values. Raises + * an exception if there is no reasonable GCD, such as with + * years and days. + * + * The result is placed in 'out_meta'. + * + * Returns 0 on success, -1 on failure. + */ +NPY_NO_EXPORT int compute_datetime_metadata_greatest_common_divisor( - PyArray_Descr *type1, - PyArray_Descr *type2, - int strict_with_nonlinear_units) + PyArray_DatetimeMetaData *meta1, + PyArray_DatetimeMetaData *meta2, + PyArray_DatetimeMetaData *out_meta, + int strict_with_nonlinear_units1, + int strict_with_nonlinear_units2) { - PyArray_DatetimeMetaData *meta1, *meta2, *dt_data; NPY_DATETIMEUNIT base; npy_uint64 num1, num2, num; int events = 1; - if ((type1->type_num != NPY_DATETIME && - type1->type_num != NPY_TIMEDELTA) || - (type2->type_num != NPY_DATETIME && - type2->type_num != NPY_TIMEDELTA)) { - PyErr_SetString(PyExc_TypeError, - "Require datetime types for metadata " - "greatest common divisor operation"); - return NULL; - } - - meta1 = get_datetime_metadata_from_dtype(type1); - if (meta1 == NULL) { - return NULL; + /* If either unit is generic, adopt the metadata from the other one */ + if (meta1->base == NPY_FR_GENERIC) { + *out_meta = *meta2; + return 0; } - meta2 = get_datetime_metadata_from_dtype(type2); - if (meta2 == NULL) { - return NULL; + else if (meta2->base == NPY_FR_GENERIC) { + *out_meta = *meta1; + return 0; } /* Take the maximum of the events */ @@ -1679,7 +1692,7 @@ compute_datetime_metadata_greatest_common_divisor( } else { /* - * Years, Months, and Business days are incompatible with + * Years and Months are incompatible with * all other units (except years and months are compatible * with each other). */ @@ -1688,7 +1701,7 @@ compute_datetime_metadata_greatest_common_divisor( base = NPY_FR_M; num1 *= 12; } - else if (strict_with_nonlinear_units) { + else if (strict_with_nonlinear_units1) { goto incompatible_units; } else { @@ -1701,7 +1714,7 @@ compute_datetime_metadata_greatest_common_divisor( base = NPY_FR_M; num2 *= 12; } - else if (strict_with_nonlinear_units) { + else if (strict_with_nonlinear_units2) { goto incompatible_units; } else { @@ -1709,34 +1722,28 @@ compute_datetime_metadata_greatest_common_divisor( /* Don't multiply num2 since there is no even factor */ } } - else if (meta1->base == NPY_FR_M || - meta1->base == NPY_FR_B || - meta2->base == NPY_FR_M || - meta2->base == NPY_FR_B) { - if (strict_with_nonlinear_units) { + else if (meta1->base == NPY_FR_M) { + if (strict_with_nonlinear_units1) { goto incompatible_units; } else { - if (meta1->base > meta2->base) { - base = meta1->base; - } - else { - base = meta2->base; - } - - /* - * When combining business days with other units, end - * up with days instead of business days. - */ - if (base == NPY_FR_B) { - base = NPY_FR_D; - } + base = meta2->base; + /* Don't multiply num1 since there is no even factor */ } } - - /* Take the greater base (unit sizes are decreasing in enum) */ - if (meta1->base > meta2->base) { - base = meta1->base; + else if (meta2->base == NPY_FR_M) { + if (strict_with_nonlinear_units2) { + goto incompatible_units; + } + else { + base = meta1->base; + /* Don't multiply num2 since there is no even factor */ + } + } + + /* Take the greater base (unit sizes are decreasing in enum) */ + if (meta1->base > meta2->base) { + base = meta1->base; num2 *= get_datetime_units_factor(meta2->base, meta1->base); if (num2 == 0) { goto units_overflow; @@ -1754,75 +1761,129 @@ compute_datetime_metadata_greatest_common_divisor( /* Compute the GCD of the resulting multipliers */ num = _uint64_euclidean_gcd(num1, num2); - /* Create and return the metadata capsule */ - dt_data = PyArray_malloc(sizeof(PyArray_DatetimeMetaData)); - if (dt_data == NULL) { - return PyErr_NoMemory(); - } - - dt_data->base = base; - dt_data->num = (int)num; - if (dt_data->num <= 0 || num != (npy_uint64)dt_data->num) { + /* Fill the 'out_meta' values */ + out_meta->base = base; + out_meta->num = (int)num; + if (out_meta->num <= 0 || num != (npy_uint64)out_meta->num) { goto units_overflow; } - dt_data->events = events; + out_meta->events = events; - return NpyCapsule_FromVoidPtr((void *)dt_data, simple_capsule_dtor); + return 0; incompatible_units: { PyObject *errmsg; errmsg = PyUString_FromString("Cannot get " - "a common metadata divisor for types "); - PyUString_ConcatAndDel(&errmsg, - PyObject_Repr((PyObject *)type1)); + "a common metadata divisor for " + "NumPy datetime metadata "); + errmsg = append_metastr_to_string(meta1, 0, errmsg); PyUString_ConcatAndDel(&errmsg, PyUString_FromString(" and ")); - PyUString_ConcatAndDel(&errmsg, - PyObject_Repr((PyObject *)type2)); + errmsg = append_metastr_to_string(meta2, 0, errmsg); PyUString_ConcatAndDel(&errmsg, PyUString_FromString(" because they have " "incompatible nonlinear base time units")); PyErr_SetObject(PyExc_TypeError, errmsg); - return NULL; + return -1; } units_overflow: { PyObject *errmsg; errmsg = PyUString_FromString("Integer overflow " - "getting a common metadata divisor for types "); - PyUString_ConcatAndDel(&errmsg, - PyObject_Repr((PyObject *)type1)); + "getting a common metadata divisor for " + "NumPy datetime metadata "); + errmsg = append_metastr_to_string(meta1, 0, errmsg); PyUString_ConcatAndDel(&errmsg, PyUString_FromString(" and ")); - PyUString_ConcatAndDel(&errmsg, - PyObject_Repr((PyObject *)type2)); + errmsg = append_metastr_to_string(meta2, 0, errmsg); PyErr_SetObject(PyExc_OverflowError, errmsg); + return -1; + } +} + +/* + * Computes the GCD of the two date-time metadata values. Raises + * an exception if there is no reasonable GCD, such as with + * years and days. + * + * Returns a capsule with the GCD metadata. + */ +NPY_NO_EXPORT PyObject * +compute_datetime_metadata_greatest_common_divisor_capsule( + PyArray_Descr *type1, + PyArray_Descr *type2, + int strict_with_nonlinear_units1, + int strict_with_nonlinear_units2) +{ + PyArray_DatetimeMetaData *meta1, *meta2, *dt_data; + + if ((type1->type_num != NPY_DATETIME && + type1->type_num != NPY_TIMEDELTA) || + (type2->type_num != NPY_DATETIME && + type2->type_num != NPY_TIMEDELTA)) { + PyErr_SetString(PyExc_TypeError, + "Require datetime types for metadata " + "greatest common divisor operation"); + return NULL; + } + + meta1 = get_datetime_metadata_from_dtype(type1); + if (meta1 == NULL) { + return NULL; + } + meta2 = get_datetime_metadata_from_dtype(type2); + if (meta2 == NULL) { + return NULL; + } + + /* Create and return the metadata capsule */ + dt_data = PyArray_malloc(sizeof(PyArray_DatetimeMetaData)); + if (dt_data == NULL) { + return PyErr_NoMemory(); + } + + if (compute_datetime_metadata_greatest_common_divisor(meta1, meta2, + dt_data, strict_with_nonlinear_units1, + strict_with_nonlinear_units2) < 0) { + PyArray_free(dt_data); return NULL; } + + return NpyCapsule_FromVoidPtr((void *)dt_data, simple_capsule_dtor); } /* - * Uses type1's type_num and the gcd of the metadata to create - * the result type. + * Both type1 and type2 must be either NPY_DATETIME or NPY_TIMEDELTA. + * Applies the type promotion rules between the two types, returning + * the promoted type. */ -static PyArray_Descr * -datetime_gcd_type_promotion(PyArray_Descr *type1, PyArray_Descr *type2) +NPY_NO_EXPORT PyArray_Descr * +datetime_type_promotion(PyArray_Descr *type1, PyArray_Descr *type2) { + int type_num1, type_num2; PyObject *gcdmeta; PyArray_Descr *dtype; + int is_datetime; + + type_num1 = type1->type_num; + type_num2 = type2->type_num; + + is_datetime = (type_num1 == NPY_DATETIME || type_num2 == NPY_DATETIME); /* * Get the metadata GCD, being strict about nonlinear units for * timedelta and relaxed for datetime. */ - gcdmeta = compute_datetime_metadata_greatest_common_divisor( + gcdmeta = compute_datetime_metadata_greatest_common_divisor_capsule( type1, type2, - type1->type_num == NPY_TIMEDELTA); + type_num1 == NPY_TIMEDELTA, + type_num2 == NPY_TIMEDELTA); if (gcdmeta == NULL) { return NULL; } /* Create a DATETIME or TIMEDELTA dtype */ - dtype = PyArray_DescrNewFromType(type1->type_num); + dtype = PyArray_DescrNewFromType(is_datetime ? NPY_DATETIME : + NPY_TIMEDELTA); if (dtype == NULL) { Py_DECREF(gcdmeta); return NULL; @@ -1847,39 +1908,7 @@ datetime_gcd_type_promotion(PyArray_Descr *type1, PyArray_Descr *type2) Py_DECREF(gcdmeta); return dtype; -} - -/* - * Both type1 and type2 must be either NPY_DATETIME or NPY_TIMEDELTA. - * Applies the type promotion rules between the two types, returning - * the promoted type. - */ -NPY_NO_EXPORT PyArray_Descr * -datetime_type_promotion(PyArray_Descr *type1, PyArray_Descr *type2) -{ - int type_num1, type_num2; - - type_num1 = type1->type_num; - type_num2 = type2->type_num; - if (type_num1 == NPY_DATETIME) { - if (type_num2 == NPY_DATETIME) { - return datetime_gcd_type_promotion(type1, type2); - } - else if (type_num2 == NPY_TIMEDELTA) { - Py_INCREF(type1); - return type1; - } - } - else if (type_num1 == NPY_TIMEDELTA) { - if (type_num2 == NPY_DATETIME) { - Py_INCREF(type2); - return type2; - } - else if (type_num2 == NPY_TIMEDELTA) { - return datetime_gcd_type_promotion(type1, type2); - } - } PyErr_SetString(PyExc_RuntimeError, "Called datetime_type_promotion on non-datetype type"); @@ -1891,6 +1920,8 @@ datetime_type_promotion(PyArray_Descr *type1, PyArray_Descr *type2) * a date time unit enum value. The 'metastr' parameter * is used for error messages, and may be NULL. * + * Generic units have no representation as a string in this form. + * * Returns 0 on success, -1 on failure. */ NPY_NO_EXPORT NPY_DATETIMEUNIT @@ -1905,8 +1936,6 @@ parse_datetime_unit_from_string(char *str, Py_ssize_t len, char *metastr) return NPY_FR_M; case 'W': return NPY_FR_W; - case 'B': - return NPY_FR_B; case 'D': return NPY_FR_D; case 'h': @@ -2075,7 +2104,7 @@ convert_datetime_metadata_tuple_to_metacobj(PyObject *tuple) */ NPY_NO_EXPORT int convert_pyobject_to_datetime_metadata(PyObject *obj, - PyArray_DatetimeMetaData *out_meta) + PyArray_DatetimeMetaData *out_meta) { PyObject *ascii = NULL; char *str = NULL; @@ -2146,6 +2175,18 @@ append_metastr_to_string(PyArray_DatetimeMetaData *meta, return NULL; } + if (meta->base == NPY_FR_GENERIC) { + /* Without brackets, give a string "generic" */ + if (skip_brackets) { + PyUString_ConcatAndDel(&ret, PyUString_FromString("generic")); + return ret; + } + /* But with brackets, append nothing */ + else { + return ret; + } + } + num = meta->num; events = meta->events; if (meta->base >= 0 && meta->base < NPY_DATETIME_NUMUNITS) { @@ -2278,14 +2319,34 @@ add_minutes_to_datetimestruct(npy_datetimestruct *dts, int minutes) * day according to local time) and "Now" (current time in UTC). * * 'str' must be a NULL-terminated string, and 'len' must be its length. + * 'unit' should contain -1 if the unit is unknown, or the unit + * which will be used if it is. + * + * 'out' gets filled with the parsed date-time. + * 'out_local' gets set to 1 if the parsed time was in local time, + * to 0 otherwise. The values 'now' and 'today' don't get counted + * as local, and neither do UTC +/-#### timezone offsets, because + * they aren't using the computer's local timezone offset. + * 'out_bestunit' gives a suggested unit based on the amount of + * resolution provided in the string, or -1 for NaT. + * 'out_special' gets set to 1 if the parsed time was 'today', + * 'now', or ''/'NaT'. For 'today', the unit recommended is + * 'D', for 'now', the unit recommended is 's', and for 'NaT' + * the unit recommended is 'Y'. + * * * Returns 0 on success, -1 on failure. */ NPY_NO_EXPORT int -parse_iso_8601_date(char *str, int len, npy_datetimestruct *out) +parse_iso_8601_date(char *str, int len, + NPY_DATETIMEUNIT unit, + npy_datetimestruct *out, + npy_bool *out_local, + NPY_DATETIMEUNIT *out_bestunit, + npy_bool *out_special) { int year_leap = 0; - int i; + int i, numdigits; char *substr, sublen; /* Initialize the output to all zeros */ @@ -2299,9 +2360,31 @@ parse_iso_8601_date(char *str, int len, npy_datetimestruct *out) tolower(str[1]) == 'a' && tolower(str[2]) == 't')) { out->year = NPY_DATETIME_NAT; + + /* + * Indicate that this was a special value, and + * recommend generic units. + */ + if (out_local != NULL) { + *out_local = 0; + } + if (out_bestunit != NULL) { + *out_bestunit = NPY_FR_GENERIC; + } + if (out_special != NULL) { + *out_special = 1; + } + return 0; } + if (unit == NPY_FR_GENERIC) { + PyErr_SetString(PyExc_ValueError, + "Cannot create a NumPy datetime other than NaT " + "with generic units"); + return -1; + } + /* * The string "today" resolves to midnight of today's local date in UTC. * This is perhaps a little weird, but done so that further truncation @@ -2317,6 +2400,14 @@ parse_iso_8601_date(char *str, int len, npy_datetimestruct *out) time_t rawtime = 0; struct tm tm_; + /* 'today' only works for units of days or larger */ + if (unit != -1 && unit > NPY_FR_D) { + PyErr_SetString(PyExc_ValueError, + "Special value 'today' can only be converted " + "to a NumPy datetime with 'D' or larger units"); + return -1; + } + time(&rawtime); #if defined(_WIN32) if (localtime_s(&tm_, &rawtime) != 0) { @@ -2335,6 +2426,21 @@ parse_iso_8601_date(char *str, int len, npy_datetimestruct *out) out->year = tm_.tm_year + 1900; out->month = tm_.tm_mon + 1; out->day = tm_.tm_mday; + + /* + * Indicate that this was a special value, and + * is a date (unit 'D'). + */ + if (out_local != NULL) { + *out_local = 0; + } + if (out_bestunit != NULL) { + *out_bestunit = NPY_FR_D; + } + if (out_special != NULL) { + *out_special = 1; + } + return 0; } @@ -2344,6 +2450,15 @@ parse_iso_8601_date(char *str, int len, npy_datetimestruct *out) tolower(str[2]) == 'w') { time_t rawtime = 0; PyArray_DatetimeMetaData meta; + + /* 'now' only works for units of hours or smaller */ + if (unit != -1 && unit < NPY_FR_h) { + PyErr_SetString(PyExc_ValueError, + "Special value 'now' can only be converted " + "to a NumPy datetime with 'h' or smaller units"); + return -1; + } + time(&rawtime); /* Set up a dummy metadata for the conversion */ @@ -2351,9 +2466,29 @@ parse_iso_8601_date(char *str, int len, npy_datetimestruct *out) meta.num = 1; meta.events = 1; + /* + * Indicate that this was a special value, and + * use 's' because the time() function has resolution + * seconds. + */ + if (out_local != NULL) { + *out_local = 0; + } + if (out_bestunit != NULL) { + *out_bestunit = NPY_FR_s; + } + if (out_special != NULL) { + *out_special = 1; + } + return convert_datetime_to_datetimestruct(&meta, rawtime, out); } + /* Anything else isn't a special value */ + if (out_special != NULL) { + *out_special = 0; + } + substr = str; sublen = len; @@ -2390,6 +2525,12 @@ parse_iso_8601_date(char *str, int len, npy_datetimestruct *out) /* Next character must be a '-' or the end of the string */ if (sublen == 0) { + if (out_local != NULL) { + *out_local = 0; + } + if (out_bestunit != NULL) { + *out_bestunit = NPY_FR_Y; + } goto finish; } else if (*substr == '-') { @@ -2423,6 +2564,12 @@ parse_iso_8601_date(char *str, int len, npy_datetimestruct *out) /* Next character must be a '-' or the end of the string */ if (sublen == 0) { + if (out_local != NULL) { + *out_local = 0; + } + if (out_bestunit != NULL) { + *out_bestunit = NPY_FR_M; + } goto finish; } else if (*substr == '-') { @@ -2457,6 +2604,12 @@ parse_iso_8601_date(char *str, int len, npy_datetimestruct *out) /* Next character must be a 'T', ' ', or end of string */ if (sublen == 0) { + if (out_local != NULL) { + *out_local = 0; + } + if (out_bestunit != NULL) { + *out_bestunit = NPY_FR_D; + } goto finish; } else if (*substr != 'T' && *substr != ' ') { @@ -2489,6 +2642,9 @@ parse_iso_8601_date(char *str, int len, npy_datetimestruct *out) --sublen; } else { + if (out_bestunit != NULL) { + *out_bestunit = NPY_FR_h; + } goto parse_timezone; } @@ -2519,6 +2675,9 @@ parse_iso_8601_date(char *str, int len, npy_datetimestruct *out) --sublen; } else { + if (out_bestunit != NULL) { + *out_bestunit = NPY_FR_m; + } goto parse_timezone; } @@ -2549,44 +2708,78 @@ parse_iso_8601_date(char *str, int len, npy_datetimestruct *out) --sublen; } else { + if (out_bestunit != NULL) { + *out_bestunit = NPY_FR_s; + } goto parse_timezone; } /* PARSE THE MICROSECONDS (0 to 6 digits) */ + numdigits = 0; for (i = 0; i < 6; ++i) { out->us *= 10; if (sublen > 0 && isdigit(*substr)) { out->us += (*substr - '0'); ++substr; --sublen; + ++numdigits; } } if (sublen == 0 || !isdigit(*substr)) { + if (out_bestunit != NULL) { + if (numdigits > 3) { + *out_bestunit = NPY_FR_us; + } + else { + *out_bestunit = NPY_FR_ms; + } + } goto parse_timezone; } /* PARSE THE PICOSECONDS (0 to 6 digits) */ + numdigits = 0; for (i = 0; i < 6; ++i) { out->ps *= 10; if (sublen > 0 && isdigit(*substr)) { out->ps += (*substr - '0'); ++substr; --sublen; + ++numdigits; } } if (sublen == 0 || !isdigit(*substr)) { + if (out_bestunit != NULL) { + if (numdigits > 3) { + *out_bestunit = NPY_FR_ps; + } + else { + *out_bestunit = NPY_FR_ns; + } + } goto parse_timezone; } /* PARSE THE ATTOSECONDS (0 to 6 digits) */ + numdigits = 0; for (i = 0; i < 6; ++i) { out->as *= 10; if (sublen > 0 && isdigit(*substr)) { out->as += (*substr - '0'); ++substr; --sublen; + ++numdigits; + } + } + + if (out_bestunit != NULL) { + if (numdigits > 3) { + *out_bestunit = NPY_FR_as; + } + else { + *out_bestunit = NPY_FR_fs; } } @@ -2642,11 +2835,21 @@ parse_iso_8601_date(char *str, int len, npy_datetimestruct *out) out->year = tm_.tm_year + 1900; } + /* Since neither "Z" nor a time-zone was specified, it's local */ + if (out_local != NULL) { + *out_local = 1; + } + goto finish; } /* UTC specifier */ if (*substr == 'Z') { + /* "Z" means not local */ + if (out_local != NULL) { + *out_local = 0; + } + if (sublen == 1) { goto finish; } @@ -2658,6 +2861,15 @@ parse_iso_8601_date(char *str, int len, npy_datetimestruct *out) /* Time zone offset */ else if (*substr == '-' || *substr == '+') { int offset_neg = 0, offset_hour = 0, offset_minute = 0; + + /* + * Since "local" means local with respect to the current + * machine, we say this is non-local. + */ + if (out_local != NULL) { + *out_local = 0; + } + if (*substr == '-') { offset_neg = 1; } @@ -2713,24 +2925,6 @@ parse_iso_8601_date(char *str, int len, npy_datetimestruct *out) add_minutes_to_datetimestruct(out, -60 * offset_hour - offset_minute); } - /* May have a ' ' followed by an event number */ - if (sublen == 0) { - goto finish; - } - else if (sublen > 0 && *substr == ' ') { - ++substr; - --sublen; - - while (sublen > 0 && isdigit(*substr)) { - out->event = 10 * out->event + (*substr - '0'); - ++substr; - --sublen; - } - } - else { - goto parse_error; - } - /* Skip trailing whitespace */ while (sublen > 0 && isspace(*substr)) { ++substr; @@ -2769,6 +2963,9 @@ get_datetime_iso_8601_strlen(int local, NPY_DATETIMEUNIT base) } switch (base) { + /* Generic units can only be used to represent NaT */ + case NPY_FR_GENERIC: + return 4; case NPY_FR_as: len += 3; /* "###" */ case NPY_FR_fs: @@ -2788,7 +2985,6 @@ get_datetime_iso_8601_strlen(int local, NPY_DATETIMEUNIT base) case NPY_FR_h: len += 3; /* "T##" */ case NPY_FR_D: - case NPY_FR_B: case NPY_FR_W: len += 3; /* "-##" */ case NPY_FR_M: @@ -2839,8 +3035,8 @@ make_iso_8601_date(npy_datetimestruct *dts, char *outstr, int outlen, char *substr = outstr, sublen = outlen; int tmplen; - /* Handle NaT */ - if (dts->year == NPY_DATETIME_NAT) { + /* Handle NaT, and treat a datetime with generic units as NaT */ + if (dts->year == NPY_DATETIME_NAT || base == NPY_FR_GENERIC) { if (outlen < 4) { goto string_too_short; } @@ -2893,12 +3089,12 @@ make_iso_8601_date(npy_datetimestruct *dts, char *outstr, int outlen, } } /* - * Print business days and weeks with the same precision as days. + * Print weeks with the same precision as days. * * TODO: Could print weeks with YYYY-Www format if the week * epoch is a Monday. */ - else if (base == NPY_FR_B || base == NPY_FR_W) { + else if (base == NPY_FR_W) { base = NPY_FR_D; } @@ -3293,11 +3489,15 @@ make_iso_8601_date(npy_datetimestruct *dts, char *outstr, int outlen, * datetime duck typing. The tzinfo time zone conversion would require * this style of access anyway. * + * 'out_bestunit' gives a suggested unit based on whether the object + * was a datetime.date or datetime.datetime object. + * * Returns -1 on error, 0 on success, and 1 (with no error set) * if obj doesn't have the neeeded date or datetime attributes. */ NPY_NO_EXPORT int -convert_pydatetime_to_datetimestruct(PyObject *obj, npy_datetimestruct *out) +convert_pydatetime_to_datetimestruct(PyObject *obj, npy_datetimestruct *out, + NPY_DATETIMEUNIT *out_bestunit) { PyObject *tmp; int isleap; @@ -3364,6 +3564,10 @@ convert_pydatetime_to_datetimestruct(PyObject *obj, npy_datetimestruct *out) !PyObject_HasAttrString(obj, "minute") || !PyObject_HasAttrString(obj, "second") || !PyObject_HasAttrString(obj, "microsecond")) { + /* The best unit for date is 'D' */ + if (out_bestunit != NULL) { + *out_bestunit = NPY_FR_D; + } return 0; } @@ -3465,6 +3669,11 @@ convert_pydatetime_to_datetimestruct(PyObject *obj, npy_datetimestruct *out) } } + /* The resolution of Python's datetime is 'us' */ + if (out_bestunit != NULL) { + *out_bestunit = NPY_FR_us; + } + return 0; invalid_date: @@ -3482,7 +3691,11 @@ convert_pydatetime_to_datetimestruct(PyObject *obj, npy_datetimestruct *out) } /* - * Converts a PyObject * into a datetime, in any of the forms supported + * Converts a PyObject * into a datetime, in any of the forms supported. + * + * If the units metadata isn't known ahead of time, set meta->base + * to -1, and this function will populate meta with either default + * values or values from the input object. * * Returns -1 on error, 0 on success. */ @@ -3493,8 +3706,10 @@ convert_pyobject_to_datetime(PyArray_DatetimeMetaData *meta, PyObject *obj, if (PyBytes_Check(obj) || PyUnicode_Check(obj)) { PyObject *bytes = NULL; char *str = NULL; - int len = 0; + Py_ssize_t len = 0; npy_datetimestruct dts; + NPY_DATETIMEUNIT bestunit = -1; + /* Convert to an ASCII string for the date parser */ if (PyUnicode_Check(obj)) { bytes = PyUnicode_AsASCIIString(obj); @@ -3512,12 +3727,20 @@ convert_pyobject_to_datetime(PyArray_DatetimeMetaData *meta, PyObject *obj, } /* Parse the ISO date */ - if (parse_iso_8601_date(str, len, &dts) < 0) { + if (parse_iso_8601_date(str, len, meta->base, &dts, + NULL, &bestunit, NULL) < 0) { Py_DECREF(bytes); return -1; } Py_DECREF(bytes); + /* Use the detected unit if none was specified */ + if (meta->base == -1) { + meta->base = bestunit; + meta->num = 1; + meta->events = 1; + } + if (convert_datetimestruct_to_datetime(meta, &dts, out) < 0) { return -1; } @@ -3525,11 +3748,13 @@ convert_pyobject_to_datetime(PyArray_DatetimeMetaData *meta, PyObject *obj, return 0; } /* Do no conversion on raw integers */ - else if (PyInt_Check(obj)) { - *out = PyInt_AS_LONG(obj); - return 0; - } - else if (PyLong_Check(obj)) { + else if (PyInt_Check(obj) || PyLong_Check(obj)) { + /* Don't allow conversion from an integer without specifying a unit */ + if (meta->base == -1 || meta->base == NPY_FR_GENERIC) { + PyErr_SetString(PyExc_ValueError, "Converting an integer to a " + "NumPy datetime requires a specified unit"); + return -1; + } *out = PyLong_AsLongLong(obj); return 0; } @@ -3562,7 +3787,18 @@ convert_pyobject_to_datetime(PyArray_DatetimeMetaData *meta, PyObject *obj, else if (PyArray_IsScalar(obj, Datetime)) { PyDatetimeScalarObject *dts = (PyDatetimeScalarObject *)obj; - return cast_datetime_to_datetime(&dts->obmeta, meta, dts->obval, out); + /* Copy the scalar directly if units weren't specified */ + if (meta->base == -1) { + *meta = dts->obmeta; + *out = dts->obval; + + return 0; + } + /* Otherwise do a casting transformation */ + else { + return cast_datetime_to_datetime(&dts->obmeta, meta, + dts->obval, out); + } } /* Datetime zero-dimensional array */ else if (PyArray_Check(obj) && @@ -3580,18 +3816,36 @@ convert_pyobject_to_datetime(PyArray_DatetimeMetaData *meta, PyObject *obj, !PyArray_ISNOTSWAPPED(obj), obj); - return cast_datetime_to_datetime(obj_meta, meta, dt, out); + /* Copy the value directly if units weren't specified */ + if (meta->base == -1) { + *meta = *obj_meta; + *out = dt; + + return 0; + } + /* Otherwise do a casting transformation */ + else { + return cast_datetime_to_datetime(obj_meta, meta, dt, out); + } } /* Convert from a Python date or datetime object */ else { int code; npy_datetimestruct dts; + NPY_DATETIMEUNIT bestunit = -1; - code = convert_pydatetime_to_datetimestruct(obj, &dts); + code = convert_pydatetime_to_datetimestruct(obj, &dts, &bestunit); if (code == -1) { return -1; } else if (code == 0) { + /* Use the detected unit if none was specified */ + if (meta->base == -1) { + meta->base = bestunit; + meta->num = 1; + meta->events = 1; + } + if (convert_datetimestruct_to_datetime(meta, &dts, out) < 0) { return -1; } @@ -3608,6 +3862,10 @@ convert_pyobject_to_datetime(PyArray_DatetimeMetaData *meta, PyObject *obj, /* * Converts a PyObject * into a timedelta, in any of the forms supported * + * If the units metadata isn't known ahead of time, set meta->base + * to -1, and this function will populate meta with either default + * values or values from the input object. + * * Returns -1 on error, 0 on success. */ NPY_NO_EXPORT int @@ -3615,11 +3873,14 @@ convert_pyobject_to_timedelta(PyArray_DatetimeMetaData *meta, PyObject *obj, npy_timedelta *out) { /* Do no conversion on raw integers */ - if (PyInt_Check(obj)) { - *out = PyInt_AS_LONG(obj); - return 0; - } - else if (PyLong_Check(obj)) { + if (PyInt_Check(obj) || PyLong_Check(obj)) { + /* Use the default unit if none was specified */ + if (meta->base == -1) { + meta->base = NPY_DATETIME_DEFAULTUNIT; + meta->num = 1; + meta->events = 1; + } + *out = PyLong_AsLongLong(obj); return 0; } @@ -3627,8 +3888,18 @@ convert_pyobject_to_timedelta(PyArray_DatetimeMetaData *meta, PyObject *obj, else if (PyArray_IsScalar(obj, Timedelta)) { PyTimedeltaScalarObject *dts = (PyTimedeltaScalarObject *)obj; - return cast_timedelta_to_timedelta(&dts->obmeta, meta, - dts->obval, out); + /* Copy the scalar directly if units weren't specified */ + if (meta->base == -1) { + *meta = dts->obmeta; + *out = dts->obval; + + return 0; + } + /* Otherwise do a casting transformation */ + else { + return cast_timedelta_to_timedelta(&dts->obmeta, meta, + dts->obval, out); + } } /* Timedelta zero-dimensional array */ else if (PyArray_Check(obj) && @@ -3646,7 +3917,17 @@ convert_pyobject_to_timedelta(PyArray_DatetimeMetaData *meta, PyObject *obj, !PyArray_ISNOTSWAPPED(obj), obj); - return cast_timedelta_to_timedelta(obj_meta, meta, dt, out); + /* Copy the value directly if units weren't specified */ + if (meta->base == -1) { + *meta = *obj_meta; + *out = dt; + + return 0; + } + /* Otherwise do a casting transformation */ + else { + return cast_timedelta_to_timedelta(obj_meta, meta, dt, out); + } } /* Convert from a Python timedelta object */ else if (PyObject_HasAttrString(obj, "days") && @@ -3694,16 +3975,29 @@ convert_pyobject_to_timedelta(PyArray_DatetimeMetaData *meta, PyObject *obj, } Py_DECREF(tmp); - /* - * Convert to a microseconds timedelta, then cast to the - * desired units. - */ td = days*(24*60*60*1000000LL) + seconds*1000000LL + useconds; - us_meta.base = NPY_FR_us; - us_meta.num = 1; - us_meta.events = 1; - - return cast_timedelta_to_timedelta(&us_meta, meta, td, out); + + /* Use microseconds if none was specified */ + if (meta->base == -1) { + meta->base = NPY_FR_us; + meta->num = 1; + meta->events = 1; + + *out = td; + + return 0; + } + else { + /* + * Convert to a microseconds timedelta, then cast to the + * desired units. + */ + us_meta.base = NPY_FR_us; + us_meta.num = 1; + us_meta.events = 1; + + return cast_timedelta_to_timedelta(&us_meta, meta, td, out); + } } PyErr_SetString(PyExc_ValueError, @@ -3725,8 +4019,8 @@ convert_datetime_to_pyobject(npy_datetime dt, PyArray_DatetimeMetaData *meta) PyObject *ret = NULL, *tup = NULL; npy_datetimestruct dts; - /* Handle not-a-time */ - if (dt == NPY_DATETIME_NAT) { + /* Handle not-a-time, and generic units as NaT as well */ + if (dt == NPY_DATETIME_NAT || meta->base == NPY_FR_GENERIC) { return PyUString_FromString("NaT"); } @@ -3806,13 +4100,13 @@ convert_timedelta_to_pyobject(npy_timedelta td, PyArray_DatetimeMetaData *meta) } /* - * If the type's precision is greater than microseconds or is - * Y/M/B (nonlinear units), return an int + * If the type's precision is greater than microseconds, is + * Y/M/B (nonlinear units), or is generic units, return an int */ if (meta->base > NPY_FR_us || meta->base == NPY_FR_Y || meta->base == NPY_FR_M || - meta->base == NPY_FR_B) { + meta->base == NPY_FR_GENERIC) { /* Skip use of a tuple for the events, just return the raw int */ return PyLong_FromLongLong(td); } @@ -3932,6 +4226,11 @@ has_equivalent_datetime_metadata(PyArray_Descr *type1, PyArray_Descr *type2) return 0; } + /* For generic units, the num and events are ignored */ + if (meta1->base == NPY_FR_GENERIC && meta2->base == NPY_FR_GENERIC) { + return 1; + } + return meta1->base == meta2->base && meta1->num == meta2->num && meta1->events == meta2->events; @@ -4002,9 +4301,6 @@ cast_timedelta_to_timedelta(PyArray_DatetimeMetaData *src_meta, get_datetime_conversion_factor(src_meta, dst_meta, &num, &denom); if (num == 0) { - PyErr_SetString(PyExc_OverflowError, - "Integer overflow getting a conversion factor between " - "different timedelta types"); return -1; } @@ -4035,3 +4331,653 @@ cast_timedelta_to_timedelta(PyArray_DatetimeMetaData *src_meta, return 0; } +/* + * Returns true if the object is something that is best considered + * a Datetime, false otherwise. + */ +static npy_bool +is_any_numpy_datetime(PyObject *obj) +{ + return (PyArray_IsScalar(obj, Datetime) || + (PyArray_Check(obj) && ( + PyArray_DESCR(obj)->type_num == NPY_DATETIME)) || + PyDate_Check(obj) || + PyDateTime_Check(obj)); +} + +/* + * Returns true if the object is something that is best considered + * a Timedelta, false otherwise. + */ +static npy_bool +is_any_numpy_timedelta(PyObject *obj) +{ + return (PyArray_IsScalar(obj, Timedelta) || + (PyArray_Check(obj) && ( + PyArray_DESCR(obj)->type_num == NPY_TIMEDELTA)) || + PyDelta_Check(obj)); +} + +/* + * Returns true if the object is something that is best considered + * a Datetime or Timedelta, false otherwise. + */ +NPY_NO_EXPORT npy_bool +is_any_numpy_datetime_or_timedelta(PyObject *obj) +{ + return obj != NULL && + (is_any_numpy_datetime(obj) || + is_any_numpy_timedelta(obj)); +} + +/* + * Converts an array of PyObject * into datetimes and/or timedeltas, + * based on the values in type_nums. + * + * If inout_meta->base is -1, uses GCDs to calculate the metadata, filling + * in 'inout_meta' with the resulting metadata. Otherwise uses the provided + * 'inout_meta' for all the conversions. + * + * When obj[i] is NULL, out_value[i] will be set to NPY_DATETIME_NAT. + * + * Returns 0 on success, -1 on failure. + */ +NPY_NO_EXPORT int +convert_pyobjects_to_datetimes(int count, + PyObject **objs, int *type_nums, + npy_int64 *out_values, + PyArray_DatetimeMetaData *inout_meta) +{ + int i, is_out_strict; + PyArray_DatetimeMetaData *meta; + + /* No values trivially succeeds */ + if (count == 0) { + return 0; + } + + /* Use the inputs to resolve the unit metadata if requested */ + if (inout_meta->base == -1) { + /* Allocate an array of metadata corresponding to the objects */ + meta = PyArray_malloc(count * sizeof(PyArray_DatetimeMetaData)); + if (meta == NULL) { + PyErr_NoMemory(); + return -1; + } + + /* Convert all the objects into timedeltas or datetimes */ + for (i = 0; i < count; ++i) { + meta[i].base = -1; + meta[i].num = 1; + meta[i].events = 1; + + /* NULL -> NaT */ + if (objs[i] == NULL) { + out_values[i] = NPY_DATETIME_NAT; + meta[i].base = NPY_FR_GENERIC; + } + else if (type_nums[i] == NPY_DATETIME) { + if (convert_pyobject_to_datetime(&meta[i], objs[i], + &out_values[i]) < 0) { + PyArray_free(meta); + return -1; + } + } + else if (type_nums[i] == NPY_TIMEDELTA) { + if (convert_pyobject_to_timedelta(&meta[i], objs[i], + &out_values[i]) < 0) { + PyArray_free(meta); + return -1; + } + } + else { + PyErr_SetString(PyExc_ValueError, + "convert_pyobjects_to_datetimes requires that " + "all the type_nums provided be datetime or timedelta"); + PyArray_free(meta); + return -1; + } + } + + /* Merge all the metadatas, starting with the first one */ + *inout_meta = meta[0]; + is_out_strict = (type_nums[0] == NPY_TIMEDELTA); + + for (i = 1; i < count; ++i) { + if (compute_datetime_metadata_greatest_common_divisor( + &meta[i], inout_meta, inout_meta, + type_nums[i] == NPY_TIMEDELTA, + is_out_strict) < 0) { + PyArray_free(meta); + return -1; + } + is_out_strict = is_out_strict || (type_nums[i] == NPY_TIMEDELTA); + } + + /* Convert all the values into the resolved unit metadata */ + for (i = 0; i < count; ++i) { + if (type_nums[i] == NPY_DATETIME) { + if (cast_datetime_to_datetime(&meta[i], inout_meta, + out_values[i], &out_values[i]) < 0) { + PyArray_free(meta); + return -1; + } + } + else if (type_nums[i] == NPY_TIMEDELTA) { + if (cast_timedelta_to_timedelta(&meta[i], inout_meta, + out_values[i], &out_values[i]) < 0) { + PyArray_free(meta); + return -1; + } + } + } + + PyArray_free(meta); + } + /* Otherwise convert to the provided unit metadata */ + else { + /* Convert all the objects into timedeltas or datetimes */ + for (i = 0; i < count; ++i) { + /* NULL -> NaT */ + if (objs[i] == NULL) { + out_values[i] = NPY_DATETIME_NAT; + } + else if (type_nums[i] == NPY_DATETIME) { + if (convert_pyobject_to_datetime(inout_meta, objs[i], + &out_values[i]) < 0) { + return -1; + } + } + else if (type_nums[i] == NPY_TIMEDELTA) { + if (convert_pyobject_to_timedelta(inout_meta, objs[i], + &out_values[i]) < 0) { + return -1; + } + } + else { + PyErr_SetString(PyExc_ValueError, + "convert_pyobjects_to_datetimes requires that " + "all the type_nums provided be datetime or timedelta"); + return -1; + } + } + } + + return 0; +} + +NPY_NO_EXPORT PyArrayObject * +datetime_arange(PyObject *start, PyObject *stop, PyObject *step, + PyArray_Descr *dtype) +{ + PyArray_DatetimeMetaData meta; + /* + * Both datetime and timedelta are stored as int64, so they can + * share value variables. + */ + npy_int64 values[3]; + PyObject *objs[3]; + int type_nums[3]; + + npy_intp i, length; + PyArrayObject *ret; + npy_int64 *ret_data; + + /* + * First normalize the input parameters so there is no Py_None, + * and start is moved to stop if stop is unspecified. + */ + if (step == Py_None) { + step = NULL; + } + if (stop == NULL || stop == Py_None) { + stop = start; + start = NULL; + /* If start was NULL or None, raise an exception */ + if (stop == NULL || stop == Py_None) { + PyErr_SetString(PyExc_ValueError, + "arange needs at least a stopping value"); + return NULL; + } + } + if (start == Py_None) { + start = NULL; + } + + /* Step must not be a Datetime */ + if (step != NULL && is_any_numpy_datetime(step)) { + PyErr_SetString(PyExc_ValueError, + "cannot use a datetime as a step in arange"); + return NULL; + } + + /* Check if the units of the given dtype are generic, in which + * case we use the code path that detects the units + */ + if (dtype != NULL) { + PyArray_DatetimeMetaData *meta_tmp; + + type_nums[0] = dtype->type_num; + if (type_nums[0] != NPY_DATETIME && type_nums[0] != NPY_TIMEDELTA) { + PyErr_SetString(PyExc_ValueError, + "datetime_arange was given a non-datetime dtype"); + return NULL; + } + + meta_tmp = get_datetime_metadata_from_dtype(dtype); + if (meta_tmp == NULL) { + return NULL; + } + + /* + * If the dtype specified is in generic units, detect the + * units from the input parameters. + */ + if (meta_tmp->base == NPY_FR_GENERIC) { + dtype = NULL; + meta.base = -1; + } + /* Otherwise use the provided metadata */ + else { + meta = *meta_tmp; + } + } + else { + if (is_any_numpy_datetime(start) || is_any_numpy_datetime(stop)) { + type_nums[0] = NPY_DATETIME; + } + else { + type_nums[0] = NPY_TIMEDELTA; + } + + meta.base = -1; + } + + if (type_nums[0] == NPY_DATETIME && start == NULL) { + PyErr_SetString(PyExc_ValueError, + "arange requires both a start and a stop for " + "NumPy datetime64 ranges"); + return NULL; + } + + /* Set up to convert the objects to a common datetime unit metadata */ + objs[0] = start; + objs[1] = stop; + objs[2] = step; + if (type_nums[0] == NPY_TIMEDELTA) { + type_nums[1] = NPY_TIMEDELTA; + type_nums[2] = NPY_TIMEDELTA; + } + else { + if (PyInt_Check(objs[1]) || + PyLong_Check(objs[1]) || + PyArray_IsScalar(objs[1], Integer) || + is_any_numpy_timedelta(objs[1])) { + type_nums[1] = NPY_TIMEDELTA; + } + else { + type_nums[1] = NPY_DATETIME; + } + type_nums[2] = NPY_TIMEDELTA; + } + + /* Convert all the arguments */ + if (convert_pyobjects_to_datetimes(3, objs, type_nums, + values, &meta) < 0) { + return NULL; + } + + /* If no step was provided, default to 1 */ + if (step == NULL) { + values[2] = 1; + } + + /* + * In the case of arange(datetime, timedelta), convert + * the timedelta into a datetime by adding the start datetime. + */ + if (type_nums[0] == NPY_DATETIME && type_nums[1] == NPY_TIMEDELTA) { + values[1] += values[0]; + } + + /* Now start, stop, and step have their values and matching metadata */ + if (values[0] == NPY_DATETIME_NAT || + values[1] == NPY_DATETIME_NAT || + values[2] == NPY_DATETIME_NAT) { + PyErr_SetString(PyExc_ValueError, + "arange: cannot use NaT (not-a-time) datetime values"); + return NULL; + } + + /* Calculate the array length */ + if (values[2] > 0 && values[1] > values[0]) { + length = (values[1] - values[0] + (values[2] - 1)) / values[2]; + } + else if (values[2] < 0 && values[1] < values[0]) { + length = (values[1] - values[0] + (values[2] + 1)) / values[2]; + } + else if (values[2] != 0) { + length = 0; + } + else { + PyErr_SetString(PyExc_ValueError, + "arange: step cannot be zero"); + return NULL; + } + + /* Create the dtype of the result */ + if (dtype != NULL) { + Py_INCREF(dtype); + } + else { + dtype = create_datetime_dtype(type_nums[0], &meta); + if (dtype == NULL) { + return NULL; + } + } + + /* Create the result array */ + ret = (PyArrayObject *)PyArray_NewFromDescr( + &PyArray_Type, dtype, 1, &length, NULL, + NULL, 0, NULL); + if (ret == NULL) { + return NULL; + } + + if (length > 0) { + /* Extract the data pointer */ + ret_data = (npy_int64 *)PyArray_DATA(ret); + + /* Create the timedeltas or datetimes */ + for (i = 0; i < length; ++i) { + *ret_data = values[0]; + values[0] += values[2]; + ret_data++; + } + } + + return ret; +} + + +/* + * Recursively determines the metadata for an NPY_DATETIME dtype. + * + * Returns 0 on success, -1 on failure. + */ +static int +recursive_find_object_datetime64_type(PyObject *obj, + PyArray_DatetimeMetaData *meta) +{ + /* Array -> use its metadata */ + if (PyArray_Check(obj)) { + PyArray_Descr *obj_dtype = PyArray_DESCR(obj); + /* If the array has metadata, use it */ + if (obj_dtype->type_num == NPY_DATETIME || + obj_dtype->type_num == NPY_TIMEDELTA) { + PyArray_DatetimeMetaData *tmp_meta; + + /* Get the metadata from the type */ + tmp_meta = get_datetime_metadata_from_dtype(obj_dtype); + if (tmp_meta == NULL) { + return -1; + } + + /* Combine it with 'meta' */ + if (compute_datetime_metadata_greatest_common_divisor(meta, + tmp_meta, meta, 0, 0) < 0) { + return -1; + } + + return 0; + } + /* If it's not an object array, stop looking */ + else if (obj_dtype->type_num != NPY_OBJECT) { + return 0; + } + } + /* Datetime scalar -> use its metadata */ + else if (PyArray_IsScalar(obj, Datetime)) { + PyDatetimeScalarObject *dts = (PyDatetimeScalarObject *)obj; + + /* Combine it with 'meta' */ + if (compute_datetime_metadata_greatest_common_divisor(meta, + &dts->obmeta, meta, 0, 0) < 0) { + return -1; + } + + return 0; + } + /* String -> parse it to find out */ + else if (PyBytes_Check(obj) || PyUnicode_Check(obj)) { + npy_datetime tmp = 0; + PyArray_DatetimeMetaData tmp_meta; + + tmp_meta.base = -1; + tmp_meta.num = 1; + tmp_meta.events = 1; + + if (convert_pyobject_to_datetime(&tmp_meta, obj, &tmp) < 0) { + /* If it's a value error, clear the error */ + if (PyErr_Occurred() && + PyErr_GivenExceptionMatches(PyErr_Occurred(), + PyExc_ValueError)) { + PyErr_Clear(); + return 0; + } + /* Otherwise propagate the error */ + else { + return -1; + } + } + + /* Combine it with 'meta' */ + if (compute_datetime_metadata_greatest_common_divisor(meta, + &tmp_meta, meta, 0, 0) < 0) { + return -1; + } + + return 0; + } + /* Python date object -> 'D' */ + else if (PyDate_Check(obj)) { + PyArray_DatetimeMetaData tmp_meta; + + tmp_meta.base = NPY_FR_D; + tmp_meta.num = 1; + tmp_meta.events = 1; + + /* Combine it with 'meta' */ + if (compute_datetime_metadata_greatest_common_divisor(meta, + &tmp_meta, meta, 0, 0) < 0) { + return -1; + } + + return 0; + } + /* Python datetime object -> 'us' */ + else if (PyDateTime_Check(obj)) { + PyArray_DatetimeMetaData tmp_meta; + + tmp_meta.base = NPY_FR_us; + tmp_meta.num = 1; + tmp_meta.events = 1; + + /* Combine it with 'meta' */ + if (compute_datetime_metadata_greatest_common_divisor(meta, + &tmp_meta, meta, 0, 0) < 0) { + return -1; + } + + return 0; + } + + /* Now check if what we have left is a sequence for recursion */ + if (PySequence_Check(obj)) { + Py_ssize_t i, len = PySequence_Size(obj); + if (len < 0 && PyErr_Occurred()) { + return -1; + } + + for (i = 0; i < len; ++i) { + PyObject *f = PySequence_GetItem(obj, i); + if (f == NULL) { + return -1; + } + if (f == obj) { + Py_DECREF(f); + return 0; + } + if (recursive_find_object_datetime64_type(f, meta) < 0) { + Py_DECREF(f); + return -1; + } + Py_DECREF(f); + } + + return 0; + } + /* Otherwise ignore it */ + else { + return 0; + } +} + +/* + * Recursively determines the metadata for an NPY_TIMEDELTA dtype. + * + * Returns 0 on success, -1 on failure. + */ +static int +recursive_find_object_timedelta64_type(PyObject *obj, + PyArray_DatetimeMetaData *meta) +{ + /* Array -> use its metadata */ + if (PyArray_Check(obj)) { + PyArray_Descr *obj_dtype = PyArray_DESCR(obj); + /* If the array has metadata, use it */ + if (obj_dtype->type_num == NPY_DATETIME || + obj_dtype->type_num == NPY_TIMEDELTA) { + PyArray_DatetimeMetaData *tmp_meta; + + /* Get the metadata from the type */ + tmp_meta = get_datetime_metadata_from_dtype(obj_dtype); + if (tmp_meta == NULL) { + return -1; + } + + /* Combine it with 'meta' */ + if (compute_datetime_metadata_greatest_common_divisor(meta, + tmp_meta, meta, 0, 0) < 0) { + return -1; + } + + return 0; + } + /* If it's not an object array, stop looking */ + else if (obj_dtype->type_num != NPY_OBJECT) { + return 0; + } + } + /* Datetime scalar -> use its metadata */ + else if (PyArray_IsScalar(obj, Timedelta)) { + PyTimedeltaScalarObject *dts = (PyTimedeltaScalarObject *)obj; + + /* Combine it with 'meta' */ + if (compute_datetime_metadata_greatest_common_divisor(meta, + &dts->obmeta, meta, 1, 1) < 0) { + return -1; + } + + return 0; + } + /* String -> parse it to find out */ + else if (PyBytes_Check(obj) || PyUnicode_Check(obj)) { + /* No timedelta parser yet */ + return 0; + } + /* Python timedelta object -> 'us' */ + else if (PyDelta_Check(obj)) { + PyArray_DatetimeMetaData tmp_meta; + + tmp_meta.base = NPY_FR_us; + tmp_meta.num = 1; + tmp_meta.events = 1; + + /* Combine it with 'meta' */ + if (compute_datetime_metadata_greatest_common_divisor(meta, + &tmp_meta, meta, 0, 0) < 0) { + return -1; + } + + return 0; + } + + /* Now check if what we have left is a sequence for recursion */ + if (PySequence_Check(obj)) { + Py_ssize_t i, len = PySequence_Size(obj); + if (len < 0 && PyErr_Occurred()) { + return -1; + } + + for (i = 0; i < len; ++i) { + PyObject *f = PySequence_GetItem(obj, i); + if (f == NULL) { + return -1; + } + if (f == obj) { + Py_DECREF(f); + return 0; + } + if (recursive_find_object_timedelta64_type(f, meta) < 0) { + Py_DECREF(f); + return -1; + } + Py_DECREF(f); + } + + return 0; + } + /* Otherwise ignore it */ + else { + return 0; + } +} + +/* + * Examines all the objects in the given Python object by + * recursively descending the sequence structure. Returns a + * datetime or timedelta type with metadata based on the data. + */ +NPY_NO_EXPORT PyArray_Descr * +find_object_datetime_type(PyObject *obj, int type_num) +{ + PyArray_DatetimeMetaData meta; + + meta.base = NPY_FR_GENERIC; + meta.num = 1; + meta.events = 1; + + if (type_num == NPY_DATETIME) { + if (recursive_find_object_datetime64_type(obj, &meta) < 0) { + return NULL; + } + else { + return create_datetime_dtype(type_num, &meta); + } + } + else if (type_num == NPY_TIMEDELTA) { + if (recursive_find_object_timedelta64_type(obj, &meta) < 0) { + return NULL; + } + else { + return create_datetime_dtype(type_num, &meta); + } + } + else { + PyErr_SetString(PyExc_ValueError, + "find_object_datetime_type needs a datetime or " + "timedelta type number"); + return NULL; + } +} diff --git a/numpy/core/src/multiarray/datetime_busday.c b/numpy/core/src/multiarray/datetime_busday.c new file mode 100644 index 000000000000..2d83786a6b4b --- /dev/null +++ b/numpy/core/src/multiarray/datetime_busday.c @@ -0,0 +1,1310 @@ +/* + * This file implements business day functionality for NumPy datetime. + * + * Written by Mark Wiebe (mwwiebe@gmail.com) + * Copyright (c) 2011 by Enthought, Inc. + * + * See LICENSE.txt for the license. + */ + +#define PY_SSIZE_T_CLEAN +#include + +#define _MULTIARRAYMODULE +#include + +#include "npy_config.h" +#include "numpy/npy_3kcompat.h" + +#include "numpy/arrayscalars.h" +#include "lowlevel_strided_loops.h" +#include "_datetime.h" +#include "datetime_busday.h" +#include "datetime_busdaycal.h" + +/* Gets the day of the week for a datetime64[D] value */ +static int +get_day_of_week(npy_datetime date) +{ + int day_of_week; + + /* Get the day of the week for 'date' (1970-01-05 is Monday) */ + day_of_week = (int)((date - 4) % 7); + if (day_of_week < 0) { + day_of_week += 7; + } + + return day_of_week; +} + +/* + * Returns 1 if the date is a holiday (contained in the sorted + * list of dates), 0 otherwise. + * + * The holidays list should be normalized, which means any NaT (not-a-time) + * values, duplicates, and dates already excluded by the weekmask should + * be removed, and the list should be sorted. + */ +static int +is_holiday(npy_datetime date, + npy_datetime *holidays_begin, npy_datetime *holidays_end) +{ + npy_datetime *trial; + + /* Simple binary search */ + while (holidays_begin < holidays_end) { + trial = holidays_begin + (holidays_end - holidays_begin) / 2; + + if (date < *trial) { + holidays_end = trial; + } + else if (date > *trial) { + holidays_begin = trial + 1; + } + else { + return 1; + } + } + + /* Not found */ + return 0; +} + +/* + * Finds the earliest holiday which is on or after 'date'. If 'date' does not + * appear within the holiday range, returns 'holidays_begin' if 'date' + * is before all holidays, or 'holidays_end' if 'date' is after all + * holidays. + * + * To remove all the holidays before 'date' from a holiday range, do: + * + * holidays_begin = find_holiday_earliest_on_or_after(date, + * holidays_begin, holidays_end); + * + * The holidays list should be normalized, which means any NaT (not-a-time) + * values, duplicates, and dates already excluded by the weekmask should + * be removed, and the list should be sorted. + */ +static npy_datetime * +find_earliest_holiday_on_or_after(npy_datetime date, + npy_datetime *holidays_begin, npy_datetime *holidays_end) +{ + npy_datetime *trial; + + /* Simple binary search */ + while (holidays_begin < holidays_end) { + trial = holidays_begin + (holidays_end - holidays_begin) / 2; + + if (date < *trial) { + holidays_end = trial; + } + else if (date > *trial) { + holidays_begin = trial + 1; + } + else { + return trial; + } + } + + return holidays_begin; +} + +/* + * Finds the earliest holiday which is after 'date'. If 'date' does not + * appear within the holiday range, returns 'holidays_begin' if 'date' + * is before all holidays, or 'holidays_end' if 'date' is after all + * holidays. + * + * To remove all the holidays after 'date' from a holiday range, do: + * + * holidays_end = find_holiday_earliest_after(date, + * holidays_begin, holidays_end); + * + * The holidays list should be normalized, which means any NaT (not-a-time) + * values, duplicates, and dates already excluded by the weekmask should + * be removed, and the list should be sorted. + */ +static npy_datetime * +find_earliest_holiday_after(npy_datetime date, + npy_datetime *holidays_begin, npy_datetime *holidays_end) +{ + npy_datetime *trial; + + /* Simple binary search */ + while (holidays_begin < holidays_end) { + trial = holidays_begin + (holidays_end - holidays_begin) / 2; + + if (date < *trial) { + holidays_end = trial; + } + else if (date > *trial) { + holidays_begin = trial + 1; + } + else { + return trial + 1; + } + } + + return holidays_begin; +} + +/* + * Applies the 'roll' strategy to 'date', placing the result in 'out' + * and setting 'out_day_of_week' to the day of the week that results. + * + * Returns 0 on success, -1 on failure. + */ +static int +apply_business_day_roll(npy_datetime date, npy_datetime *out, + int *out_day_of_week, + NPY_BUSDAY_ROLL roll, + npy_bool *weekmask, + npy_datetime *holidays_begin, npy_datetime *holidays_end) +{ + int day_of_week; + + /* Deal with NaT input */ + if (date == NPY_DATETIME_NAT) { + *out = NPY_DATETIME_NAT; + if (roll == NPY_BUSDAY_RAISE) { + PyErr_SetString(PyExc_ValueError, + "NaT input in busday_offset"); + return -1; + } + else { + return 0; + } + } + + /* Get the day of the week for 'date' */ + day_of_week = get_day_of_week(date); + + /* Apply the 'roll' if it's not a business day */ + if (weekmask[day_of_week] == 0 || + is_holiday(date, holidays_begin, holidays_end)) { + npy_datetime start_date = date; + int start_day_of_week = day_of_week; + + switch (roll) { + case NPY_BUSDAY_FOLLOWING: + case NPY_BUSDAY_MODIFIEDFOLLOWING: { + do { + ++date; + if (++day_of_week == 7) { + day_of_week = 0; + } + } while (weekmask[day_of_week] == 0 || + is_holiday(date, holidays_begin, holidays_end)); + + if (roll == NPY_BUSDAY_MODIFIEDFOLLOWING) { + /* If we crossed a month boundary, do preceding instead */ + if (days_to_month_number(start_date) != + days_to_month_number(date)) { + date = start_date; + day_of_week = start_day_of_week; + + do { + --date; + if (--day_of_week == -1) { + day_of_week = 6; + } + } while (weekmask[day_of_week] == 0 || + is_holiday(date, holidays_begin, holidays_end)); + } + } + break; + } + case NPY_BUSDAY_PRECEDING: + case NPY_BUSDAY_MODIFIEDPRECEDING: { + do { + --date; + if (--day_of_week == -1) { + day_of_week = 6; + } + } while (weekmask[day_of_week] == 0 || + is_holiday(date, holidays_begin, holidays_end)); + + if (roll == NPY_BUSDAY_MODIFIEDPRECEDING) { + /* If we crossed a month boundary, do following instead */ + if (days_to_month_number(start_date) != + days_to_month_number(date)) { + date = start_date; + day_of_week = start_day_of_week; + + do { + ++date; + if (++day_of_week == 7) { + day_of_week = 0; + } + } while (weekmask[day_of_week] == 0 || + is_holiday(date, holidays_begin, holidays_end)); + } + } + break; + } + case NPY_BUSDAY_NAT: { + date = NPY_DATETIME_NAT; + break; + } + case NPY_BUSDAY_RAISE: { + *out = NPY_DATETIME_NAT; + PyErr_SetString(PyExc_ValueError, + "Non-business day date in busday_offset"); + return -1; + } + } + } + + *out = date; + *out_day_of_week = day_of_week; + + return 0; +} + +/* + * Applies a single business day offset. See the function + * business_day_offset for the meaning of all the parameters. + * + * Returns 0 on success, -1 on failure. + */ +static int +apply_business_day_offset(npy_datetime date, npy_int64 offset, + npy_datetime *out, + NPY_BUSDAY_ROLL roll, + npy_bool *weekmask, int busdays_in_weekmask, + npy_datetime *holidays_begin, npy_datetime *holidays_end) +{ + int day_of_week = 0; + npy_datetime *holidays_temp; + + /* Roll the date to a business day */ + if (apply_business_day_roll(date, &date, &day_of_week, + roll, + weekmask, + holidays_begin, holidays_end) < 0) { + return -1; + } + + /* If we get a NaT, just return it */ + if (date == NPY_DATETIME_NAT) { + return 0; + } + + /* Now we're on a valid business day */ + if (offset > 0) { + /* Remove any earlier holidays */ + holidays_begin = find_earliest_holiday_on_or_after(date, + holidays_begin, holidays_end); + + /* Jump by as many weeks as we can */ + date += (offset / busdays_in_weekmask) * 7; + offset = offset % busdays_in_weekmask; + + /* Adjust based on the number of holidays we crossed */ + holidays_temp = find_earliest_holiday_after(date, + holidays_begin, holidays_end); + offset += holidays_temp - holidays_begin; + holidays_begin = holidays_temp; + + /* Step until we use up the rest of the offset */ + while (offset > 0) { + ++date; + if (++day_of_week == 7) { + day_of_week = 0; + } + if (weekmask[day_of_week] && !is_holiday(date, + holidays_begin, holidays_end)) { + offset--; + } + } + } + else if (offset < 0) { + /* Remove any later holidays */ + holidays_end = find_earliest_holiday_after(date, + holidays_begin, holidays_end); + + /* Jump by as many weeks as we can */ + date += (offset / busdays_in_weekmask) * 7; + offset = offset % busdays_in_weekmask; + + /* Adjust based on the number of holidays we crossed */ + holidays_temp = find_earliest_holiday_on_or_after(date, + holidays_begin, holidays_end); + offset -= holidays_end - holidays_temp; + holidays_end = holidays_temp; + + /* Step until we use up the rest of the offset */ + while (offset < 0) { + --date; + if (--day_of_week == -1) { + day_of_week = 6; + } + if (weekmask[day_of_week] && !is_holiday(date, + holidays_begin, holidays_end)) { + offset++; + } + } + } + + *out = date; + return 0; +} + +/* + * Applies a single business day count operation. See the function + * business_day_count for the meaning of all the parameters. + * + * Returns 0 on success, -1 on failure. + */ +static int +apply_business_day_count(npy_datetime date_begin, npy_datetime date_end, + npy_int64 *out, + npy_bool *weekmask, int busdays_in_weekmask, + npy_datetime *holidays_begin, npy_datetime *holidays_end) +{ + npy_int64 count, whole_weeks; + int day_of_week = 0; + + /* If we get a NaT, raise an error */ + if (date_begin == NPY_DATETIME_NAT || date_end == NPY_DATETIME_NAT) { + PyErr_SetString(PyExc_ValueError, + "Cannot compute a business day count with a NaT (not-a-time) " + "date"); + return -1; + } + + /* Trivial empty date range */ + if (date_begin >= date_end) { + *out = 0; + return 0; + } + + /* Remove any earlier holidays */ + holidays_begin = find_earliest_holiday_on_or_after(date_begin, + holidays_begin, holidays_end); + /* Remove any later holidays */ + holidays_end = find_earliest_holiday_on_or_after(date_end, + holidays_begin, holidays_end); + + /* Start the count as negative the number of holidays in the range */ + count = -(holidays_end - holidays_begin); + + /* Add the whole weeks between date_begin and date_end */ + whole_weeks = (date_end - date_begin) / 7; + count += whole_weeks * busdays_in_weekmask; + date_begin += whole_weeks * 7; + + if (date_begin < date_end) { + /* Get the day of the week for 'date_begin' */ + day_of_week = get_day_of_week(date_begin); + + /* Count the remaining days one by one */ + while (date_begin < date_end) { + if (weekmask[day_of_week]) { + count++; + } + ++date_begin; + if (++day_of_week == 7) { + day_of_week = 0; + } + } + } + + *out = count; + return 0; +} + +/* + * Applies the given offsets in business days to the dates provided. + * This is the low-level function which requires already cleaned input + * data. + * + * dates: An array of dates with 'datetime64[D]' data type. + * offsets: An array safely convertible into type int64. + * out: Either NULL, or an array with 'datetime64[D]' data type + * in which to place the resulting dates. + * roll: A rule for how to treat non-business day dates. + * weekmask: A 7-element boolean mask, 1 for possible business days and 0 + * for non-business days. + * busdays_in_weekmask: A count of how many 1's there are in weekmask. + * holidays_begin/holidays_end: A sorted list of dates matching '[D]' + * unit metadata, with any dates falling on a day of the + * week without weekmask[i] == 1 already filtered out. + * + * For each (date, offset) in the broadcasted pair of (dates, offsets), + * does the following: + * + Applies the 'roll' rule to the date to either produce NaT, raise + * an exception, or land on a valid business day. + * + Adds 'offset' business days to the valid business day found. + * + Sets the value in 'out' if provided, or the allocated output array + * otherwise. + */ +NPY_NO_EXPORT PyArrayObject * +business_day_offset(PyArrayObject *dates, PyArrayObject *offsets, + PyArrayObject *out, + NPY_BUSDAY_ROLL roll, + npy_bool *weekmask, int busdays_in_weekmask, + npy_datetime *holidays_begin, npy_datetime *holidays_end) +{ + PyArray_DatetimeMetaData temp_meta; + PyArray_Descr *dtypes[3] = {NULL, NULL, NULL}; + + NpyIter *iter = NULL; + PyArrayObject *op[3] = {NULL, NULL, NULL}; + npy_uint32 op_flags[3], flags; + + PyArrayObject *ret = NULL; + + if (busdays_in_weekmask == 0) { + PyErr_SetString(PyExc_ValueError, + "the business day weekmask must have at least one " + "valid business day"); + return NULL; + } + + /* First create the data types for dates and offsets */ + temp_meta.base = NPY_FR_D; + temp_meta.num = 1; + temp_meta.events = 1; + dtypes[0] = create_datetime_dtype(NPY_DATETIME, &temp_meta); + if (dtypes[0] == NULL) { + goto fail; + } + dtypes[1] = PyArray_DescrFromType(NPY_INT64); + if (dtypes[1] == NULL) { + goto fail; + } + dtypes[2] = dtypes[0]; + Py_INCREF(dtypes[2]); + + /* Set up the iterator parameters */ + flags = NPY_ITER_EXTERNAL_LOOP| + NPY_ITER_BUFFERED| + NPY_ITER_ZEROSIZE_OK; + op[0] = dates; + op_flags[0] = NPY_ITER_READONLY | NPY_ITER_ALIGNED; + op[1] = offsets; + op_flags[1] = NPY_ITER_READONLY | NPY_ITER_ALIGNED; + op[2] = out; + op_flags[2] = NPY_ITER_WRITEONLY | NPY_ITER_ALLOCATE | NPY_ITER_ALIGNED; + + /* Allocate the iterator */ + iter = NpyIter_MultiNew(3, op, flags, NPY_KEEPORDER, NPY_SAFE_CASTING, + op_flags, dtypes); + if (iter == NULL) { + goto fail; + } + + /* Loop over all elements */ + if (NpyIter_GetIterSize(iter) > 0) { + NpyIter_IterNextFunc *iternext; + char **dataptr; + npy_intp *strideptr, *innersizeptr; + + iternext = NpyIter_GetIterNext(iter, NULL); + if (iternext == NULL) { + goto fail; + } + dataptr = NpyIter_GetDataPtrArray(iter); + strideptr = NpyIter_GetInnerStrideArray(iter); + innersizeptr = NpyIter_GetInnerLoopSizePtr(iter); + + do { + char *data_dates = dataptr[0]; + char *data_offsets = dataptr[1]; + char *data_out = dataptr[2]; + npy_intp stride_dates = strideptr[0]; + npy_intp stride_offsets = strideptr[1]; + npy_intp stride_out = strideptr[2]; + npy_intp count = *innersizeptr; + + while (count--) { + if (apply_business_day_offset(*(npy_int64 *)data_dates, + *(npy_int64 *)data_offsets, + (npy_int64 *)data_out, + roll, + weekmask, busdays_in_weekmask, + holidays_begin, holidays_end) < 0) { + goto fail; + } + + data_dates += stride_dates; + data_offsets += stride_offsets; + data_out += stride_out; + } + } while (iternext(iter)); + } + + /* Get the return object from the iterator */ + ret = NpyIter_GetOperandArray(iter)[2]; + Py_INCREF(ret); + + goto finish; + +fail: + Py_XDECREF(ret); + ret = NULL; + +finish: + Py_XDECREF(dtypes[0]); + Py_XDECREF(dtypes[1]); + Py_XDECREF(dtypes[2]); + if (iter != NULL) { + if (NpyIter_Deallocate(iter) != NPY_SUCCEED) { + Py_XDECREF(ret); + ret = NULL; + } + } + return ret; +} + +/* + * Counts the number of business days between two dates, not including + * the end date. This is the low-level function which requires already + * cleaned input data. + * + * dates_begin: An array of dates with 'datetime64[D]' data type. + * dates_end: An array of dates with 'datetime64[D]' data type. + * out: Either NULL, or an array with 'int64' data type + * in which to place the resulting dates. + * weekmask: A 7-element boolean mask, 1 for possible business days and 0 + * for non-business days. + * busdays_in_weekmask: A count of how many 1's there are in weekmask. + * holidays_begin/holidays_end: A sorted list of dates matching '[D]' + * unit metadata, with any dates falling on a day of the + * week without weekmask[i] == 1 already filtered out. + */ +NPY_NO_EXPORT PyArrayObject * +business_day_count(PyArrayObject *dates_begin, PyArrayObject *dates_end, + PyArrayObject *out, + npy_bool *weekmask, int busdays_in_weekmask, + npy_datetime *holidays_begin, npy_datetime *holidays_end) +{ + PyArray_DatetimeMetaData temp_meta; + PyArray_Descr *dtypes[3] = {NULL, NULL, NULL}; + + NpyIter *iter = NULL; + PyArrayObject *op[3] = {NULL, NULL, NULL}; + npy_uint32 op_flags[3], flags; + + PyArrayObject *ret = NULL; + + if (busdays_in_weekmask == 0) { + PyErr_SetString(PyExc_ValueError, + "the business day weekmask must have at least one " + "valid business day"); + return NULL; + } + + /* First create the data types for the dates and the int64 output */ + temp_meta.base = NPY_FR_D; + temp_meta.num = 1; + temp_meta.events = 1; + dtypes[0] = create_datetime_dtype(NPY_DATETIME, &temp_meta); + if (dtypes[0] == NULL) { + goto fail; + } + dtypes[1] = dtypes[0]; + Py_INCREF(dtypes[1]); + dtypes[2] = PyArray_DescrFromType(NPY_INT64); + if (dtypes[2] == NULL) { + goto fail; + } + + /* Set up the iterator parameters */ + flags = NPY_ITER_EXTERNAL_LOOP| + NPY_ITER_BUFFERED| + NPY_ITER_ZEROSIZE_OK; + op[0] = dates_begin; + op_flags[0] = NPY_ITER_READONLY | NPY_ITER_ALIGNED; + op[1] = dates_end; + op_flags[1] = NPY_ITER_READONLY | NPY_ITER_ALIGNED; + op[2] = out; + op_flags[2] = NPY_ITER_WRITEONLY | NPY_ITER_ALLOCATE | NPY_ITER_ALIGNED; + + /* Allocate the iterator */ + iter = NpyIter_MultiNew(3, op, flags, NPY_KEEPORDER, NPY_SAFE_CASTING, + op_flags, dtypes); + if (iter == NULL) { + goto fail; + } + + /* Loop over all elements */ + if (NpyIter_GetIterSize(iter) > 0) { + NpyIter_IterNextFunc *iternext; + char **dataptr; + npy_intp *strideptr, *innersizeptr; + + iternext = NpyIter_GetIterNext(iter, NULL); + if (iternext == NULL) { + goto fail; + } + dataptr = NpyIter_GetDataPtrArray(iter); + strideptr = NpyIter_GetInnerStrideArray(iter); + innersizeptr = NpyIter_GetInnerLoopSizePtr(iter); + + do { + char *data_dates_begin = dataptr[0]; + char *data_dates_end = dataptr[1]; + char *data_out = dataptr[2]; + npy_intp stride_dates_begin = strideptr[0]; + npy_intp stride_dates_end = strideptr[1]; + npy_intp stride_out = strideptr[2]; + npy_intp count = *innersizeptr; + + while (count--) { + if (apply_business_day_count(*(npy_int64 *)data_dates_begin, + *(npy_int64 *)data_dates_end, + (npy_int64 *)data_out, + weekmask, busdays_in_weekmask, + holidays_begin, holidays_end) < 0) { + goto fail; + } + + data_dates_begin += stride_dates_begin; + data_dates_end += stride_dates_end; + data_out += stride_out; + } + } while (iternext(iter)); + } + + /* Get the return object from the iterator */ + ret = NpyIter_GetOperandArray(iter)[2]; + Py_INCREF(ret); + + goto finish; + +fail: + Py_XDECREF(ret); + ret = NULL; + +finish: + Py_XDECREF(dtypes[0]); + Py_XDECREF(dtypes[1]); + Py_XDECREF(dtypes[2]); + if (iter != NULL) { + if (NpyIter_Deallocate(iter) != NPY_SUCCEED) { + Py_XDECREF(ret); + ret = NULL; + } + } + return ret; +} + +/* + * Returns a boolean array with True for input dates which are valid + * business days, and False for dates which are not. This is the + * low-level function which requires already cleaned input data. + * + * dates: An array of dates with 'datetime64[D]' data type. + * out: Either NULL, or an array with 'bool' data type + * in which to place the resulting dates. + * weekmask: A 7-element boolean mask, 1 for possible business days and 0 + * for non-business days. + * busdays_in_weekmask: A count of how many 1's there are in weekmask. + * holidays_begin/holidays_end: A sorted list of dates matching '[D]' + * unit metadata, with any dates falling on a day of the + * week without weekmask[i] == 1 already filtered out. + */ +NPY_NO_EXPORT PyArrayObject * +is_business_day(PyArrayObject *dates, PyArrayObject *out, + npy_bool *weekmask, int busdays_in_weekmask, + npy_datetime *holidays_begin, npy_datetime *holidays_end) +{ + PyArray_DatetimeMetaData temp_meta; + PyArray_Descr *dtypes[2] = {NULL, NULL}; + + NpyIter *iter = NULL; + PyArrayObject *op[2] = {NULL, NULL}; + npy_uint32 op_flags[2], flags; + + PyArrayObject *ret = NULL; + + if (busdays_in_weekmask == 0) { + PyErr_SetString(PyExc_ValueError, + "the business day weekmask must have at least one " + "valid business day"); + return NULL; + } + + /* First create the data types for the dates and the bool output */ + temp_meta.base = NPY_FR_D; + temp_meta.num = 1; + temp_meta.events = 1; + dtypes[0] = create_datetime_dtype(NPY_DATETIME, &temp_meta); + if (dtypes[0] == NULL) { + goto fail; + } + dtypes[1] = PyArray_DescrFromType(NPY_BOOL); + if (dtypes[1] == NULL) { + goto fail; + } + + /* Set up the iterator parameters */ + flags = NPY_ITER_EXTERNAL_LOOP| + NPY_ITER_BUFFERED| + NPY_ITER_ZEROSIZE_OK; + op[0] = dates; + op_flags[0] = NPY_ITER_READONLY | NPY_ITER_ALIGNED; + op[1] = out; + op_flags[1] = NPY_ITER_WRITEONLY | NPY_ITER_ALLOCATE | NPY_ITER_ALIGNED; + + /* Allocate the iterator */ + iter = NpyIter_MultiNew(2, op, flags, NPY_KEEPORDER, NPY_SAFE_CASTING, + op_flags, dtypes); + if (iter == NULL) { + goto fail; + } + + /* Loop over all elements */ + if (NpyIter_GetIterSize(iter) > 0) { + NpyIter_IterNextFunc *iternext; + char **dataptr; + npy_intp *strideptr, *innersizeptr; + + iternext = NpyIter_GetIterNext(iter, NULL); + if (iternext == NULL) { + goto fail; + } + dataptr = NpyIter_GetDataPtrArray(iter); + strideptr = NpyIter_GetInnerStrideArray(iter); + innersizeptr = NpyIter_GetInnerLoopSizePtr(iter); + + do { + char *data_dates = dataptr[0]; + char *data_out = dataptr[1]; + npy_intp stride_dates = strideptr[0]; + npy_intp stride_out = strideptr[1]; + npy_intp count = *innersizeptr; + + npy_datetime date; + int day_of_week; + + while (count--) { + /* Check if it's a business day */ + date = *(npy_datetime *)data_dates; + day_of_week = get_day_of_week(date); + *(npy_bool *)data_out = weekmask[day_of_week] && + !is_holiday(date, + holidays_begin, holidays_end) && + date != NPY_DATETIME_NAT; + + data_dates += stride_dates; + data_out += stride_out; + } + } while (iternext(iter)); + } + + /* Get the return object from the iterator */ + ret = NpyIter_GetOperandArray(iter)[1]; + Py_INCREF(ret); + + goto finish; + +fail: + Py_XDECREF(ret); + ret = NULL; + +finish: + Py_XDECREF(dtypes[0]); + Py_XDECREF(dtypes[1]); + if (iter != NULL) { + if (NpyIter_Deallocate(iter) != NPY_SUCCEED) { + Py_XDECREF(ret); + ret = NULL; + } + } + return ret; +} + +static int +PyArray_BusDayRollConverter(PyObject *roll_in, NPY_BUSDAY_ROLL *roll) +{ + PyObject *obj = roll_in; + char *str; + Py_ssize_t len; + + /* Make obj into an ASCII string */ + Py_INCREF(obj); + if (PyUnicode_Check(obj)) { + /* accept unicode input */ + PyObject *obj_str; + obj_str = PyUnicode_AsASCIIString(obj); + if (obj_str == NULL) { + Py_DECREF(obj); + return 0; + } + Py_DECREF(obj); + obj = obj_str; + } + + if (PyBytes_AsStringAndSize(obj, &str, &len) < 0) { + Py_DECREF(obj); + return 0; + } + + /* Use switch statements to quickly isolate the right enum value */ + switch (str[0]) { + case 'b': + if (strcmp(str, "backward") == 0) { + *roll = NPY_BUSDAY_BACKWARD; + goto finish; + } + break; + case 'f': + if (len > 2) switch (str[2]) { + case 'r': + if (strcmp(str, "forward") == 0) { + *roll = NPY_BUSDAY_FORWARD; + goto finish; + } + break; + case 'l': + if (strcmp(str, "following") == 0) { + *roll = NPY_BUSDAY_FOLLOWING; + goto finish; + } + break; + } + break; + case 'm': + if (len > 8) switch (str[8]) { + case 'f': + if (strcmp(str, "modifiedfollowing") == 0) { + *roll = NPY_BUSDAY_MODIFIEDFOLLOWING; + goto finish; + } + break; + case 'p': + if (strcmp(str, "modifiedpreceding") == 0) { + *roll = NPY_BUSDAY_MODIFIEDFOLLOWING; + goto finish; + } + break; + } + break; + case 'n': + if (strcmp(str, "nat") == 0) { + *roll = NPY_BUSDAY_NAT; + goto finish; + } + break; + case 'p': + if (strcmp(str, "preceding") == 0) { + *roll = NPY_BUSDAY_PRECEDING; + goto finish; + } + break; + case 'r': + if (strcmp(str, "raise") == 0) { + *roll = NPY_BUSDAY_RAISE; + goto finish; + } + break; + } + + PyErr_Format(PyExc_ValueError, + "Invalid business day roll parameter \"%s\"", + str); + Py_DECREF(obj); + return 0; + +finish: + Py_DECREF(obj); + return 1; +} + +/* + * This is the 'busday_offset' function exposed for calling + * from Python. + */ +NPY_NO_EXPORT PyObject * +array_busday_offset(PyObject *NPY_UNUSED(self), + PyObject *args, PyObject *kwds) +{ + char *kwlist[] = {"dates", "offsets", "roll", + "weekmask", "holidays", "busdaycal", "out", NULL}; + + PyObject *dates_in = NULL, *offsets_in = NULL, *out_in = NULL; + + PyArrayObject *dates = NULL, *offsets = NULL, *out = NULL, *ret; + NPY_BUSDAY_ROLL roll = NPY_BUSDAY_RAISE; + npy_bool weekmask[7] = {2, 1, 1, 1, 1, 0, 0}; + NpyBusDayCalendar *busdaycal = NULL; + int i, busdays_in_weekmask; + npy_holidayslist holidays = {NULL, NULL}; + int allocated_holidays = 1; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, + "OO|O&O&O&O!O:busday_offset", kwlist, + &dates_in, + &offsets_in, + &PyArray_BusDayRollConverter, &roll, + &PyArray_WeekMaskConverter, &weekmask[0], + &PyArray_HolidaysConverter, &holidays, + &NpyBusDayCalendar_Type, &busdaycal, + &out_in)) { + goto fail; + } + + /* Make sure only one of the weekmask/holidays and busdaycal is supplied */ + if (busdaycal != NULL) { + if (weekmask[0] != 2 || holidays.begin != NULL) { + PyErr_SetString(PyExc_ValueError, + "Cannot supply both the weekmask/holidays and the " + "busdaycal parameters to busday_offset()"); + goto fail; + } + + /* Indicate that the holidays weren't allocated by us */ + allocated_holidays = 0; + + /* Copy the private normalized weekmask/holidays data */ + holidays = busdaycal->holidays; + busdays_in_weekmask = busdaycal->busdays_in_weekmask; + memcpy(weekmask, busdaycal->weekmask, 7); + } + else { + /* + * Fix up the weekmask from the uninitialized + * signal value to a proper default. + */ + if (weekmask[0] == 2) { + weekmask[0] = 1; + } + + /* Count the number of business days in a week */ + busdays_in_weekmask = 0; + for (i = 0; i < 7; ++i) { + busdays_in_weekmask += weekmask[i]; + } + + /* The holidays list must be normalized before using it */ + normalize_holidays_list(&holidays, weekmask); + } + + /* Make 'dates' into an array */ + if (PyArray_Check(dates_in)) { + dates = (PyArrayObject *)dates_in; + Py_INCREF(dates); + } + else { + PyArray_Descr *datetime_dtype; + + /* Use the datetime dtype with generic units so it fills it in */ + datetime_dtype = PyArray_DescrFromType(NPY_DATETIME); + if (datetime_dtype == NULL) { + goto fail; + } + + /* This steals the datetime_dtype reference */ + dates = (PyArrayObject *)PyArray_FromAny(dates_in, datetime_dtype, + 0, 0, 0, dates_in); + if (dates == NULL) { + goto fail; + } + } + + /* Make 'offsets' into an array */ + offsets = (PyArrayObject *)PyArray_FromAny(offsets_in, + PyArray_DescrFromType(NPY_INT64), + 0, 0, 0, offsets_in); + if (offsets == NULL) { + goto fail; + } + + /* Make sure 'out' is an array if it's provided */ + if (out_in != NULL) { + if (!PyArray_Check(out_in)) { + PyErr_SetString(PyExc_ValueError, + "busday_offset: must provide a NumPy array for 'out'"); + goto fail; + } + out = (PyArrayObject *)out_in; + } + + ret = business_day_offset(dates, offsets, out, roll, + weekmask, busdays_in_weekmask, + holidays.begin, holidays.end); + + Py_DECREF(dates); + Py_DECREF(offsets); + if (allocated_holidays && holidays.begin != NULL) { + PyArray_free(holidays.begin); + } + + return out == NULL ? PyArray_Return(ret) : (PyObject *)ret; + +fail: + Py_XDECREF(dates); + Py_XDECREF(offsets); + if (allocated_holidays && holidays.begin != NULL) { + PyArray_free(holidays.begin); + } + + return NULL; +} + +/* + * This is the 'busday_count' function exposed for calling + * from Python. + */ +NPY_NO_EXPORT PyObject * +array_busday_count(PyObject *NPY_UNUSED(self), + PyObject *args, PyObject *kwds) +{ + char *kwlist[] = {"begindates", "enddates", + "weekmask", "holidays", "busdaycal", "out", NULL}; + + PyObject *dates_begin_in = NULL, *dates_end_in = NULL, *out_in = NULL; + + PyArrayObject *dates_begin = NULL, *dates_end = NULL, *out = NULL, *ret; + npy_bool weekmask[7] = {2, 1, 1, 1, 1, 0, 0}; + NpyBusDayCalendar *busdaycal = NULL; + int i, busdays_in_weekmask; + npy_holidayslist holidays = {NULL, NULL}; + int allocated_holidays = 1; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, + "OO|O&O&O!O:busday_count", kwlist, + &dates_begin_in, + &dates_end_in, + &PyArray_WeekMaskConverter, &weekmask[0], + &PyArray_HolidaysConverter, &holidays, + &NpyBusDayCalendar_Type, &busdaycal, + &out_in)) { + goto fail; + } + + /* Make sure only one of the weekmask/holidays and busdaycal is supplied */ + if (busdaycal != NULL) { + if (weekmask[0] != 2 || holidays.begin != NULL) { + PyErr_SetString(PyExc_ValueError, + "Cannot supply both the weekmask/holidays and the " + "busdaycal parameters to busday_count()"); + goto fail; + } + + /* Indicate that the holidays weren't allocated by us */ + allocated_holidays = 0; + + /* Copy the private normalized weekmask/holidays data */ + holidays = busdaycal->holidays; + busdays_in_weekmask = busdaycal->busdays_in_weekmask; + memcpy(weekmask, busdaycal->weekmask, 7); + } + else { + /* + * Fix up the weekmask from the uninitialized + * signal value to a proper default. + */ + if (weekmask[0] == 2) { + weekmask[0] = 1; + } + + /* Count the number of business days in a week */ + busdays_in_weekmask = 0; + for (i = 0; i < 7; ++i) { + busdays_in_weekmask += weekmask[i]; + } + + /* The holidays list must be normalized before using it */ + normalize_holidays_list(&holidays, weekmask); + } + + /* Make 'dates_begin' into an array */ + if (PyArray_Check(dates_begin_in)) { + dates_begin = (PyArrayObject *)dates_begin_in; + Py_INCREF(dates_begin); + } + else { + PyArray_Descr *datetime_dtype; + + /* Use the datetime dtype with generic units so it fills it in */ + datetime_dtype = PyArray_DescrFromType(NPY_DATETIME); + if (datetime_dtype == NULL) { + goto fail; + } + + /* This steals the datetime_dtype reference */ + dates_begin = (PyArrayObject *)PyArray_FromAny(dates_begin_in, + datetime_dtype, + 0, 0, 0, dates_begin_in); + if (dates_begin == NULL) { + goto fail; + } + } + + /* Make 'dates_end' into an array */ + if (PyArray_Check(dates_end_in)) { + dates_end = (PyArrayObject *)dates_end_in; + Py_INCREF(dates_end); + } + else { + PyArray_Descr *datetime_dtype; + + /* Use the datetime dtype with generic units so it fills it in */ + datetime_dtype = PyArray_DescrFromType(NPY_DATETIME); + if (datetime_dtype == NULL) { + goto fail; + } + + /* This steals the datetime_dtype reference */ + dates_end = (PyArrayObject *)PyArray_FromAny(dates_end_in, + datetime_dtype, + 0, 0, 0, dates_end_in); + if (dates_end == NULL) { + goto fail; + } + } + + /* Make sure 'out' is an array if it's provided */ + if (out_in != NULL) { + if (!PyArray_Check(out_in)) { + PyErr_SetString(PyExc_ValueError, + "busday_offset: must provide a NumPy array for 'out'"); + goto fail; + } + out = (PyArrayObject *)out_in; + } + + ret = business_day_count(dates_begin, dates_end, out, + weekmask, busdays_in_weekmask, + holidays.begin, holidays.end); + + Py_DECREF(dates_begin); + Py_DECREF(dates_end); + if (allocated_holidays && holidays.begin != NULL) { + PyArray_free(holidays.begin); + } + + return out == NULL ? PyArray_Return(ret) : (PyObject *)ret; + +fail: + Py_XDECREF(dates_begin); + Py_XDECREF(dates_end); + if (allocated_holidays && holidays.begin != NULL) { + PyArray_free(holidays.begin); + } + + return NULL; +} + +/* + * This is the 'is_busday' function exposed for calling + * from Python. + */ +NPY_NO_EXPORT PyObject * +array_is_busday(PyObject *NPY_UNUSED(self), + PyObject *args, PyObject *kwds) +{ + char *kwlist[] = {"dates", + "weekmask", "holidays", "busdaycal", "out", NULL}; + + PyObject *dates_in = NULL, *out_in = NULL; + + PyArrayObject *dates = NULL,*out = NULL, *ret; + npy_bool weekmask[7] = {2, 1, 1, 1, 1, 0, 0}; + NpyBusDayCalendar *busdaycal = NULL; + int i, busdays_in_weekmask; + npy_holidayslist holidays = {NULL, NULL}; + int allocated_holidays = 1; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, + "O|O&O&O!O:is_busday", kwlist, + &dates_in, + &PyArray_WeekMaskConverter, &weekmask[0], + &PyArray_HolidaysConverter, &holidays, + &NpyBusDayCalendar_Type, &busdaycal, + &out_in)) { + goto fail; + } + + /* Make sure only one of the weekmask/holidays and busdaycal is supplied */ + if (busdaycal != NULL) { + if (weekmask[0] != 2 || holidays.begin != NULL) { + PyErr_SetString(PyExc_ValueError, + "Cannot supply both the weekmask/holidays and the " + "busdaycal parameters to is_busday()"); + goto fail; + } + + /* Indicate that the holidays weren't allocated by us */ + allocated_holidays = 0; + + /* Copy the private normalized weekmask/holidays data */ + holidays = busdaycal->holidays; + busdays_in_weekmask = busdaycal->busdays_in_weekmask; + memcpy(weekmask, busdaycal->weekmask, 7); + } + else { + /* + * Fix up the weekmask from the uninitialized + * signal value to a proper default. + */ + if (weekmask[0] == 2) { + weekmask[0] = 1; + } + + /* Count the number of business days in a week */ + busdays_in_weekmask = 0; + for (i = 0; i < 7; ++i) { + busdays_in_weekmask += weekmask[i]; + } + + /* The holidays list must be normalized before using it */ + normalize_holidays_list(&holidays, weekmask); + } + + /* Make 'dates' into an array */ + if (PyArray_Check(dates_in)) { + dates = (PyArrayObject *)dates_in; + Py_INCREF(dates); + } + else { + PyArray_Descr *datetime_dtype; + + /* Use the datetime dtype with generic units so it fills it in */ + datetime_dtype = PyArray_DescrFromType(NPY_DATETIME); + if (datetime_dtype == NULL) { + goto fail; + } + + /* This steals the datetime_dtype reference */ + dates = (PyArrayObject *)PyArray_FromAny(dates_in, + datetime_dtype, + 0, 0, 0, dates_in); + if (dates == NULL) { + goto fail; + } + } + + /* Make sure 'out' is an array if it's provided */ + if (out_in != NULL) { + if (!PyArray_Check(out_in)) { + PyErr_SetString(PyExc_ValueError, + "busday_offset: must provide a NumPy array for 'out'"); + goto fail; + } + out = (PyArrayObject *)out_in; + } + + ret = is_business_day(dates, out, + weekmask, busdays_in_weekmask, + holidays.begin, holidays.end); + + Py_DECREF(dates); + if (allocated_holidays && holidays.begin != NULL) { + PyArray_free(holidays.begin); + } + + return out == NULL ? PyArray_Return(ret) : (PyObject *)ret; + +fail: + Py_XDECREF(dates); + if (allocated_holidays && holidays.begin != NULL) { + PyArray_free(holidays.begin); + } + + return NULL; +} diff --git a/numpy/core/src/multiarray/datetime_busday.h b/numpy/core/src/multiarray/datetime_busday.h new file mode 100644 index 000000000000..483151122b2a --- /dev/null +++ b/numpy/core/src/multiarray/datetime_busday.h @@ -0,0 +1,28 @@ +#ifndef _NPY_PRIVATE__DATETIME_BUSDAY_H_ +#define _NPY_PRIVATE__DATETIME_BUSDAY_H_ + +/* + * This is the 'busday_offset' function exposed for calling + * from Python. + */ +NPY_NO_EXPORT PyObject * +array_busday_offset(PyObject *NPY_UNUSED(self), + PyObject *args, PyObject *kwds); + +/* + * This is the 'busday_count' function exposed for calling + * from Python. + */ +NPY_NO_EXPORT PyObject * +array_busday_count(PyObject *NPY_UNUSED(self), + PyObject *args, PyObject *kwds); + +/* + * This is the 'is_busday' function exposed for calling + * from Python. + */ +NPY_NO_EXPORT PyObject * +array_is_busday(PyObject *NPY_UNUSED(self), + PyObject *args, PyObject *kwds); + +#endif diff --git a/numpy/core/src/multiarray/datetime_busdaycal.c b/numpy/core/src/multiarray/datetime_busdaycal.c new file mode 100644 index 000000000000..5ad4ee5536cf --- /dev/null +++ b/numpy/core/src/multiarray/datetime_busdaycal.c @@ -0,0 +1,541 @@ +/* + * This file implements an object encapsulating a business day + * calendar object for accelerating NumPy datetime business day functions. + * + * Written by Mark Wiebe (mwwiebe@gmail.com) + * Copyright (c) 2011 by Enthought, Inc. + * + * See LICENSE.txt for the license. + */ + +#define PY_SSIZE_T_CLEAN +#include + +#define _MULTIARRAYMODULE +#include + +#include "npy_config.h" +#include "numpy/npy_3kcompat.h" + +#include "numpy/arrayscalars.h" +#include "lowlevel_strided_loops.h" +#include "_datetime.h" +#include "datetime_busday.h" +#include "datetime_busdaycal.h" + +NPY_NO_EXPORT int +PyArray_WeekMaskConverter(PyObject *weekmask_in, npy_bool *weekmask) +{ + PyObject *obj = weekmask_in; + + /* Make obj into an ASCII string if it is UNICODE */ + Py_INCREF(obj); + if (PyUnicode_Check(obj)) { + /* accept unicode input */ + PyObject *obj_str; + obj_str = PyUnicode_AsASCIIString(obj); + if (obj_str == NULL) { + Py_DECREF(obj); + return 0; + } + Py_DECREF(obj); + obj = obj_str; + } + + if (PyBytes_Check(obj)) { + char *str; + Py_ssize_t len; + + if (PyBytes_AsStringAndSize(obj, &str, &len) < 0) { + Py_DECREF(obj); + return 0; + } + + /* Length 7 is a string like "1111100" */ + if (len == 7) { + int i; + for (i = 0; i < 7; ++i) { + switch(str[i]) { + case '0': + weekmask[i] = 0; + break; + case '1': + weekmask[i] = 1; + break; + default: + goto invalid_weekmask_string; + } + } + + goto finish; + } + /* Length divisible by 3 is a string like "Mon" or "MonWedFri" */ + else if (len % 3 == 0) { + int i; + memset(weekmask, 0, 7); + for (i = 0; i < len; i += 3) { + switch (str[i]) { + case 'M': + if (str[i+1] == 'o' && str[i+2] == 'n') { + weekmask[0] = 1; + } + else { + goto invalid_weekmask_string; + } + break; + case 'T': + if (str[i+1] == 'u' && str[i+2] == 'e') { + weekmask[1] = 1; + } + else if (str[i+1] == 'h' && str[i+2] == 'u') { + weekmask[3] = 1; + } + else { + goto invalid_weekmask_string; + } + break; + case 'W': + if (str[i+1] == 'e' && str[i+2] == 'd') { + weekmask[2] = 1; + } + else { + goto invalid_weekmask_string; + } + break; + case 'F': + if (str[i+1] == 'r' && str[i+2] == 'i') { + weekmask[4] = 1; + } + else { + goto invalid_weekmask_string; + } + break; + case 'S': + if (str[i+1] == 'a' && str[i+2] == 't') { + weekmask[5] = 1; + } + else if (str[i+1] == 'u' && str[i+2] == 'n') { + weekmask[6] = 1; + } + else { + goto invalid_weekmask_string; + } + break; + } + } + + goto finish; + } + +invalid_weekmask_string: + PyErr_Format(PyExc_ValueError, + "Invalid business day weekmask string \"%s\"", + str); + Py_DECREF(obj); + return 0; + } + /* Something like [1,1,1,1,1,0,0] */ + else if (PySequence_Check(obj)) { + if (PySequence_Size(obj) != 7 || + (PyArray_Check(obj) && PyArray_NDIM(obj) != 1)) { + PyErr_SetString(PyExc_ValueError, + "A business day weekmask array must have length 7"); + Py_DECREF(obj); + return 0; + } + else { + int i; + PyObject *f; + + for (i = 0; i < 7; ++i) { + long val; + + f = PySequence_GetItem(obj, i); + if (f == NULL) { + Py_DECREF(obj); + return 0; + } + + val = PyInt_AsLong(f); + if (val == -1 && PyErr_Occurred()) { + Py_DECREF(obj); + return 0; + } + if (val == 0) { + weekmask[i] = 0; + } + else if (val == 1) { + weekmask[i] = 1; + } + else { + PyErr_SetString(PyExc_ValueError, + "A business day weekmask array must have all " + "1's and 0's"); + Py_DECREF(obj); + return 0; + } + } + + goto finish; + } + } + + PyErr_SetString(PyExc_ValueError, + "Couldn't convert object into a business day weekmask"); + Py_DECREF(obj); + return 0; + +finish: + Py_DECREF(obj); + return 1; +} + +static int +qsort_datetime_compare(const void *elem1, const void *elem2) +{ + npy_datetime e1 = *(npy_datetime *)elem1; + npy_datetime e2 = *(npy_datetime *)elem2; + + return (e1 < e2) ? -1 : (e1 == e2) ? 0 : 1; +} + +/* + * Sorts the the array of dates provided in place and removes + * NaT, duplicates and any date which is already excluded on account + * of the weekmask. + * + * Returns the number of dates left after removing weekmask-excluded + * dates. + */ +NPY_NO_EXPORT void +normalize_holidays_list(npy_holidayslist *holidays, npy_bool *weekmask) +{ + npy_datetime *dates = holidays->begin; + npy_intp count = holidays->end - dates; + + npy_datetime lastdate = NPY_DATETIME_NAT; + npy_intp trimcount, i; + int day_of_week; + + /* Sort the dates */ + qsort(dates, count, sizeof(npy_datetime), &qsort_datetime_compare); + + /* Sweep throught the array, eliminating unnecessary values */ + trimcount = 0; + for (i = 0; i < count; ++i) { + npy_datetime date = dates[i]; + + /* Skip any NaT or duplicate */ + if (date != NPY_DATETIME_NAT && date != lastdate) { + /* Get the day of the week (1970-01-05 is Monday) */ + day_of_week = (int)((date - 4) % 7); + if (day_of_week < 0) { + day_of_week += 7; + } + + /* + * If the holiday falls on a possible business day, + * then keep it. + */ + if (weekmask[day_of_week] == 1) { + dates[trimcount++] = date; + lastdate = date; + } + } + } + + /* Adjust the end of the holidays array */ + holidays->end = dates + trimcount; +} + +/* + * Converts a Python input into a non-normalized list of holidays. + * + * IMPORTANT: This function can't do the normalization, because it doesn't + * know the weekmask. You must call 'normalize_holiday_list' + * on the result before using it. + */ +NPY_NO_EXPORT int +PyArray_HolidaysConverter(PyObject *dates_in, npy_holidayslist *holidays) +{ + PyArrayObject *dates = NULL; + PyArray_Descr *date_dtype = NULL; + npy_intp count; + + /* Make 'dates' into an array */ + if (PyArray_Check(dates_in)) { + dates = (PyArrayObject *)dates_in; + Py_INCREF(dates); + } + else { + PyArray_Descr *datetime_dtype; + + /* Use the datetime dtype with generic units so it fills it in */ + datetime_dtype = PyArray_DescrFromType(NPY_DATETIME); + if (datetime_dtype == NULL) { + goto fail; + } + + /* This steals the datetime_dtype reference */ + dates = (PyArrayObject *)PyArray_FromAny(dates_in, datetime_dtype, + 0, 0, 0, dates_in); + if (dates == NULL) { + goto fail; + } + } + + date_dtype = create_datetime_dtype_with_unit(NPY_DATETIME, NPY_FR_D); + if (date_dtype == NULL) { + goto fail; + } + + if (!PyArray_CanCastTypeTo(PyArray_DESCR(dates), + date_dtype, NPY_SAFE_CASTING)) { + PyErr_SetString(PyExc_ValueError, "Cannot safely convert " + "provided holidays input into an array of dates"); + goto fail; + } + if (PyArray_NDIM(dates) != 1) { + PyErr_SetString(PyExc_ValueError, "holidays must be a provided " + "as a one-dimensional array"); + goto fail; + } + + /* Allocate the memory for the dates */ + count = PyArray_DIM(dates, 0); + holidays->begin = PyArray_malloc(sizeof(npy_datetime) * count); + if (holidays->begin == NULL) { + PyErr_NoMemory(); + goto fail; + } + holidays->end = holidays->begin + count; + + /* Cast the data into a raw date array */ + if (PyArray_CastRawArrays(count, + PyArray_BYTES(dates), (char *)holidays->begin, + PyArray_STRIDE(dates, 0), sizeof(npy_datetime), + PyArray_DESCR(dates), date_dtype, + 0) != NPY_SUCCEED) { + goto fail; + } + + Py_DECREF(dates); + Py_DECREF(date_dtype); + + return 1; + +fail: + Py_XDECREF(dates); + Py_XDECREF(date_dtype); + return 0; +} + +static PyObject * +busdaycalendar_new(PyTypeObject *subtype, + PyObject *NPY_UNUSED(args), PyObject *NPY_UNUSED(kwds)) +{ + NpyBusDayCalendar *self; + + self = (NpyBusDayCalendar *)subtype->tp_alloc(subtype, 0); + if (self != NULL) { + /* Start with an empty holidays list */ + self->holidays.begin = NULL; + self->holidays.end = NULL; + + /* Set the weekmask to the default */ + self->busdays_in_weekmask = 5; + self->weekmask[0] = 1; + self->weekmask[1] = 1; + self->weekmask[2] = 1; + self->weekmask[3] = 1; + self->weekmask[4] = 1; + self->weekmask[5] = 0; + self->weekmask[6] = 0; + } + + return (PyObject *)self; +} + +static int +busdaycalendar_init(NpyBusDayCalendar *self, PyObject *args, PyObject *kwds) +{ + static char *kwlist[] = {"weekmask", "holidays", NULL}; + int i, busdays_in_weekmask; + + /* Clear the holidays if necessary */ + if (self->holidays.begin != NULL) { + PyArray_free(self->holidays.begin); + self->holidays.begin = NULL; + self->holidays.end = NULL; + } + + /* Reset the weekmask to the default */ + self->busdays_in_weekmask = 5; + self->weekmask[0] = 1; + self->weekmask[1] = 1; + self->weekmask[2] = 1; + self->weekmask[3] = 1; + self->weekmask[4] = 1; + self->weekmask[5] = 0; + self->weekmask[6] = 0; + + /* Parse the parameters */ + if (!PyArg_ParseTupleAndKeywords(args, kwds, + "|O&O&:busdaycal", kwlist, + &PyArray_WeekMaskConverter, &self->weekmask[0], + &PyArray_HolidaysConverter, &self->holidays)) { + return -1; + } + + /* Count the number of business days in a week */ + busdays_in_weekmask = 0; + for (i = 0; i < 7; ++i) { + busdays_in_weekmask += self->weekmask[i]; + } + self->busdays_in_weekmask = busdays_in_weekmask; + + /* Normalize the holidays list */ + normalize_holidays_list(&self->holidays, self->weekmask); + + if (self->busdays_in_weekmask == 0) { + PyErr_SetString(PyExc_ValueError, + "Cannot construct a numpy.busdaycal with a weekmask of " + "all zeros"); + return -1; + } + + return 0; +} + +static void +busdaycalendar_dealloc(NpyBusDayCalendar *self) +{ + /* Clear the holidays */ + if (self->holidays.begin != NULL) { + PyArray_free(self->holidays.begin); + self->holidays.begin = NULL; + self->holidays.end = NULL; + } + + Py_TYPE(self)->tp_free((PyObject*)self); +} + +static PyObject * +busdaycalendar_weekmask_get(NpyBusDayCalendar *self) +{ + PyArrayObject *ret; + npy_intp size = 7; + + /* Allocate a 7-element boolean array */ + ret = (PyArrayObject *)PyArray_SimpleNew(1, &size, NPY_BOOL); + if (ret == NULL) { + return NULL; + } + + /* Copy the weekmask data */ + memcpy(PyArray_DATA(ret), self->weekmask, 7); + + return (PyObject *)ret; +} + +static PyObject * +busdaycalendar_holidays_get(NpyBusDayCalendar *self) +{ + PyArrayObject *ret; + PyArray_Descr *date_dtype; + npy_intp size = self->holidays.end - self->holidays.begin; + + /* Create a date dtype */ + date_dtype = create_datetime_dtype_with_unit(NPY_DATETIME, NPY_FR_D); + if (date_dtype == NULL) { + return NULL; + } + + /* Allocate a date array (this steals the date_dtype reference) */ + ret = (PyArrayObject *)PyArray_SimpleNewFromDescr(1, &size, date_dtype); + if (ret == NULL) { + return NULL; + } + + /* Copy the holidays */ + if (size > 0) { + memcpy(PyArray_DATA(ret), self->holidays.begin, + size * sizeof(npy_datetime)); + } + + return (PyObject *)ret; +} + +static PyGetSetDef busdaycalendar_getsets[] = { + {"weekmask", + (getter)busdaycalendar_weekmask_get, + NULL, NULL, NULL}, + {"holidays", + (getter)busdaycalendar_holidays_get, + NULL, NULL, NULL}, + + {NULL, NULL, NULL, NULL, NULL} +}; + +NPY_NO_EXPORT PyTypeObject NpyBusDayCalendar_Type = { +#if defined(NPY_PY3K) + PyVarObject_HEAD_INIT(NULL, 0) +#else + PyObject_HEAD_INIT(NULL) + 0, /* ob_size */ +#endif + "numpy.busdaycalendar", /* tp_name */ + sizeof(NpyBusDayCalendar), /* tp_basicsize */ + 0, /* tp_itemsize */ + /* methods */ + (destructor)busdaycalendar_dealloc, /* tp_dealloc */ + 0, /* tp_print */ + 0, /* tp_getattr */ + 0, /* tp_setattr */ +#if defined(NPY_PY3K) + 0, /* tp_reserved */ +#else + 0, /* tp_compare */ +#endif + 0, /* tp_repr */ + 0, /* tp_as_number */ + 0, /* tp_as_sequence */ + 0, /* tp_as_mapping */ + 0, /* tp_hash */ + 0, /* tp_call */ + 0, /* tp_str */ + 0, /* tp_getattro */ + 0, /* tp_setattro */ + 0, /* tp_as_buffer */ + Py_TPFLAGS_DEFAULT, /* tp_flags */ + 0, /* tp_doc */ + 0, /* tp_traverse */ + 0, /* tp_clear */ + 0, /* tp_richcompare */ + 0, /* tp_weaklistoffset */ + 0, /* tp_iter */ + 0, /* tp_iternext */ + 0, /* tp_methods */ + 0, /* tp_members */ + busdaycalendar_getsets, /* tp_getset */ + 0, /* tp_base */ + 0, /* tp_dict */ + 0, /* tp_descr_get */ + 0, /* tp_descr_set */ + 0, /* tp_dictoffset */ + (initproc)busdaycalendar_init, /* tp_init */ + 0, /* tp_alloc */ + busdaycalendar_new, /* tp_new */ + 0, /* tp_free */ + 0, /* tp_is_gc */ + 0, /* tp_bases */ + 0, /* tp_mro */ + 0, /* tp_cache */ + 0, /* tp_subclasses */ + 0, /* tp_weaklist */ + 0, /* tp_del */ +#if PY_VERSION_HEX >= 0x02060000 + 0, /* tp_version_tag */ +#endif +}; + diff --git a/numpy/core/src/multiarray/datetime_busdaycal.h b/numpy/core/src/multiarray/datetime_busdaycal.h new file mode 100644 index 000000000000..8401f38b6fe8 --- /dev/null +++ b/numpy/core/src/multiarray/datetime_busdaycal.h @@ -0,0 +1,61 @@ +#ifndef _NPY_PRIVATE__DATETIME_BUSDAYDEF_H_ +#define _NPY_PRIVATE__DATETIME_BUSDAYDEF_H_ + +/* + * A list of holidays, which should be sorted, not contain any + * duplicates or NaTs, and not include any days already excluded + * by the associated weekmask. + * + * The data is manually managed with PyArray_malloc/PyArray_free. + */ +typedef struct { + npy_datetime *begin, *end; +} npy_holidayslist; + +/* + * This object encapsulates a weekmask and normalized holidays list, + * so that the business day API can use this data without having + * to normalize it repeatedly. All the data of this object is private + * and cannot be modified from Python. Copies are made when giving + * the weekmask and holidays data to Python code. + */ +typedef struct { + PyObject_HEAD + npy_holidayslist holidays; + int busdays_in_weekmask; + npy_bool weekmask[7]; +} NpyBusDayCalendar; + +NPY_NO_EXPORT PyTypeObject NpyBusDayCalendar_Type; + +/* + * Converts a Python input into a 7-element weekmask, where 0 means + * weekend and 1 means business day. + */ +NPY_NO_EXPORT int +PyArray_WeekMaskConverter(PyObject *weekmask_in, npy_bool *weekmask); + +/* + * Sorts the the array of dates provided in place and removes + * NaT, duplicates and any date which is already excluded on account + * of the weekmask. + * + * Returns the number of dates left after removing weekmask-excluded + * dates. + */ +NPY_NO_EXPORT void +normalize_holidays_list(npy_holidayslist *holidays, npy_bool *weekmask); + +/* + * Converts a Python input into a non-normalized list of holidays. + * + * IMPORTANT: This function can't do the normalization, because it doesn't + * know the weekmask. You must call 'normalize_holiday_list' + * on the result before using it. + */ +NPY_NO_EXPORT int +PyArray_HolidaysConverter(PyObject *dates_in, npy_holidayslist *holidays); + + + +#endif diff --git a/numpy/core/src/multiarray/descriptor.c b/numpy/core/src/multiarray/descriptor.c index 1fe8f748da34..6016917115c4 100644 --- a/numpy/core/src/multiarray/descriptor.c +++ b/numpy/core/src/multiarray/descriptor.c @@ -930,24 +930,27 @@ PyArray_DescrConverter2(PyObject *obj, PyArray_Descr **at) * * This is the central code that converts Python objects to * Type-descriptor objects that are used throughout numpy. - * new reference in *at + * + * Returns a new reference in *at, but the returned should not be + * modified as it may be one of the canonical immutable objects or + * a reference to the input obj. */ NPY_NO_EXPORT int PyArray_DescrConverter(PyObject *obj, PyArray_Descr **at) { - char *type; int check_num = PyArray_NOTYPE + 10; - int len; PyObject *item; int elsize = 0; char endian = '='; *at = NULL; + /* default */ if (obj == Py_None) { *at = PyArray_DescrFromType(PyArray_DEFAULT); return PY_SUCCEED; } + if (PyArray_DescrCheck(obj)) { *at = (PyArray_Descr *)obj; Py_INCREF(*at); @@ -957,50 +960,43 @@ PyArray_DescrConverter(PyObject *obj, PyArray_Descr **at) if (PyType_Check(obj)) { if (PyType_IsSubtype((PyTypeObject *)obj, &PyGenericArrType_Type)) { *at = PyArray_DescrFromTypeObject(obj); - if (*at) { - return PY_SUCCEED; - } - else { - return PY_FAIL; - } + return (*at) ? PY_SUCCEED : PY_FAIL; } - check_num = PyArray_OBJECT; + check_num = NPY_OBJECT; #if !defined(NPY_PY3K) if (obj == (PyObject *)(&PyInt_Type)) { - check_num = PyArray_LONG; + check_num = NPY_LONG; } else if (obj == (PyObject *)(&PyLong_Type)) { - check_num = PyArray_LONGLONG; + check_num = NPY_LONGLONG; } #else if (obj == (PyObject *)(&PyLong_Type)) { - check_num = PyArray_LONG; + check_num = NPY_LONG; } #endif else if (obj == (PyObject *)(&PyFloat_Type)) { - check_num = PyArray_DOUBLE; + check_num = NPY_DOUBLE; } else if (obj == (PyObject *)(&PyComplex_Type)) { - check_num = PyArray_CDOUBLE; + check_num = NPY_CDOUBLE; } else if (obj == (PyObject *)(&PyBool_Type)) { - check_num = PyArray_BOOL; + check_num = NPY_BOOL; } else if (obj == (PyObject *)(&PyBytes_Type)) { - check_num = PyArray_STRING; + check_num = NPY_STRING; } else if (obj == (PyObject *)(&PyUnicode_Type)) { - check_num = PyArray_UNICODE; + check_num = NPY_UNICODE; } #if defined(NPY_PY3K) else if (obj == (PyObject *)(&PyMemoryView_Type)) { - check_num = PyArray_VOID; - } #else else if (obj == (PyObject *)(&PyBuffer_Type)) { - check_num = PyArray_VOID; - } #endif + check_num = NPY_VOID; + } else { *at = _arraydescr_fromobj(obj); if (*at) { @@ -1026,61 +1022,102 @@ PyArray_DescrConverter(PyObject *obj, PyArray_Descr **at) } if (PyBytes_Check(obj)) { + char *type = NULL; + Py_ssize_t len = 0; + /* Check for a string typecode. */ - type = PyBytes_AS_STRING(obj); - len = PyBytes_GET_SIZE(obj); - if (len <= 0) { - goto fail; + if (PyBytes_AsStringAndSize(obj, &type, &len) < 0) { + goto error; } - /* check for datetime format */ - if (is_datetime_typestr(type, len)) { - *at = parse_dtype_from_datetime_typestr(type, len); - return (*at) ? PY_SUCCEED : PY_FAIL; + + /* Empty string is invalid */ + if (len == 0) { + goto fail; } + /* check for commas present or first (or second) element a digit */ if (_check_for_commastring(type, len)) { *at = _convert_from_commastring(obj, 0); return (*at) ? PY_SUCCEED : PY_FAIL; } - check_num = (int) type[0]; - if ((char) check_num == '>' - || (char) check_num == '<' - || (char) check_num == '|' - || (char) check_num == '=') { - if (len <= 1) { - goto fail; - } - endian = (char) check_num; - type++; len--; - check_num = (int) type[0]; - if (endian == '|') { + + /* Process the endian character */ + switch (type[0]) { + case '>': + case '<': + case '=': + endian = type[0]; + ++type; + --len; + break; + + case '|': endian = '='; - } + ++type; + --len; + break; } - if (len > 1) { - elsize = atoi(type + 1); - if (elsize == 0) { - check_num = PyArray_NOTYPE+10; - } - /* - * When specifying length of UNICODE - * the number of characters is given to match - * the STRING interface. Each character can be - * more than one byte and itemsize must be - * the number of bytes. - */ - else if (check_num == PyArray_UNICODELTR) { - elsize <<= 2; - } - /* Support for generic processing c4, i4, f8, etc...*/ - else if ((check_num != PyArray_STRINGLTR) - && (check_num != PyArray_VOIDLTR) - && (check_num != PyArray_STRINGLTR2)) { - check_num = PyArray_TypestrConvert(elsize, check_num); - if (check_num == PyArray_NOTYPE) { - check_num += 10; + + /* Just an endian character is invalid */ + if (len == 0) { + goto fail; + } + + /* Check for datetime format */ + if (is_datetime_typestr(type, len)) { + *at = parse_dtype_from_datetime_typestr(type, len); + return (*at) ? PY_SUCCEED : PY_FAIL; + } + + /* A typecode like 'd' */ + if (len == 1) { + check_num = type[0]; + } + /* A kind + size like 'f8' */ + else { + char *typeend = NULL; + int kind; + + /* Parse the integer, make sure it's the rest of the string */ + elsize = (int)strtol(type + 1, &typeend, 10); + if (typeend - type == len) { + + kind = type[0]; + switch (kind) { + case NPY_STRINGLTR: + case NPY_STRINGLTR2: + check_num = NPY_STRING; + break; + + /* + * When specifying length of UNICODE + * the number of characters is given to match + * the STRING interface. Each character can be + * more than one byte and itemsize must be + * the number of bytes. + */ + case NPY_UNICODELTR: + check_num = NPY_UNICODE; + elsize <<= 2; + break; + + case NPY_VOIDLTR: + check_num = NPY_VOID; + break; + + default: + if (elsize == 0) { + check_num = NPY_NOTYPE+10; + } + /* Support for generic processing c8, i4, f8, etc...*/ + else { + check_num = PyArray_TypestrConvert(elsize, kind); + if (check_num == NPY_NOTYPE) { + check_num += 10; + } + elsize = 0; + } } - elsize = 0; } } } @@ -1133,12 +1170,8 @@ PyArray_DescrConverter(PyObject *obj, PyArray_Descr **at) if (PyErr_Occurred()) { goto fail; } - /* if (check_num == PyArray_NOTYPE) { - return PY_FAIL; - } - */ - finish: +finish: if ((check_num == PyArray_NOTYPE + 10) || (*at = PyArray_DescrFromType(check_num)) == NULL) { PyErr_Clear(); @@ -1176,7 +1209,7 @@ PyArray_DescrConverter(PyObject *obj, PyArray_Descr **at) } return PY_SUCCEED; - fail: +fail: if (PyBytes_Check(obj)) { PyErr_Format(PyExc_TypeError, "data type \"%s\" not understood", PyBytes_AS_STRING(obj)); @@ -1184,6 +1217,8 @@ PyArray_DescrConverter(PyObject *obj, PyArray_Descr **at) else { PyErr_SetString(PyExc_TypeError, "data type not understood"); } + +error: *at = NULL; return PY_FAIL; } diff --git a/numpy/core/src/multiarray/dtype_transfer.c b/numpy/core/src/multiarray/dtype_transfer.c index 14a6c9d6a9a9..fa5573ad54f6 100644 --- a/numpy/core/src/multiarray/dtype_transfer.c +++ b/numpy/core/src/multiarray/dtype_transfer.c @@ -708,8 +708,9 @@ typedef struct { /* The number of events in the source and destination */ int src_events, dst_events; /* - * The metadata for when dealing with Months, Years, or - * Business Days (all of which behave non-linearly). + * The metadata for when dealing with Months or Years + * which behave non-linearly with respect to the other + * units. */ PyArray_DatetimeMetaData src_meta, dst_meta; } _strided_datetime_cast_data; @@ -868,16 +869,6 @@ get_nbo_cast_datetime_transfer_function(int aligned, get_datetime_conversion_factor(src_meta, dst_meta, &num, &denom); if (num == 0) { - PyObject *errmsg; - errmsg = PyUString_FromString("Integer overflow " - "getting a conversion factor between types "); - PyUString_ConcatAndDel(&errmsg, - PyObject_Repr((PyObject *)src_dtype)); - PyUString_ConcatAndDel(&errmsg, - PyUString_FromString(" and ")); - PyUString_ConcatAndDel(&errmsg, - PyObject_Repr((PyObject *)dst_dtype)); - PyErr_SetObject(PyExc_OverflowError, errmsg); return NPY_FAIL; } @@ -899,16 +890,14 @@ get_nbo_cast_datetime_transfer_function(int aligned, /* * Special case the datetime (but not timedelta) with the nonlinear - * units (years, months, business days). For timedelta, an average + * units (years and months). For timedelta, an average * years and months value is used. */ if (src_dtype->type_num == NPY_DATETIME && (src_meta->base == NPY_FR_Y || src_meta->base == NPY_FR_M || - src_meta->base == NPY_FR_B || dst_meta->base == NPY_FR_Y || - dst_meta->base == NPY_FR_M || - dst_meta->base == NPY_FR_B)) { + dst_meta->base == NPY_FR_M)) { memcpy(&data->src_meta, src_meta, sizeof(data->src_meta)); memcpy(&data->dst_meta, dst_meta, sizeof(data->dst_meta)); *out_stransfer = &_strided_to_strided_datetime_general_cast; @@ -3245,3 +3234,52 @@ PyArray_GetDTypeTransferFunction(int aligned, out_stransfer, out_transferdata, out_needs_api); } + +NPY_NO_EXPORT int +PyArray_CastRawArrays(npy_intp count, + char *src, char *dst, + npy_intp src_stride, npy_intp dst_stride, + PyArray_Descr *src_dtype, PyArray_Descr *dst_dtype, + int move_references) +{ + PyArray_StridedTransferFn *stransfer = NULL; + void *transferdata = NULL; + int aligned = 1, needs_api = 0; + + /* Make sure the copy is reasonable */ + if (dst_stride == 0 && count > 1) { + PyErr_SetString(PyExc_ValueError, + "NumPy CastRawArrays cannot do a reduction"); + return NPY_FAIL; + } + else if (count == 0) { + return NPY_SUCCEED; + } + + /* Check data alignment */ + aligned = (((npy_intp)src_dtype | src_stride) & + (src_dtype->alignment - 1)) == 0 && + (((npy_intp)dst_dtype | dst_stride) & + (dst_dtype->alignment - 1)) == 0; + + /* Get the function to do the casting */ + if (PyArray_GetDTypeTransferFunction(aligned, + src_stride, dst_stride, + src_dtype, dst_dtype, + move_references, + &stransfer, &transferdata, + &needs_api) != NPY_SUCCEED) { + return NPY_FAIL; + } + + /* Cast */ + stransfer(dst, dst_stride, src, src_stride, count, + src_dtype->elsize, transferdata); + + /* Cleanup */ + PyArray_FreeStridedTransferData(transferdata); + + /* If needs_api was set to 1, it may have raised a Python exception */ + return (needs_api && PyErr_Occurred()) ? NPY_FAIL : NPY_SUCCEED; +} + diff --git a/numpy/core/src/multiarray/multiarraymodule.c b/numpy/core/src/multiarray/multiarraymodule.c index f1103ef7f97e..3b136e072c00 100644 --- a/numpy/core/src/multiarray/multiarraymodule.c +++ b/numpy/core/src/multiarray/multiarraymodule.c @@ -45,6 +45,8 @@ NPY_NO_EXPORT int NPY_NUMUSERTYPES = 0; #include "convert_datatype.h" #include "nditer_pywrap.h" #include "_datetime.h" +#include "datetime_busday.h" +#include "datetime_busdaycal.h" /* Only here for API compatibility */ NPY_NO_EXPORT PyTypeObject PyBigArray_Type; @@ -3590,12 +3592,23 @@ static struct PyMethodDef array_module_methods[] = { {"result_type", (PyCFunction)array_result_type, METH_VARARGS, NULL}, + /* Datetime-related functions */ {"datetime_data", (PyCFunction)array_datetime_data, METH_VARARGS, NULL}, {"datetime_as_string", (PyCFunction)array_datetime_as_string, METH_VARARGS | METH_KEYWORDS, NULL}, + /* Datetime business-day API */ + {"busday_offset", + (PyCFunction)array_busday_offset, + METH_VARARGS | METH_KEYWORDS, NULL}, + {"busday_count", + (PyCFunction)array_busday_count, + METH_VARARGS | METH_KEYWORDS, NULL}, + {"is_busday", + (PyCFunction)array_is_busday, + METH_VARARGS | METH_KEYWORDS, NULL}, #if !defined(NPY_PY3K) {"newbuffer", (PyCFunction)new_buffer, @@ -3909,6 +3922,10 @@ PyMODINIT_FUNC initmultiarray(void) { if (PyType_Ready(&PyArrayFlags_Type) < 0) { return RETVAL; } + NpyBusDayCalendar_Type.tp_new = PyType_GenericNew; + if (PyType_Ready(&NpyBusDayCalendar_Type) < 0) { + return RETVAL; + } /* FIXME * There is no error handling here */ @@ -3988,6 +4005,11 @@ PyMODINIT_FUNC initmultiarray(void) { Py_INCREF(&PyArrayFlags_Type); PyDict_SetItemString(d, "flagsobj", (PyObject *)&PyArrayFlags_Type); + /* Business day calendar object */ + Py_INCREF(&NpyBusDayCalendar_Type); + PyDict_SetItemString(d, "busdaycalendar", + (PyObject *)&NpyBusDayCalendar_Type); + set_flaginfo(d); if (set_typeinfo(d) != 0) { diff --git a/numpy/core/src/multiarray/multiarraymodule_onefile.c b/numpy/core/src/multiarray/multiarraymodule_onefile.c index 8cf50d9be48f..4459e6b4ccc2 100644 --- a/numpy/core/src/multiarray/multiarraymodule_onefile.c +++ b/numpy/core/src/multiarray/multiarraymodule_onefile.c @@ -11,6 +11,8 @@ #include "scalarapi.c" #include "datetime.c" +#include "datetime_busday.c" +#include "datetime_busdaycal.c" #include "arraytypes.c" #include "hashdescr.c" diff --git a/numpy/core/src/multiarray/nditer.c.src b/numpy/core/src/multiarray/nditer.c.src index 676d9496918c..3d7be4137d43 100644 --- a/numpy/core/src/multiarray/nditer.c.src +++ b/numpy/core/src/multiarray/nditer.c.src @@ -3342,11 +3342,20 @@ npyiter_check_casting(int nop, PyArrayObject **op, !PyArray_CanCastArrayTo(op[iop], op_dtype[iop], casting)) { - PyErr_Format(PyExc_TypeError, - "Iterator operand %d dtype could not be cast " - "to the requested dtype, according to " - "the casting rule given, %s", (int)iop, - npyiter_casting_to_string(casting)); + PyObject *errmsg; + errmsg = PyUString_FromFormat( + "Iterator operand %d dtype could not be cast from ", + (int)iop); + PyUString_ConcatAndDel(&errmsg, + PyObject_Repr((PyObject *)PyArray_DESCR(op[iop]))); + PyUString_ConcatAndDel(&errmsg, + PyUString_FromString(" to ")); + PyUString_ConcatAndDel(&errmsg, + PyObject_Repr((PyObject *)op_dtype[iop])); + PyUString_ConcatAndDel(&errmsg, + PyUString_FromFormat(" according to the rule %s", + npyiter_casting_to_string(casting))); + PyErr_SetObject(PyExc_TypeError, errmsg); return 0; } /* Check write (temp -> op) casting */ @@ -3354,11 +3363,21 @@ npyiter_check_casting(int nop, PyArrayObject **op, !PyArray_CanCastTypeTo(op_dtype[iop], PyArray_DESCR(op[iop]), casting)) { - PyErr_Format(PyExc_TypeError, - "Iterator requested dtype could not be cast " - "to the operand %d dtype, according to " - "the casting rule given, %s", (int)iop, - npyiter_casting_to_string(casting)); + PyObject *errmsg; + errmsg = PyUString_FromString( + "Iterator requested dtype could not be cast from "); + PyUString_ConcatAndDel(&errmsg, + PyObject_Repr((PyObject *)op_dtype[iop])); + PyUString_ConcatAndDel(&errmsg, + PyUString_FromString(" to ")); + PyUString_ConcatAndDel(&errmsg, + PyObject_Repr((PyObject *)PyArray_DESCR(op[iop]))); + PyUString_ConcatAndDel(&errmsg, + PyUString_FromFormat(", the operand %d dtype, " + "according to the rule %s", + (int)iop, + npyiter_casting_to_string(casting))); + PyErr_SetObject(PyExc_TypeError, errmsg); return 0; } diff --git a/numpy/core/src/multiarray/nditer_pywrap.c b/numpy/core/src/multiarray/nditer_pywrap.c index 5227f9c1e9fe..b89da50fca90 100644 --- a/numpy/core/src/multiarray/nditer_pywrap.c +++ b/numpy/core/src/multiarray/nditer_pywrap.c @@ -2401,7 +2401,7 @@ static PyGetSetDef npyiter_getsets[] = { (getter)npyiter_finished_get, NULL, NULL, NULL}, - {NULL, NULL, NULL, NULL, NULL}, + {NULL, NULL, NULL, NULL, NULL} }; NPY_NO_EXPORT PySequenceMethods npyiter_as_sequence = { diff --git a/numpy/core/src/multiarray/scalartypes.c.src b/numpy/core/src/multiarray/scalartypes.c.src index c024dccccf76..380ec3493d7d 100644 --- a/numpy/core/src/multiarray/scalartypes.c.src +++ b/numpy/core/src/multiarray/scalartypes.c.src @@ -691,7 +691,6 @@ static char *_datetime_verbose_strings[] = { "years", "months", "weeks", - "business days", "days", "hours", "minutes", @@ -701,7 +700,8 @@ static char *_datetime_verbose_strings[] = { "nanoseconds", "picoseconds", "femtoseconds", - "attoseconds" + "attoseconds", + "generic time units" }; static PyObject * @@ -2492,6 +2492,7 @@ finish: * #name = datetime, timedelta# * #Name = Datetime, Timedelta# * #NAME = DATETIME, TIMEDELTA# + * #is_datetime = 1, 0# */ static PyObject * @@ -2521,50 +2522,31 @@ static PyObject * } } else { - ret->obmeta.base = NPY_DATETIME_DEFAULTUNIT; - ret->obmeta.num = 1; - ret->obmeta.events = 1; + /* + * A unit of -1 signals that convert_pyobject_to_datetime + * should populate. + */ + ret->obmeta.base = -1; } if (obj == NULL) { - ret->obval = 0; - } - /* - * If the metadata was unspecified and the input is a datetime/timedelta - * scalar, then copy the input's value and metadata exactly. - */ - else if (meta_obj == NULL && PyArray_IsScalar(obj, @Name@)) { - ret->obval = ((Py@Name@ScalarObject *)obj)->obval; - ret->obmeta = ((Py@Name@ScalarObject *)obj)->obmeta; - } - /* - * If the metadata was unspecified and the input is a datetime/timedelta - * zero-dimensional array, then copy the input's value and metadata - * directly. - */ - else if (meta_obj == NULL && - PyArray_Check(obj) && - PyArray_NDIM(obj) == 0 && - PyArray_DESCR(obj)->type_num == NPY_@NAME@) { - PyArray_DatetimeMetaData *meta; - - meta = get_datetime_metadata_from_dtype(PyArray_DESCR(obj)); - if (meta == NULL) { - Py_DECREF(ret); - return NULL; + if (ret->obmeta.base == -1) { + ret->obmeta.base = NPY_DATETIME_DEFAULTUNIT; + ret->obmeta.num = 1; + ret->obmeta.events = 1; } - PyArray_DESCR(obj)->f->copyswap(&ret->obval, - PyArray_DATA(obj), - !PyArray_ISNOTSWAPPED(obj), - obj); - ret->obmeta = *meta; + + /* Make datetime default to NaT, timedelta default to zero */ +#if @is_datetime@ + ret->obval = NPY_DATETIME_NAT; +#else + ret->obval = 0; +#endif } - else { - if (convert_pyobject_to_@name@(&ret->obmeta, obj, &ret->obval) + else if (convert_pyobject_to_@name@(&ret->obmeta, obj, &ret->obval) < 0) { - Py_DECREF(ret); - return NULL; - } + Py_DECREF(ret); + return NULL; } return (PyObject *)ret; diff --git a/numpy/core/src/private/lowlevel_strided_loops.h b/numpy/core/src/private/lowlevel_strided_loops.h index 5fc42bc40616..79b8f6fd7a20 100644 --- a/numpy/core/src/private/lowlevel_strided_loops.h +++ b/numpy/core/src/private/lowlevel_strided_loops.h @@ -173,6 +173,19 @@ PyArray_GetDTypeTransferFunction(int aligned, void **out_transferdata, int *out_needs_api); +/* + * Casts the specified number of elements from 'src' with data type + * 'src_dtype' to 'dst' with 'dst_dtype'. See + * PyArray_GetDTypeTransferFunction for more details. + * + * returns NPY_SUCCEED or NPY_FAIL. + */ +NPY_NO_EXPORT int +PyArray_CastRawArrays(npy_intp count, + char *src, char *dst, + npy_intp src_stride, npy_intp dst_stride, + PyArray_Descr *src_dtype, PyArray_Descr *dst_dtype, + int move_references); /* * These two functions copy or convert the data of an n-dimensional array * to/from a 1-dimensional strided buffer. These functions will only call diff --git a/numpy/core/src/umath/loops.c.src b/numpy/core/src/umath/loops.c.src index 31345ff4539c..b23f1018c815 100644 --- a/numpy/core/src/umath/loops.c.src +++ b/numpy/core/src/umath/loops.c.src @@ -1229,6 +1229,21 @@ TIMEDELTA_md_m_divide(char **args, intp *dimensions, intp *steps, void *NPY_UNUS } } +NPY_NO_EXPORT void +TIMEDELTA_mm_d_divide(char **args, intp *dimensions, intp *steps, void *NPY_UNUSED(func)) +{ + BINARY_LOOP { + const npy_timedelta in1 = *(npy_timedelta *)ip1; + const npy_timedelta in2 = *(npy_timedelta *)ip2; + if (in1 == NPY_DATETIME_NAT || in2 == NPY_DATETIME_NAT) { + *((double *)op1) = NPY_NAN; + } + else { + *((double *)op1) = (double)in1 / (double)in2; + } + } +} + /* ***************************************************************************** ** FLOAT LOOPS ** diff --git a/numpy/core/src/umath/loops.h b/numpy/core/src/umath/loops.h index 2a827e168aa0..2a792bf5b429 100644 --- a/numpy/core/src/umath/loops.h +++ b/numpy/core/src/umath/loops.h @@ -2661,6 +2661,9 @@ TIMEDELTA_mq_m_divide(char **args, intp *dimensions, intp *steps, void *NPY_UNUS NPY_NO_EXPORT void TIMEDELTA_md_m_divide(char **args, intp *dimensions, intp *steps, void *NPY_UNUSED(func)); +NPY_NO_EXPORT void +TIMEDELTA_mm_d_divide(char **args, intp *dimensions, intp *steps, void *NPY_UNUSED(func)); + /* Special case equivalents to above functions */ #define TIMEDELTA_mq_m_true_divide TIMEDELTA_mq_m_divide @@ -2678,27 +2681,27 @@ TIMEDELTA_md_m_divide(char **args, intp *dimensions, intp *steps, void *NPY_UNUS ***************************************************************************** */ -#line 511 +#line 514 NPY_NO_EXPORT void OBJECT_equal(char **args, intp *dimensions, intp *steps, void *NPY_UNUSED(func)); -#line 511 +#line 514 NPY_NO_EXPORT void OBJECT_not_equal(char **args, intp *dimensions, intp *steps, void *NPY_UNUSED(func)); -#line 511 +#line 514 NPY_NO_EXPORT void OBJECT_greater(char **args, intp *dimensions, intp *steps, void *NPY_UNUSED(func)); -#line 511 +#line 514 NPY_NO_EXPORT void OBJECT_greater_equal(char **args, intp *dimensions, intp *steps, void *NPY_UNUSED(func)); -#line 511 +#line 514 NPY_NO_EXPORT void OBJECT_less(char **args, intp *dimensions, intp *steps, void *NPY_UNUSED(func)); -#line 511 +#line 514 NPY_NO_EXPORT void OBJECT_less_equal(char **args, intp *dimensions, intp *steps, void *NPY_UNUSED(func)); diff --git a/numpy/core/src/umath/loops.h.src b/numpy/core/src/umath/loops.h.src index bf41e41871f3..fdedc1933390 100644 --- a/numpy/core/src/umath/loops.h.src +++ b/numpy/core/src/umath/loops.h.src @@ -487,6 +487,9 @@ TIMEDELTA_mq_m_divide(char **args, intp *dimensions, intp *steps, void *NPY_UNUS NPY_NO_EXPORT void TIMEDELTA_md_m_divide(char **args, intp *dimensions, intp *steps, void *NPY_UNUSED(func)); +NPY_NO_EXPORT void +TIMEDELTA_mm_d_divide(char **args, intp *dimensions, intp *steps, void *NPY_UNUSED(func)); + /* Special case equivalents to above functions */ #define TIMEDELTA_mq_m_true_divide TIMEDELTA_mq_m_divide diff --git a/numpy/core/src/umath/ufunc_object.c b/numpy/core/src/umath/ufunc_object.c index 009876bab457..dcc29f2ba797 100644 --- a/numpy/core/src/umath/ufunc_object.c +++ b/numpy/core/src/umath/ufunc_object.c @@ -1705,6 +1705,22 @@ find_specified_ufunc_inner_loop(PyUFuncObject *self, return -1; } +/* + * Returns a new reference to type if it is already NBO, otherwise + * returns a copy converted to NBO. + */ +static PyArray_Descr * +ensure_dtype_nbo(PyArray_Descr *type) +{ + if (PyArray_ISNBO(type->byteorder)) { + Py_INCREF(type); + return type; + } + else { + return PyArray_DescrNewByteorder(type, NPY_NATIVE); + } +} + /*UFUNC_API * * This function applies the default type resolution rules @@ -1827,9 +1843,12 @@ PyUFunc_SimpleBinaryComparisonTypeResolution(PyUFuncObject *ufunc, return -1; } - out_dtypes[0] = (PyArray_Descr *)PyTuple_GET_ITEM(type_tup, 0); + out_dtypes[0] = ensure_dtype_nbo( + (PyArray_Descr *)PyTuple_GET_ITEM(type_tup, 0)); + if (out_dtypes[0] == NULL) { + return -1; + } out_dtypes[1] = out_dtypes[0]; - Py_INCREF(out_dtypes[0]); Py_INCREF(out_dtypes[1]); } @@ -1924,8 +1943,10 @@ PyUFunc_SimpleUnaryOperationTypeResolution(PyUFuncObject *ufunc, if (type_tup == NULL) { /* Input types are the result type */ - out_dtypes[0] = PyArray_DESCR(operands[0]); - Py_INCREF(out_dtypes[0]); + out_dtypes[0] = ensure_dtype_nbo(PyArray_DESCR(operands[0])); + if (out_dtypes[0] == NULL) { + return -1; + } out_dtypes[1] = out_dtypes[0]; Py_INCREF(out_dtypes[1]); } @@ -1945,8 +1966,11 @@ PyUFunc_SimpleUnaryOperationTypeResolution(PyUFuncObject *ufunc, return -1; } - out_dtypes[0] = (PyArray_Descr *)PyTuple_GET_ITEM(type_tup, 0); - Py_INCREF(out_dtypes[0]); + out_dtypes[0] = ensure_dtype_nbo( + (PyArray_Descr *)PyTuple_GET_ITEM(type_tup, 0)); + if (out_dtypes[0] == NULL) { + return -1; + } out_dtypes[1] = out_dtypes[0]; Py_INCREF(out_dtypes[1]); } @@ -2060,8 +2084,11 @@ PyUFunc_SimpleBinaryOperationTypeResolution(PyUFuncObject *ufunc, return -1; } - out_dtypes[0] = (PyArray_Descr *)PyTuple_GET_ITEM(type_tup, 0); - Py_INCREF(out_dtypes[0]); + out_dtypes[0] = ensure_dtype_nbo( + (PyArray_Descr *)PyTuple_GET_ITEM(type_tup, 0)); + if (out_dtypes[0] == NULL) { + return -1; + } out_dtypes[1] = out_dtypes[0]; Py_INCREF(out_dtypes[1]); out_dtypes[2] = out_dtypes[0]; @@ -2233,8 +2260,6 @@ timedelta_dtype_with_copied_meta(PyArray_Descr *dtype) return ret; } - - /* * This function applies the type resolution rules for addition. * In particular, there are a number of special cases with datetime: @@ -2243,8 +2268,8 @@ timedelta_dtype_with_copied_meta(PyArray_Descr *dtype) * int + m8[] => m8[] + m8[] * M8[] + int => M8[] + m8[] * int + M8[] => m8[] + M8[] - * M8[] + m8[] => M8[] + m8[] - * m8[] + M8[] => m8[] + M8[] + * M8[] + m8[] => M8[gcd(,)] + m8[gcd(,)] + * m8[] + M8[] => m8[gcd(,)] + M8[gcd(,)] * TODO: Non-linear time unit cases require highly special-cased loops * M8[] + m8[Y|M|B] * m8[Y|M|B] + M8[] @@ -2287,24 +2312,30 @@ PyUFunc_AdditionTypeResolution(PyUFuncObject *ufunc, out_dtypes[2] = out_dtypes[0]; Py_INCREF(out_dtypes[2]); } - /* m8[] + M8[] => m8[] + M8[] */ + /* m8[] + M8[] => m8[gcd(,)] + M8[gcd(,)] */ else if (type_num2 == NPY_DATETIME) { - /* Make a new NPY_TIMEDELTA, and copy type2's metadata */ - out_dtypes[0] = timedelta_dtype_with_copied_meta( - PyArray_DESCR(operands[1])); + out_dtypes[1] = PyArray_PromoteTypes(PyArray_DESCR(operands[0]), + PyArray_DESCR(operands[1])); + if (out_dtypes[1] == NULL) { + return -1; + } + /* Make a new NPY_TIMEDELTA, and copy the datetime's metadata */ + out_dtypes[0] = timedelta_dtype_with_copied_meta(out_dtypes[1]); if (out_dtypes[0] == NULL) { + Py_DECREF(out_dtypes[1]); + out_dtypes[1] = NULL; return -1; } - out_dtypes[1] = PyArray_DESCR(operands[1]); - Py_INCREF(out_dtypes[1]); out_dtypes[2] = out_dtypes[1]; Py_INCREF(out_dtypes[2]); } /* m8[] + int => m8[] + m8[] */ else if (PyTypeNum_ISINTEGER(type_num2) || PyTypeNum_ISBOOL(type_num2)) { - out_dtypes[0] = PyArray_DESCR(operands[0]); - Py_INCREF(out_dtypes[0]); + out_dtypes[0] = ensure_dtype_nbo(PyArray_DESCR(operands[0])); + if (out_dtypes[0] == NULL) { + return -1; + } out_dtypes[1] = out_dtypes[0]; Py_INCREF(out_dtypes[1]); out_dtypes[2] = out_dtypes[0]; @@ -2317,19 +2348,38 @@ PyUFunc_AdditionTypeResolution(PyUFuncObject *ufunc, } } else if (type_num1 == NPY_DATETIME) { - /* M8[] + m8[] => M8[] + m8[] */ + /* M8[] + m8[] => M8[gcd(,)] + m8[gcd(,)] */ + if (type_num2 == NPY_TIMEDELTA) { + out_dtypes[0] = PyArray_PromoteTypes(PyArray_DESCR(operands[0]), + PyArray_DESCR(operands[1])); + if (out_dtypes[0] == NULL) { + return -1; + } + /* Make a new NPY_TIMEDELTA, and copy the datetime's metadata */ + out_dtypes[1] = timedelta_dtype_with_copied_meta(out_dtypes[0]); + if (out_dtypes[1] == NULL) { + Py_DECREF(out_dtypes[0]); + out_dtypes[0] = NULL; + return -1; + } + out_dtypes[2] = out_dtypes[0]; + Py_INCREF(out_dtypes[2]); + } /* M8[] + int => M8[] + m8[] */ - if (type_num2 == NPY_TIMEDELTA || - PyTypeNum_ISINTEGER(type_num2) || + else if (PyTypeNum_ISINTEGER(type_num2) || PyTypeNum_ISBOOL(type_num2)) { + out_dtypes[0] = ensure_dtype_nbo(PyArray_DESCR(operands[0])); + if (out_dtypes[0] == NULL) { + return -1; + } /* Make a new NPY_TIMEDELTA, and copy type1's metadata */ out_dtypes[1] = timedelta_dtype_with_copied_meta( PyArray_DESCR(operands[0])); if (out_dtypes[1] == NULL) { + Py_DECREF(out_dtypes[0]); + out_dtypes[0] = NULL; return -1; } - out_dtypes[0] = PyArray_DESCR(operands[0]); - Py_INCREF(out_dtypes[0]); out_dtypes[2] = out_dtypes[0]; Py_INCREF(out_dtypes[2]); @@ -2342,8 +2392,10 @@ PyUFunc_AdditionTypeResolution(PyUFuncObject *ufunc, else if (PyTypeNum_ISINTEGER(type_num1) || PyTypeNum_ISBOOL(type_num1)) { /* int + m8[] => m8[] + m8[] */ if (type_num2 == NPY_TIMEDELTA) { - out_dtypes[0] = PyArray_DESCR(operands[1]); - Py_INCREF(out_dtypes[0]); + out_dtypes[0] = ensure_dtype_nbo(PyArray_DESCR(operands[1])); + if (out_dtypes[0] == NULL) { + return -1; + } out_dtypes[1] = out_dtypes[0]; Py_INCREF(out_dtypes[1]); out_dtypes[2] = out_dtypes[0]; @@ -2358,8 +2410,12 @@ PyUFunc_AdditionTypeResolution(PyUFuncObject *ufunc, if (out_dtypes[0] == NULL) { return -1; } - out_dtypes[1] = PyArray_DESCR(operands[1]); - Py_INCREF(out_dtypes[1]); + out_dtypes[1] = ensure_dtype_nbo(PyArray_DESCR(operands[1])); + if (out_dtypes[1] == NULL) { + Py_DECREF(out_dtypes[0]); + out_dtypes[0] = NULL; + return -1; + } out_dtypes[2] = out_dtypes[1]; Py_INCREF(out_dtypes[2]); @@ -2421,7 +2477,7 @@ type_reso_error: { * m8[] - int => m8[] - m8[] * int - m8[] => m8[] - m8[] * M8[] - int => M8[] - m8[] - * M8[] - m8[] => M8[] - m8[] + * M8[] - m8[] => M8[gcd(,)] - m8[gcd(,)] * TODO: Non-linear time unit cases require highly special-cased loops * M8[] - m8[Y|M|B] */ @@ -2466,8 +2522,10 @@ PyUFunc_SubtractionTypeResolution(PyUFuncObject *ufunc, /* m8[] - int => m8[] - m8[] */ else if (PyTypeNum_ISINTEGER(type_num2) || PyTypeNum_ISBOOL(type_num2)) { - out_dtypes[0] = PyArray_DESCR(operands[0]); - Py_INCREF(out_dtypes[0]); + out_dtypes[0] = ensure_dtype_nbo(PyArray_DESCR(operands[0])); + if (out_dtypes[0] == NULL) { + return -1; + } out_dtypes[1] = out_dtypes[0]; Py_INCREF(out_dtypes[1]); out_dtypes[2] = out_dtypes[0]; @@ -2480,57 +2538,58 @@ PyUFunc_SubtractionTypeResolution(PyUFuncObject *ufunc, } } else if (type_num1 == NPY_DATETIME) { - /* M8[] - m8[] => M8[] - m8[] */ + /* M8[] - m8[] => M8[gcd(,)] - m8[gcd(,)] */ + if (type_num2 == NPY_TIMEDELTA) { + out_dtypes[0] = PyArray_PromoteTypes(PyArray_DESCR(operands[0]), + PyArray_DESCR(operands[1])); + if (out_dtypes[0] == NULL) { + return -1; + } + /* Make a new NPY_TIMEDELTA, and copy the datetime's metadata */ + out_dtypes[1] = timedelta_dtype_with_copied_meta(out_dtypes[0]); + if (out_dtypes[1] == NULL) { + Py_DECREF(out_dtypes[0]); + out_dtypes[0] = NULL; + return -1; + } + out_dtypes[2] = out_dtypes[0]; + Py_INCREF(out_dtypes[2]); + } /* M8[] - int => M8[] - m8[] */ - if (type_num2 == NPY_TIMEDELTA || - PyTypeNum_ISINTEGER(type_num2) || + else if (PyTypeNum_ISINTEGER(type_num2) || PyTypeNum_ISBOOL(type_num2)) { + out_dtypes[0] = ensure_dtype_nbo(PyArray_DESCR(operands[0])); + if (out_dtypes[0] == NULL) { + return -1; + } /* Make a new NPY_TIMEDELTA, and copy type1's metadata */ out_dtypes[1] = timedelta_dtype_with_copied_meta( PyArray_DESCR(operands[0])); if (out_dtypes[1] == NULL) { + Py_DECREF(out_dtypes[0]); + out_dtypes[0] = NULL; return -1; } - out_dtypes[0] = PyArray_DESCR(operands[0]); - Py_INCREF(out_dtypes[0]); out_dtypes[2] = out_dtypes[0]; Py_INCREF(out_dtypes[2]); type_num2 = NPY_TIMEDELTA; } - /* M8[] - M8[] (producing m8[])*/ + /* M8[] - M8[] => M8[gcd(,)] - M8[gcd(,)] */ else if (type_num2 == NPY_DATETIME) { - PyArray_DatetimeMetaData *meta1, *meta2; - - meta1 = get_datetime_metadata_from_dtype( - PyArray_DESCR(operands[0])); - if (meta1 == NULL) { + out_dtypes[0] = PyArray_PromoteTypes(PyArray_DESCR(operands[0]), + PyArray_DESCR(operands[1])); + if (out_dtypes[0] == NULL) { return -1; } - meta2 = get_datetime_metadata_from_dtype( - PyArray_DESCR(operands[1])); - if (meta2 == NULL) { + /* Make a new NPY_TIMEDELTA, and copy type1's metadata */ + out_dtypes[2] = timedelta_dtype_with_copied_meta(out_dtypes[0]); + if (out_dtypes[2] == NULL) { + Py_DECREF(out_dtypes[0]); return -1; } - - /* If the metadata matches up, the subtraction is ok */ - if (meta1->num == meta2->num && - meta1->base == meta2->base && - meta1->events == meta2->events) { - out_dtypes[0] = PyArray_DESCR(operands[1]); - Py_INCREF(out_dtypes[0]); - out_dtypes[1] = out_dtypes[0]; - Py_INCREF(out_dtypes[1]); - /* Make a new NPY_TIMEDELTA, and copy type1's metadata */ - out_dtypes[2] = timedelta_dtype_with_copied_meta( - PyArray_DESCR(operands[0])); - if (out_dtypes[2] == NULL) { - return -1; - } - } - else { - goto type_reso_error; - } + out_dtypes[1] = out_dtypes[0]; + Py_INCREF(out_dtypes[1]); } else { goto type_reso_error; @@ -2539,8 +2598,10 @@ PyUFunc_SubtractionTypeResolution(PyUFuncObject *ufunc, else if (PyTypeNum_ISINTEGER(type_num1) || PyTypeNum_ISBOOL(type_num1)) { /* int - m8[] => m8[] - m8[] */ if (type_num2 == NPY_TIMEDELTA) { - out_dtypes[0] = PyArray_DESCR(operands[1]); - Py_INCREF(out_dtypes[0]); + out_dtypes[0] = ensure_dtype_nbo(PyArray_DESCR(operands[1])); + if (out_dtypes[0] == NULL) { + return -1; + } out_dtypes[1] = out_dtypes[0]; Py_INCREF(out_dtypes[1]); out_dtypes[2] = out_dtypes[0]; @@ -2633,8 +2694,10 @@ PyUFunc_MultiplicationTypeResolution(PyUFuncObject *ufunc, if (type_num1 == NPY_TIMEDELTA) { /* m8[] * int## => m8[] * int64 */ if (PyTypeNum_ISINTEGER(type_num2) || PyTypeNum_ISBOOL(type_num2)) { - out_dtypes[0] = PyArray_DESCR(operands[0]); - Py_INCREF(out_dtypes[0]); + out_dtypes[0] = ensure_dtype_nbo(PyArray_DESCR(operands[0])); + if (out_dtypes[0] == NULL) { + return -1; + } out_dtypes[1] = PyArray_DescrNewFromType(NPY_LONGLONG); if (out_dtypes[1] == NULL) { Py_DECREF(out_dtypes[0]); @@ -2648,8 +2711,10 @@ PyUFunc_MultiplicationTypeResolution(PyUFuncObject *ufunc, } /* m8[] * float## => m8[] * float64 */ else if (PyTypeNum_ISFLOAT(type_num2)) { - out_dtypes[0] = PyArray_DESCR(operands[0]); - Py_INCREF(out_dtypes[0]); + out_dtypes[0] = ensure_dtype_nbo(PyArray_DESCR(operands[0])); + if (out_dtypes[0] == NULL) { + return -1; + } out_dtypes[1] = PyArray_DescrNewFromType(NPY_DOUBLE); if (out_dtypes[1] == NULL) { Py_DECREF(out_dtypes[0]); @@ -2672,8 +2737,12 @@ PyUFunc_MultiplicationTypeResolution(PyUFuncObject *ufunc, if (out_dtypes[0] == NULL) { return -1; } - out_dtypes[1] = PyArray_DESCR(operands[1]); - Py_INCREF(out_dtypes[1]); + out_dtypes[1] = ensure_dtype_nbo(PyArray_DESCR(operands[1])); + if (out_dtypes[1] == NULL) { + Py_DECREF(out_dtypes[0]); + out_dtypes[0] = NULL; + return -1; + } out_dtypes[2] = out_dtypes[1]; Py_INCREF(out_dtypes[2]); @@ -2690,8 +2759,12 @@ PyUFunc_MultiplicationTypeResolution(PyUFuncObject *ufunc, if (out_dtypes[0] == NULL) { return -1; } - out_dtypes[1] = PyArray_DESCR(operands[1]); - Py_INCREF(out_dtypes[1]); + out_dtypes[1] = ensure_dtype_nbo(PyArray_DESCR(operands[1])); + if (out_dtypes[1] == NULL) { + Py_DECREF(out_dtypes[0]); + out_dtypes[0] = NULL; + return -1; + } out_dtypes[2] = out_dtypes[1]; Py_INCREF(out_dtypes[2]); @@ -2749,8 +2822,9 @@ type_reso_error: { /* * This function applies the type resolution rules for division. * In particular, there are a number of special cases with datetime: - * m8[] / int## => m8[] / int64 - * m8[] / float## => m8[] / float64 + * m8[] / m8[] to m8[gcd(,)] / m8[gcd(,)] -> float64 + * m8[] / int## to m8[] / int64 -> m8[] + * m8[] / float## to m8[] / float64 -> m8[] */ NPY_NO_EXPORT int PyUFunc_DivisionTypeResolution(PyUFuncObject *ufunc, @@ -2778,11 +2852,34 @@ PyUFunc_DivisionTypeResolution(PyUFuncObject *ufunc, } if (type_num1 == NPY_TIMEDELTA) { + /* + * m8[] / m8[] to + * m8[gcd(,)] / m8[gcd(,)] -> float64 + */ + if (type_num2 == NPY_TIMEDELTA) { + out_dtypes[0] = PyArray_PromoteTypes(PyArray_DESCR(operands[0]), + PyArray_DESCR(operands[1])); + if (out_dtypes[0] == NULL) { + return -1; + } + out_dtypes[1] = out_dtypes[0]; + Py_INCREF(out_dtypes[1]); + out_dtypes[2] = PyArray_DescrFromType(NPY_DOUBLE); + if (out_dtypes[2] == NULL) { + Py_DECREF(out_dtypes[0]); + out_dtypes[0] = NULL; + Py_DECREF(out_dtypes[1]); + out_dtypes[1] = NULL; + return -1; + } + } /* m8[] / int## => m8[] / int64 */ - if (PyTypeNum_ISINTEGER(type_num2)) { - out_dtypes[0] = PyArray_DESCR(operands[0]); - Py_INCREF(out_dtypes[0]); - out_dtypes[1] = PyArray_DescrNewFromType(NPY_LONGLONG); + else if (PyTypeNum_ISINTEGER(type_num2)) { + out_dtypes[0] = ensure_dtype_nbo(PyArray_DESCR(operands[0])); + if (out_dtypes[0] == NULL) { + return -1; + } + out_dtypes[1] = PyArray_DescrFromType(NPY_LONGLONG); if (out_dtypes[1] == NULL) { Py_DECREF(out_dtypes[0]); out_dtypes[0] = NULL; @@ -2795,8 +2892,10 @@ PyUFunc_DivisionTypeResolution(PyUFuncObject *ufunc, } /* m8[] / float## => m8[] / float64 */ else if (PyTypeNum_ISFLOAT(type_num2)) { - out_dtypes[0] = PyArray_DESCR(operands[0]); - Py_INCREF(out_dtypes[0]); + out_dtypes[0] = ensure_dtype_nbo(PyArray_DESCR(operands[0])); + if (out_dtypes[0] == NULL) { + return -1; + } out_dtypes[1] = PyArray_DescrNewFromType(NPY_DOUBLE); if (out_dtypes[1] == NULL) { Py_DECREF(out_dtypes[0]); @@ -4110,8 +4209,24 @@ PyUFunc_ReductionOp(PyUFuncObject *self, PyArrayObject *arr, ndim = PyArray_NDIM(arr); - /* Set up the output data type */ - op_dtypes[0] = PyArray_DescrFromType(otype_final); + /* + * Set up the output data type, using the input's exact + * data type if the type number didn't change to preserve + * metadata + */ + if (PyArray_DESCR(arr)->type_num == otype_final) { + if (PyArray_ISNBO(PyArray_DESCR(arr)->byteorder)) { + op_dtypes[0] = PyArray_DESCR(arr); + Py_INCREF(op_dtypes[0]); + } + else { + op_dtypes[0] = PyArray_DescrNewByteorder(PyArray_DESCR(arr), + NPY_NATIVE); + } + } + else { + op_dtypes[0] = PyArray_DescrFromType(otype_final); + } if (op_dtypes[0] == NULL) { goto fail; } @@ -4761,8 +4876,24 @@ PyUFunc_Reduceat(PyUFuncObject *self, PyArrayObject *arr, PyArrayObject *ind, ndim = PyArray_NDIM(arr); - /* Set up the output data type */ - op_dtypes[0] = PyArray_DescrFromType(otype_final); + /* + * Set up the output data type, using the input's exact + * data type if the type number didn't change to preserve + * metadata + */ + if (PyArray_DESCR(arr)->type_num == otype_final) { + if (PyArray_ISNBO(PyArray_DESCR(arr)->byteorder)) { + op_dtypes[0] = PyArray_DESCR(arr); + Py_INCREF(op_dtypes[0]); + } + else { + op_dtypes[0] = PyArray_DescrNewByteorder(PyArray_DESCR(arr), + NPY_NATIVE); + } + } + else { + op_dtypes[0] = PyArray_DescrFromType(otype_final); + } if (op_dtypes[0] == NULL) { goto fail; } diff --git a/numpy/core/tests/test_arrayprint.py b/numpy/core/tests/test_arrayprint.py index b1dbb1d68622..5588d244afbf 100644 --- a/numpy/core/tests/test_arrayprint.py +++ b/numpy/core/tests/test_arrayprint.py @@ -82,7 +82,7 @@ def _format_function(x): "[. o O]") assert_(np.array2string(x, formatter={'all':lambda x: "%.4f" % x}) == \ "[0.0000 1.0000 2.0000]") - assert_(np.array2string(x, formatter={'int':lambda x: hex(x)}) == \ + assert_equal(np.array2string(x, formatter={'int':lambda x: hex(x)}), \ "[0x0L 0x1L 0x2L]") x = np.arange(3.) diff --git a/numpy/core/tests/test_datetime.py b/numpy/core/tests/test_datetime.py index bb6594a9a95c..c533f3b2c2e3 100644 --- a/numpy/core/tests/test_datetime.py +++ b/numpy/core/tests/test_datetime.py @@ -7,14 +7,32 @@ class TestDateTime(TestCase): def test_datetime_dtype_creation(self): - for unit in ['Y', 'M', 'W', 'B', 'D', + for unit in ['Y', 'M', 'W', 'D', 'h', 'm', 's', 'ms', 'us', 'ns', 'ps', 'fs', 'as']: dt1 = np.dtype('M8[750%s]'%unit) assert_(dt1 == np.dtype('datetime64[750%s]' % unit)) dt2 = np.dtype('m8[%s]' % unit) assert_(dt2 == np.dtype('timedelta64[%s]' % unit)) - + + # Generic units shouldn't add [] to the end + assert_equal(str(np.dtype("M8")), "datetime64") + + # Should be possible to specify the endianness + assert_equal(np.dtype("=M8"), np.dtype("M8")) + assert_equal(np.dtype("=M8[s]"), np.dtype("M8[s]")) + assert_(np.dtype(">M8") == np.dtype("M8") or + np.dtype("M8[D]") == np.dtype("M8[D]") or + np.dtype("m8") == np.dtype("m8") or + np.dtype("m8[D]") == np.dtype("m8[D]") or + np.dtype(" timedelta raises when there is no reasonable gcd assert_raises(TypeError, np.promote_types, np.dtype('m8[Y]'), np.dtype('m8[D]')) - assert_raises(TypeError, np.promote_types, - np.dtype('m8[Y]'), np.dtype('m8[B]')) - assert_raises(TypeError, np.promote_types, - np.dtype('m8[D]'), np.dtype('m8[B]')) assert_raises(TypeError, np.promote_types, np.dtype('m8[M]'), np.dtype('m8[W]')) # timedelta timedelta may overflow with big unit ranges @@ -385,14 +500,10 @@ def test_different_unit_comparison(self): np.array('1932-02-17T00:00:00Z', dtype=dt2)) assert_equal(np.array('10000-04-27', dtype=dt1), np.array('10000-04-27T00:00:00Z', dtype=dt2)) - assert_equal(np.array('today', dtype=dt1), - np.array('today', dtype=dt2)) assert_equal(np.datetime64('1932-02-17', unit1), np.datetime64('1932-02-17T00:00:00Z', unit2)) assert_equal(np.datetime64('10000-04-27', unit1), np.datetime64('10000-04-27T00:00:00Z', unit2)) - assert_equal(np.datetime64('today', unit1), - np.datetime64('today', unit2)) # Shouldn't be able to compare datetime and timedelta # TODO: Changing to 'same_kind' or 'safe' casting in the ufuncs by @@ -444,13 +555,16 @@ def test_datetime_unary(self): assert_equal(np.sign(tdzero), tdzero) assert_equal(np.sign(tda).dtype, tda.dtype) + # The ufuncs always produce native-endian results + assert_ + def test_datetime_add(self): for dta, dtb, dtc, dtnat, tda, tdb, tdc in \ [ # One-dimensional arrays (np.array(['2012-12-21'], dtype='M8[D]'), np.array(['2012-12-24'], dtype='M8[D]'), - np.array(['1940-12-24'], dtype='M8[D]'), + np.array(['2012-12-21T11Z'], dtype='M8[h]'), np.array(['NaT'], dtype='M8[D]'), np.array([3], dtype='m8[D]'), np.array([11], dtype='m8[h]'), @@ -458,7 +572,7 @@ def test_datetime_add(self): # NumPy scalars (np.datetime64('2012-12-21', '[D]'), np.datetime64('2012-12-24', '[D]'), - np.datetime64('1940-12-24', '[D]'), + np.datetime64('2012-12-21T11Z', '[h]'), np.datetime64('NaT', '[D]'), np.timedelta64(3, '[D]'), np.timedelta64(11, '[h]'), @@ -503,27 +617,24 @@ def test_datetime_add(self): assert_equal(tda + dtnat, dtnat) assert_equal((tda + dta).dtype, np.dtype('M8[D]')) - # In M8 + m8, the M8 controls the result type - assert_equal(dta + tdb, dta) - assert_equal((dta + tdb).dtype, np.dtype('M8[D]')) - assert_equal(dtc + tdb, dtc) - assert_equal((dtc + tdb).dtype, np.dtype('M8[D]')) - assert_equal(tdb + dta, dta) - assert_equal((tdb + dta).dtype, np.dtype('M8[D]')) - assert_equal(tdb + dtc, dtc) - assert_equal((tdb + dtc).dtype, np.dtype('M8[D]')) + # In M8 + m8, the result goes to higher precision + assert_equal(dta + tdb, dtc) + assert_equal((dta + tdb).dtype, np.dtype('M8[h]')) + assert_equal(tdb + dta, dtc) + assert_equal((tdb + dta).dtype, np.dtype('M8[h]')) # M8 + M8 assert_raises(TypeError, np.add, dta, dtb) def test_datetime_subtract(self): - for dta, dtb, dtc, dtd, dtnat, tda, tdb, tdc in \ + for dta, dtb, dtc, dtd, dte, dtnat, tda, tdb, tdc in \ [ # One-dimensional arrays (np.array(['2012-12-21'], dtype='M8[D]'), np.array(['2012-12-24'], dtype='M8[D]'), np.array(['1940-12-24'], dtype='M8[D]'), np.array(['1940-12-24'], dtype='M8[h]'), + np.array(['1940-12-23T13Z'], dtype='M8[h]'), np.array(['NaT'], dtype='M8[D]'), np.array([3], dtype='m8[D]'), np.array([11], dtype='m8[h]'), @@ -533,6 +644,7 @@ def test_datetime_subtract(self): np.datetime64('2012-12-24', '[D]'), np.datetime64('1940-12-24', '[D]'), np.datetime64('1940-12-24', '[h]'), + np.datetime64('1940-12-23T13Z', '[h]'), np.datetime64('NaT', '[D]'), np.timedelta64(3, '[D]'), np.timedelta64(11, '[h]'), @@ -567,14 +679,16 @@ def test_datetime_subtract(self): assert_equal(dtnat - tda, dtnat) assert_equal((dtb - tda).dtype, np.dtype('M8[D]')) - # In M8 - m8, the M8 controls the result type - assert_equal(dta - tdb, dta) - assert_equal((dta - tdb).dtype, np.dtype('M8[D]')) - assert_equal(dtc - tdb, dtc) - assert_equal((dtc - tdb).dtype, np.dtype('M8[D]')) + # In M8 - m8, the result goes to higher precision + assert_equal(dtc - tdb, dte) + assert_equal((dtc - tdb).dtype, np.dtype('M8[h]')) + + # M8 - M8 with different goes to higher precision + assert_equal(dtc - dtd, np.timedelta64(0,'h')) + assert_equal((dtc - dtd).dtype, np.dtype('m8[h]')) + assert_equal(dtd - dtc, np.timedelta64(0,'h')) + assert_equal((dtd - dtc).dtype, np.dtype('m8[h]')) - # M8 - M8 with different metadata - assert_raises(TypeError, np.subtract, dtc, dtd) # m8 - M8 assert_raises(TypeError, np.subtract, tda, dta) # bool - M8 @@ -624,31 +738,37 @@ def test_datetime_multiply(self): assert_raises(TypeError, np.multiply, 1.5, dta) def test_datetime_divide(self): - for dta, tda, tdb, tdc in \ + for dta, tda, tdb, tdc, tdd in \ [ # One-dimensional arrays (np.array(['2012-12-21'], dtype='M8[D]'), np.array([6], dtype='m8[h]'), np.array([9], dtype='m8[h]'), - np.array([12], dtype='m8[h]')), + np.array([12], dtype='m8[h]'), + np.array([6], dtype='m8[m]')), # NumPy scalars (np.datetime64('2012-12-21', '[D]'), np.timedelta64(6, '[h]'), np.timedelta64(9, '[h]'), - np.timedelta64(12, '[h]'))]: + np.timedelta64(12, '[h]'), + np.timedelta64(6, '[m]'))]: # m8 / int assert_equal(tdc / 2, tda) assert_equal((tdc / 2).dtype, np.dtype('m8[h]')) # m8 / float assert_equal(tda / 0.5, tdc) assert_equal((tda / 0.5).dtype, np.dtype('m8[h]')) + # m8 / m8 + assert_equal(tda / tdb, 6.0 / 9.0) + assert_equal(tdb / tda, 9.0 / 6.0) + assert_equal((tda / tdb).dtype, np.dtype('f8')) + assert_equal(tda / tdd, 60.0) + assert_equal(tdd / tda, 1.0 / 60.0) # int / m8 assert_raises(TypeError, np.divide, 2, tdb) # float / m8 assert_raises(TypeError, np.divide, 0.5, tdb) - # m8 / m8 - assert_raises(TypeError, np.divide, tda, tdb) # m8 / M8 assert_raises(TypeError, np.divide, dta, tda) # M8 / m8 @@ -733,16 +853,10 @@ def test_divisor_conversion_month(self): assert_(np.dtype('M8[3M/40]') == np.dtype('M8[54h]')) def test_divisor_conversion_week(self): - assert_(np.dtype('m8[W/5]') == np.dtype('m8[B]')) assert_(np.dtype('m8[W/7]') == np.dtype('m8[D]')) assert_(np.dtype('m8[3W/14]') == np.dtype('m8[36h]')) assert_(np.dtype('m8[5W/140]') == np.dtype('m8[360m]')) - def test_divisor_conversion_bday(self): - assert_(np.dtype('M8[B/12]') == np.dtype('M8[2h]')) - assert_(np.dtype('M8[B/120]') == np.dtype('M8[12m]')) - assert_(np.dtype('M8[3B/960]') == np.dtype('M8[270s]')) - def test_divisor_conversion_day(self): assert_(np.dtype('M8[D/12]') == np.dtype('M8[2h]')) assert_(np.dtype('M8[D/120]') == np.dtype('M8[12m]')) @@ -769,96 +883,102 @@ def test_divisor_conversion_as(self): def test_string_parser_variants(self): # Allow space instead of 'T' between date and time - assert_equal(np.array(['1980-02-29T01:02:03'], np.dtype('M8')), - np.array(['1980-02-29 01:02:03'], np.dtype('M8'))) + assert_equal(np.array(['1980-02-29T01:02:03'], np.dtype('M8[s]')), + np.array(['1980-02-29 01:02:03'], np.dtype('M8[s]'))) # Allow negative years - assert_equal(np.array(['-1980-02-29T01:02:03'], np.dtype('M8')), - np.array(['-1980-02-29 01:02:03'], np.dtype('M8'))) + assert_equal(np.array(['-1980-02-29T01:02:03'], np.dtype('M8[s]')), + np.array(['-1980-02-29 01:02:03'], np.dtype('M8[s]'))) # UTC specifier - assert_equal(np.array(['-1980-02-29T01:02:03Z'], np.dtype('M8')), - np.array(['-1980-02-29 01:02:03Z'], np.dtype('M8'))) + assert_equal(np.array(['-1980-02-29T01:02:03Z'], np.dtype('M8[s]')), + np.array(['-1980-02-29 01:02:03Z'], np.dtype('M8[s]'))) # Time zone offset - assert_equal(np.array(['1980-02-29T02:02:03Z'], np.dtype('M8')), - np.array(['1980-02-29 00:32:03-0130'], np.dtype('M8'))) - assert_equal(np.array(['1980-02-28T22:32:03Z'], np.dtype('M8')), - np.array(['1980-02-29 00:02:03+01:30'], np.dtype('M8'))) - assert_equal(np.array(['1980-02-29T02:32:03.506Z'], np.dtype('M8')), - np.array(['1980-02-29 00:32:03.506-02'], np.dtype('M8'))) + assert_equal(np.array(['1980-02-29T02:02:03Z'], np.dtype('M8[s]')), + np.array(['1980-02-29 00:32:03-0130'], np.dtype('M8[s]'))) + assert_equal(np.array(['1980-02-28T22:32:03Z'], np.dtype('M8[s]')), + np.array(['1980-02-29 00:02:03+01:30'], np.dtype('M8[s]'))) + assert_equal(np.array(['1980-02-29T02:32:03.506Z'], np.dtype('M8[s]')), + np.array(['1980-02-29 00:32:03.506-02'], np.dtype('M8[s]'))) assert_equal(np.datetime64('1977-03-02T12:30-0230'), np.datetime64('1977-03-02T15:00Z')) def test_string_parser_error_check(self): # Arbitrary bad string - assert_raises(ValueError, np.array, ['badvalue'], np.dtype('M8')) + assert_raises(ValueError, np.array, ['badvalue'], np.dtype('M8[us]')) # Character after year must be '-' - assert_raises(ValueError, np.array, ['1980X'], np.dtype('M8')) + assert_raises(ValueError, np.array, ['1980X'], np.dtype('M8[us]')) # Cannot have trailing '-' - assert_raises(ValueError, np.array, ['1980-'], np.dtype('M8')) + assert_raises(ValueError, np.array, ['1980-'], np.dtype('M8[us]')) # Month must be in range [1,12] - assert_raises(ValueError, np.array, ['1980-00'], np.dtype('M8')) - assert_raises(ValueError, np.array, ['1980-13'], np.dtype('M8')) + assert_raises(ValueError, np.array, ['1980-00'], np.dtype('M8[us]')) + assert_raises(ValueError, np.array, ['1980-13'], np.dtype('M8[us]')) # Month must have two digits - assert_raises(ValueError, np.array, ['1980-1'], np.dtype('M8')) - assert_raises(ValueError, np.array, ['1980-1-02'], np.dtype('M8')) + assert_raises(ValueError, np.array, ['1980-1'], np.dtype('M8[us]')) + assert_raises(ValueError, np.array, ['1980-1-02'], np.dtype('M8[us]')) # 'Mor' is not a valid month - assert_raises(ValueError, np.array, ['1980-Mor'], np.dtype('M8')) + assert_raises(ValueError, np.array, ['1980-Mor'], np.dtype('M8[us]')) # Cannot have trailing '-' - assert_raises(ValueError, np.array, ['1980-01-'], np.dtype('M8')) + assert_raises(ValueError, np.array, ['1980-01-'], np.dtype('M8[us]')) # Day must be in range [1,len(month)] - assert_raises(ValueError, np.array, ['1980-01-0'], np.dtype('M8')) - assert_raises(ValueError, np.array, ['1980-01-00'], np.dtype('M8')) - assert_raises(ValueError, np.array, ['1980-01-32'], np.dtype('M8')) - assert_raises(ValueError, np.array, ['1979-02-29'], np.dtype('M8')) - assert_raises(ValueError, np.array, ['1980-02-30'], np.dtype('M8')) - assert_raises(ValueError, np.array, ['1980-03-32'], np.dtype('M8')) - assert_raises(ValueError, np.array, ['1980-04-31'], np.dtype('M8')) - assert_raises(ValueError, np.array, ['1980-05-32'], np.dtype('M8')) - assert_raises(ValueError, np.array, ['1980-06-31'], np.dtype('M8')) - assert_raises(ValueError, np.array, ['1980-07-32'], np.dtype('M8')) - assert_raises(ValueError, np.array, ['1980-08-32'], np.dtype('M8')) - assert_raises(ValueError, np.array, ['1980-09-31'], np.dtype('M8')) - assert_raises(ValueError, np.array, ['1980-10-32'], np.dtype('M8')) - assert_raises(ValueError, np.array, ['1980-11-31'], np.dtype('M8')) - assert_raises(ValueError, np.array, ['1980-12-32'], np.dtype('M8')) + assert_raises(ValueError, np.array, ['1980-01-0'], np.dtype('M8[us]')) + assert_raises(ValueError, np.array, ['1980-01-00'], np.dtype('M8[us]')) + assert_raises(ValueError, np.array, ['1980-01-32'], np.dtype('M8[us]')) + assert_raises(ValueError, np.array, ['1979-02-29'], np.dtype('M8[us]')) + assert_raises(ValueError, np.array, ['1980-02-30'], np.dtype('M8[us]')) + assert_raises(ValueError, np.array, ['1980-03-32'], np.dtype('M8[us]')) + assert_raises(ValueError, np.array, ['1980-04-31'], np.dtype('M8[us]')) + assert_raises(ValueError, np.array, ['1980-05-32'], np.dtype('M8[us]')) + assert_raises(ValueError, np.array, ['1980-06-31'], np.dtype('M8[us]')) + assert_raises(ValueError, np.array, ['1980-07-32'], np.dtype('M8[us]')) + assert_raises(ValueError, np.array, ['1980-08-32'], np.dtype('M8[us]')) + assert_raises(ValueError, np.array, ['1980-09-31'], np.dtype('M8[us]')) + assert_raises(ValueError, np.array, ['1980-10-32'], np.dtype('M8[us]')) + assert_raises(ValueError, np.array, ['1980-11-31'], np.dtype('M8[us]')) + assert_raises(ValueError, np.array, ['1980-12-32'], np.dtype('M8[us]')) # Cannot have trailing characters - assert_raises(ValueError, np.array, ['1980-02-03%'], np.dtype('M8')) - assert_raises(ValueError, np.array, ['1980-02-03 q'], np.dtype('M8')) + assert_raises(ValueError, np.array, ['1980-02-03%'], + np.dtype('M8[us]')) + assert_raises(ValueError, np.array, ['1980-02-03 q'], + np.dtype('M8[us]')) # Hours must be in range [0, 23] - assert_raises(ValueError, np.array, ['1980-02-03 25'], np.dtype('M8')) - assert_raises(ValueError, np.array, ['1980-02-03T25'], np.dtype('M8')) + assert_raises(ValueError, np.array, ['1980-02-03 25'], + np.dtype('M8[us]')) + assert_raises(ValueError, np.array, ['1980-02-03T25'], + np.dtype('M8[us]')) assert_raises(ValueError, np.array, ['1980-02-03 24:01'], - np.dtype('M8')) + np.dtype('M8[us]')) assert_raises(ValueError, np.array, ['1980-02-03T24:01'], - np.dtype('M8')) - assert_raises(ValueError, np.array, ['1980-02-03 -1'], np.dtype('M8')) + np.dtype('M8[us]')) + assert_raises(ValueError, np.array, ['1980-02-03 -1'], + np.dtype('M8[us]')) # No trailing ':' - assert_raises(ValueError, np.array, ['1980-02-03 01:'], np.dtype('M8')) + assert_raises(ValueError, np.array, ['1980-02-03 01:'], + np.dtype('M8[us]')) # Minutes must be in range [0, 59] assert_raises(ValueError, np.array, ['1980-02-03 01:-1'], - np.dtype('M8')) + np.dtype('M8[us]')) assert_raises(ValueError, np.array, ['1980-02-03 01:60'], - np.dtype('M8')) + np.dtype('M8[us]')) # No trailing ':' assert_raises(ValueError, np.array, ['1980-02-03 01:60:'], - np.dtype('M8')) + np.dtype('M8[us]')) # Seconds must be in range [0, 59] assert_raises(ValueError, np.array, ['1980-02-03 01:10:-1'], - np.dtype('M8')) + np.dtype('M8[us]')) assert_raises(ValueError, np.array, ['1980-02-03 01:01:60'], - np.dtype('M8')) + np.dtype('M8[us]')) # Timezone offset must within a reasonable range assert_raises(ValueError, np.array, ['1980-02-03 01:01:00+0661'], - np.dtype('M8')) + np.dtype('M8[us]')) assert_raises(ValueError, np.array, ['1980-02-03 01:01:00+2500'], - np.dtype('M8')) + np.dtype('M8[us]')) assert_raises(ValueError, np.array, ['1980-02-03 01:01:00-0070'], - np.dtype('M8')) + np.dtype('M8[us]')) assert_raises(ValueError, np.array, ['1980-02-03 01:01:00-3000'], - np.dtype('M8')) + np.dtype('M8[us]')) assert_raises(ValueError, np.array, ['1980-02-03 01:01:00-25:00'], - np.dtype('M8')) + np.dtype('M8[us]')) def test_creation_overflow(self): @@ -906,7 +1026,7 @@ def test_datetime_as_string(self): date = '1969-12-31T23:59:57.789012345678901234Z' assert_equal(np.datetime_as_string(np.datetime64(date, 'as')), - date); + date) date = '1970-01-01T00:34:56.789012345678901234Z' assert_equal(np.datetime_as_string(np.datetime64(date, 'ns')), @@ -919,7 +1039,7 @@ def test_datetime_as_string(self): date = '1970-01-01T00:00:05.789012345678901234Z' assert_equal(np.datetime_as_string(np.datetime64(date, 'as')), - date); + date) # String conversion with the unit= parameter a = np.datetime64('2032-07-18T12:23:34.123456Z', 'us') @@ -992,11 +1112,355 @@ def test_datetime_as_string(self): assert_equal(np.datetime_as_string(a, local=True, tzoffset=-5*60), '2010-03-15T01:30-0500') + def test_datetime_arange(self): + # With two datetimes provided as strings + a = np.arange('2010-01-05', '2010-01-10', dtype='M8[D]') + assert_equal(a.dtype, np.dtype('M8[D]')) + assert_equal(a, + np.array(['2010-01-05', '2010-01-06', '2010-01-07', + '2010-01-08', '2010-01-09'], dtype='M8[D]')) + + a = np.arange('1950-02-10', '1950-02-06', -1, dtype='M8[D]') + assert_equal(a.dtype, np.dtype('M8[D]')) + assert_equal(a, + np.array(['1950-02-10', '1950-02-09', '1950-02-08', + '1950-02-07'], dtype='M8[D]')) + + # Unit should be detected as months here + a = np.arange('1969-05', '1970-05', 2, dtype='M8') + assert_equal(a.dtype, np.dtype('M8[M]')) + assert_equal(a, + np.datetime64('1969-05') + np.arange(12, step=2)) + + # datetime, integer|timedelta works as well + # produces arange (start, start + stop) in this case + a = np.arange('1969', 18, 3, dtype='M8') + assert_equal(a.dtype, np.dtype('M8[Y]')) + assert_equal(a, + np.datetime64('1969') + np.arange(18, step=3)) + a = np.arange('1969-12-19', 22, np.timedelta64(2), dtype='M8') + assert_equal(a.dtype, np.dtype('M8[D]')) + assert_equal(a, + np.datetime64('1969-12-19') + np.arange(22, step=2)) + + # Step of 0 is disallowed + assert_raises(ValueError, np.arange, np.datetime64('today'), + np.datetime64('today') + 3, 0) + # Promotion across nonlinear unit boundaries is disallowed + assert_raises(TypeError, np.arange, np.datetime64('2011-03-01','D'), + np.timedelta64(5,'M')) + assert_raises(TypeError, np.arange, + np.datetime64('2012-02-03T14Z','s'), + np.timedelta64(5,'Y')) + + def test_timedelta_arange(self): + a = np.arange(3, 10, dtype='m8') + assert_equal(a.dtype, np.dtype('m8')) + assert_equal(a, np.timedelta64(0) + np.arange(3, 10)) + + a = np.arange(np.timedelta64(3,'s'), 10, 2, dtype='m8') + assert_equal(a.dtype, np.dtype('m8[s]')) + assert_equal(a, np.timedelta64(0, 's') + np.arange(3, 10, 2)) + + # Step of 0 is disallowed + assert_raises(ValueError, np.arange, np.timedelta64(0), + np.timedelta64(5), 0) + # Promotion across nonlinear unit boundaries is disallowed + assert_raises(TypeError, np.arange, np.timedelta64(0,'D'), + np.timedelta64(5,'M')) + assert_raises(TypeError, np.arange, np.timedelta64(0,'Y'), + np.timedelta64(5,'D')) + + def test_datetime_maximum_reduce(self): + a = np.array(['2010-01-02','1999-03-14','1833-03'], dtype='M8[D]') + assert_equal(np.maximum.reduce(a).dtype, np.dtype('M8[D]')) + assert_equal(np.maximum.reduce(a), + np.datetime64('2010-01-02')) + + a = np.array([1,4,0,7,2], dtype='m8[s]') + assert_equal(np.maximum.reduce(a).dtype, np.dtype('m8[s]')) + assert_equal(np.maximum.reduce(a), + np.timedelta64(7, 's')) + + def test_datetime_busday_offset(self): + # First Monday in June + assert_equal( + np.busday_offset('2011-06',0,roll='forward',weekmask='Mon'), + np.datetime64('2011-06-06')) + # Last Monday in June + assert_equal( + np.busday_offset('2011-07',-1,roll='forward',weekmask='Mon'), + np.datetime64('2011-06-27')) + assert_equal( + np.busday_offset('2011-07',-1,roll='forward',weekmask='Mon'), + np.datetime64('2011-06-27')) + + # Default M-F business days, different roll modes + assert_equal(np.busday_offset('2010-08', 0, roll='backward'), + np.datetime64('2010-07-30')) + assert_equal(np.busday_offset('2010-08', 0, roll='preceding'), + np.datetime64('2010-07-30')) + assert_equal(np.busday_offset('2010-08', 0, roll='modifiedpreceding'), + np.datetime64('2010-08-02')) + assert_equal(np.busday_offset('2010-08', 0, roll='modifiedfollowing'), + np.datetime64('2010-08-02')) + assert_equal(np.busday_offset('2010-08', 0, roll='forward'), + np.datetime64('2010-08-02')) + assert_equal(np.busday_offset('2010-08', 0, roll='following'), + np.datetime64('2010-08-02')) + assert_equal(np.busday_offset('2010-10-30', 0, roll='following'), + np.datetime64('2010-11-01')) + assert_equal( + np.busday_offset('2010-10-30', 0, roll='modifiedfollowing'), + np.datetime64('2010-10-29')) + assert_equal( + np.busday_offset('2010-10-30', 0, roll='modifiedpreceding'), + np.datetime64('2010-10-29')) + # roll='raise' by default + assert_raises(ValueError, np.busday_offset, '2011-06-04', 0) + + # Bigger offset values + assert_equal(np.busday_offset('2006-02-01', 25), + np.datetime64('2006-03-08')) + assert_equal(np.busday_offset('2006-03-08', -25), + np.datetime64('2006-02-01')) + assert_equal(np.busday_offset('2007-02-25', 11, weekmask='SatSun'), + np.datetime64('2007-04-07')) + assert_equal(np.busday_offset('2007-04-07', -11, weekmask='SatSun'), + np.datetime64('2007-02-25')) + + def test_datetime_busdaycalendar(self): + # Check that it removes NaT, duplicates, and weekends + # and sorts the result. + bdd = np.busdaycalendar( + holidays=['NaT', '2011-01-17', '2011-03-06', 'NaT', + '2011-12-26','2011-05-30','2011-01-17']) + assert_equal(bdd.holidays, + np.array(['2011-01-17','2011-05-30','2011-12-26'], dtype='M8')) + # Default M-F weekmask + assert_equal(bdd.weekmask, np.array([1,1,1,1,1,0,0], dtype='?')) + + # All-zeros weekmask should raise + assert_raises(ValueError, np.busdaycalendar, weekmask=[0,0,0,0,0,0,0]) + + def test_datetime_busday_holidays_offset(self): + # With exactly one holiday + assert_equal( + np.busday_offset('2011-11-10', 1, holidays=['2011-11-11']), + np.datetime64('2011-11-14')) + assert_equal( + np.busday_offset('2011-11-04', 5, holidays=['2011-11-11']), + np.datetime64('2011-11-14')) + assert_equal( + np.busday_offset('2011-11-10', 5, holidays=['2011-11-11']), + np.datetime64('2011-11-18')) + assert_equal( + np.busday_offset('2011-11-14', -1, holidays=['2011-11-11']), + np.datetime64('2011-11-10')) + assert_equal( + np.busday_offset('2011-11-18', -5, holidays=['2011-11-11']), + np.datetime64('2011-11-10')) + assert_equal( + np.busday_offset('2011-11-14', -5, holidays=['2011-11-11']), + np.datetime64('2011-11-04')) + # With the holiday appearing twice + assert_equal( + np.busday_offset('2011-11-10', 1, + holidays=['2011-11-11', '2011-11-11']), + np.datetime64('2011-11-14')) + assert_equal( + np.busday_offset('2011-11-14', -1, + holidays=['2011-11-11', '2011-11-11']), + np.datetime64('2011-11-10')) + # With a NaT holiday + assert_equal( + np.busday_offset('2011-11-10', 1, + holidays=['2011-11-11', 'NaT']), + np.datetime64('2011-11-14')) + assert_equal( + np.busday_offset('2011-11-14', -1, + holidays=['NaT', '2011-11-11']), + np.datetime64('2011-11-10')) + # With another holiday after + assert_equal( + np.busday_offset('2011-11-10', 1, + holidays=['2011-11-11', '2011-11-24']), + np.datetime64('2011-11-14')) + assert_equal( + np.busday_offset('2011-11-14', -1, + holidays=['2011-11-11', '2011-11-24']), + np.datetime64('2011-11-10')) + # With another holiday before + assert_equal( + np.busday_offset('2011-11-10', 1, + holidays=['2011-10-10', '2011-11-11']), + np.datetime64('2011-11-14')) + assert_equal( + np.busday_offset('2011-11-14', -1, + holidays=['2011-10-10', '2011-11-11']), + np.datetime64('2011-11-10')) + # With another holiday before and after + assert_equal( + np.busday_offset('2011-11-10', 1, + holidays=['2011-10-10', '2011-11-11', '2011-11-24']), + np.datetime64('2011-11-14')) + assert_equal( + np.busday_offset('2011-11-14', -1, + holidays=['2011-10-10', '2011-11-11', '2011-11-24']), + np.datetime64('2011-11-10')) + + # A bigger forward jump across more than one week/holiday + holidays=['2011-10-10', '2011-11-11', '2011-11-24', + '2011-12-25', '2011-05-30', '2011-02-21', + '2011-12-26', '2012-01-02'] + bdd = np.busdaycalendar(weekmask='1111100', holidays=holidays) + assert_equal( + np.busday_offset('2011-10-03', 4, holidays=holidays), + np.busday_offset('2011-10-03', 4)) + assert_equal( + np.busday_offset('2011-10-03', 5, holidays=holidays), + np.busday_offset('2011-10-03', 5 + 1)) + assert_equal( + np.busday_offset('2011-10-03', 27, holidays=holidays), + np.busday_offset('2011-10-03', 27 + 1)) + assert_equal( + np.busday_offset('2011-10-03', 28, holidays=holidays), + np.busday_offset('2011-10-03', 28 + 2)) + assert_equal( + np.busday_offset('2011-10-03', 35, holidays=holidays), + np.busday_offset('2011-10-03', 35 + 2)) + assert_equal( + np.busday_offset('2011-10-03', 36, holidays=holidays), + np.busday_offset('2011-10-03', 36 + 3)) + assert_equal( + np.busday_offset('2011-10-03', 56, holidays=holidays), + np.busday_offset('2011-10-03', 56 + 3)) + assert_equal( + np.busday_offset('2011-10-03', 57, holidays=holidays), + np.busday_offset('2011-10-03', 57 + 4)) + assert_equal( + np.busday_offset('2011-10-03', 60, holidays=holidays), + np.busday_offset('2011-10-03', 60 + 4)) + assert_equal( + np.busday_offset('2011-10-03', 61, holidays=holidays), + np.busday_offset('2011-10-03', 61 + 5)) + assert_equal( + np.busday_offset('2011-10-03', 61, busdaycal=bdd), + np.busday_offset('2011-10-03', 61 + 5)) + # A bigger backward jump across more than one week/holiday + assert_equal( + np.busday_offset('2012-01-03', -1, holidays=holidays), + np.busday_offset('2012-01-03', -1 - 1)) + assert_equal( + np.busday_offset('2012-01-03', -4, holidays=holidays), + np.busday_offset('2012-01-03', -4 - 1)) + assert_equal( + np.busday_offset('2012-01-03', -5, holidays=holidays), + np.busday_offset('2012-01-03', -5 - 2)) + assert_equal( + np.busday_offset('2012-01-03', -25, holidays=holidays), + np.busday_offset('2012-01-03', -25 - 2)) + assert_equal( + np.busday_offset('2012-01-03', -26, holidays=holidays), + np.busday_offset('2012-01-03', -26 - 3)) + assert_equal( + np.busday_offset('2012-01-03', -33, holidays=holidays), + np.busday_offset('2012-01-03', -33 - 3)) + assert_equal( + np.busday_offset('2012-01-03', -34, holidays=holidays), + np.busday_offset('2012-01-03', -34 - 4)) + assert_equal( + np.busday_offset('2012-01-03', -56, holidays=holidays), + np.busday_offset('2012-01-03', -56 - 4)) + assert_equal( + np.busday_offset('2012-01-03', -57, holidays=holidays), + np.busday_offset('2012-01-03', -57 - 5)) + assert_equal( + np.busday_offset('2012-01-03', -57, busdaycal=bdd), + np.busday_offset('2012-01-03', -57 - 5)) + + # Can't supply both a weekmask/holidays and busdaycal + assert_raises(ValueError, np.busday_offset, '2012-01-03', -15, + weekmask='1111100', busdaycal=bdd) + assert_raises(ValueError, np.busday_offset, '2012-01-03', -15, + holidays=holidays, busdaycal=bdd) + + # Roll with the holidays + assert_equal( + np.busday_offset('2011-12-25', 0, + roll='forward', holidays=holidays), + np.datetime64('2011-12-27')) + assert_equal( + np.busday_offset('2011-12-26', 0, + roll='forward', holidays=holidays), + np.datetime64('2011-12-27')) + assert_equal( + np.busday_offset('2011-12-26', 0, + roll='backward', holidays=holidays), + np.datetime64('2011-12-23')) + assert_equal( + np.busday_offset('2012-02-27', 0, + roll='modifiedfollowing', + holidays=['2012-02-27', '2012-02-26', '2012-02-28', + '2012-03-01', '2012-02-29']), + np.datetime64('2012-02-24')) + assert_equal( + np.busday_offset('2012-03-06', 0, + roll='modifiedpreceding', + holidays=['2012-03-02', '2012-03-03', '2012-03-01', + '2012-03-05', '2012-03-07', '2012-03-06']), + np.datetime64('2012-03-08')) + + def test_datetime_busday_holidays_count(self): + holidays=['2011-01-01', '2011-10-10', '2011-11-11', '2011-11-24', + '2011-12-25', '2011-05-30', '2011-02-21', '2011-01-17', + '2011-12-26', '2012-01-02', '2011-02-21', '2011-05-30', + '2011-07-01', '2011-07-04', '2011-09-05', '2011-10-10'] + bdd = np.busdaycalendar(weekmask='1111100', holidays=holidays) + + # Validate against busday_offset broadcast against + # a range of offsets + dates = np.busday_offset('2011-01-01', np.arange(366), + roll='forward', busdaycal=bdd) + assert_equal(np.busday_count('2011-01-01', dates, busdaycal=bdd), + np.arange(366)) + + dates = np.busday_offset('2011-12-31', -np.arange(366), + roll='forward', busdaycal=bdd) + assert_equal(np.busday_count(dates, '2011-12-31', busdaycal=bdd), + np.arange(366)) + + # Can't supply both a weekmask/holidays and busdaycal + assert_raises(ValueError, np.busday_offset, '2012-01-03', '2012-02-03', + weekmask='1111100', busdaycal=bdd) + assert_raises(ValueError, np.busday_offset, '2012-01-03', '2012-02-03', + holidays=holidays, busdaycal=bdd) + + # Number of Mondays in March 2011 + assert_equal(np.busday_count('2011-03', '2011-04', weekmask='Mon'), 4) + + def test_datetime_is_busday(self): + holidays=['2011-01-01', '2011-10-10', '2011-11-11', '2011-11-24', + '2011-12-25', '2011-05-30', '2011-02-21', '2011-01-17', + '2011-12-26', '2012-01-02', '2011-02-21', '2011-05-30', + '2011-07-01', '2011-07-04', '2011-09-05', '2011-10-10', + 'NaT'] + bdd = np.busdaycalendar(weekmask='1111100', holidays=holidays) + + # Weekend/weekday tests + assert_equal(np.is_busday('2011-01-01'), False) + assert_equal(np.is_busday('2011-01-02'), False) + assert_equal(np.is_busday('2011-01-03'), True) + + # All the holidays are not business days + assert_equal(np.is_busday(holidays, busdaycal=bdd), + np.zeros(len(holidays), dtype='?')) + class TestDateTimeData(TestCase): def test_basic(self): a = np.array(['1980-03-23'], dtype=np.datetime64) - assert_equal(np.datetime_data(a.dtype), (asbytes('us'), 1, 1)) + assert_equal(np.datetime_data(a.dtype), (asbytes('D'), 1, 1)) if __name__ == "__main__": run_module_suite() diff --git a/numpy/core/tests/test_numeric.py b/numpy/core/tests/test_numeric.py index 945e05001200..75ecd398a15f 100644 --- a/numpy/core/tests/test_numeric.py +++ b/numpy/core/tests/test_numeric.py @@ -464,6 +464,29 @@ def res_type(a, b): def test_result_type(self): self.check_promotion_cases(np.result_type) + def test_promote_types_endian(self): + # promote_types should always return native-endian types + assert_equal(np.promote_types('i8', '>i8'), np.dtype('i8')) + + assert_equal(np.promote_types('>i8', '>U16'), np.dtype('U16')) + assert_equal(np.promote_types('U16', '>i8'), np.dtype('U16')) + assert_equal(np.promote_types('S5', '>U8'), np.dtype('U8')) + assert_equal(np.promote_types('U8', '>S5'), np.dtype('U8')) + assert_equal(np.promote_types('U8', '>U5'), np.dtype('U8')) + + assert_equal(np.promote_types('M8', '>M8'), np.dtype('M8')) + assert_equal(np.promote_types('m8', '>m8'), np.dtype('m8')) + + def test_can_cast(self): assert_(np.can_cast(np.int32, np.int64)) assert_(np.can_cast(np.float64, np.complex)) diff --git a/numpy/core/tests/test_regression.py b/numpy/core/tests/test_regression.py index 764bf9a41a67..5db874ecd5f6 100644 --- a/numpy/core/tests/test_regression.py +++ b/numpy/core/tests/test_regression.py @@ -1545,7 +1545,7 @@ def test_string_astype(self): s3 = asbytes('other') a = np.array([[s1],[s2],[s3]]) assert_equal(a.dtype, np.dtype('S5')) - b = a.astype('str') + b = a.astype(np.dtype('S0')) assert_equal(b.dtype, np.dtype('S5')) def test_ticket_1756(self): diff --git a/numpy/core/tests/test_ufunc.py b/numpy/core/tests/test_ufunc.py index c75b2d385f6a..578a819c34ff 100644 --- a/numpy/core/tests/test_ufunc.py +++ b/numpy/core/tests/test_ufunc.py @@ -347,6 +347,19 @@ def test_endian(self): a = np.arange(6, dtype='