diff --git a/arrow/parser.py b/arrow/parser.py index 7efc419a8..a13b7152e 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -427,6 +427,21 @@ def _build_datetime(parts): elif am_pm == "am" and hour == 12: hour = 0 + # Support for midnight at the end of day + if hour == 24: + if parts.get("minute", 0) != 0: + raise ParserError("Midnight at the end of day must not contain minutes") + if parts.get("second", 0) != 0: + raise ParserError("Midnight at the end of day must not contain seconds") + if parts.get("microsecond", 0) != 0: + raise ParserError( + "Midnight at the end of day must not contain microseconds" + ) + hour = 0 + day_increment = 1 + else: + day_increment = 0 + # account for rounding up to 1000000 microsecond = parts.get("microsecond", 0) if microsecond == 1000000: @@ -435,7 +450,7 @@ def _build_datetime(parts): else: second_increment = 0 - increment = timedelta(seconds=second_increment) + increment = timedelta(days=day_increment, seconds=second_increment) return ( datetime( diff --git a/tests/parser_tests.py b/tests/parser_tests.py index 9677c88b0..93eefc5c5 100644 --- a/tests/parser_tests.py +++ b/tests/parser_tests.py @@ -586,6 +586,48 @@ def test_parse_DDDD_only(self): with self.assertRaises(ParserError): self.parser.parse("145", "DDDD") + def test_parse_HH_24(self): + self.assertEqual( + self.parser.parse("2019-10-30T24:00:00", "YYYY-MM-DDTHH:mm:ss"), + datetime(2019, 10, 31, 0, 0, 0, 0), + ) + self.assertEqual( + self.parser.parse("2019-10-30T24:00", "YYYY-MM-DDTHH:mm"), + datetime(2019, 10, 31, 0, 0, 0, 0), + ) + self.assertEqual( + self.parser.parse("2019-10-30T24", "YYYY-MM-DDTHH"), + datetime(2019, 10, 31, 0, 0, 0, 0), + ) + self.assertEqual( + self.parser.parse("2019-10-30T24:00:00.0", "YYYY-MM-DDTHH:mm:ss.S"), + datetime(2019, 10, 31, 0, 0, 0, 0), + ) + self.assertEqual( + self.parser.parse("2019-10-31T24:00:00", "YYYY-MM-DDTHH:mm:ss"), + datetime(2019, 11, 1, 0, 0, 0, 0), + ) + self.assertEqual( + self.parser.parse("2019-12-31T24:00:00", "YYYY-MM-DDTHH:mm:ss"), + datetime(2020, 1, 1, 0, 0, 0, 0), + ) + self.assertEqual( + self.parser.parse("2019-12-31T23:59:59.9999999", "YYYY-MM-DDTHH:mm:ss.S"), + datetime(2020, 1, 1, 0, 0, 0, 0), + ) + + with self.assertRaises(ParserError): + self.parser.parse("2019-12-31T24:01:00", "YYYY-MM-DDTHH:mm:ss") + + with self.assertRaises(ParserError): + self.parser.parse("2019-12-31T24:00:01", "YYYY-MM-DDTHH:mm:ss") + + with self.assertRaises(ParserError): + self.parser.parse("2019-12-31T24:00:00.1", "YYYY-MM-DDTHH:mm:ss.S") + + with self.assertRaises(ParserError): + self.parser.parse("2019-12-31T24:00:00.999999", "YYYY-MM-DDTHH:mm:ss.S") + class DateTimeParserRegexTests(Chai): def setUp(self): @@ -1176,6 +1218,44 @@ def test_iso8601_basic_format(self): with self.assertRaises(ParserError): self.parser.parse_iso("20180517T1055213Z") + def test_midnight_end_day(self): + self.assertEqual( + self.parser.parse_iso("2019-10-30T24:00:00"), + datetime(2019, 10, 31, 0, 0, 0, 0), + ) + self.assertEqual( + self.parser.parse_iso("2019-10-30T24:00"), + datetime(2019, 10, 31, 0, 0, 0, 0), + ) + self.assertEqual( + self.parser.parse_iso("2019-10-30T24:00:00.0"), + datetime(2019, 10, 31, 0, 0, 0, 0), + ) + self.assertEqual( + self.parser.parse_iso("2019-10-31T24:00:00"), + datetime(2019, 11, 1, 0, 0, 0, 0), + ) + self.assertEqual( + self.parser.parse_iso("2019-12-31T24:00:00"), + datetime(2020, 1, 1, 0, 0, 0, 0), + ) + self.assertEqual( + self.parser.parse_iso("2019-12-31T23:59:59.9999999"), + datetime(2020, 1, 1, 0, 0, 0, 0), + ) + + with self.assertRaises(ParserError): + self.parser.parse_iso("2019-12-31T24:01:00") + + with self.assertRaises(ParserError): + self.parser.parse_iso("2019-12-31T24:00:01") + + with self.assertRaises(ParserError): + self.parser.parse_iso("2019-12-31T24:00:00.1") + + with self.assertRaises(ParserError): + self.parser.parse_iso("2019-12-31T24:00:00.999999") + class TzinfoParserTests(Chai): def setUp(self):