diff options
Diffstat (limited to 'numpy')
-rw-r--r-- | numpy/core/src/multiarray/datetime.c | 4 | ||||
-rw-r--r-- | numpy/core/src/multiarray/datetime_strings.c | 138 | ||||
-rw-r--r-- | numpy/core/src/multiarray/datetime_strings.h | 8 | ||||
-rw-r--r-- | numpy/core/src/multiarray/scalartypes.c.src | 24 | ||||
-rw-r--r-- | numpy/core/tests/test_datetime.py | 21 |
5 files changed, 150 insertions, 45 deletions
diff --git a/numpy/core/src/multiarray/datetime.c b/numpy/core/src/multiarray/datetime.c index 278cbe94b..c594bd02f 100644 --- a/numpy/core/src/multiarray/datetime.c +++ b/numpy/core/src/multiarray/datetime.c @@ -3147,7 +3147,7 @@ convert_pyobject_to_timedelta(PyArray_DatetimeMetaData *meta, PyObject *obj, NPY_NO_EXPORT PyObject * convert_datetime_to_pyobject(npy_datetime dt, PyArray_DatetimeMetaData *meta) { - PyObject *ret = NULL, *tup = NULL; + PyObject *ret = NULL; npy_datetimestruct dts; /* @@ -3201,7 +3201,7 @@ convert_datetime_to_pyobject(npy_datetime dt, PyArray_DatetimeMetaData *meta) NPY_NO_EXPORT PyObject * convert_timedelta_to_pyobject(npy_timedelta td, PyArray_DatetimeMetaData *meta) { - PyObject *ret = NULL, *tup = NULL; + PyObject *ret = NULL; npy_timedelta value; int days = 0, seconds = 0, useconds = 0; diff --git a/numpy/core/src/multiarray/datetime_strings.c b/numpy/core/src/multiarray/datetime_strings.c index 46e9db56d..b75920920 100644 --- a/numpy/core/src/multiarray/datetime_strings.c +++ b/numpy/core/src/multiarray/datetime_strings.c @@ -735,6 +735,51 @@ get_datetime_iso_8601_strlen(int local, NPY_DATETIMEUNIT base) } /* + * Finds the largest unit whose value is nonzero, and for which + * the remainder for the rest of the units is zero. + */ +static NPY_DATETIMEUNIT +lossless_unit_from_datetimestruct(npy_datetimestruct *dts) +{ + if (dts->as % 1000 != 0) { + return NPY_FR_as; + } + else if (dts->as != 0) { + return NPY_FR_fs; + } + else if (dts->ps % 1000 != 0) { + return NPY_FR_ps; + } + else if (dts->ps != 0) { + return NPY_FR_ns; + } + else if (dts->us % 1000 != 0) { + return NPY_FR_us; + } + else if (dts->us != 0) { + return NPY_FR_ms; + } + else if (dts->sec != 0) { + return NPY_FR_s; + } + else if (dts->min != 0) { + return NPY_FR_m; + } + else if (dts->hour != 0) { + return NPY_FR_h; + } + else if (dts->day != 1) { + return NPY_FR_D; + } + else if (dts->month != 1) { + return NPY_FR_M; + } + else { + return NPY_FR_Y; + } +} + +/* * Converts an npy_datetimestruct to an (almost) ISO 8601 * NULL-terminated string. * @@ -748,12 +793,18 @@ get_datetime_iso_8601_strlen(int local, NPY_DATETIMEUNIT base) * set to a value other than -1. This is a manual override for * the local time zone to use, as an offset in minutes. * + * 'casting' controls whether data loss is allowed by truncating + * the data to a coarser unit. This interacts with 'local', slightly, + * in order to form a date unit string as a local time, the casting + * must be unsafe. + * * Returns 0 on success, -1 on failure (for example if the output * string was too short). */ NPY_NO_EXPORT int make_iso_8601_datetime(npy_datetimestruct *dts, char *outstr, int outlen, - int local, NPY_DATETIMEUNIT base, int tzoffset) + int local, NPY_DATETIMEUNIT base, int tzoffset, + NPY_CASTING casting) { npy_datetimestruct dts_local; int timezone_offset = 0; @@ -781,36 +832,16 @@ make_iso_8601_datetime(npy_datetimestruct *dts, char *outstr, int outlen, /* Automatically detect a good unit */ if (base == -1) { - if (dts->as % 1000 != 0) { - base = NPY_FR_as; - } - else if (dts->as != 0) { - base = NPY_FR_fs; - } - else if (dts->ps % 1000 != 0) { - base = NPY_FR_ps; - } - else if (dts->ps != 0) { - base = NPY_FR_ns; - } - else if (dts->us % 1000 != 0) { - base = NPY_FR_us; - } - else if (dts->us != 0) { - base = NPY_FR_ms; - } - else if (dts->sec != 0) { - base = NPY_FR_s; - } + base = lossless_unit_from_datetimestruct(dts); /* - * hours and minutes don't get split up by default, and printing - * in local time forces minutes + * If there's a timezone, use at least minutes precision, + * and never split up hours and minutes by default */ - else if (local || dts->min != 0 || dts->hour != 0) { + if ((base < NPY_FR_m && local) || base == NPY_FR_h) { base = NPY_FR_m; } - /* dates don't get split up by default */ - else { + /* Don't split up dates by default */ + else if (base < NPY_FR_D) { base = NPY_FR_D; } } @@ -886,6 +917,37 @@ make_iso_8601_datetime(npy_datetimestruct *dts, char *outstr, int outlen, add_minutes_to_datetimestruct(dts, timezone_offset); } + /* + * Now the datetimestruct data is in the final form for + * the string representation, so ensure that the data + * isn't being cast according to the casting rule. + */ + if (casting != NPY_UNSAFE_CASTING) { + /* Producing a date as a local time is always 'unsafe' */ + if (base <= NPY_FR_D && local) { + PyErr_SetString(PyExc_TypeError, "Cannot create a local " + "timezone-based date string from a NumPy " + "datetime without forcing 'unsafe' casting"); + return -1; + } + /* Only 'unsafe' and 'same_kind' allow data loss */ + else { + NPY_DATETIMEUNIT unitprec; + + unitprec = lossless_unit_from_datetimestruct(dts); + if (casting != NPY_SAME_KIND_CASTING && unitprec > base) { + PyErr_Format(PyExc_TypeError, "Cannot create a " + "string with unit precision '%s' " + "from the NumPy datetime, which has data at " + "unit precision '%s', " + "requires 'unsafe' or 'same_kind' casting", + _datetime_strings[base], + _datetime_strings[unitprec]); + return -1; + } + } + } + /* YEAR */ #ifdef _WIN32 tmplen = _snprintf(substr, sublen, "%04" NPY_INT64_FMT, dts->year); @@ -1210,9 +1272,10 @@ array_datetime_as_string(PyObject *NPY_UNUSED(self), PyObject *args, PyObject *kwds) { PyObject *arr_in = NULL, *unit_in = NULL, *timezone = NULL; + NPY_DATETIMEUNIT unit; + NPY_CASTING casting = NPY_SAME_KIND_CASTING; int local = 0; - NPY_DATETIMEUNIT unit; PyArray_DatetimeMetaData *meta; int strsize; @@ -1223,13 +1286,14 @@ array_datetime_as_string(PyObject *NPY_UNUSED(self), PyObject *args, PyArray_Descr *op_dtypes[2] = {NULL, NULL}; npy_uint32 flags, op_flags[2]; - static char *kwlist[] = {"arr", "unit", "timezone", NULL}; + static char *kwlist[] = {"arr", "unit", "timezone", "casting", NULL}; if(!PyArg_ParseTupleAndKeywords(args, kwds, - "O|OO:datetime_as_string", kwlist, + "O|OOO&:datetime_as_string", kwlist, &arr_in, &unit_in, - &timezone)) { + &timezone, + &PyArray_CastingConverter, &casting)) { return NULL; } @@ -1287,6 +1351,16 @@ array_datetime_as_string(PyObject *NPY_UNUSED(self), PyObject *args, } } Py_DECREF(strobj); + + if (!can_cast_datetime64_units(meta->base, unit, casting)) { + PyErr_Format(PyExc_TypeError, "Cannot create a datetime " + "string as units '%s' from a NumPy datetime " + "with units '%s' according to the rule %s", + _datetime_strings[unit], + _datetime_strings[meta->base], + npy_casting_to_string(casting)); + goto fail; + } } /* Get the input time zone */ @@ -1390,7 +1464,7 @@ array_datetime_as_string(PyObject *NPY_UNUSED(self), PyObject *args, memset(dataptr[1], 0, strsize); /* Convert that into a string */ if (make_iso_8601_datetime(&dts, (char *)dataptr[1], strsize, - local, unit, tzoffset) < 0) { + local, unit, tzoffset, casting) < 0) { goto fail; } } while(iternext(iter)); diff --git a/numpy/core/src/multiarray/datetime_strings.h b/numpy/core/src/multiarray/datetime_strings.h index e1d96b125..f27856b89 100644 --- a/numpy/core/src/multiarray/datetime_strings.h +++ b/numpy/core/src/multiarray/datetime_strings.h @@ -66,12 +66,18 @@ get_datetime_iso_8601_strlen(int local, NPY_DATETIMEUNIT base); * set to a value other than -1. This is a manual override for * the local time zone to use, as an offset in minutes. * + * 'casting' controls whether data loss is allowed by truncating + * the data to a coarser unit. This interacts with 'local', slightly, + * in order to form a date unit string as a local time, the casting + * must be unsafe. + * * Returns 0 on success, -1 on failure (for example if the output * string was too short). */ NPY_NO_EXPORT int make_iso_8601_datetime(npy_datetimestruct *dts, char *outstr, int outlen, - int local, NPY_DATETIMEUNIT base, int tzoffset); + int local, NPY_DATETIMEUNIT base, int tzoffset, + NPY_CASTING casting); /* * This is the Python-exposed datetime_as_string function. diff --git a/numpy/core/src/multiarray/scalartypes.c.src b/numpy/core/src/multiarray/scalartypes.c.src index 21d8804b7..0e7eb5914 100644 --- a/numpy/core/src/multiarray/scalartypes.c.src +++ b/numpy/core/src/multiarray/scalartypes.c.src @@ -606,6 +606,7 @@ datetimetype_repr(PyObject *self) PyObject *ret; char iso[NPY_DATETIME_MAX_ISO8601_STRLEN]; int local; + NPY_DATETIMEUNIT unit; if (!PyArray_IsScalar(self, Datetime)) { PyErr_SetString(PyExc_RuntimeError, @@ -621,8 +622,16 @@ datetimetype_repr(PyObject *self) } local = (scal->obmeta.base > NPY_FR_D); + /* + * Because we're defaulting to local time, display hours with + * minutes precision, so that 30-minute timezone offsets can work. + */ + unit = scal->obmeta.base; + if (unit == NPY_FR_h) { + unit = NPY_FR_m; + } if (make_iso_8601_datetime(&dts, iso, sizeof(iso), local, - scal->obmeta.base, -1) < 0) { + unit, -1, NPY_SAFE_CASTING) < 0) { return NULL; } @@ -630,7 +639,7 @@ datetimetype_repr(PyObject *self) * For straight units or generic units, the unit will be deduced * from the string, so it's not necessary to specify it. */ - if ((scal->obmeta.num == 1) || + if ((scal->obmeta.num == 1 && scal->obmeta.base != NPY_FR_h) || scal->obmeta.base == NPY_FR_GENERIC) { ret = PyUString_FromString("numpy.datetime64('"); PyUString_ConcatAndDel(&ret, @@ -696,6 +705,7 @@ datetimetype_str(PyObject *self) npy_datetimestruct dts; char iso[NPY_DATETIME_MAX_ISO8601_STRLEN]; int local; + NPY_DATETIMEUNIT unit; if (!PyArray_IsScalar(self, Datetime)) { PyErr_SetString(PyExc_RuntimeError, @@ -711,8 +721,16 @@ datetimetype_str(PyObject *self) } local = (scal->obmeta.base > NPY_FR_D); + /* + * Because we're defaulting to local time, display hours with + * minutes precision, so that 30-minute timezone offsets can work. + */ + unit = scal->obmeta.base; + if (unit == NPY_FR_h) { + unit = NPY_FR_m; + } if (make_iso_8601_datetime(&dts, iso, sizeof(iso), local, - scal->obmeta.base, -1) < 0) { + unit, -1, NPY_SAFE_CASTING) < 0) { return NULL; } diff --git a/numpy/core/tests/test_datetime.py b/numpy/core/tests/test_datetime.py index 310b8be76..207aee27e 100644 --- a/numpy/core/tests/test_datetime.py +++ b/numpy/core/tests/test_datetime.py @@ -1127,10 +1127,14 @@ class TestDateTime(TestCase): # String conversion with the unit= parameter a = np.datetime64('2032-07-18T12:23:34.123456Z', 'us') - assert_equal(np.datetime_as_string(a, unit='Y'), '2032') - assert_equal(np.datetime_as_string(a, unit='M'), '2032-07') - assert_equal(np.datetime_as_string(a, unit='W'), '2032-07-18') - assert_equal(np.datetime_as_string(a, unit='D'), '2032-07-18') + assert_equal(np.datetime_as_string(a, unit='Y', casting='unsafe'), + '2032') + assert_equal(np.datetime_as_string(a, unit='M', casting='unsafe'), + '2032-07') + assert_equal(np.datetime_as_string(a, unit='W', casting='unsafe'), + '2032-07-18') + assert_equal(np.datetime_as_string(a, unit='D', casting='unsafe'), + '2032-07-18') assert_equal(np.datetime_as_string(a, unit='h'), '2032-07-18T12Z') assert_equal(np.datetime_as_string(a, unit='m'), '2032-07-18T12:23Z') @@ -1185,7 +1189,7 @@ class TestDateTime(TestCase): unit='auto'), '2032-01-01') - def test_datetime_as_string_timzone(self): + def test_datetime_as_string_timezone(self): # timezone='local' vs 'UTC' a = np.datetime64('2010-03-15T06:30Z', 'm') assert_equal(np.datetime_as_string(a, timezone='UTC'), @@ -1213,12 +1217,15 @@ class TestDateTime(TestCase): assert_equal(np.datetime_as_string(b, timezone=tz('US/Pacific')), '2010-02-14T22:30-0800') + # Dates to strings with a timezone attached is disabled by default + assert_raises(TypeError, np.datetime_as_string, a, unit='D', + timezone=tz('US/Pacific')) # Check that we can print out the date in the specified time zone assert_equal(np.datetime_as_string(a, unit='D', - timezone=tz('US/Pacific')), + timezone=tz('US/Pacific'), casting='unsafe'), '2010-03-14') assert_equal(np.datetime_as_string(b, unit='D', - timezone=tz('US/Central')), + timezone=tz('US/Central'), casting='unsafe'), '2010-02-15') except ImportError: import warnings |