Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changelog/bugfix-datetime-milliseconds.json
Original file line number Diff line number Diff line change
@@ -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."
}
18 changes: 17 additions & 1 deletion src/aws-cpp-sdk-core/source/utils/DateTimeCommon.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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; }

Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -1447,6 +1457,7 @@ void DateTime::ConvertTimestampStringToTimePoint(const char* timestamp, DateForm
{
std::tm timeStruct;
bool isUtc = true;
int milliseconds = 0;

switch (format)
{
Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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);
Expand All @@ -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;
Expand All @@ -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);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit/minor: Time is assigned by comparing to how long it has been since 1970, an edgecase of date before 1970 would render a negative number. if m_time is negative, % 1000 on a negative number stays negative.

Fix: floor when splitting into seconds + ms so the fraction is always 0–999

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure if this is a valid case, can you create a string such that a negative value for a millisecond exists? the state machine should take care of that.

}
else
{
Expand Down
46 changes: 46 additions & 0 deletions tests/aws-cpp-sdk-core-tests/utils/DateTimeTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);

@sbiscigl sbiscigl Sep 22, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't gmtDate.Millis() be 16? and not gmtDate.Millis() % 1000? 2026-09-15T18:00:00.016Z means that theres .016 seconds on the time meaning 16 miliseconds. shouldnt we not need the modulo operation on it? let me know if im mistaken Millis in this context is epoch millis not millis of datetime

}

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";
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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);
}
Loading