diff --git a/include/znc/Utils.h b/include/znc/Utils.h index 5cc74d84..2ba62996 100644 --- a/include/znc/Utils.h +++ b/include/znc/Utils.h @@ -79,6 +79,23 @@ class CUtils { static CString CTime(time_t t, const CString& sTZ); static CString FormatTime(time_t t, const CString& sFormat, const CString& sTZ); + /** Supports additional format specifiers for formatting sub-second values: + * + * - %L - millisecond, 3 digits (ruby extension) + * - %N - sub-second fraction (ruby extension) + * - %3N - millisecond + * - %6N - microsecond + * - %9N - nanosecond (default if no digit specified) + * + * However, note that timeval only supports microsecond precision + * (thus, formatting with higher-than-microsecond precision will + * always result in trailing zeroes), and IRC server-time is specified + * in millisecond precision (thus formatting received timestamps with + * higher-than-millisecond precision will always result in trailing + * zeroes). + */ + static CString FormatTime(const timeval& tv, const CString& sFormat, + const CString& sTZ); static CString FormatServerTime(const timeval& tv); static timeval ParseServerTime(const CString& sTime); static SCString GetTimezones(); diff --git a/src/Utils.cpp b/src/Utils.cpp index 5ea7985b..72d49f58 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -499,6 +499,67 @@ CString CUtils::FormatTime(time_t t, const CString& sFormat, return s; } +CString CUtils::FormatTime(const timeval& tv, const CString& sFormat, + const CString& sTimezone) { + // Parse additional format specifiers before passing them to + // strftime, since the way strftime treats unknown format + // specifiers is undefined. + CString sFormat2; + + // Make sure %% is parsed correctly, i.e. %%L is passed through to + // strftime to become %L, and not 123. + bool bInFormat = false; + int iDigits; + CString::size_type uLastCopied = 0, uFormatStart; + + for (CString::size_type i = 0; i < sFormat.length(); i++) { + if (!bInFormat) { + if (sFormat[i] == '%') { + uFormatStart = i; + bInFormat = true; + iDigits = 9; + } + } else { + switch (sFormat[i]) { + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + iDigits = sFormat[i] - '0'; + break; + case 'L': + iDigits = 3; + // fall-through + case 'N': { + int iVal = tv.tv_usec; + int iDigitDelta = iDigits - 6; // tv_user is in 10^-6 seconds + for (; iDigitDelta > 0; iDigitDelta--) + iVal *= 10; + for (; iDigitDelta < 0; iDigitDelta++) + iVal /= 10; + sFormat2 += sFormat.substr(uLastCopied, + uFormatStart - uLastCopied); + CString sVal = CString(iVal); + sFormat2 += CString(iDigits - sVal.length(), '0'); + sFormat2 += sVal; + uLastCopied = i + 1; + bInFormat = false; + break; + } + default: + bInFormat = false; + } + } + } + + if (uLastCopied) { + sFormat2 += sFormat.substr(uLastCopied); + return FormatTime(tv.tv_sec, sFormat2, sTimezone); + } else { + // If there are no extended format specifiers, avoid doing any + // memory allocations entirely. + return FormatTime(tv.tv_sec, sFormat, sTimezone); + } +} + CString CUtils::FormatServerTime(const timeval& tv) { CString s_msec(tv.tv_usec / 1000); while (s_msec.length() < 3) { diff --git a/test/UtilsTest.cpp b/test/UtilsTest.cpp index 9560d5c0..ed6a683e 100644 --- a/test/UtilsTest.cpp +++ b/test/UtilsTest.cpp @@ -117,3 +117,46 @@ TEST(UtilsTest, ServerTime) { } tzset(); } + +TEST(UtilsTest, FormatTime) { + struct timeTest { + int sec, usec; + CString sResultL, sResultN, sResult3N; + }; + timeTest aTimeTests[] = { + {42, 12345, "42.012", "42.012345000", "42.012"}, // leading zeroes + {42, 999999, "42.999", "42.999999000", "42.999"}, // (no) rounding + {42, 0, "42.000", "42.000000000", "42.000"}, // no tv_usec part + }; + + for (const timeTest& t : aTimeTests) { + timeval tv; + tv.tv_sec = t.sec; + tv.tv_usec = t.usec; + + CString strL = CUtils::FormatTime(tv, "%s.%L", "UTC"); + EXPECT_EQ(t.sResultL, strL); + CString strN = CUtils::FormatTime(tv, "%s.%N", "UTC"); + EXPECT_EQ(t.sResultN, strN); + CString str3N = CUtils::FormatTime(tv, "%s.%3N", "UTC"); + EXPECT_EQ(t.sResult3N, str3N); + } + + // Test passthrough + timeval tv1; + tv1.tv_sec = 42; + tv1.tv_usec = 123456; + CString str1 = CUtils::FormatTime(tv1, "%s", "UTC"); + EXPECT_EQ("42", str1); + + // Test escapes + timeval tv2; + tv2.tv_sec = 42; + tv2.tv_usec = 123456; + CString str2 = CUtils::FormatTime(tv2, "%%L", "UTC"); + EXPECT_EQ("%L", str2); + + // Test suffix + CString str3 = CUtils::FormatTime(tv2, "a%Lb", "UTC"); + EXPECT_EQ("a123b", str3); +}