Skip to content

Commit 719aac5

Browse files
committed
fix(Foundation): %c format specifier emits centiseconds (#3949)
Documentation describes %c as "centisecond" but the implementation was emitting a single-digit decisecond (millisecond / 100). Bring the code in line with the documentation: %c now formats and parses a zero-padded two-digit centisecond value (00 .. 99), i.e. millisecond / 10. Affects DateTimeFormatter (DateTime and Timespan overloads), DateTimeParser, and PatternFormatter. The format/parse pair stays consistent so round-tripping a value via "%H:%M:%S.%c" preserves the centisecond resolution. Updated docs in DateTimeFormatter.h and PatternFormatter.h to read "centisecond (00 .. 99)". Adjusted the existing testCustom assertion that hard-coded the old single-digit output, and added focused testFractionalSpecifiers tests in both DateTimeFormatterTest and DateTimeParserTest covering the boundary values and a format/parse round trip. Note: this is a behavioural change for callers that relied on %c producing a single digit. PatternFormatter timestamps that include %c (e.g. the format used by the Logger and FastLogger samples, "%H:%M:%S.%c") will now contain one extra character.
1 parent 47b8c67 commit 719aac5

9 files changed

Lines changed: 92 additions & 10 deletions

Foundation/include/Poco/DateTimeFormatter.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ class Foundation_API DateTimeFormatter
7272
/// * %S - second (00 .. 59)
7373
/// * %s - seconds and microseconds (equivalent to %S.%F)
7474
/// * %i - millisecond (000 .. 999)
75-
/// * %c - centisecond (0 .. 9)
75+
/// * %c - centisecond (00 .. 99)
7676
/// * %F - fractional seconds/microseconds (000000 - 999999)
7777
/// * %z - time zone differential in ISO 8601 format (Z or +NN:NN)
7878
/// * %Z - time zone differential in RFC format (GMT or +NNNN)
@@ -102,7 +102,7 @@ class Foundation_API DateTimeFormatter
102102
/// * %S - seconds (00 .. 59)
103103
/// * %s - total seconds (0 .. n)
104104
/// * %i - milliseconds (000 .. 999)
105-
/// * %c - centisecond (0 .. 9)
105+
/// * %c - centisecond (00 .. 99)
106106
/// * %F - fractional seconds/microseconds (000000 - 999999)
107107
/// * %% - percent sign
108108

Foundation/include/Poco/PatternFormatter.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ class Foundation_API PatternFormatter: public Formatter
6767
/// * %M - message date/time minute (00 .. 59)
6868
/// * %S - message date/time second (00 .. 59)
6969
/// * %i - message date/time millisecond (000 .. 999)
70-
/// * %c - message date/time centisecond (0 .. 9)
70+
/// * %c - message date/time centisecond (00 .. 99)
7171
/// * %F - message date/time fractional seconds/microseconds (000000 - 999999)
7272
/// * %z - time zone differential in ISO 8601 format (Z or +NN:NN)
7373
/// * %Z - time zone differential in RFC format (GMT or +NNNN)

Foundation/src/DateTimeFormatter.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ void DateTimeFormatter::append(std::string& str, const DateTime& dateTime, const
6262
NumberFormatter::append0(str, dateTime.millisecond()*1000 + dateTime.microsecond(), 6);
6363
break;
6464
case 'i': NumberFormatter::append0(str, dateTime.millisecond(), 3); break;
65-
case 'c': NumberFormatter::append(str, dateTime.millisecond()/100); break;
65+
case 'c': NumberFormatter::append0(str, dateTime.millisecond()/10, 2); break;
6666
case 'F': NumberFormatter::append0(str, dateTime.millisecond()*1000 + dateTime.microsecond(), 6); break;
6767
case 'z': tzdISO(str, timeZoneDifferential); break;
6868
case 'Z': tzdRFC(str, timeZoneDifferential); break;
@@ -96,7 +96,7 @@ void DateTimeFormatter::append(std::string& str, const Timespan& timespan, const
9696
case 'S': NumberFormatter::append0(str, timespan.seconds(), 2); break;
9797
case 's': NumberFormatter::append(str, timespan.totalSeconds()); break;
9898
case 'i': NumberFormatter::append0(str, timespan.milliseconds(), 3); break;
99-
case 'c': NumberFormatter::append(str, timespan.milliseconds()/100); break;
99+
case 'c': NumberFormatter::append0(str, timespan.milliseconds()/10, 2); break;
100100
case 'F': NumberFormatter::append0(str, timespan.milliseconds()*1000 + timespan.microseconds(), 6); break;
101101
default: str += *it;
102102
}

Foundation/src/DateTimeParser.cpp

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,23 @@ void DateTimeParser::parse(const std::string& fmt, const std::string& dtStr, Dat
171171
std::string::const_iterator itf = fmt.begin();
172172
std::string::const_iterator endf = fmt.end();
173173

174+
// %S optionally swallows '.NNN'/',NNN' so a trailing %z can still reach
175+
// the timezone designator, but only when the format does not capture the
176+
// fractional itself via %c/%i/%F/%s.
177+
bool fmtHasFracSpec = false;
178+
for (auto p = fmt.begin(); p + 1 < fmt.end(); ++p)
179+
{
180+
if (*p == '%')
181+
{
182+
char n = *(p + 1);
183+
if (n == 'c' || n == 'i' || n == 'F' || n == 's')
184+
{
185+
fmtHasFracSpec = true;
186+
break;
187+
}
188+
}
189+
}
190+
174191
while (itf != endf && it != end)
175192
{
176193
if (*itf == '%')
@@ -246,7 +263,9 @@ void DateTimeParser::parse(const std::string& fmt, const std::string& dtStr, Dat
246263
// Consume optional fractional seconds ('.NNN' or ',NNN') so that a
247264
// subsequent %z specifier can reach the timezone designator.
248265
// A decimal point/comma not followed by a digit is an error.
249-
if (it != end && (*it == '.' || *it == ','))
266+
// Skipped when the format captures the fractional itself via
267+
// %c/%i/%F/%s -- those specifiers must see the digits.
268+
if (!fmtHasFracSpec && it != end && (*it == '.' || *it == ','))
250269
{
251270
++it;
252271
if (it == end || !Ascii::isDigit(*it))
@@ -280,8 +299,8 @@ void DateTimeParser::parse(const std::string& fmt, const std::string& dtStr, Dat
280299
break;
281300
case 'c':
282301
it = skipNonDigits(it, end);
283-
millis = parseNumberN(dtStr, it, end, 1);
284-
millis *= 100;
302+
millis = parseNumberN(dtStr, it, end, 2);
303+
millis *= 10;
285304
break;
286305
case 'F':
287306
it = skipNonDigits(it, end);

Foundation/src/PatternFormatter.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ void PatternFormatter::format(const Message& msg, std::string& text)
110110
case 'M': NumberFormatter::append0(text, dateTime.minute(), 2); break;
111111
case 'S': NumberFormatter::append0(text, dateTime.second(), 2); break;
112112
case 'i': NumberFormatter::append0(text, dateTime.millisecond(), 3); break;
113-
case 'c': NumberFormatter::append(text, dateTime.millisecond()/100); break;
113+
case 'c': NumberFormatter::append0(text, dateTime.millisecond()/10, 2); break;
114114
case 'F': NumberFormatter::append0(text, dateTime.millisecond()*1000 + dateTime.microsecond(), 6); break;
115115
case 'z': text.append(DateTimeFormatter::tzdISO(localTime ? Timezone::tzd() : DateTimeFormatter::UTC)); break;
116116
case 'Z': text.append(DateTimeFormatter::tzdRFC(localTime ? Timezone::tzd() : DateTimeFormatter::UTC)); break;

Foundation/testsuite/src/DateTimeFormatterTest.cpp

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,34 @@ void DateTimeFormatterTest::testCustom()
183183
DateTime dt(2005, 1, 8, 12, 30, 00, 250);
184184

185185
std::string str = DateTimeFormatter::format(dt, "%w/%W/%b/%B/%d/%e/%f/%m/%n/%o/%y/%Y/%H/%h/%a/%A/%M/%S/%i/%c/%z/%Z/%%");
186-
assertTrue (str == "Sat/Saturday/Jan/January/08/8/ 8/01/1/ 1/05/2005/12/12/pm/PM/30/00/250/2/Z/GMT/%");
186+
assertTrue (str == "Sat/Saturday/Jan/January/08/8/ 8/01/1/ 1/05/2005/12/12/pm/PM/30/00/250/25/Z/GMT/%");
187+
}
188+
189+
190+
void DateTimeFormatterTest::testFractionalSpecifiers()
191+
{
192+
// %c is a zero-padded centisecond (millisecond / 10) in the range 00..99,
193+
// matching the documentation in DateTimeFormatter.h. See issue #3949 --
194+
// previously %c was emitting a single-digit decisecond (millisecond / 100).
195+
DateTime dt(2005, 1, 8, 12, 30, 0, 0);
196+
assertTrue (DateTimeFormatter::format(dt, "%i") == "000");
197+
assertTrue (DateTimeFormatter::format(dt, "%c") == "00");
198+
assertTrue (DateTimeFormatter::format(dt, "%F") == "000000");
199+
200+
DateTime dtLow(2005, 1, 8, 12, 30, 0, 5);
201+
assertTrue (DateTimeFormatter::format(dtLow, "%i") == "005");
202+
assertTrue (DateTimeFormatter::format(dtLow, "%c") == "00");
203+
assertTrue (DateTimeFormatter::format(dtLow, "%F") == "005000");
204+
205+
DateTime dtMid(2005, 1, 8, 12, 30, 0, 250);
206+
assertTrue (DateTimeFormatter::format(dtMid, "%i") == "250");
207+
assertTrue (DateTimeFormatter::format(dtMid, "%c") == "25");
208+
assertTrue (DateTimeFormatter::format(dtMid, "%F") == "250000");
209+
210+
DateTime dtMax(2005, 1, 8, 12, 30, 0, 999, 999);
211+
assertTrue (DateTimeFormatter::format(dtMax, "%i") == "999");
212+
assertTrue (DateTimeFormatter::format(dtMax, "%c") == "99");
213+
assertTrue (DateTimeFormatter::format(dtMax, "%F") == "999999");
187214
}
188215

189216

@@ -240,6 +267,7 @@ CppUnit::Test* DateTimeFormatterTest::suite()
240267
CppUnit_addTest(pSuite, DateTimeFormatterTest, testASCTIME);
241268
CppUnit_addTest(pSuite, DateTimeFormatterTest, testSORTABLE);
242269
CppUnit_addTest(pSuite, DateTimeFormatterTest, testCustom);
270+
CppUnit_addTest(pSuite, DateTimeFormatterTest, testFractionalSpecifiers);
243271
CppUnit_addTest(pSuite, DateTimeFormatterTest, testTimespan);
244272

245273
return pSuite;

Foundation/testsuite/src/DateTimeFormatterTest.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ class DateTimeFormatterTest: public CppUnit::TestCase
3535
void testASCTIME();
3636
void testSORTABLE();
3737
void testCustom();
38+
void testFractionalSpecifiers();
3839
void testTimespan();
3940

4041
void setUp();

Foundation/testsuite/src/DateTimeParserTest.cpp

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
#include "CppUnit/TestCaller.h"
1313
#include "CppUnit/TestSuite.h"
1414
#include "Poco/DateTimeParser.h"
15+
#include "Poco/DateTimeFormatter.h"
1516
#include "Poco/DateTimeFormat.h"
1617
#include "Poco/DateTime.h"
1718
#include "Poco/Timestamp.h"
@@ -20,6 +21,7 @@
2021

2122
using Poco::DateTime;
2223
using Poco::DateTimeFormat;
24+
using Poco::DateTimeFormatter;
2325
using Poco::DateTimeParser;
2426
using Poco::Timestamp;
2527
using Poco::SyntaxException;
@@ -669,6 +671,36 @@ void DateTimeParserTest::testISO8601FracSeconds()
669671
}
670672

671673

674+
void DateTimeParserTest::testFractionalSpecifiers()
675+
{
676+
// %c is now a two-digit centisecond (millisecond / 10) rather than a
677+
// single-digit decisecond (millisecond / 100). Round-trip every value
678+
// against DateTimeFormatter to ensure parser and formatter agree --
679+
// see issue #3949.
680+
int tzd = 0;
681+
682+
// Parse "00" -> 0 ms, "25" -> 250 ms, "99" -> 990 ms.
683+
DateTime dt = DateTimeParser::parse("%H:%M:%S.%c", "12:30:00.00", tzd);
684+
assertTrue (dt.millisecond() == 0);
685+
686+
dt = DateTimeParser::parse("%H:%M:%S.%c", "12:30:00.25", tzd);
687+
assertTrue (dt.millisecond() == 250);
688+
689+
dt = DateTimeParser::parse("%H:%M:%S.%c", "12:30:00.99", tzd);
690+
assertTrue (dt.millisecond() == 990);
691+
692+
// Format-then-parse round trip.
693+
DateTime original(2005, 1, 8, 12, 30, 0, 250);
694+
std::string formatted = DateTimeFormatter::format(original, "%H:%M:%S.%c");
695+
assertTrue (formatted == "12:30:00.25");
696+
dt = DateTimeParser::parse("%H:%M:%S.%c", formatted, tzd);
697+
assertTrue (dt.hour() == 12);
698+
assertTrue (dt.minute() == 30);
699+
assertTrue (dt.second() == 0);
700+
assertTrue (dt.millisecond() == 250);
701+
}
702+
703+
672704
void DateTimeParserTest::testGuess()
673705
{
674706
int tzd;
@@ -949,6 +981,7 @@ CppUnit::Test* DateTimeParserTest::suite()
949981
CppUnit_addTest(pSuite, DateTimeParserTest, testSORTABLE);
950982
CppUnit_addTest(pSuite, DateTimeParserTest, testCustom);
951983
CppUnit_addTest(pSuite, DateTimeParserTest, testISO8601FracSeconds);
984+
CppUnit_addTest(pSuite, DateTimeParserTest, testFractionalSpecifiers);
952985
CppUnit_addTest(pSuite, DateTimeParserTest, testGuess);
953986
CppUnit_addTest(pSuite, DateTimeParserTest, testCleanup);
954987
CppUnit_addTest(pSuite, DateTimeParserTest, testParseMonth);

Foundation/testsuite/src/DateTimeParserTest.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ class DateTimeParserTest: public CppUnit::TestCase
3535
void testSORTABLE();
3636
void testCustom();
3737
void testISO8601FracSeconds();
38+
void testFractionalSpecifiers();
3839
void testGuess();
3940
void testCleanup();
4041
void testParseMonth();

0 commit comments

Comments
 (0)