[libc-commits] [libc] [libc] Implement strftime (PR #122556)
Michael Jones via libc-commits
libc-commits at lists.llvm.org
Wed Feb 12 13:35:22 PST 2025
================
@@ -0,0 +1,2329 @@
+//===-- Unittests for strftime --------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "hdr/types/struct_tm.h"
+#include "src/__support/CPP/array.h"
+#include "src/__support/integer_to_string.h"
+#include "src/time/strftime.h"
+#include "src/time/time_constants.h"
+#include "test/UnitTest/Test.h"
+
+// Copied from sprintf_test.cpp.
+// TODO: put this somewhere more reusable, it's handy.
+// Subtract 1 from sizeof(expected_str) to account for the null byte.
+#define EXPECT_STREQ_LEN(actual_written, actual_str, expected_str) \
+ EXPECT_EQ(actual_written, sizeof(expected_str) - 1); \
+ EXPECT_STREQ(actual_str, expected_str);
+
+constexpr int get_adjusted_year(int year) {
+ // tm_year counts years since 1900, so subtract 1900 to get the tm_year for a
+ // given raw year.
+ return year - LIBC_NAMESPACE::time_constants::TIME_YEAR_BASE;
+}
+
+// TODO: Move this somewhere it can be reused. It seems like a useful tool to
+// have.
+// A helper class to generate simple padded numbers. It places the result in its
+// internal buffer, which is cleared on every call.
+class SimplePaddedNum {
+ static constexpr size_t BUFF_LEN = 16;
+ char buff[BUFF_LEN];
+ size_t cur_len; // length of string currently in buff
+
+ void clear_buff() {
+ // TODO: builtin_memset?
+ for (size_t i = 0; i < BUFF_LEN; ++i)
+ buff[i] = '\0';
+ }
+
+public:
+ SimplePaddedNum() = default;
+
+ // PRECONDITIONS: 0 < num < 2**31, min_width < 16
+ // Returns: Pointer to the start of the padded number as a string, stored in
+ // the internal buffer.
+ char *get_padded_num(int num, size_t min_width, char padding_char = '0') {
+ clear_buff();
+
+ // we're not handling the negative sign here, so padding on negative numbers
+ // will be incorrect. For this use case I consider that to be a reasonable
+ // tradeoff for simplicity. This is more meant for the cases where we can
+ // loop through all the possibilities, and for time those are all positive.
+ LIBC_NAMESPACE::IntegerToString<int> raw(num);
+ auto str = raw.view();
+ int leading_zeroes = min_width - raw.size();
+
+ size_t i = 0;
+ for (; static_cast<int>(i) < leading_zeroes; ++i)
+ buff[i] = padding_char;
+ for (size_t str_cur = 0; str_cur < str.size(); ++i, ++str_cur)
+ buff[i] = str[str_cur];
+ cur_len = i;
+ return buff;
+ }
+
+ size_t get_str_len() { return cur_len; }
+};
+
+TEST(LlvmLibcStrftimeTest, ConstantConversions) {
+ // this tests %n, %t, and %%, which read nothing.
+ struct tm time;
+ char buffer[100];
+ size_t written = 0;
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%n", &time);
+ EXPECT_STREQ_LEN(written, buffer, "\n");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%t", &time);
+ EXPECT_STREQ_LEN(written, buffer, "\t");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%%", &time);
+ EXPECT_STREQ_LEN(written, buffer, "%");
+}
+
+TEST(LlvmLibcStrftimeTest, CenturyTests) {
+ // this tests %C, which reads: [tm_year]
+ struct tm time;
+ char buffer[100];
+ size_t written = 0;
+
+ // basic tests
+ time.tm_year = get_adjusted_year(2022);
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%C", &time);
+ EXPECT_STREQ_LEN(written, buffer, "20");
+
+ time.tm_year = get_adjusted_year(11900);
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%C", &time);
+ EXPECT_STREQ_LEN(written, buffer, "119");
+
+ time.tm_year = get_adjusted_year(1900);
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%C", &time);
+ EXPECT_STREQ_LEN(written, buffer, "19");
+
+ time.tm_year = get_adjusted_year(900);
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%C", &time);
+ EXPECT_STREQ_LEN(written, buffer, "09");
+
+ time.tm_year = get_adjusted_year(0);
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%C", &time);
+ EXPECT_STREQ_LEN(written, buffer, "00");
+
+ // This case does not match what glibc does.
+ // Both the C standard and Posix say %C is "Replaced by the year divided by
+ // 100 and truncated to an integer, as a decimal number."
+ // What glibc does is it returns the century for the provided year.
+ // The difference is that glibc returns "-1" as the century for year -1, and
+ // "-2" for year -101.
+ // This case demonstrates that LLVM-libc instead just divides by 100, and
+ // returns the result. "00" for year -1, and "-1" for year -101.
+ // Personally, neither of these really feels right. Posix has a table of
+ // examples where it treats "%C%y" as identical to "%Y". Neither of these
+ // behaviors would handle that properly, you'd either get "-199" or "0099"
+ // (since %y always returns a number in the range [00-99]).
+ time.tm_year = get_adjusted_year(-1);
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%C", &time);
+ EXPECT_STREQ_LEN(written, buffer, "00");
+
+ time.tm_year = get_adjusted_year(-101);
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%C", &time);
+ EXPECT_STREQ_LEN(written, buffer, "-1");
+
+ time.tm_year = get_adjusted_year(-9001);
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%C", &time);
+ EXPECT_STREQ_LEN(written, buffer, "-90");
+
+ time.tm_year = get_adjusted_year(-10001);
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%C", &time);
+ EXPECT_STREQ_LEN(written, buffer, "-100");
+
+ // width tests (with the 0 flag, since the default padding is undefined).
+ time.tm_year = get_adjusted_year(2023);
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%01C", &time);
+ EXPECT_STREQ_LEN(written, buffer, "20");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%02C", &time);
+ EXPECT_STREQ_LEN(written, buffer, "20");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%05C", &time);
+ EXPECT_STREQ_LEN(written, buffer, "00020");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%010C", &time);
+ EXPECT_STREQ_LEN(written, buffer, "0000000020");
+
+ time.tm_year = get_adjusted_year(900);
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%01C", &time);
+ EXPECT_STREQ_LEN(written, buffer, "9");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%02C", &time);
+ EXPECT_STREQ_LEN(written, buffer, "09");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%05C", &time);
+ EXPECT_STREQ_LEN(written, buffer, "00009");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%010C", &time);
+ EXPECT_STREQ_LEN(written, buffer, "0000000009");
+
+ time.tm_year = get_adjusted_year(12345);
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%01C", &time);
+ EXPECT_STREQ_LEN(written, buffer, "123");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%02C", &time);
+ EXPECT_STREQ_LEN(written, buffer, "123");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%05C", &time);
+ EXPECT_STREQ_LEN(written, buffer, "00123");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%010C", &time);
+ EXPECT_STREQ_LEN(written, buffer, "0000000123");
+
+ time.tm_year = get_adjusted_year(-123);
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%01C", &time);
+ EXPECT_STREQ_LEN(written, buffer, "-1");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%02C", &time);
+ EXPECT_STREQ_LEN(written, buffer, "-1");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%05C", &time);
+ EXPECT_STREQ_LEN(written, buffer, "-0001");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%010C", &time);
+ EXPECT_STREQ_LEN(written, buffer, "-000000001");
+
+ // '+' flag tests
+ time.tm_year = get_adjusted_year(2023);
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%+1C", &time);
+ EXPECT_STREQ_LEN(written, buffer, "20");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%+2C", &time);
+ EXPECT_STREQ_LEN(written, buffer, "20");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%+5C", &time);
+ EXPECT_STREQ_LEN(written, buffer, "+0020");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%+10C", &time);
+ EXPECT_STREQ_LEN(written, buffer, "+000000020");
+
+ time.tm_year = get_adjusted_year(900);
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%+1C", &time);
+ EXPECT_STREQ_LEN(written, buffer, "9");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%+2C", &time);
+ EXPECT_STREQ_LEN(written, buffer, "09");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%+5C", &time);
+ EXPECT_STREQ_LEN(written, buffer, "+0009");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%+10C", &time);
+ EXPECT_STREQ_LEN(written, buffer, "+000000009");
+
+ time.tm_year = get_adjusted_year(12345);
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%+1C", &time);
+ EXPECT_STREQ_LEN(written, buffer, "+123");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%+2C", &time);
+ EXPECT_STREQ_LEN(written, buffer, "+123");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%+5C", &time);
+ EXPECT_STREQ_LEN(written, buffer, "+0123");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%+10C", &time);
+ EXPECT_STREQ_LEN(written, buffer, "+000000123");
+
+ time.tm_year = get_adjusted_year(-123);
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%+1C", &time);
+ EXPECT_STREQ_LEN(written, buffer, "-1");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%+2C", &time);
+ EXPECT_STREQ_LEN(written, buffer, "-1");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%+5C", &time);
+ EXPECT_STREQ_LEN(written, buffer, "-0001");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%+10C", &time);
+ EXPECT_STREQ_LEN(written, buffer, "-000000001");
+
+ // Posix specified tests:
+ time.tm_year = get_adjusted_year(17);
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%C", &time);
+ EXPECT_STREQ_LEN(written, buffer, "00");
+
+ time.tm_year = get_adjusted_year(270);
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%C", &time);
+ EXPECT_STREQ_LEN(written, buffer, "02");
+
+ time.tm_year = get_adjusted_year(270);
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%+3C", &time);
+ EXPECT_STREQ_LEN(written, buffer, "+02");
+
+ time.tm_year = get_adjusted_year(12345);
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%+3C", &time);
+ EXPECT_STREQ_LEN(written, buffer, "+123");
+
+ time.tm_year = get_adjusted_year(12345);
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%04C", &time);
+ EXPECT_STREQ_LEN(written, buffer, "0123");
+
+ time.tm_year = get_adjusted_year(12345);
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%+4C", &time);
+ EXPECT_STREQ_LEN(written, buffer, "+123");
+
+ time.tm_year = get_adjusted_year(123456);
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%06C", &time);
+ EXPECT_STREQ_LEN(written, buffer, "001234");
+
+ time.tm_year = get_adjusted_year(123456);
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%+6C", &time);
+ EXPECT_STREQ_LEN(written, buffer, "+01234");
+}
+
+TEST(LlvmLibcStrftimeTest, TwoDigitDayOfMonth) {
+ // this tests %d, which reads: [tm_mday]
+ struct tm time;
+ char buffer[100];
+ size_t written = 0;
+ SimplePaddedNum spn;
+
+ // Tests on all the well defined values
+ for (size_t i = 1; i < 32; ++i) {
+ time.tm_mday = i;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%d", &time);
+ char *result = spn.get_padded_num(i, 2);
+
+ ASSERT_STREQ(buffer, result);
+ ASSERT_EQ(written, size_t(2));
+ }
+
+ // padding is technically undefined for this conversion, but we support it, so
+ // we need to test it.
+ time.tm_mday = 5;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%01d", &time);
+ EXPECT_STREQ_LEN(written, buffer, "5");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%02d", &time);
+ EXPECT_STREQ_LEN(written, buffer, "05");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%05d", &time);
+ EXPECT_STREQ_LEN(written, buffer, "00005");
+
+ time.tm_mday = 31;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%01d", &time);
+ EXPECT_STREQ_LEN(written, buffer, "31");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%02d", &time);
+ EXPECT_STREQ_LEN(written, buffer, "31");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%05d", &time);
+ EXPECT_STREQ_LEN(written, buffer, "00031");
+}
+
+TEST(LlvmLibcStrftimeTest, MinDigitDayOfMonth) {
+ // this tests %e, which reads: [tm_mday]
+ struct tm time;
+ char buffer[100];
+ size_t written = 0;
+ SimplePaddedNum spn;
+
+ // Tests on all the well defined values
+ for (size_t i = 1; i < 32; ++i) {
+ time.tm_mday = i;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%e", &time);
+ char *result = spn.get_padded_num(i, 2, ' ');
+
+ ASSERT_STREQ(buffer, result);
+ ASSERT_EQ(written, spn.get_str_len());
+ }
+
+ // padding is technically undefined for this conversion, but we support it, so
+ // we need to test it.
+ time.tm_mday = 5;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%01e", &time);
+ EXPECT_STREQ_LEN(written, buffer, "5");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%02e", &time);
+ EXPECT_STREQ_LEN(written, buffer, "05");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%05e", &time);
+ EXPECT_STREQ_LEN(written, buffer, "00005");
+
+ time.tm_mday = 31;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%01e", &time);
+ EXPECT_STREQ_LEN(written, buffer, "31");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%02e", &time);
+ EXPECT_STREQ_LEN(written, buffer, "31");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%05e", &time);
+ EXPECT_STREQ_LEN(written, buffer, "00031");
+}
+
+TEST(LlvmLibcStrftimeTest, ISOYearOfCentury) {
+ // this tests %g, which reads: [tm_year, tm_wday, tm_yday]
+
+ // A brief primer on ISO dates:
+ // 1) ISO weeks start on Monday and end on Sunday
+ // 2) ISO years start on the Monday of the 1st ISO week of the year
+ // 3) The 1st ISO week of the ISO year has the 4th day of the Gregorian year.
+
+ struct tm time;
+ char buffer[100];
+ size_t written = 0;
+ SimplePaddedNum spn;
+
+ // a sunday in the middle of the year. No need to worry about rounding
+ time.tm_wday = 0;
+ time.tm_yday = 100;
+
+ // Test the easy cases
+ for (size_t i = 0; i < 102; ++i) {
----------------
michaelrj-google wrote:
making sure that the modulo arithmetic works, but I don't have a strong opinion on keeping it.
https://github.com/llvm/llvm-project/pull/122556
More information about the libc-commits
mailing list