Issue #10827: Changed the rules for 2-digit years. The time.asctime

function will now format any year when time.accept2dyear is false and
will accept years >= 1000 otherwise.  The year range accepted by
time.mktime and time.strftime is still system dependent, but
time.mktime will now accept full range supported by the OS. Conversion
of 2-digit years to 4-digit is deprecated.
This commit is contained in:
Alexander Belopolsky 2011-01-07 19:59:19 +00:00
parent 696efdd03f
commit c64708ae48
4 changed files with 102 additions and 56 deletions

View File

@ -24,9 +24,9 @@ An explanation of some terminology and conventions is in order.
.. index:: single: Year 2038 .. index:: single: Year 2038
* The functions in this module do not handle dates and times before the epoch or * The functions in this module may not handle dates and times before the epoch or
far in the future. The cut-off point in the future is determined by the C far in the future. The cut-off point in the future is determined by the C
library; for Unix, it is typically in 2038. library; for 32-bit systems, it is typically in 2038.
.. index:: .. index::
single: Year 2000 single: Year 2000
@ -36,18 +36,29 @@ An explanation of some terminology and conventions is in order.
* **Year 2000 (Y2K) issues**: Python depends on the platform's C library, which * **Year 2000 (Y2K) issues**: Python depends on the platform's C library, which
generally doesn't have year 2000 issues, since all dates and times are generally doesn't have year 2000 issues, since all dates and times are
represented internally as seconds since the epoch. Functions accepting a represented internally as seconds since the epoch. Function :func:`strptime`
:class:`struct_time` (see below) generally require a 4-digit year. For backward can parse 2-digit years when given ``%y`` format code. When 2-digit years are
compatibility, 2-digit years are supported if the module variable parsed, they are converted according to the POSIX and ISO C standards: values
``accept2dyear`` is a non-zero integer; this variable is initialized to ``1`` 69--99 are mapped to 1969--1999, and values 0--68 are mapped to 2000--2068.
unless the environment variable :envvar:`PYTHONY2K` is set to a non-empty
string, in which case it is initialized to ``0``. Thus, you can set For backward compatibility, years with less than 4 digits are treated
:envvar:`PYTHONY2K` to a non-empty string in the environment to require 4-digit specially by :func:`asctime`, :func:`mktime`, and :func:`strftime` functions
years for all year input. When 2-digit years are accepted, they are converted that operate on a 9-tuple or :class:`struct_time` values. If year (the first
according to the POSIX or X/Open standard: values 69-99 are mapped to 1969-1999, value in the 9-tuple) is specified with less than 4 digits, its interpretation
and values 0--68 are mapped to 2000--2068. Values 100--1899 are always illegal. depends on the value of ``accept2dyear`` variable.
Note that this is new as of Python 1.5.2(a2); earlier versions, up to Python
1.5.1 and 1.5.2a1, would add 1900 to year values below 1900. If ``accept2dyear`` is true (default), a backward compatibility behavior is
invoked as follows:
- for 2-digit year, century is guessed according to POSIX rules for
``%y`` strptime format. A deprecation warning is issued when century
information is guessed in this way.
- for 3-digit or negative year, a :exc:`ValueError` exception is raised.
If ``accept2dyear`` is false (set by the program or as a result of a
non-empty value assigned to ``PYTHONY2K`` environment variable) all year
values are interpreted as given.
.. index:: .. index::
single: UTC single: UTC

View File

@ -3,6 +3,7 @@ import time
import unittest import unittest
import locale import locale
import sysconfig import sysconfig
import warnings
class TimeTestCase(unittest.TestCase): class TimeTestCase(unittest.TestCase):
@ -19,10 +20,10 @@ class TimeTestCase(unittest.TestCase):
time.clock() time.clock()
def test_conversions(self): def test_conversions(self):
self.assertTrue(time.ctime(self.t) self.assertEqual(time.ctime(self.t),
== time.asctime(time.localtime(self.t))) time.asctime(time.localtime(self.t)))
self.assertTrue(int(time.mktime(time.localtime(self.t))) self.assertEqual(int(time.mktime(time.localtime(self.t))),
== int(self.t)) int(self.t))
def test_sleep(self): def test_sleep(self):
time.sleep(1.2) time.sleep(1.2)
@ -44,7 +45,7 @@ class TimeTestCase(unittest.TestCase):
# Check year [1900, max(int)] # Check year [1900, max(int)]
self.assertRaises(ValueError, func, self.assertRaises(ValueError, func,
(1899, 1, 1, 0, 0, 0, 0, 1, -1)) (999, 1, 1, 0, 0, 0, 0, 1, -1))
if time.accept2dyear: if time.accept2dyear:
self.assertRaises(ValueError, func, self.assertRaises(ValueError, func,
(-1, 1, 1, 0, 0, 0, 0, 1, -1)) (-1, 1, 1, 0, 0, 0, 0, 1, -1))
@ -97,6 +98,7 @@ class TimeTestCase(unittest.TestCase):
# No test for daylight savings since strftime() does not change output # No test for daylight savings since strftime() does not change output
# based on its value. # based on its value.
expected = "2000 01 01 00 00 00 1 001" expected = "2000 01 01 00 00 00 1 001"
with support.check_warnings():
result = time.strftime("%Y %m %d %H %M %S %w %j", (0,)*9) result = time.strftime("%Y %m %d %H %M %S %w %j", (0,)*9)
self.assertEqual(expected, result) self.assertEqual(expected, result)
@ -141,14 +143,15 @@ class TimeTestCase(unittest.TestCase):
self.assertEqual(time.ctime(t), 'Sun Sep 16 01:03:52 1973') self.assertEqual(time.ctime(t), 'Sun Sep 16 01:03:52 1973')
t = time.mktime((2000, 1, 1, 0, 0, 0, 0, 0, -1)) t = time.mktime((2000, 1, 1, 0, 0, 0, 0, 0, -1))
self.assertEqual(time.ctime(t), 'Sat Jan 1 00:00:00 2000') self.assertEqual(time.ctime(t), 'Sat Jan 1 00:00:00 2000')
for year in [-100, 100, 1000, 2000, 10000]:
try: try:
bigval = time.mktime((10000, 1, 10) + (0,)*6) testval = time.mktime((year, 1, 10) + (0,)*6)
except (ValueError, OverflowError): except (ValueError, OverflowError):
# If mktime fails, ctime will fail too. This may happen # If mktime fails, ctime will fail too. This may happen
# on some platforms. # on some platforms.
pass pass
else: else:
self.assertEqual(time.ctime(bigval)[-5:], '10000') self.assertEqual(time.ctime(testval)[20:], str(year))
@unittest.skipIf(not hasattr(time, "tzset"), @unittest.skipIf(not hasattr(time, "tzset"),
"time module has no attribute tzset") "time module has no attribute tzset")
@ -239,14 +242,14 @@ class TimeTestCase(unittest.TestCase):
gt1 = time.gmtime(None) gt1 = time.gmtime(None)
t0 = time.mktime(gt0) t0 = time.mktime(gt0)
t1 = time.mktime(gt1) t1 = time.mktime(gt1)
self.assertTrue(0 <= (t1-t0) < 0.2) self.assertAlmostEqual(t1, t0, delta=0.2)
def test_localtime_without_arg(self): def test_localtime_without_arg(self):
lt0 = time.localtime() lt0 = time.localtime()
lt1 = time.localtime(None) lt1 = time.localtime(None)
t0 = time.mktime(lt0) t0 = time.mktime(lt0)
t1 = time.mktime(lt1) t1 = time.mktime(lt1)
self.assertTrue(0 <= (t1-t0) < 0.2) self.assertAlmostEqual(t1, t0, delta=0.2)
class TestLocale(unittest.TestCase): class TestLocale(unittest.TestCase):
def setUp(self): def setUp(self):
@ -274,16 +277,18 @@ class TestAccept2Year(unittest.TestCase):
time.accept2dyear = self.saved_accept2dyear time.accept2dyear = self.saved_accept2dyear
def yearstr(self, y): def yearstr(self, y):
return time.strftime('%Y', (y,) + (0,) * 8) # return time.strftime('%Y', (y,) + (0,) * 8)
return time.asctime((y,) + (0,) * 8).split()[-1]
def test_2dyear(self): def test_2dyear(self):
with support.check_warnings():
self.assertEqual(self.yearstr(0), '2000') self.assertEqual(self.yearstr(0), '2000')
self.assertEqual(self.yearstr(69), '1969') self.assertEqual(self.yearstr(69), '1969')
self.assertEqual(self.yearstr(68), '2068') self.assertEqual(self.yearstr(68), '2068')
self.assertEqual(self.yearstr(99), '1999') self.assertEqual(self.yearstr(99), '1999')
def test_invalid(self): def test_invalid(self):
self.assertRaises(ValueError, self.yearstr, 1899) self.assertRaises(ValueError, self.yearstr, 999)
self.assertRaises(ValueError, self.yearstr, 100) self.assertRaises(ValueError, self.yearstr, 100)
self.assertRaises(ValueError, self.yearstr, -1) self.assertRaises(ValueError, self.yearstr, -1)
@ -293,10 +298,15 @@ class TestAccept2YearBool(TestAccept2Year):
class TestDontAccept2Year(TestAccept2Year): class TestDontAccept2Year(TestAccept2Year):
accept2dyear = 0 accept2dyear = 0
def test_2dyear(self): def test_2dyear(self):
self.assertRaises(ValueError, self.yearstr, 0) self.assertEqual(self.yearstr(0), '0')
self.assertRaises(ValueError, self.yearstr, 69) self.assertEqual(self.yearstr(69), '69')
self.assertRaises(ValueError, self.yearstr, 68) self.assertEqual(self.yearstr(68), '68')
self.assertRaises(ValueError, self.yearstr, 99) self.assertEqual(self.yearstr(99), '99')
self.assertEqual(self.yearstr(999), '999')
self.assertEqual(self.yearstr(9999), '9999')
def test_invalid(self):
pass
class TestDontAccept2YearBool(TestDontAccept2Year): class TestDontAccept2YearBool(TestDontAccept2Year):
accept2dyear = False accept2dyear = False

View File

@ -36,6 +36,14 @@ Core and Builtins
Library Library
------- -------
- Issue #10827: Changed the rules for 2-digit years. The time.asctime
function will now format any year when ``time.accept2dyear`` is
false and will accept years >= 1000 otherwise. The year range
accepted by ``time.mktime`` and ``time.strftime`` is still system
dependent, but ``time.mktime`` will now accept full range supported
by the OS. Conversion of 2-digit years to 4-digit is deprecated.
- Issue #7858: Raise an error properly when os.utime() fails under Windows - Issue #7858: Raise an error properly when os.utime() fails under Windows
on an existing file. on an existing file.

View File

@ -312,33 +312,41 @@ gettmarg(PyObject *args, struct tm *p)
&p->tm_wday, &p->tm_yday, &p->tm_isdst)) &p->tm_wday, &p->tm_yday, &p->tm_isdst))
return 0; return 0;
/* XXX: Why 1900? If the goal is to interpret 2-digit years as those in /* If year is specified with less than 4 digits, its interpretation
* 20th / 21st century according to the POSIX standard, we can just treat * depends on the accept2dyear value.
* 0 <= y < 100 as special. Year 100 is probably too ambiguous and should *
* be rejected, but years 101 through 1899 can be passed through. * If accept2dyear is true (default), a backward compatibility behavior is
* invoked as follows:
*
* - for 2-digit year, century is guessed according to POSIX rules for
* %y strptime format: 21st century for y < 69, 20th century
* otherwise. A deprecation warning is issued when century
* information is guessed in this way.
*
* - for 3-digit or negative year, a ValueError exception is raised.
*
* If accept2dyear is false (set by the program or as a result of a
* non-empty value assigned to PYTHONY2K environment variable) all year
* values are interpreted as given.
*/ */
if (y < 1900) { if (y < 1000) {
PyObject *accept = PyDict_GetItemString(moddict, PyObject *accept = PyDict_GetItemString(moddict,
"accept2dyear"); "accept2dyear");
int acceptval = accept != NULL && PyObject_IsTrue(accept); int acceptval = accept != NULL && PyObject_IsTrue(accept);
if (acceptval == -1) if (acceptval == -1)
return 0; return 0;
if (acceptval) { if (acceptval) {
if (69 <= y && y <= 99) if (0 <= y && y < 69)
y += 1900;
else if (0 <= y && y <= 68)
y += 2000; y += 2000;
else if (69 <= y && y < 100)
y += 1900;
else { else {
PyErr_SetString(PyExc_ValueError, PyErr_SetString(PyExc_ValueError,
"year out of range"); "year out of range");
return 0; return 0;
} }
} if (PyErr_WarnEx(PyExc_DeprecationWarning,
/* XXX: When accept2dyear is false, we don't have to reject y < 1900. "Century info guessed for a 2-digit year.", 1) != 0)
* Consider removing the following else-clause. */
else {
PyErr_SetString(PyExc_ValueError,
"year out of range");
return 0; return 0;
} }
} }
@ -462,6 +470,15 @@ time_strftime(PyObject *self, PyObject *args)
else if (!gettmarg(tup, &buf) || !checktm(&buf)) else if (!gettmarg(tup, &buf) || !checktm(&buf))
return NULL; return NULL;
/* XXX: Reportedly, some systems have issues formating dates prior to year
* 1900. These systems should be identified and this check should be
* moved to appropriate system specific section below. */
if (buf.tm_year < 0) {
PyErr_Format(PyExc_ValueError, "year=%d is before 1900; "
"the strftime() method requires year >= 1900",
buf.tm_year + 1900);
}
/* Normalize tm_isdst just in case someone foolishly implements %Z /* Normalize tm_isdst just in case someone foolishly implements %Z
based on the assumption that tm_isdst falls within the range of based on the assumption that tm_isdst falls within the range of
[-1, 1] */ [-1, 1] */