summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--numpy/core/src/multiarray/datetime.c4
-rw-r--r--numpy/core/src/multiarray/datetime_strings.c138
-rw-r--r--numpy/core/src/multiarray/datetime_strings.h8
-rw-r--r--numpy/core/src/multiarray/scalartypes.c.src24
-rw-r--r--numpy/core/tests/test_datetime.py21
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