Add regression tests for 12-hour AM/PM parse and format - #371
Conversation
| EXPECT_EQ("12 AM", cctz::format("%I %p", tp, tz)); | ||
| tp = convert(civil_second(1970, 1, 1, 12, 0, 0), tz); | ||
| EXPECT_EQ("12 PM", cctz::format("%I %p", tp, tz)); | ||
| } |
There was a problem hiding this comment.
These would be better sitting with the other "%p" expectations in Format.LocaleSpecific (they are locale specific, after all). So, instead, ...
@@ -217,6 +217,10 @@ TEST(Format, LocaleSpecific) {
TestFormatSpecifier(tp, tz, "%x", "01/01/70");
TestFormatSpecifier(tp, tz, "%X", "00:00:00");
+ // %I renders midnight and noon as 12, with %p disambiguating them.
+ TestFormatSpecifier(tp, tz, "%I %p", "12 AM");
+ TestFormatSpecifier(tp + chrono::hours(12), tz, "%I %p", "12 PM");
+
#if defined(__linux__)
// SU/C99/TZ extensions
TestFormatSpecifier(tp, tz, "%h", "Jan"); // Same as %b
| TEST(Parse, TwelveHour) { | ||
| const time_zone tz = utc_time_zone(); | ||
| time_point<chrono::nanoseconds> tp; | ||
|
|
||
| // %I maps 12 to 0 and a following %p shifts the afternoon, so "12 AM" is | ||
| // midnight and "12 PM" is noon. These boundary values are the 12-hour cases | ||
| // most prone to cross-platform bugs (see, e.g., newlib's handling of %I). | ||
| EXPECT_TRUE(parse("%I %p", "12 AM", tz, &tp)); | ||
| EXPECT_EQ(0, convert(tp, tz).hour()); | ||
| EXPECT_TRUE(parse("%I %p", "12 PM", tz, &tp)); | ||
| EXPECT_EQ(12, convert(tp, tz).hour()); | ||
| EXPECT_TRUE(parse("%I %p", "01 AM", tz, &tp)); | ||
| EXPECT_EQ(1, convert(tp, tz).hour()); | ||
| EXPECT_TRUE(parse("%I %p", "11 PM", tz, &tp)); | ||
| EXPECT_EQ(23, convert(tp, tz).hour()); | ||
|
|
||
| #if defined(__linux__) | ||
| // %r is the locale's full 12-hour time; it must land on the correct side of | ||
| // the AM/PM boundary too. | ||
| EXPECT_TRUE(parse("%r", "12:00:00 AM", tz, &tp)); | ||
| EXPECT_EQ(0, convert(tp, tz).hour()); | ||
| EXPECT_TRUE(parse("%r", "12:00:00 PM", tz, &tp)); | ||
| EXPECT_EQ(12, convert(tp, tz).hour()); | ||
| #endif | ||
| } |
There was a problem hiding this comment.
Similarly, these would also be better as part of Parse.LocaleSpecific, so, instead, ...
@@ -1080,6 +1094,22 @@ TEST(Parse, LocaleSpecific) {
EXPECT_TRUE(parse("%I %p", "5 PM", tz, &tp));
EXPECT_EQ(17, convert(tp, tz).hour());
+ // %I maps 12 to 0 and a following %p shifts to the afternoon,
+ // so "12 AM" is midnight and "12 PM" is noon.
+ tp = reset;
+ EXPECT_TRUE(parse("%I %p", "12 AM", tz, &tp));
+ EXPECT_EQ(0, convert(tp, tz).hour());
+ tp = reset;
+ EXPECT_TRUE(parse("%I %p", "12 PM", tz, &tp));
+ EXPECT_EQ(12, convert(tp, tz).hour());
+
+ tp = reset;
+ EXPECT_TRUE(parse("%I %p", "01 AM", tz, &tp));
+ EXPECT_EQ(1, convert(tp, tz).hour());
+ tp = reset;
+ EXPECT_TRUE(parse("%I %p", "11 PM", tz, &tp));
+ EXPECT_EQ(23, convert(tp, tz).hour());
+
tp = reset;
EXPECT_TRUE(parse("%x", "02/03/04", tz, &tp));
if (convert(tp, tz).month() == 2) {
and
@@ -1115,6 +1145,18 @@ TEST(Parse, LocaleSpecific) {
EXPECT_EQ(44, convert(tp, tz).minute());
EXPECT_EQ(55, convert(tp, tz).second());
+ tp = reset;
+ EXPECT_TRUE(parse("%r", "12:00:00 AM", tz, &tp));
+ EXPECT_EQ(0, convert(tp, tz).hour());
+ EXPECT_EQ(0, convert(tp, tz).minute());
+ EXPECT_EQ(0, convert(tp, tz).second());
+
+ tp = reset;
+ EXPECT_TRUE(parse("%r", "12:00:00 PM", tz, &tp));
+ EXPECT_EQ(12, convert(tp, tz).hour());
+ EXPECT_EQ(0, convert(tp, tz).minute());
+ EXPECT_EQ(0, convert(tp, tz).second());
+
#if defined(__GLIBC__)
tp = reset;
EXPECT_TRUE(parse("%Ec", "Tue Nov 19 05:06:07 2013", tz, &tp));
|
In the PR description:
So that just leaves, "the cases most prone to bugs." |
Drop the 'cross-platform' qualifier and the reference to newlib's handling of %I, as requested in review, leaving just 'the 12-hour cases most prone to bugs.'
When parsing a 12-hour time with "%I %p", the parser re-parses the
matched %p text against a known hour ("1" + the AM/PM token) to learn
whether the platform's strptime() shifts the hour into the afternoon.
Some strptime() implementations, notably the std::get_time()-based
fallback used on Windows, cannot parse "%I%p" together and leave the
hour unchanged, so PM times were treated as AM.
When that check does not indicate afternoon, fall back to inspecting the
matched %p text directly: a leading 'p' (case-insensitively) denotes PM.
This makes the 12-hour AM/PM parse regression tests pass on Windows.
|
Thank you for the review. I've simplified the wording as requested: the "cross-platform" qualifier and the reference to newlib's handling are removed, leaving just "the 12-hour cases most prone to bugs." The matching comment in While finalizing I also found that the I also merged the current Could you please take another look? |
Please revert this. We're not in the business of working around bugs in a platform's
Don't forget to move the new expectations into |
|
Got it. I will revert the custom %p probe and move the test cases into Format.LocaleSpecific and Parse.LocaleSpecific. Please take another look once updated. |
…allback" This reverts commit 0b670f6.
The %I/%p and %r boundary checks are locale-specific, so they belong with the existing %p expectations in Format.LocaleSpecific and Parse.LocaleSpecific rather than standing alone in TwelveHour tests.
|
Thank you for the follow-up. Both points are addressed:
I verified the assertions against a local build of the library: the pre-existing Could you please take another look? |
devbww
left a comment
There was a problem hiding this comment.
Looks good to me. Thanks.
Passing off to a Google owner now; perhaps @derekmauro or @pkumar0508.
@derekmauro: These new expectations appear to pass, so I am fine with merging them. Thanks. |
The 12-hour (%I/%l with %p) conversion is handled subtly: %I maps 12 to 0 and a following %p shifts the afternoon hours. The boundary values "12 AM" (midnight) and "12 PM" (noon) are the cases most prone to cross-platform bugs (newlib's strptime handled "12 AM" incorrectly until recently).
These tests pin the correct behavior on every supported platform:
No production code changes; this only adds regression coverage. Verified against a local build of the library sources.