From 5a80e31defd906950a0653b2ba4a09d3bef66503 Mon Sep 17 00:00:00 2001 From: Avery Kuo Date: Thu, 24 Sep 2026 08:45:51 -0400 Subject: [PATCH] Preserve millisecond precision when constructing DateTime from an ISO-8601 string --- .changelog/bugfix-datetime-milliseconds.json | 6 +++ .../source/utils/DateTimeCommon.cpp | 18 +++++++- .../utils/DateTimeTest.cpp | 46 +++++++++++++++++++ 3 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 .changelog/bugfix-datetime-milliseconds.json diff --git a/.changelog/bugfix-datetime-milliseconds.json b/.changelog/bugfix-datetime-milliseconds.json new file mode 100644 index 00000000000..19bf2455af7 --- /dev/null +++ b/.changelog/bugfix-datetime-milliseconds.json @@ -0,0 +1,6 @@ +{ + "type": "bugfix", + "category": "aws-cpp-sdk-core", + "contributor": "averyk22", + "description": "Preserve millisecond precision when constructing DateTime from an ISO-8601 string." +} diff --git a/src/aws-cpp-sdk-core/source/utils/DateTimeCommon.cpp b/src/aws-cpp-sdk-core/source/utils/DateTimeCommon.cpp index 937a4ec3820..3204e94c8ba 100644 --- a/src/aws-cpp-sdk-core/source/utils/DateTimeCommon.cpp +++ b/src/aws-cpp-sdk-core/source/utils/DateTimeCommon.cpp @@ -384,7 +384,7 @@ namespace { class DateParser { public: - DateParser(const char* toParse) : m_error(false), m_toParse(toParse), m_utcAssumed(true) + DateParser(const char* toParse) : m_error(false), m_toParse(toParse), m_utcAssumed(true), m_milliseconds(0) { m_parsedTimestamp = CreateZeroedTm(); memset(m_tz, 0, 7); @@ -395,6 +395,7 @@ class DateParser virtual void Parse() = 0; bool WasParseSuccessful() const { return !m_error; } std::tm& GetParsedTimestamp() { return m_parsedTimestamp; } + int GetParsedMilliseconds() const { return m_milliseconds; } bool ShouldIAssumeThisIsUTC() const { return m_utcAssumed; } const char* GetParsedTimezone() const { return m_tz; } @@ -405,6 +406,7 @@ class DateParser bool m_utcAssumed; // The size should be at least one byte greater than the maximum possible size so that we could use the last char to indicate the end of the string. char m_tz[7]; + int m_milliseconds{0}; }; static const int MAX_LEN = 100; @@ -852,6 +854,10 @@ class ISO_8601DateParser : public DateParser m_state = 7; stateStartIndex = index + 1; } + else if (isdigit(c) && (index - stateStartIndex) < 3) + { + m_milliseconds = m_milliseconds * 10 + (c - '0'); + } else if(!isdigit(c)) { m_error = true; @@ -1049,6 +1055,10 @@ class ISO_8601BasicDateParser : public DateParser m_state = 7; stateStartIndex = index + 1; } + else if (isdigit(c) && (index - stateStartIndex) < 3) + { + m_milliseconds = m_milliseconds * 10 + (c - '0'); + } else if (!isdigit(c) || index - stateStartIndex > 3) { m_error = true; @@ -1447,6 +1457,7 @@ void DateTime::ConvertTimestampStringToTimePoint(const char* timestamp, DateForm { std::tm timeStruct; bool isUtc = true; + int milliseconds = 0; switch (format) { @@ -1466,6 +1477,7 @@ void DateTime::ConvertTimestampStringToTimePoint(const char* timestamp, DateForm m_valid = parser.WasParseSuccessful(); isUtc = parser.ShouldIAssumeThisIsUTC(); timeStruct = parser.GetParsedTimestamp(); + milliseconds = parser.GetParsedMilliseconds(); break; } case DateFormat::ISO_8601_BASIC: @@ -1475,6 +1487,7 @@ void DateTime::ConvertTimestampStringToTimePoint(const char* timestamp, DateForm m_valid = parser.WasParseSuccessful(); isUtc = parser.ShouldIAssumeThisIsUTC(); timeStruct = parser.GetParsedTimestamp(); + milliseconds = parser.GetParsedMilliseconds(); break; } case DateFormat::AutoDetect: @@ -1495,6 +1508,7 @@ void DateTime::ConvertTimestampStringToTimePoint(const char* timestamp, DateForm m_valid = true; isUtc = isoParser.ShouldIAssumeThisIsUTC(); timeStruct = isoParser.GetParsedTimestamp(); + milliseconds = isoParser.GetParsedMilliseconds(); break; } ISO_8601BasicDateParser isoBasicParser(timestamp); @@ -1504,6 +1518,7 @@ void DateTime::ConvertTimestampStringToTimePoint(const char* timestamp, DateForm m_valid = true; isUtc = isoBasicParser.ShouldIAssumeThisIsUTC(); timeStruct = isoBasicParser.GetParsedTimestamp(); + milliseconds = isoBasicParser.GetParsedMilliseconds(); break; } m_valid = false; @@ -1529,6 +1544,7 @@ void DateTime::ConvertTimestampStringToTimePoint(const char* timestamp, DateForm if (IsSecondsSinceEpochRepresentable(tt)) { m_time = std::chrono::system_clock::from_time_t(tt); + m_time += std::chrono::milliseconds(milliseconds); } else { diff --git a/tests/aws-cpp-sdk-core-tests/utils/DateTimeTest.cpp b/tests/aws-cpp-sdk-core-tests/utils/DateTimeTest.cpp index ff370864550..ad263019668 100644 --- a/tests/aws-cpp-sdk-core-tests/utils/DateTimeTest.cpp +++ b/tests/aws-cpp-sdk-core-tests/utils/DateTimeTest.cpp @@ -264,6 +264,20 @@ TEST_F(DateTimeTest, TestISO_8601ParsingNSPrecision) ASSERT_EQ(gmtDate, DateTime(utcOffsetVersion, DateFormat::ISO_8601)); } +TEST_F(DateTimeTest, TestISO_8601ParsingPreservesMilliseconds) +{ + DateTime gmtDate("2026-09-15T18:00:00.016Z", DateFormat::ISO_8601); + ASSERT_TRUE(gmtDate.WasParseSuccessful()); + ASSERT_EQ(16, gmtDate.Millis() % 1000); +} + +TEST_F(DateTimeTest, TestISO_8601MillisecondRoundTrip) +{ + DateTime gmtDate("2026-09-15T18:00:00.016Z", DateFormat::ISO_8601); + ASSERT_TRUE(gmtDate.WasParseSuccessful()); + ASSERT_EQ("2026-09-15T18:00:00.016", gmtDate.ToGmtStringWithMs()); +} + TEST_F(DateTimeTest, TestISO_8601ParsingTooLowPrecision) { const char* gmtDateStr = "2002-10-02T08:05:09.12Z"; @@ -296,6 +310,20 @@ TEST_F(DateTimeTest, TestISO_8601BasicParsingMSPrecision) ASSERT_EQ(gmtDate, DateTime(utcOffsetVersion, DateFormat::ISO_8601_BASIC)); } +TEST_F(DateTimeTest, TestISO_8601BasicParsingPreservesMilliseconds) +{ + DateTime gmtDate("20021002T080509016Z", DateFormat::ISO_8601_BASIC); + ASSERT_TRUE(gmtDate.WasParseSuccessful()); + ASSERT_EQ(16, gmtDate.Millis() % 1000); +} + +TEST_F(DateTimeTest, TestISO_8601BasicMillisecondRoundTrip) +{ + DateTime gmtDate("20021002T080509016Z", DateFormat::ISO_8601_BASIC); + ASSERT_TRUE(gmtDate.WasParseSuccessful()); + ASSERT_EQ("2002-10-02T08:05:09.016", gmtDate.ToGmtStringWithMs()); +} + TEST_F(DateTimeTest, TestISO_8601Parsing_DOS_Stopped) { const char* gmtDateStr = "Weddkasdiweijbnawei8eriojngsdgasdgsdf1gasd8asdgfasdfgsdikweisdfksdnsdksdklasdfsdklasdfdfsdfsdfsdfsadfasdafsdfgjjfgghdfgsdfsfsdfsdfasdfsdfasdfsdfasdfsdf"; @@ -380,3 +408,21 @@ TEST_F(DateTimeTest, TestFormatAutoDetect) DateTime parsedBadDate(badDate, DateFormat::AutoDetect); ASSERT_FALSE(parsedBadDate.WasParseSuccessful()); } + +TEST_F(DateTimeTest, TestAutoDetectPreservesMilliseconds) +{ + const char* isoMsDate = "2026-09-15T18:00:00.016Z"; + DateTime parsed(isoMsDate, DateFormat::AutoDetect); + ASSERT_TRUE(parsed.WasParseSuccessful()); + ASSERT_EQ(16, parsed.Millis() % 1000); + ASSERT_EQ(DateTime(isoMsDate, DateFormat::ISO_8601), parsed); +} + +TEST_F(DateTimeTest, TestAutoDetectBasicPreservesMilliseconds) +{ + const char* basicMsDate = "20021002T080509016Z"; + DateTime parsed(basicMsDate, DateFormat::AutoDetect); + ASSERT_TRUE(parsed.WasParseSuccessful()); + ASSERT_EQ(16, parsed.Millis() % 1000); + ASSERT_EQ(DateTime(basicMsDate, DateFormat::ISO_8601_BASIC), parsed); +}