diff --git a/Doc/library/datetime.rst b/Doc/library/datetime.rst --- a/Doc/library/datetime.rst +++ b/Doc/library/datetime.rst @@ -430,6 +430,18 @@ d``. +Other constructors, all static methods: + +.. staticmethod:: date.strptime(date_string, format) + + Return a :class:`date` corresponding to *date_string*, parsed according to + *format*. :exc:`ValueError` is raised if the date string and format can't be + parsed by `time.strptime`, or if it returns a value where the time part is + nonzero. + + .. versionadded:: 3.5 + + Class attributes: .. attribute:: date.min @@ -798,7 +810,7 @@ Return a :class:`.datetime` corresponding to *date_string*, parsed according to *format*. This is equivalent to ``datetime(*(time.strptime(date_string, format)[0:6]))``. :exc:`ValueError` is raised if the date_string and format - can't be parsed by :func:`time.strptime` or if it returns a value which isn't a + cannot be parsed by :func:`time.strptime` or if it returns a value which isn't a time tuple. For a complete list of formatting directives, see :ref:`strftime-strptime-behavior`. @@ -1305,6 +1317,19 @@ If an argument outside those ranges is given, :exc:`ValueError` is raised. All default to ``0`` except *tzinfo*, which defaults to :const:`None`. + +Other constructors, all static methods: + +.. staticmethod:: time.strptime(date_string, format) + + Return a :class:`time` corresponding to *date_string, parsed according to + *format*. :exc:`ValueError` is raised if the date string and format can't be + parsed by `time.strptime`, if it returns a value which isn't a time tuple, + or if the time part is nonzero. + + .. versionadded:: 3.5 + + Class attributes: @@ -1779,13 +1804,13 @@ corresponding format string. ``datetime.strptime(date_string, format)`` is equivalent to ``datetime(*(time.strptime(date_string, format)[0:6]))``. -For :class:`.time` objects, the format codes for year, month, and day should not -be used, as time objects have no such values. If they're used anyway, ``1900`` -is substituted for the year, and ``1`` for the month and day. - -For :class:`date` objects, the format codes for hours, minutes, seconds, and -microseconds should not be used, as :class:`date` objects have no such -values. If they're used anyway, ``0`` is substituted for them. +The :meth:`date.strptime` static method creates a :class:`date` object from a +string representing a date and a corresponding format string. :exc:`ValueError` +raised if the format codes for hours, minutes, seconds, and microseconds are used. + +The :meth:`.time.strptime` static method creates a :class:`.time` object from a +string representing a time and a corresponding format string. :exc:`ValueError` +raised if the format codes for year, month, and day are used. The full set of format codes supported varies across platforms, because Python calls the platform C library's :func:`strftime` function, and platform diff --git a/Lib/_strptime.py b/Lib/_strptime.py --- a/Lib/_strptime.py +++ b/Lib/_strptime.py @@ -17,6 +17,7 @@ from re import IGNORECASE from re import escape as re_escape from datetime import (date as datetime_date, + datetime as datetime_datetime, timedelta as datetime_timedelta, timezone as datetime_timezone) try: @@ -488,6 +489,10 @@ hour, minute, second, weekday, julian, tz, tzname, gmtoff), fraction +date_specs = ('%a', '%A', '%b', '%B', '%c', '%d', '%j', '%m', '%U', + '%w', '%W', '%x', '%y', '%Y',) +time_specs = ('%T', '%R', '%H', '%I', '%M', '%S', '%f', '%i', '%s',) + def _strptime_time(data_string, format="%a %b %d %H:%M:%S %Y"): """Return a time struct based on the input string and the format string.""" @@ -509,3 +514,32 @@ args += (tz,) return cls(*args) + +def _strptime_datetime_date(data_string, format): + """Return a date based on the input string and the format string.""" + if not format: + raise ValueError("Date format is not valid.") + msg = "'{!s}' {} not valid in date format specification." + if _check_invalid_datetime_specs(format, time_specs, msg): + _date = _strptime_datetime(datetime_datetime, data_string, format) + return _date.date() + +def _strptime_datetime_time(data_string, format): + """Return a time based on the input string and the format string.""" + if not format: + raise ValueError("Date format is not valid.") + msg = "'{!s}' {} not valid in time format specification." + if _check_invalid_datetime_specs(format, date_specs, msg): + _time = _strptime_datetime(datetime_datetime, data_string, format) + return _time.time() + +def _check_invalid_datetime_specs(fmt, specs, msg): + found_invalid_specs = [] + for spec in specs: + if spec in fmt: + found_invalid_specs.append(spec) + if found_invalid_specs: + suffix = "are" if len(found_invalid_specs) > 1 else "is" + raise ValueError(msg.format(", ".join(found_invalid_specs), + suffix)) + return True diff --git a/Lib/datetime.py b/Lib/datetime.py --- a/Lib/datetime.py +++ b/Lib/datetime.py @@ -622,6 +622,7 @@ fromtimestamp() today() fromordinal() + strptime() Operators: @@ -687,6 +688,16 @@ y, m, d = _ord2ymd(n) return cls(y, m, d) + @staticmethod + def strptime(date_string, format): + """string, format -> new date instance parsed from a string. + + >>> datetime.date.strptime('2012/07/20', '%Y/%m/%d') + datetime.date(2012, 7, 20) + """ + import _strptime + return _strptime._strptime_datetime_date(date_string, format) + # Conversions to string def __repr__(self): @@ -981,6 +992,7 @@ Constructors: __new__() + strptime() Operators: @@ -1022,6 +1034,16 @@ self._tzinfo = tzinfo return self + @staticmethod + def strptime(time_string, format): + """string, format -> new time instance parsed from a string. + + >>> datetime.time.strptime('10:40am', '%H:%M%p') + datetime.time(10, 40) + """ + import _strptime + return _strptime._strptime_datetime_time(time_string, format) + # Read-only field accessors @property def hour(self): @@ -1582,7 +1604,11 @@ @classmethod def strptime(cls, date_string, format): - 'string, format -> new datetime parsed from a string (like time.strptime()).' + """string, format -> new datetime parsed from a string. + + >>> datetime.datetime.strptime('2012/07/20 10:40am', '%Y/%m/%d %H:%M%p') + datetime.datetime(2012, 7, 20, 10, 40) + """ import _strptime return _strptime._strptime_datetime(cls, date_string, format) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -754,6 +754,23 @@ dt2 = dt - delta self.assertEqual(dt2, dt - days) + def test_strptime_valid_format(self): + tests = [(('2004-12-01', '%Y-%m-%d'), + date(2004, 12, 1)), + (('2004', '%Y'), date(2004, 1, 1)),] + for (date_string, date_format), expected in tests: + self.assertEqual(expected, date.strptime(date_string, date_format)) + + def test_strptime_invalid_format(self): + tests = [('2004-12-01 13:02:47.197', + '%Y-%m-%d %H:%M:%S.%f'), + ('01', '%M'), + ('02', '%H'),] + for test in tests: + with self.assertRaises(ValueError): + date.strptime(test[0], test[1]) + + class SubclassDate(date): sub_var = 1 @@ -2203,6 +2220,22 @@ # A naive object replaces %z and %Z with empty strings. self.assertEqual(t.strftime("'%z' '%Z'"), "'' ''") + def test_strptime_invalid(self): + tests = [('2004-12-01 13:02:47.197', + '%Y-%m-%d %H:%M:%S.%f'), + ('2004-12-01', '%Y-%m-%d'),] + for date_string, date_format in tests: + with self.assertRaises(ValueError): + time.strptime(date_string, date_format) + + def test_strptime_valid(self): + string = '13:02:47.197' + format = '%H:%M:%S.%f' + result, frac = _strptime._strptime(string, format) + expected = self.theclass(*(result[3:6] + (frac, ))) + got = time.strptime(string, format) + self.assertEqual(expected, got) + def test_format(self): t = self.theclass(1, 2, 3, 4) self.assertEqual(t.__format__(''), str(t)) diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -109,6 +109,9 @@ static PyTypeObject PyDateTime_TimeType; static PyTypeObject PyDateTime_TZInfoType; static PyTypeObject PyDateTime_TimeZoneType; +static PyObject *datetime_strptime(PyObject *cls, PyObject *args); +static PyObject *datetime_getdate(PyDateTime_DateTime *self); +static PyObject *datetime_gettime(PyDateTime_DateTime *self); _Py_IDENTIFIER(as_integer_ratio); _Py_IDENTIFIER(fromutc); @@ -2544,6 +2547,30 @@ return result; } +/* Return new date from time.strptime(). */ +static PyObject * +date_strptime(PyObject *cls, PyObject *args) +{ + PyObject *date = NULL; + PyObject *datetime; + + datetime = datetime_strptime((PyObject *)&PyDateTime_DateTimeType, args); + + if (datetime == NULL) + return NULL; + + if (DATE_GET_HOUR(datetime) || + DATE_GET_MINUTE(datetime) || + DATE_GET_SECOND(datetime) || + DATE_GET_MICROSECOND(datetime)) + PyErr_SetString(PyExc_ValueError, + "date.strptime value cannot have a time part"); + else + date = datetime_getdate((PyDateTime_DateTime *)datetime); + + Py_DECREF(datetime); + return date; +} /* * Date arithmetic. */ @@ -2838,6 +2865,11 @@ PyDoc_STR("Current date or datetime: same as " "self.__class__.fromtimestamp(time.time()).")}, + {"strptime", (PyCFunction)date_strptime, METH_VARARGS | METH_CLASS, + PyDoc_STR("string, format -> new date instance parsed from a string.\n\n" + ">>> datetime.date.strptime('2012/07/20', '%Y/%m/%d')\n" + "datetime.date(2012, 7, 20)")}, + /* Instance methods: */ {"ctime", (PyCFunction)date_ctime, METH_NOARGS, @@ -3534,6 +3566,48 @@ return self; } +/* Return new time from time.strptime(). */ +static PyObject * +time_strptime(PyObject *cls, PyObject *args) +{ + PyObject *time = NULL; + PyObject *datetime; + + static PyObject *emptyDatetime = NULL; + + /* To ensure that the given string does not contain a date, + * compare with the result of an empty date string. + */ + if (emptyDatetime == NULL) { + PyObject *emptyStringPair = Py_BuildValue("ss", "", ""); + if (emptyStringPair == NULL) + return NULL; + emptyDatetime = datetime_strptime( + (PyObject *)&PyDateTime_DateTimeType, + emptyStringPair); + Py_DECREF(emptyStringPair); + if (emptyDatetime == NULL) + return NULL; + } + + datetime = datetime_strptime((PyObject *)&PyDateTime_DateTimeType, args); + + if (datetime == NULL) + return NULL; + + if (GET_YEAR(datetime) != GET_YEAR(emptyDatetime) + || GET_MONTH(datetime) != GET_MONTH(emptyDatetime) + || GET_DAY(datetime) != GET_DAY(emptyDatetime)) + PyErr_SetString(PyExc_ValueError, + "time.strptime value cannot have a date part"); + else + time = datetime_gettime((PyDateTime_DateTime *)datetime); + + Py_DECREF(datetime); + return time; +} + + /* * Destructor. */ @@ -3838,6 +3912,15 @@ static PyMethodDef time_methods[] = { + /* Class methods: */ + + {"strptime", (PyCFunction)time_strptime, METH_VARARGS | METH_CLASS, + PyDoc_STR("string, format -> new time parsed from a string.\n\n" + ">>> datetime.time.strptime('10:40am', '%H:%M%p')\n" + "datetime.time(10, 40)")}, + + /* Instance methods: */ + {"isoformat", (PyCFunction)time_isoformat, METH_NOARGS, PyDoc_STR("Return string in ISO 8601 format, HH:MM:SS[.mmmmmm]" "[+HH:MM].")}, @@ -5025,8 +5108,10 @@ {"strptime", (PyCFunction)datetime_strptime, METH_VARARGS | METH_CLASS, - PyDoc_STR("string, format -> new datetime parsed from a string " - "(like time.strptime()).")}, + PyDoc_STR("string, format -> new datetime parsed from a string.\n\n" + ">>> datetime.datetime.strptime('2012/07/20 10:40am', " + "'%Y/%m/%d %H:%M%p')\n" + "datetime.datetime(2012, 7, 20, 10, 40)")}, {"combine", (PyCFunction)datetime_combine, METH_VARARGS | METH_KEYWORDS | METH_CLASS,