diff -r 1002a1bdc5b1 Doc/library/datetime.rst --- a/Doc/library/datetime.rst Sun Aug 07 10:19:20 2016 -0700 +++ b/Doc/library/datetime.rst Mon Aug 08 09:10:45 2016 +0200 @@ -431,8 +431,17 @@ Return the date corresponding to the proleptic Gregorian ordinal, where January 1 of year 1 has ordinal 1. :exc:`ValueError` is raised unless ``1 <= ordinal <= - date.max.toordinal()``. For any date *d*, ``date.fromordinal(d.toordinal()) == - d``. + date.max.toordinal()``. For any date *d*, ``date.fromordinal(d.toordinal()) == d``. + + +.. classmethod:: 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.6 Class attributes: @@ -817,7 +826,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`. @@ -1356,6 +1365,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 class methods: + +.. classmethod:: 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.6 + + Class attributes: @@ -1873,13 +1895,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` class 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` class 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 -r 1002a1bdc5b1 Lib/_strptime.py --- a/Lib/_strptime.py Sun Aug 07 10:19:20 2016 -0700 +++ b/Lib/_strptime.py Mon Aug 08 09:10:45 2016 +0200 @@ -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: @@ -553,6 +554,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.""" @@ -574,3 +579,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 -r 1002a1bdc5b1 Lib/datetime.py --- a/Lib/datetime.py Sun Aug 07 10:19:20 2016 -0700 +++ b/Lib/datetime.py Mon Aug 08 09:10:45 2016 +0200 @@ -662,6 +662,7 @@ fromtimestamp() today() fromordinal() + strptime() Operators: @@ -729,6 +730,16 @@ y, m, d = _ord2ymd(n) return cls(y, m, d) + @classmethod + def strptime(cls, 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): @@ -1024,6 +1035,7 @@ Constructors: __new__() + strptime() Operators: @@ -1072,6 +1084,16 @@ self._fold = fold 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): @@ -1706,7 +1728,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 -r 1002a1bdc5b1 Lib/test/datetimetester.py --- a/Lib/test/datetimetester.py Sun Aug 07 10:19:20 2016 -0700 +++ b/Lib/test/datetimetester.py Mon Aug 08 09:10:45 2016 +0200 @@ -882,6 +882,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 @@ -2405,6 +2422,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 -r 1002a1bdc5b1 Modules/_datetimemodule.c --- a/Modules/_datetimemodule.c Sun Aug 07 10:19:20 2016 -0700 +++ b/Modules/_datetimemodule.c Mon Aug 08 09:10:45 2016 +0200 @@ -125,6 +125,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); @@ -2600,6 +2603,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. */ @@ -2894,6 +2921,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, @@ -3608,6 +3640,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. */ @@ -3972,6 +4046,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_VARARGS | METH_KEYWORDS, PyDoc_STR("Return string in ISO 8601 format, [HH[:MM[:SS[.mmm[uuu]]]]]" "[+HH:MM].\n\n" @@ -5451,8 +5534,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,