diff --git a/humanfriendly/__init__.py b/humanfriendly/__init__.py index 4c0a333..528ed70 100644 --- a/humanfriendly/__init__.py +++ b/humanfriendly/__init__.py @@ -527,7 +527,8 @@ def parse_date(datestring): :param datestring: The date/time string to parse. :returns: A tuple with the numbers ``(year, month, day, hour, minute, second)`` (all numbers are integers). - :raises: :exc:`InvalidDate` when the date cannot be parsed. + :raises: :exc:`InvalidDate` when the date cannot be parsed or is not a + valid calendar date. Supported date/time formats: @@ -582,10 +583,12 @@ def parse_date(datestring): if len(tokens) >= 2: date_parts = list(map(int, tokens[0].split('-'))) + [1, 1] time_parts = list(map(int, tokens[1].split(':'))) + [0, 0, 0] - return tuple(date_parts[0:3] + time_parts[0:3]) + result = tuple(date_parts[0:3] + time_parts[0:3]) else: year, month, day = (list(map(int, datestring.split('-'))) + [1, 1])[0:3] - return (year, month, day, 0, 0, 0) + result = (year, month, day, 0, 0, 0) + datetime.date(*result[:3]) + return result except Exception: msg = "Invalid date! (expected 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS' but got: %r)" raise InvalidDate(format(msg, datestring)) diff --git a/humanfriendly/tests.py b/humanfriendly/tests.py index 72dad99..564726e 100644 --- a/humanfriendly/tests.py +++ b/humanfriendly/tests.py @@ -490,6 +490,25 @@ def test_parse_date(self): with self.assertRaises(InvalidDate): parse_date('2013-06-XY') + def test_parse_date_invalid_calendar_dates(self): + """Reject impossible dates with and without a time component.""" + for date in ('2023-02-29', '2024-02-30', '2024-04-31', + '2024-00-10', '2024-13-01', '2024-01-00', '2024-01-32'): + for suffix in ('', ' 12:34:56'): + with self.assertRaises(InvalidDate): + parse_date(date + suffix) + + def test_parse_date_calendar_boundaries(self): + """Keep leap days, month ends, and existing partial-date defaults.""" + for date, parts in (('2024-02-29', (2024, 2, 29)), + ('2000-02-29', (2000, 2, 29)), + ('2024-04-30', (2024, 4, 30)), + ('2024-12-31', (2024, 12, 31)), + ('2024', (2024, 1, 1)), + ('2024-02', (2024, 2, 1))): + self.assertEqual(parse_date(date), parts + (0, 0, 0)) + self.assertEqual(parse_date(date + ' 12:34:56'), parts + (12, 34, 56)) + def test_format_size(self): """Test :func:`humanfriendly.format_size()`.""" self.assertEqual('0 bytes', format_size(0))