[libc-commits] [libc] [libc] Implement strftime (PR #122556)
Michael Jones via libc-commits
libc-commits at lists.llvm.org
Tue Jan 28 16:51:32 PST 2025
https://github.com/michaelrj-google updated https://github.com/llvm/llvm-project/pull/122556
>From 9e875ecb904cc15eae55de2c45c9474dfe59b439 Mon Sep 17 00:00:00 2001
From: Michael Jones <michaelrj at google.com>
Date: Thu, 9 Jan 2025 16:52:50 -0800
Subject: [PATCH 1/4] [libc] Implement strftime
TODO: DESCRIPTION
TODO: TESTS
TODO: RotFO (Rest of the Formatting Options)
Roughly based on #111305, but with major rewrites.
---
libc/config/linux/x86_64/entrypoints.txt | 1 +
libc/docs/dev/undefined_behavior.rst | 24 ++
libc/include/llvm-libc-types/struct_tm.h | 1 +
libc/include/time.yaml | 19 ++
libc/src/time/CMakeLists.txt | 17 ++
libc/src/time/mktime.cpp | 32 +--
libc/src/time/strftime.cpp | 31 +++
libc/src/time/strftime.h | 23 ++
libc/src/time/strftime_core/CMakeLists.txt | 51 ++++
libc/src/time/strftime_core/converter.cpp | 91 +++++++
libc/src/time/strftime_core/converter.h | 29 +++
libc/src/time/strftime_core/core_structs.h | 53 ++++
libc/src/time/strftime_core/num_converter.h | 165 ++++++++++++
libc/src/time/strftime_core/parser.h | 110 ++++++++
libc/src/time/strftime_core/strftime_main.cpp | 41 +++
libc/src/time/strftime_core/strftime_main.h | 26 ++
libc/src/time/time_constants.h | 14 +-
libc/src/time/time_utils.h | 185 ++++++++++++++
libc/test/src/time/CMakeLists.txt | 11 +
libc/test/src/time/strftime_test.cpp | 234 ++++++++++++++++++
20 files changed, 1138 insertions(+), 20 deletions(-)
create mode 100644 libc/src/time/strftime.cpp
create mode 100644 libc/src/time/strftime.h
create mode 100644 libc/src/time/strftime_core/CMakeLists.txt
create mode 100644 libc/src/time/strftime_core/converter.cpp
create mode 100644 libc/src/time/strftime_core/converter.h
create mode 100644 libc/src/time/strftime_core/core_structs.h
create mode 100644 libc/src/time/strftime_core/num_converter.h
create mode 100644 libc/src/time/strftime_core/parser.h
create mode 100644 libc/src/time/strftime_core/strftime_main.cpp
create mode 100644 libc/src/time/strftime_core/strftime_main.h
create mode 100644 libc/test/src/time/strftime_test.cpp
diff --git a/libc/config/linux/x86_64/entrypoints.txt b/libc/config/linux/x86_64/entrypoints.txt
index 7e549607716c02..7f8019d4986b94 100644
--- a/libc/config/linux/x86_64/entrypoints.txt
+++ b/libc/config/linux/x86_64/entrypoints.txt
@@ -1103,6 +1103,7 @@ if(LLVM_LIBC_FULL_BUILD)
libc.src.time.gmtime_r
libc.src.time.mktime
libc.src.time.nanosleep
+ libc.src.time.strftime
libc.src.time.time
libc.src.time.timespec_get
diff --git a/libc/docs/dev/undefined_behavior.rst b/libc/docs/dev/undefined_behavior.rst
index d0d882b7010e37..2360c943852b95 100644
--- a/libc/docs/dev/undefined_behavior.rst
+++ b/libc/docs/dev/undefined_behavior.rst
@@ -106,3 +106,27 @@ uninitialized spinlock and invalid spinlock is left undefined. We follow the rec
POSIX.1-2024, where EINVAL is returned if the spinlock is invalid (here we only check for null pointers) or
EBUSY is returned if the spinlock is currently locked. The lock is poisoned after a successful destroy. That is,
subsequent operations on the lock object without any reinitialization will return EINVAL.
+
+Strftime
+--------
+In the C Standard, it provides a list of modifiers, and the conversions these
+are valid on. It also says that a modifier on an unspecified conversion is
+undefined. For LLVM-libc, the conversion is treated as if the modifier isn't
+there.
+
+If a struct tm with values out of the normal range is passed, the standard says
+the result is undefined. For LLVM-libc, the result may be either the normalized
+value (e.g. weekday % 7) or the actual, out of range value. This behavior is not
+necessarily consistent between conversions, even similar ones. For conversions
+that result in strings, passing an out of range value will result in "?".
+
+Posix adds padding support to strftime, but says "the default padding character
+is unspecified." For LLVM-libc, the default padding character is ' ' (space),
+for all string-type conversions and '0' for integer-type conversions.
+
+Posix also adds flags and a minimum field width, but leaves unspecified what
+happens for most combinations of these. For LLVM-libc:
+An unspecified minimum field width defaults to 0.
+More specific flags take precedence over less specific flags (i.e. '+' takes precedence over '0')
+Any conversion with a minimum width is padded with the padding character until it is at least as long as the minimum width.
+Modifiers are applied, then the result is padded if necessary.
diff --git a/libc/include/llvm-libc-types/struct_tm.h b/libc/include/llvm-libc-types/struct_tm.h
index 9fef7c5718ea4a..2ec74ecac0293b 100644
--- a/libc/include/llvm-libc-types/struct_tm.h
+++ b/libc/include/llvm-libc-types/struct_tm.h
@@ -19,6 +19,7 @@ struct tm {
int tm_wday; // days since Sunday
int tm_yday; // days since January
int tm_isdst; // Daylight Saving Time flag
+ // TODO: add tm_gmtoff and tm_zone? (posix extensions)
};
#endif // LLVM_LIBC_TYPES_STRUCT_TM_H
diff --git a/libc/include/time.yaml b/libc/include/time.yaml
index b71b9ab72075b2..41eb318a9dbdc0 100644
--- a/libc/include/time.yaml
+++ b/libc/include/time.yaml
@@ -91,6 +91,25 @@ functions:
arguments:
- type: const struct timespec *
- type: struct timespec *
+ - name: strftime
+ standard:
+ - stdc
+ return_type: size_t
+ arguments:
+ - type: char *__restrict
+ - type: size_t
+ - type: const char *__restrict
+ - type: const struct tm *__restrict
+ - name: strftime_l
+ standard:
+ - stdc
+ return_type: size_t
+ arguments:
+ - type: char *__restrict
+ - type: size_t
+ - type: const char *__restrict
+ - type: const struct tm *__restrict
+ - type: locale_t
- name: time
standard:
- stdc
diff --git a/libc/src/time/CMakeLists.txt b/libc/src/time/CMakeLists.txt
index ef9bfe57bc4ec2..8332e8ab66f971 100644
--- a/libc/src/time/CMakeLists.txt
+++ b/libc/src/time/CMakeLists.txt
@@ -22,6 +22,8 @@ add_object_library(
DEPENDS
libc.include.time
libc.src.__support.CPP.limits
+ libc.src.__support.CPP.string_view
+ libc.src.__support.CPP.optional
libc.src.errno.errno
.time_constants
libc.hdr.types.time_t
@@ -133,6 +135,21 @@ add_entrypoint_object(
libc.hdr.types.struct_tm
)
+add_subdirectory(strftime_core) #TODO: Move to top
+
+add_entrypoint_object(
+ strftime
+ SRCS
+ strftime.cpp
+ HDRS
+ strftime.h
+ DEPENDS
+ libc.hdr.types.size_t
+ libc.hdr.types.struct_tm
+ libc.src.stdio.printf_core.writer
+ libc.src.time.strftime_core.strftime_main
+)
+
add_entrypoint_object(
time
SRCS
diff --git a/libc/src/time/mktime.cpp b/libc/src/time/mktime.cpp
index 3874cad02facbd..636f2ff7e74e45 100644
--- a/libc/src/time/mktime.cpp
+++ b/libc/src/time/mktime.cpp
@@ -14,16 +14,6 @@
namespace LIBC_NAMESPACE_DECL {
-// Returns number of years from (1, year).
-static constexpr int64_t get_num_of_leap_years_before(int64_t year) {
- return (year / 4) - (year / 100) + (year / 400);
-}
-
-// Returns True if year is a leap year.
-static constexpr bool is_leap_year(const int64_t year) {
- return (((year) % 4) == 0 && (((year) % 100) != 0 || ((year) % 400) == 0));
-}
-
LLVM_LIBC_FUNCTION(time_t, mktime, (struct tm * tm_out)) {
// Unlike most C Library functions, mktime doesn't just die on bad input.
// TODO(rtenneti); Handle leap seconds.
@@ -69,7 +59,7 @@ LLVM_LIBC_FUNCTION(time_t, mktime, (struct tm * tm_out)) {
}
tm_year_from_base += years;
}
- bool tm_year_is_leap = is_leap_year(tm_year_from_base);
+ bool tm_year_is_leap = time_utils::is_leap_year(tm_year_from_base);
// Calculate total number of days based on the month and the day (tm_mday).
int64_t total_days = tm_out->tm_mday - 1;
@@ -83,21 +73,25 @@ LLVM_LIBC_FUNCTION(time_t, mktime, (struct tm * tm_out)) {
total_days += (tm_year_from_base - time_constants::EPOCH_YEAR) *
time_constants::DAYS_PER_NON_LEAP_YEAR;
if (tm_year_from_base >= time_constants::EPOCH_YEAR) {
- total_days += get_num_of_leap_years_before(tm_year_from_base - 1) -
- get_num_of_leap_years_before(time_constants::EPOCH_YEAR);
+ total_days +=
+ time_utils::get_num_of_leap_years_before(tm_year_from_base - 1) -
+ time_utils::get_num_of_leap_years_before(time_constants::EPOCH_YEAR);
} else if (tm_year_from_base >= 1) {
- total_days -= get_num_of_leap_years_before(time_constants::EPOCH_YEAR) -
- get_num_of_leap_years_before(tm_year_from_base - 1);
+ total_days -=
+ time_utils::get_num_of_leap_years_before(time_constants::EPOCH_YEAR) -
+ time_utils::get_num_of_leap_years_before(tm_year_from_base - 1);
} else {
// Calculate number of leap years until 0th year.
- total_days -= get_num_of_leap_years_before(time_constants::EPOCH_YEAR) -
- get_num_of_leap_years_before(0);
+ total_days -=
+ time_utils::get_num_of_leap_years_before(time_constants::EPOCH_YEAR) -
+ time_utils::get_num_of_leap_years_before(0);
if (tm_year_from_base <= 0) {
total_days -= 1; // Subtract 1 for 0th year.
// Calculate number of leap years until -1 year
if (tm_year_from_base < 0) {
- total_days -= get_num_of_leap_years_before(-tm_year_from_base) -
- get_num_of_leap_years_before(1);
+ total_days -=
+ time_utils::get_num_of_leap_years_before(-tm_year_from_base) -
+ time_utils::get_num_of_leap_years_before(1);
}
}
}
diff --git a/libc/src/time/strftime.cpp b/libc/src/time/strftime.cpp
new file mode 100644
index 00000000000000..f72bedb6fbf72f
--- /dev/null
+++ b/libc/src/time/strftime.cpp
@@ -0,0 +1,31 @@
+//===-- Implementation of strftime function -------------------------------===//
+//
+// 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 "src/time/strftime.h"
+#include "hdr/types/size_t.h"
+#include "hdr/types/struct_tm.h"
+#include "src/__support/common.h"
+#include "src/__support/macros/config.h"
+#include "src/stdio/printf_core/writer.h"
+#include "src/time/strftime_core/strftime_main.h"
+
+namespace LIBC_NAMESPACE_DECL {
+
+LLVM_LIBC_FUNCTION(size_t, strftime,
+ (char *__restrict buffer, size_t buffsz,
+ const char *__restrict format, const struct tm *timeptr)) {
+
+ printf_core::WriteBuffer wb(buffer, (buffsz > 0 ? buffsz - 1 : 0));
+ printf_core::Writer writer(&wb);
+ int ret = strftime_core::strftime_main(&writer, format, timeptr);
+ if (buffsz > 0) // if the buffsz is 0 the buffer may be a null pointer.
+ wb.buff[wb.buff_cur] = '\0';
+ return (ret < 0 || static_cast<size_t>(ret) > buffsz) ? 0 : ret;
+}
+
+} // namespace LIBC_NAMESPACE_DECL
diff --git a/libc/src/time/strftime.h b/libc/src/time/strftime.h
new file mode 100644
index 00000000000000..d8f562859c395f
--- /dev/null
+++ b/libc/src/time/strftime.h
@@ -0,0 +1,23 @@
+//===-- Implementation header of strftime -----------------------*- C++ -*-===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_LIBC_SRC_TIME_STRFTIME_H
+#define LLVM_LIBC_SRC_TIME_STRFTIME_H
+
+#include "hdr/types/size_t.h"
+#include "hdr/types/struct_tm.h"
+#include "src/__support/macros/config.h"
+
+namespace LIBC_NAMESPACE_DECL {
+
+size_t strftime(char *__restrict, size_t max, const char *__restrict format,
+ const struct tm *timeptr);
+
+} // namespace LIBC_NAMESPACE_DECL
+
+#endif // LLVM_LIBC_SRC_TIME_STRFTIME_H
diff --git a/libc/src/time/strftime_core/CMakeLists.txt b/libc/src/time/strftime_core/CMakeLists.txt
new file mode 100644
index 00000000000000..deaf7a4f3e2f76
--- /dev/null
+++ b/libc/src/time/strftime_core/CMakeLists.txt
@@ -0,0 +1,51 @@
+add_header_library(
+ core_structs
+ HDRS
+ core_structs.h
+ DEPENDS
+ libc.src.__support.CPP.string_view
+ libc.hdr.types.struct_tm
+)
+
+add_header_library(
+ parser
+ HDRS
+ parser.h
+ DEPENDS
+ .core_structs
+ libc.src.__support.CPP.string_view
+ libc.src.__support.ctype_utils
+ libc.src.__support.str_to_integer
+)
+
+add_object_library(
+ converter
+ SRCS
+ converter.cpp
+ HDRS
+ converter.h
+ num_converter.h
+ # str_converter.h
+ # composite_converter.h
+ DEPENDS
+ .core_structs
+ libc.src.time.time_utils
+ libc.src.time.time_constants
+ libc.src.stdio.printf_core.writer
+ libc.src.__support.CPP.string_view
+ libc.src.__support.integer_to_string
+)
+
+add_object_library(
+ strftime_main
+ SRCS
+ strftime_main.cpp
+ HDRS
+ strftime_main.h
+ DEPENDS
+ .core_structs
+ .parser
+ .converter
+ libc.src.stdio.printf_core.writer
+ libc.hdr.types.struct_tm
+)
diff --git a/libc/src/time/strftime_core/converter.cpp b/libc/src/time/strftime_core/converter.cpp
new file mode 100644
index 00000000000000..1a6d7ad4ffc943
--- /dev/null
+++ b/libc/src/time/strftime_core/converter.cpp
@@ -0,0 +1,91 @@
+//===-- Format specifier converter implmentation for strftime -------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See htto_conv.times://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "src/__support/macros/config.h"
+#include "src/stdio/printf_core/writer.h"
+#include "src/time/strftime_core/core_structs.h"
+
+// #include "composite_converter.h"
+#include "num_converter.h"
+// #include "str_converter.h"
+
+namespace LIBC_NAMESPACE_DECL {
+namespace strftime_core {
+
+int convert(printf_core::Writer *writer, const FormatSection &to_conv,
+ const tm *timeptr) {
+ // TODO: Implement the locale support.
+ if (to_conv.modifier != ConvModifier::none) {
+ return writer->write(to_conv.raw_string);
+ }
+
+ if (!to_conv.has_conv)
+ return writer->write(to_conv.raw_string);
+ switch (to_conv.conv_name) {
+ // The cases are grouped by type, then alphabetized with lowercase before
+ // uppercase.
+
+ // raw conversions
+ case '%':
+ return writer->write("%");
+ case 'n':
+ return writer->write("\n");
+ case 't':
+ return writer->write("\t");
+
+ // numeric conversions
+ case 'C': // Century
+ case 'd': // Day of the month [01-31]
+ case 'e': // Day of the month [1-31]
+ case 'g': // last 2 digits of ISO year
+ case 'G': // ISO year
+ case 'H': // 24-hour format
+ case 'I': // 12-hour format
+ case 'j': // Day of the year
+ case 'm': // Month of the year
+ case 'M': // Minute of the hour
+ case 's': // Seconds since the epoch
+ case 'S': // Second of the minute
+ case 'u': // ISO day of the week ([1-7] starting Monday)
+ case 'U': // Week of the year ([00-53] week 1 starts on first *Sunday*)
+ case 'V': // ISO week number ([01-53], 01 is first week majority in this year)
+ case 'w': // Day of week ([0-6] starting Sunday)
+ case 'W': // Week of the year ([00-53] week 1 starts on first *Monday*)
+ case 'y': // Year of the Century
+ case 'Y': // Full year
+ return convert_int(writer, to_conv, timeptr);
+
+ // string conversions
+ case 'a': // Abbreviated weekday name
+ case 'A': // Full weekday name
+ case 'b': // Abbreviated month name
+ case 'B': // Full month name
+ case 'h': // same as %b
+ case 'p': // AM/PM designation
+ case 'z': // Timezone offset (+/-hhmm)
+ case 'Z': // Timezone name
+ // return write_str(writer, to_conv, timeptr);
+
+ // composite conversions
+ case 'c': // locale specified date and time
+ case 'D': // %m/%d/%y (month/day/year)
+ case 'F': // %Y-%m-%d (year-month-day)
+ case 'r': // %I:%M:%S %p (hour:minute:second AM/PM)
+ case 'R': // %H:%M (hour:minute)
+ case 'T': // %H:%M:%S (hour:minute:second)
+ case 'x': // locale specified date
+ case 'X': // locale specified time
+ // return write_composite(writer, to_conv, timeptr);
+ default:
+ return writer->write(to_conv.raw_string);
+ }
+ return 0;
+}
+
+} // namespace strftime_core
+} // namespace LIBC_NAMESPACE_DECL
diff --git a/libc/src/time/strftime_core/converter.h b/libc/src/time/strftime_core/converter.h
new file mode 100644
index 00000000000000..131bbc4fa25872
--- /dev/null
+++ b/libc/src/time/strftime_core/converter.h
@@ -0,0 +1,29 @@
+//===-- Format specifier converter for strftime -----------------*- C++ -*-===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_LIBC_SRC_STDIO_STRFTIME_CORE_CONVERTER_H
+#define LLVM_LIBC_SRC_STDIO_STRFTIME_CORE_CONVERTER_H
+
+#include "src/stdio/printf_core/writer.h"
+#include "src/time/strftime_core/core_structs.h"
+
+#include <stddef.h>
+
+namespace LIBC_NAMESPACE_DECL {
+namespace strftime_core {
+
+// convert will call a conversion function to convert the FormatSection into
+// its string representation, and then that will write the result to the
+// writer.
+int convert(printf_core::Writer *writer, const FormatSection &to_conv,
+ const tm *timeptr);
+
+} // namespace strftime_core
+} // namespace LIBC_NAMESPACE_DECL
+
+#endif // LLVM_LIBC_SRC_STDIO_STRFTIME_CORE_CONVERTER_H
diff --git a/libc/src/time/strftime_core/core_structs.h b/libc/src/time/strftime_core/core_structs.h
new file mode 100644
index 00000000000000..2553d6486fdef8
--- /dev/null
+++ b/libc/src/time/strftime_core/core_structs.h
@@ -0,0 +1,53 @@
+//===-- Core Structures for strftime ----------------------------*- C++ -*-===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_LIBC_SRC_STDIO_STRFTIME_CORE_CORE_STRUCTS_H
+#define LLVM_LIBC_SRC_STDIO_STRFTIME_CORE_CORE_STRUCTS_H
+
+#include "hdr/types/struct_tm.h"
+#include "src/__support/CPP/string_view.h"
+
+#include <inttypes.h>
+
+namespace LIBC_NAMESPACE_DECL {
+namespace strftime_core {
+
+enum class ConvModifier { none, E, O };
+
+// These flags intentionally have different values from the ones used by printf.
+// They have different meanings.
+enum FormatFlags : uint8_t {
+ FORCE_SIGN = 0x01, // +
+ LEADING_ZEROES = 0x02, // 0
+ // TODO: look into the glibc extension flags ('_', '-', '^', and '#')
+};
+
+struct FormatSection {
+ bool has_conv;
+ cpp::string_view raw_string;
+
+ FormatFlags flags = FormatFlags(0);
+ ConvModifier modifier;
+ char conv_name;
+ int min_width = 0;
+};
+
+// TODO: Move this to a better spot
+#define RET_IF_RESULT_NEGATIVE(func) \
+ { \
+ int result = (func); \
+ if (result < 0) \
+ return result; \
+ }
+
+constexpr int WRITE_OK = 0;
+
+} // namespace strftime_core
+} // namespace LIBC_NAMESPACE_DECL
+
+#endif // LLVM_LIBC_SRC_STDIO_STRFTIME_CORE_CORE_STRUCTS_H
diff --git a/libc/src/time/strftime_core/num_converter.h b/libc/src/time/strftime_core/num_converter.h
new file mode 100644
index 00000000000000..b196be52d928d0
--- /dev/null
+++ b/libc/src/time/strftime_core/num_converter.h
@@ -0,0 +1,165 @@
+//===-- Format specifier converter for printf -------------------*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See htto_conv.times://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_LIBC_SRC_STDIO_STRFTIME_CORE_NUM_CONVERTER_H
+#define LLVM_LIBC_SRC_STDIO_STRFTIME_CORE_NUM_CONVERTER_H
+
+#include "src/__support/CPP/string_view.h"
+#include "src/__support/integer_to_string.h"
+#include "src/__support/macros/config.h"
+#include "src/stdio/printf_core/writer.h"
+#include "src/time/strftime_core/core_structs.h"
+#include "src/time/time_constants.h"
+#include "src/time/time_utils.h"
+
+namespace LIBC_NAMESPACE_DECL {
+namespace strftime_core {
+
+using DecFmt = IntegerToString<uintmax_t>;
+
+LIBC_INLINE int convert_int(printf_core::Writer *writer,
+ const FormatSection &to_conv, const tm *timeptr) {
+ const time_utils::TMReader time_reader(timeptr);
+
+ intmax_t raw_num;
+ size_t pad_to_len;
+
+ // gets_plus_sign is only true for year conversions where the year would be
+ // positive and more than 4 digits, including leading spaces. Both the
+ // FORCE_SIGN flag and gets_plus_sign must be true for a plus sign to be
+ // output.
+ bool gets_plus_sign = false;
+
+ switch (to_conv.conv_name) {
+ case 'C': // Century [00-99]
+ raw_num = time_reader.get_year() / 100;
+ gets_plus_sign = raw_num > 99 || to_conv.min_width > 2;
+ pad_to_len = 2;
+ break;
+ case 'd': // Day of the month [01-31]
+ raw_num = time_reader.get_mday();
+ pad_to_len = 2;
+ break;
+ case 'e': // Day of the month [1-31]
+ raw_num = time_reader.get_mday();
+ pad_to_len = 1;
+ break;
+ case 'g': // last 2 digits of ISO year [00-99]
+ raw_num = time_reader.get_iso_year() % 100;
+ pad_to_len = 2;
+ break;
+ case 'G': // ISO year
+ raw_num = time_reader.get_iso_year();
+ gets_plus_sign = raw_num > 9999 || to_conv.min_width > 4;
+ pad_to_len = 0;
+ break;
+ case 'H': // 24-hour format [00-23]
+ raw_num = time_reader.get_hour();
+ pad_to_len = 2;
+ break;
+ case 'I': // 12-hour format [01-12]
+ raw_num = time_reader.get_hour() % 12;
+ pad_to_len = 2;
+ break;
+ case 'j': // Day of the year [001-366]
+ raw_num = time_reader.get_yday();
+ pad_to_len = 3;
+ break;
+ case 'm': // Month of the year [01-12]
+ raw_num = time_reader.get_mon() + 1; // get_mon is 0 indexed
+ pad_to_len = 2;
+ break;
+ case 'M': // Minute of the hour [00-59]
+ raw_num = time_reader.get_min();
+ pad_to_len = 2;
+ break;
+ case 's': // Seconds since the epoch
+ raw_num = time_reader.get_epoch();
+ pad_to_len = 0;
+ break;
+ case 'S': // Second of the minute [00-60]
+ raw_num = time_reader.get_sec();
+ pad_to_len = 2;
+ break;
+ case 'u': // ISO day of the week ([1-7] starting Monday)
+ raw_num = time_reader.get_iso_wday() + 1;
+ // need to add 1 because get_iso_wday returns the weekday [0-6].
+ pad_to_len = 1;
+ break;
+ case 'U': // Week of the year ([00-53] week 1 starts on first *Sunday*)
+ raw_num = time_reader.get_week(time_constants::SUNDAY);
+ pad_to_len = 2;
+ break;
+ case 'V': // ISO week number ([01-53], 01 is first week majority in this year)
+ raw_num = time_reader.get_iso_week();
+ pad_to_len = 2;
+ break;
+ case 'w': // Day of week ([0-6] starting Sunday)
+ raw_num = time_reader.get_wday();
+ pad_to_len = 1;
+ break;
+ case 'W': // Week of the year ([00-53] week 1 starts on first *Monday*)
+ raw_num = time_reader.get_week(time_constants::MONDAY);
+ pad_to_len = 2;
+ break;
+ case 'y': // Year of the Century [00-99]
+ raw_num = time_reader.get_year() % 100;
+ pad_to_len = 2;
+ break;
+ case 'Y': // Full year
+ raw_num = time_reader.get_year();
+ gets_plus_sign = raw_num > 9999 || to_conv.min_width > 4;
+ pad_to_len = 0;
+ break;
+ default:
+ __builtin_trap(); // this should be unreachable, but trap if you hit it.
+ }
+
+ const uintmax_t num =
+ static_cast<uintmax_t>(raw_num < 0 ? -raw_num : raw_num);
+ const bool is_negative = raw_num < 0;
+
+ DecFmt d(num);
+ auto str = d.view();
+
+ size_t digits_written = str.size();
+
+ char sign_char = 0;
+
+ // TODO: Handle locale modifiers
+
+ if (is_negative)
+ sign_char = '-';
+ else if ((to_conv.flags & FormatFlags::FORCE_SIGN) ==
+ FormatFlags::FORCE_SIGN &&
+ gets_plus_sign)
+ sign_char = '+';
+
+ // sign isn't a problem because we're taking the max. The result is always
+ // non-negative.
+ if (static_cast<int>(pad_to_len) < to_conv.min_width)
+ pad_to_len = to_conv.min_width;
+
+ // one less digit of padding if there's a sign char
+ int zeroes =
+ static_cast<int>(pad_to_len - digits_written - (sign_char == 0 ? 0 : 1));
+
+ // Format is (sign) (zeroes) digits
+ if (sign_char != 0)
+ RET_IF_RESULT_NEGATIVE(writer->write(sign_char));
+ if (zeroes > 0)
+ RET_IF_RESULT_NEGATIVE(writer->write('0', zeroes))
+ RET_IF_RESULT_NEGATIVE(writer->write(str));
+
+ return WRITE_OK;
+}
+
+} // namespace strftime_core
+} // namespace LIBC_NAMESPACE_DECL
+
+#endif
diff --git a/libc/src/time/strftime_core/parser.h b/libc/src/time/strftime_core/parser.h
new file mode 100644
index 00000000000000..0301a67a91eb2f
--- /dev/null
+++ b/libc/src/time/strftime_core/parser.h
@@ -0,0 +1,110 @@
+//===-- Format string parser for printf -------------------------*- C++ -*-===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_LIBC_SRC_STDIO_STRFTIME_CORE_PARSER_H
+#define LLVM_LIBC_SRC_STDIO_STRFTIME_CORE_PARSER_H
+
+#include "core_structs.h"
+#include "src/__support/CPP/string_view.h"
+#include "src/__support/ctype_utils.h"
+#include "src/__support/macros/config.h"
+#include "src/__support/str_to_integer.h"
+
+namespace LIBC_NAMESPACE_DECL {
+namespace strftime_core {
+
+class Parser {
+ const char *str;
+ size_t cur_pos = 0;
+
+public:
+ LIBC_INLINE Parser(const char *new_str) : str(new_str) {}
+
+ // get_next_section will parse the format string until it has a fully
+ // specified format section. This can either be a raw format section with no
+ // conversion, or a format section with a conversion that has all of its
+ // variables stored in the format section.
+ LIBC_INLINE FormatSection get_next_section() {
+ FormatSection section;
+ size_t starting_pos = cur_pos;
+
+ if (str[cur_pos] != '%') {
+ // raw section
+ section.has_conv = false;
+ while (str[cur_pos] != '%' && str[cur_pos] != '\0')
+ ++cur_pos;
+ section.raw_string = {str + starting_pos, cur_pos - starting_pos};
+ return section;
+ }
+
+ // format section
+ section.has_conv = true;
+ ++cur_pos;
+
+ // flags
+ section.flags = parse_flags(&cur_pos);
+
+ // handle width
+ section.min_width = 0;
+ if (internal::isdigit(str[cur_pos])) {
+ auto result = internal::strtointeger<int>(str + cur_pos, 10);
+ section.min_width = result.value;
+ cur_pos = cur_pos + result.parsed_len;
+ }
+
+ // modifiers
+ switch (str[cur_pos]) {
+ case ('E'):
+ section.modifier = ConvModifier::E;
+ ++cur_pos;
+ break;
+ case ('O'):
+ section.modifier = ConvModifier::O;
+ ++cur_pos;
+ break;
+ default:
+ section.modifier = ConvModifier::none;
+ }
+
+ section.conv_name = str[cur_pos];
+
+ // If the end of the format section is on the '\0'. This means we need to
+ // not advance the cur_pos.
+ if (str[cur_pos] != '\0')
+ ++cur_pos;
+
+ section.raw_string = {str + starting_pos, cur_pos - starting_pos};
+ return section;
+ }
+
+private:
+ LIBC_INLINE FormatFlags parse_flags(size_t *local_pos) {
+ bool found_flag = true;
+ FormatFlags flags = FormatFlags(0);
+ while (found_flag) {
+ switch (str[*local_pos]) {
+ case '+':
+ flags = static_cast<FormatFlags>(flags | FormatFlags::FORCE_SIGN);
+ break;
+ case '0':
+ flags = static_cast<FormatFlags>(flags | FormatFlags::LEADING_ZEROES);
+ break;
+ default:
+ found_flag = false;
+ }
+ if (found_flag)
+ ++*local_pos;
+ }
+ return flags;
+ }
+};
+
+} // namespace strftime_core
+} // namespace LIBC_NAMESPACE_DECL
+
+#endif // LLVM_LIBC_SRC_STDIO_STRFTIME_CORE_PARSER_H
diff --git a/libc/src/time/strftime_core/strftime_main.cpp b/libc/src/time/strftime_core/strftime_main.cpp
new file mode 100644
index 00000000000000..1306b16ff971d6
--- /dev/null
+++ b/libc/src/time/strftime_core/strftime_main.cpp
@@ -0,0 +1,41 @@
+//===-- Starting point 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 "src/time/strftime_core/strftime_main.h"
+
+#include "src/stdio/printf_core/writer.h"
+#include "src/time/strftime_core/converter.h"
+#include "src/time/strftime_core/core_structs.h"
+#include "src/time/strftime_core/parser.h"
+
+#include "hdr/types/struct_tm.h"
+
+namespace LIBC_NAMESPACE_DECL {
+namespace strftime_core {
+
+int strftime_main(printf_core::Writer *writer, const char *__restrict str,
+ const struct tm *timeptr) {
+ Parser parser(str);
+ int result = 0;
+ for (FormatSection cur_section = parser.get_next_section();
+ !cur_section.raw_string.empty();
+ cur_section = parser.get_next_section()) {
+ if (cur_section.has_conv)
+ result = convert(writer, cur_section, timeptr);
+ else
+ result = writer->write(cur_section.raw_string);
+
+ if (result < 0)
+ return result;
+ }
+
+ return writer->get_chars_written();
+}
+
+} // namespace strftime_core
+} // namespace LIBC_NAMESPACE_DECL
diff --git a/libc/src/time/strftime_core/strftime_main.h b/libc/src/time/strftime_core/strftime_main.h
new file mode 100644
index 00000000000000..f5c144a4153bf6
--- /dev/null
+++ b/libc/src/time/strftime_core/strftime_main.h
@@ -0,0 +1,26 @@
+//===-- Starting point for strftime ------------------------------*- C++-*-===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_LIBC_SRC_STDIO_STRFTIME_CORE_STRFTIME_MAIN_H
+#define LLVM_LIBC_SRC_STDIO_STRFTIME_CORE_STRFTIME_MAIN_H
+
+#include "src/__support/macros/config.h"
+#include "src/stdio/printf_core/writer.h"
+
+#include "hdr/types/struct_tm.h"
+
+namespace LIBC_NAMESPACE_DECL {
+namespace strftime_core {
+
+int strftime_main(printf_core::Writer *writer, const char *__restrict str,
+ const struct tm *timeptr);
+
+} // namespace strftime_core
+} // namespace LIBC_NAMESPACE_DECL
+
+#endif // LLVM_LIBC_SRC_STDIO_STRFTIME_CORE_STRFTIME_MAIN_H
diff --git a/libc/src/time/time_constants.h b/libc/src/time/time_constants.h
index 3e25f741745ab5..341b12ec9812b0 100644
--- a/libc/src/time/time_constants.h
+++ b/libc/src/time/time_constants.h
@@ -18,7 +18,7 @@ namespace LIBC_NAMESPACE_DECL {
namespace time_constants {
enum Month : int {
- JANUARY,
+ JANUARY = 0,
FEBRUARY,
MARCH,
APRIL,
@@ -32,6 +32,16 @@ enum Month : int {
DECEMBER
};
+enum WeekDay : int {
+ SUNDAY = 0,
+ MONDAY,
+ TUESDAY,
+ WEDNESDAY,
+ THURSDAY,
+ FRIDAY,
+ SATURDAY
+};
+
constexpr int SECONDS_PER_MIN = 60;
constexpr int MINUTES_PER_HOUR = 60;
constexpr int HOURS_PER_DAY = 24;
@@ -49,6 +59,8 @@ constexpr int TIME_YEAR_BASE = 1900;
constexpr int EPOCH_YEAR = 1970;
constexpr int EPOCH_WEEK_DAY = 4;
+constexpr int ISO_FIRST_DAY_OF_YEAR = 4;
+
// For asctime the behavior is undefined if struct tm's tm_wday or tm_mon are
// not within the normal ranges as defined in <time.h>, or if struct tm's
// tm_year exceeds {INT_MAX}-1990, or if the below asctime_internal algorithm
diff --git a/libc/src/time/time_utils.h b/libc/src/time/time_utils.h
index 5e0a692d4db048..af2e489f78ec0a 100644
--- a/libc/src/time/time_utils.h
+++ b/libc/src/time/time_utils.h
@@ -12,7 +12,10 @@
#include "hdr/types/size_t.h"
#include "hdr/types/struct_tm.h"
#include "hdr/types/time_t.h"
+#include "src/__support/CPP/optional.h"
+#include "src/__support/CPP/string_view.h"
#include "src/__support/common.h"
+#include "src/__support/libc_assert.h"
#include "src/__support/macros/config.h"
#include "src/errno/libc_errno.h"
#include "time_constants.h"
@@ -94,6 +97,188 @@ LIBC_INLINE struct tm *localtime(const time_t *t_ptr) {
return time_utils::gmtime_internal(t_ptr, &result);
}
+// Returns number of years from (1, year).
+LIBC_INLINE constexpr int64_t get_num_of_leap_years_before(int64_t year) {
+ return (year / 4) - (year / 100) + (year / 400);
+}
+
+// Returns True if year is a leap year.
+LIBC_INLINE constexpr bool is_leap_year(const int64_t year) {
+ return (((year) % 4) == 0 && (((year) % 100) != 0 || ((year) % 400) == 0));
+}
+
+// This is a helper class that takes a struct tm and lets you inspect its
+// values. Where relevant, results are bounds checked and returned as optionals.
+// This class does not, however, do data normalization except where necessary.
+// It will faithfully return a date of 9999-99-99, even though that makes no
+// sense.
+class TMReader final {
+ const tm *timeptr;
+
+public:
+ LIBC_INLINE constexpr TMReader(const tm *tmptr) : timeptr(tmptr) { ; }
+
+ LIBC_INLINE constexpr cpp::optional<cpp::string_view>
+ get_weekday_short_name() const {
+ if (timeptr->tm_wday > 0 &&
+ timeptr->tm_wday < time_constants::DAYS_PER_WEEK)
+ return time_constants::WEEK_DAY_NAMES[timeptr->tm_wday];
+
+ return cpp::nullopt;
+ }
+
+ LIBC_INLINE constexpr cpp::optional<cpp::string_view>
+ get_weekday_full_name() const {
+ if (timeptr->tm_wday > 0 &&
+ timeptr->tm_wday < time_constants::DAYS_PER_WEEK)
+ return time_constants::WEEK_DAY_FULL_NAMES[timeptr->tm_wday];
+
+ return cpp::nullopt;
+ }
+
+ LIBC_INLINE constexpr cpp::optional<cpp::string_view>
+ get_month_short_name() const {
+ if (timeptr->tm_mon > 0 &&
+ timeptr->tm_mon < time_constants::MONTHS_PER_YEAR)
+ return time_constants::MONTH_NAMES[timeptr->tm_mon];
+
+ return cpp::nullopt;
+ }
+
+ LIBC_INLINE constexpr cpp::optional<cpp::string_view>
+ get_month_full_name() const {
+ if (timeptr->tm_mon > 0 &&
+ timeptr->tm_mon < time_constants::MONTHS_PER_YEAR)
+ return time_constants::MONTH_FULL_NAMES[timeptr->tm_mon];
+
+ return cpp::nullopt;
+ }
+
+ LIBC_INLINE constexpr int get_sec() const { return timeptr->tm_sec; }
+ LIBC_INLINE constexpr int get_min() const { return timeptr->tm_min; }
+ LIBC_INLINE constexpr int get_hour() const { return timeptr->tm_hour; }
+ LIBC_INLINE constexpr int get_mday() const { return timeptr->tm_mday; }
+ LIBC_INLINE constexpr int get_mon() const { return timeptr->tm_mon; }
+ LIBC_INLINE constexpr int get_yday() const { return timeptr->tm_yday; }
+ LIBC_INLINE constexpr int get_wday() const { return timeptr->tm_wday; }
+ LIBC_INLINE constexpr int get_isdst() const { return timeptr->tm_isdst; }
+
+ // returns the year, counting from 1900
+ LIBC_INLINE constexpr int get_year_raw() const { return timeptr->tm_year; }
+ // returns the year, counting from 0
+ LIBC_INLINE constexpr int get_year() const {
+ return timeptr->tm_year + time_constants::TIME_YEAR_BASE;
+ }
+
+ LIBC_INLINE constexpr int is_leap_year() const {
+ return time_utils::is_leap_year(get_year());
+ }
+
+ LIBC_INLINE constexpr int get_days_in_year() const {
+ return is_leap_year() ? time_constants::DAYS_PER_LEAP_YEAR
+ : time_constants::DAYS_PER_NON_LEAP_YEAR;
+ }
+
+ LIBC_INLINE constexpr int get_iso_wday() const {
+ // ISO uses a week that starts on Monday, but struct tm starts its week on
+ // Sunday. This function normalizes the weekday so that it always returns a
+ // value 0-6
+ const int NORMALIZED_WDAY =
+ timeptr->tm_wday % time_constants::DAYS_PER_WEEK;
+ return (NORMALIZED_WDAY + (time_constants::DAYS_PER_WEEK - 1)) % 7;
+ }
+
+ // returns the week of the current year, with weeks starting on start_day.
+ LIBC_INLINE constexpr int get_week(time_constants::WeekDay start_day) const {
+ // The most recent start_day. The rest of the days into the current week
+ // don't count, so ignore them.
+ const int start_of_cur_week =
+ timeptr->tm_yday -
+ ((timeptr->tm_wday - start_day) % time_constants::DAYS_PER_WEEK);
+
+ const int ceil_weeks_since_start =
+ (start_of_cur_week + (time_constants::DAYS_PER_WEEK - 1)) /
+ time_constants::DAYS_PER_WEEK;
+ return ceil_weeks_since_start;
+ }
+
+ LIBC_INLINE constexpr int get_iso_week() const {
+ const int BASE_WEEK = get_week(time_constants::THURSDAY);
+
+ if (BASE_WEEK >= 1 && BASE_WEEK <= 52)
+ return BASE_WEEK;
+ return -1; // TODO: FINISH THIS
+ }
+
+ LIBC_INLINE constexpr int get_iso_year() const {
+ const int BASE_YEAR = get_year();
+ // The ISO year is the same as a standard year for all dates after the start
+ // of the first week and before the last week. Since the first ISO week of a
+ // year starts on the 4th, anything after that is in this year.
+ if (timeptr->tm_yday >= time_constants::ISO_FIRST_DAY_OF_YEAR &&
+ timeptr->tm_yday < time_constants::DAYS_PER_NON_LEAP_YEAR -
+ time_constants::DAYS_PER_WEEK)
+ return BASE_YEAR;
+
+ const int ISO_WDAY = get_iso_wday();
+ // The first week of the ISO year is defined as the week containing the
+ // 4th day of January.
+
+ // first week
+ if (timeptr->tm_yday < time_constants::ISO_FIRST_DAY_OF_YEAR) {
+ /*
+ If jan 4 is in this week, then we're in BASE_YEAR, else we're in the
+ previous year. The formula's been rearranged so here's the derivation:
+
+ +--------+-- days until jan 4
+ | |
+ wday + (4 - yday) < 7
+ | |
+ +---------------+-- weekday of jan 4
+
+ rearranged to get all the constants on one side:
+
+ wday - yday < 7 - 4
+ */
+ return (ISO_WDAY - timeptr->tm_yday <
+ time_constants::DAYS_PER_WEEK -
+ time_constants::ISO_FIRST_DAY_OF_YEAR)
+ ? BASE_YEAR
+ : BASE_YEAR - 1;
+ }
+
+ // last week
+ const int DAYS_LEFT_IN_YEAR = get_days_in_year() - timeptr->tm_yday;
+ /*
+ Similar to above, we're checking if jan 4 (of next year) is in this week. If
+ it is, this is in the next year. Note that this also handles the case of
+ yday > days in year gracefully.
+
+ +------------------+-- days until jan 4 (of next year)
+ | |
+ wday + (4 + remaining days) < 7
+ | |
+ +-------------------------+-- weekday of jan 4
+
+ rearranging we get:
+
+ wday + remaining days < 7 - 4
+ */
+
+ return (ISO_WDAY + DAYS_LEFT_IN_YEAR <
+ time_constants::DAYS_PER_WEEK -
+ time_constants::ISO_FIRST_DAY_OF_YEAR)
+ ? BASE_YEAR + 1
+ : BASE_YEAR;
+ }
+
+ LIBC_INLINE constexpr time_t get_epoch() const {
+ // TODO: move the implementation from mktime. Maybe make this an optional
+ // for out of bounds?
+ return -1;
+ }
+};
+
} // namespace time_utils
} // namespace LIBC_NAMESPACE_DECL
diff --git a/libc/test/src/time/CMakeLists.txt b/libc/test/src/time/CMakeLists.txt
index 12add224f386a8..618812fd8eee55 100644
--- a/libc/test/src/time/CMakeLists.txt
+++ b/libc/test/src/time/CMakeLists.txt
@@ -179,6 +179,17 @@ add_libc_test(
libc.hdr.types.struct_timespec
)
+add_libc_test(
+ strftime_test
+ SUITE
+ libc_time_unittests
+ SRCS
+ strftime_test.cpp
+ DEPENDS
+ libc.hdr.types.struct_tm
+ libc.src.time.strftime
+)
+
add_libc_unittest(
time_test
SUITE
diff --git a/libc/test/src/time/strftime_test.cpp b/libc/test/src/time/strftime_test.cpp
new file mode 100644
index 00000000000000..44c6870e9a40bd
--- /dev/null
+++ b/libc/test/src/time/strftime_test.cpp
@@ -0,0 +1,234 @@
+//===-- 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/time/strftime.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);
+
+TEST(LlvmLibcStrftimeTest, FullYearTests) {
+ // this tests %Y, which reads: [tm_year]
+ struct tm time;
+ char buffer[100];
+ size_t written = 0;
+
+ // basic tests
+ time.tm_year = 2022 - 1900; // tm_year counts years since 1900, so 122 -> 2022
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%Y", &time);
+ EXPECT_STREQ_LEN(written, buffer, "2022");
+
+ time.tm_year = 11900 - 1900;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%Y", &time);
+ EXPECT_STREQ_LEN(written, buffer, "11900");
+
+ time.tm_year = 1900 - 1900;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%Y", &time);
+ EXPECT_STREQ_LEN(written, buffer, "1900");
+
+ time.tm_year = 900 - 1900;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%Y", &time);
+ EXPECT_STREQ_LEN(written, buffer, "900");
+
+ time.tm_year = 0 - 1900;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%Y", &time);
+ EXPECT_STREQ_LEN(written, buffer, "0");
+
+ time.tm_year = -1 - 1900;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%Y", &time);
+ EXPECT_STREQ_LEN(written, buffer, "-1");
+
+ time.tm_year = -9001 - 1900;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%Y", &time);
+ EXPECT_STREQ_LEN(written, buffer, "-9001");
+
+ time.tm_year = -10001 - 1900;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%Y", &time);
+ EXPECT_STREQ_LEN(written, buffer, "-10001");
+
+ // width tests (with the 0 flag, since the default padding is undefined).
+ time.tm_year = 2023 - 1900;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%01Y", &time);
+ EXPECT_STREQ_LEN(written, buffer, "2023");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%04Y", &time);
+ EXPECT_STREQ_LEN(written, buffer, "2023");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%05Y", &time);
+ EXPECT_STREQ_LEN(written, buffer, "02023");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%010Y", &time);
+ EXPECT_STREQ_LEN(written, buffer, "0000002023");
+
+ time.tm_year = 900 - 1900;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%01Y", &time);
+ EXPECT_STREQ_LEN(written, buffer, "900");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%04Y", &time);
+ EXPECT_STREQ_LEN(written, buffer, "0900");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%05Y", &time);
+ EXPECT_STREQ_LEN(written, buffer, "00900");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%010Y", &time);
+ EXPECT_STREQ_LEN(written, buffer, "0000000900");
+
+ time.tm_year = 12345 - 1900;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%01Y", &time);
+ EXPECT_STREQ_LEN(written, buffer, "12345");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%04Y", &time);
+ EXPECT_STREQ_LEN(written, buffer, "12345");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%05Y", &time);
+ EXPECT_STREQ_LEN(written, buffer, "12345");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%010Y", &time);
+ EXPECT_STREQ_LEN(written, buffer, "0000012345");
+
+ time.tm_year = -123 - 1900;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%01Y", &time);
+ EXPECT_STREQ_LEN(written, buffer, "-123");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%04Y", &time);
+ EXPECT_STREQ_LEN(written, buffer, "-123");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%05Y", &time);
+ EXPECT_STREQ_LEN(written, buffer, "-0123");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%010Y", &time);
+ EXPECT_STREQ_LEN(written, buffer, "-000000123");
+
+ // '+' flag tests
+ time.tm_year = 2023 - 1900;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%+1Y", &time);
+ EXPECT_STREQ_LEN(written, buffer, "2023");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%+4Y", &time);
+ EXPECT_STREQ_LEN(written, buffer, "2023");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%+5Y", &time);
+ EXPECT_STREQ_LEN(written, buffer, "+2023");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%+10Y", &time);
+ EXPECT_STREQ_LEN(written, buffer, "+000002023");
+
+ time.tm_year = 900 - 1900;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%+1Y", &time);
+ EXPECT_STREQ_LEN(written, buffer, "900");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%+4Y", &time);
+ EXPECT_STREQ_LEN(written, buffer, "0900");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%+5Y", &time);
+ EXPECT_STREQ_LEN(written, buffer, "+0900");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%+10Y", &time);
+ EXPECT_STREQ_LEN(written, buffer, "+000000900");
+
+ time.tm_year = 12345 - 1900;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%+1Y", &time);
+ EXPECT_STREQ_LEN(written, buffer, "+12345");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%+4Y", &time);
+ EXPECT_STREQ_LEN(written, buffer, "+12345");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%+5Y", &time);
+ EXPECT_STREQ_LEN(written, buffer, "+12345");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%+10Y", &time);
+ EXPECT_STREQ_LEN(written, buffer, "+000012345");
+
+ time.tm_year = -123 - 1900;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%+1Y", &time);
+ EXPECT_STREQ_LEN(written, buffer, "-123");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%+4Y", &time);
+ EXPECT_STREQ_LEN(written, buffer, "-123");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%+5Y", &time);
+ EXPECT_STREQ_LEN(written, buffer, "-0123");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%+10Y", &time);
+ EXPECT_STREQ_LEN(written, buffer, "-000000123");
+
+ // Posix specified tests:
+ time.tm_year = 1970 - 1900;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%Y", &time);
+ EXPECT_STREQ_LEN(written, buffer, "1970");
+
+ time.tm_year = 1970 - 1900;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%+4Y", &time);
+ EXPECT_STREQ_LEN(written, buffer, "1970");
+
+ time.tm_year = 27 - 1900;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%Y", &time);
+ EXPECT_STREQ_LEN(written, buffer, "27");
+
+ time.tm_year = 270 - 1900;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%Y", &time);
+ EXPECT_STREQ_LEN(written, buffer, "270");
+
+ time.tm_year = 270 - 1900;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%+4Y", &time);
+ EXPECT_STREQ_LEN(written, buffer, "0270");
+
+ time.tm_year = 12345 - 1900;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%Y", &time);
+ EXPECT_STREQ_LEN(written, buffer, "12345");
+
+ time.tm_year = 12345 - 1900;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%+4Y", &time);
+ EXPECT_STREQ_LEN(written, buffer, "+12345");
+
+ time.tm_year = 12345 - 1900;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%05Y", &time);
+ EXPECT_STREQ_LEN(written, buffer, "12345");
+
+ time.tm_year = 270 - 1900;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%+5Y", &time);
+ EXPECT_STREQ_LEN(written, buffer, "+0270");
+
+ time.tm_year = 12345 - 1900;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%+5Y", &time);
+ EXPECT_STREQ_LEN(written, buffer, "+12345");
+
+ time.tm_year = 12345 - 1900;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%06Y", &time);
+ EXPECT_STREQ_LEN(written, buffer, "012345");
+
+ time.tm_year = 12345 - 1900;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%+6Y", &time);
+ EXPECT_STREQ_LEN(written, buffer, "+12345");
+
+ time.tm_year = 123456 - 1900;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%08Y", &time);
+ EXPECT_STREQ_LEN(written, buffer, "00123456");
+
+ time.tm_year = 123456 - 1900;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%+8Y", &time);
+ EXPECT_STREQ_LEN(written, buffer, "+0123456");
+}
+
+// TODO: tests for each other conversion.
+
+TEST(LlvmLibcStrftimeTest, CompositeTests) {
+ struct tm time;
+ time.tm_year = 122; // Year since 1900, so 2022
+ time.tm_mon = 9; // October (0-indexed)
+ time.tm_mday = 15; // 15th day
+
+ char buffer[100];
+ LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%Y-%m-%d", &time);
+ EXPECT_STREQ(buffer, "2022-10-15");
+}
>From 1dccaab7920fc7de0ea51b9ac4730191308fb596 Mon Sep 17 00:00:00 2001
From: Michael Jones <michaelrj at google.com>
Date: Thu, 23 Jan 2025 09:59:53 -0800
Subject: [PATCH 2/4] more tests
---
libc/src/time/strftime_core/num_converter.h | 2 +-
libc/test/src/time/strftime_test.cpp | 195 ++++++++++++++++++++
2 files changed, 196 insertions(+), 1 deletion(-)
diff --git a/libc/src/time/strftime_core/num_converter.h b/libc/src/time/strftime_core/num_converter.h
index b196be52d928d0..2a2154cc9ecd2b 100644
--- a/libc/src/time/strftime_core/num_converter.h
+++ b/libc/src/time/strftime_core/num_converter.h
@@ -142,7 +142,7 @@ LIBC_INLINE int convert_int(printf_core::Writer *writer,
// sign isn't a problem because we're taking the max. The result is always
// non-negative.
- if (static_cast<int>(pad_to_len) < to_conv.min_width)
+ if (to_conv.min_width > 0)
pad_to_len = to_conv.min_width;
// one less digit of padding if there's a sign char
diff --git a/libc/test/src/time/strftime_test.cpp b/libc/test/src/time/strftime_test.cpp
index 44c6870e9a40bd..7542996b7cb672 100644
--- a/libc/test/src/time/strftime_test.cpp
+++ b/libc/test/src/time/strftime_test.cpp
@@ -220,6 +220,201 @@ TEST(LlvmLibcStrftimeTest, FullYearTests) {
EXPECT_STREQ_LEN(written, buffer, "+0123456");
}
+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 = 2022 - 1900; // tm_year counts years since 1900, so 122 -> 2022
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%C", &time);
+ EXPECT_STREQ_LEN(written, buffer, "20");
+
+ time.tm_year = 11900 - 1900;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%C", &time);
+ EXPECT_STREQ_LEN(written, buffer, "119");
+
+ time.tm_year = 1900 - 1900;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%C", &time);
+ EXPECT_STREQ_LEN(written, buffer, "19");
+
+ time.tm_year = 900 - 1900;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%C", &time);
+ EXPECT_STREQ_LEN(written, buffer, "09");
+
+ time.tm_year = 0 - 1900;
+ 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 = -1 - 1900;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%C", &time);
+ EXPECT_STREQ_LEN(written, buffer, "00");
+
+ time.tm_year = -101 - 1900;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%C", &time);
+ EXPECT_STREQ_LEN(written, buffer, "-1");
+
+ time.tm_year = -9001 - 1900;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%C", &time);
+ EXPECT_STREQ_LEN(written, buffer, "-90");
+
+ time.tm_year = -10001 - 1900;
+ 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 = 2023 - 1900;
+ 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 = 900 - 1900;
+ 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 = 12345 - 1900;
+ 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 = -123 - 1900;
+ 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 = 2023 - 1900;
+ 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 = 900 - 1900;
+ 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 = 12345 - 1900;
+ 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 = -123 - 1900;
+ 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 = 17 - 1900;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%C", &time);
+ EXPECT_STREQ_LEN(written, buffer, "00");
+
+ time.tm_year = 270 - 1900;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%C", &time);
+ EXPECT_STREQ_LEN(written, buffer, "02");
+
+ time.tm_year = 270 - 1900;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%+3C", &time);
+ EXPECT_STREQ_LEN(written, buffer, "+02");
+
+ time.tm_year = 12345 - 1900;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%+3C", &time);
+ EXPECT_STREQ_LEN(written, buffer, "+123");
+
+ time.tm_year = 12345 - 1900;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%04C", &time);
+ EXPECT_STREQ_LEN(written, buffer, "0123");
+
+ time.tm_year = 12345 - 1900;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%+4C", &time);
+ EXPECT_STREQ_LEN(written, buffer, "+123");
+
+ time.tm_year = 123456 - 1900;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%06C", &time);
+ EXPECT_STREQ_LEN(written, buffer, "001234");
+
+ time.tm_year = 123456 - 1900;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%+6C", &time);
+ EXPECT_STREQ_LEN(written, buffer, "+01234");
+}
+
// TODO: tests for each other conversion.
TEST(LlvmLibcStrftimeTest, CompositeTests) {
>From b8050702a9496a9c4e8ca73a147357f86f917682 Mon Sep 17 00:00:00 2001
From: Michael Jones <michaelrj at google.com>
Date: Fri, 24 Jan 2025 11:53:58 -0800
Subject: [PATCH 3/4] more tests, update the docs
---
libc/docs/dev/undefined_behavior.rst | 12 +-
libc/test/src/time/strftime_test.cpp | 254 +++++++++++++++++++++++++++
2 files changed, 262 insertions(+), 4 deletions(-)
diff --git a/libc/docs/dev/undefined_behavior.rst b/libc/docs/dev/undefined_behavior.rst
index 2360c943852b95..b02b7ad76a0990 100644
--- a/libc/docs/dev/undefined_behavior.rst
+++ b/libc/docs/dev/undefined_behavior.rst
@@ -78,8 +78,8 @@ POSIX.1 leaves that when the name of a shared memory object does not begin with
Handling of NULL arguments to the 's' format specifier
------------------------------------------------------
The C standard does not specify behavior for ``printf("%s", NULL)``. We will
-print the string literal ``(null)`` unless using the
-``LIBC_COPT_PRINTF_NO_NULLPTR_CHECKS`` option described in :ref:`printf
+print the string literal ``(null)`` unless using the
+``LIBC_COPT_PRINTF_NO_NULLPTR_CHECKS`` option described in :ref:`printf
behavior<printf_behavior>`.
Unknown Math Rounding Direction
@@ -116,8 +116,12 @@ there.
If a struct tm with values out of the normal range is passed, the standard says
the result is undefined. For LLVM-libc, the result may be either the normalized
-value (e.g. weekday % 7) or the actual, out of range value. This behavior is not
-necessarily consistent between conversions, even similar ones. For conversions
+value (e.g. weekday % 7) or the actual, out of range value. For any numeric
+conversion where the result is just printing a value out of the struct
+(e.g. "%w" prints the day of the week), no normalization occurs ("%w" on a
+tm_wday of 32 prints "32"). For any numeric conversion where the value is
+calculated (e.g. "%u" prints the day of the week, starting on monday), the
+value is normalized (e.g. "%u" on a tm_wday of 32 prints "4"). For conversions
that result in strings, passing an out of range value will result in "?".
Posix adds padding support to strftime, but says "the default padding character
diff --git a/libc/test/src/time/strftime_test.cpp b/libc/test/src/time/strftime_test.cpp
index 7542996b7cb672..627448a68ecb4a 100644
--- a/libc/test/src/time/strftime_test.cpp
+++ b/libc/test/src/time/strftime_test.cpp
@@ -7,6 +7,7 @@
//===----------------------------------------------------------------------===//
#include "hdr/types/struct_tm.h"
+#include "src/__support/integer_to_string.h"
#include "src/time/strftime.h"
#include "test/UnitTest/Test.h"
@@ -415,6 +416,259 @@ TEST(LlvmLibcStrftimeTest, CenturyTests) {
EXPECT_STREQ_LEN(written, buffer, "+01234");
}
+// 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 = 8;
+ 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: num < 999, min_width < 3
+ // 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) {
+ clear_buff();
+
+ 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] = '0';
+ 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, 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, 1);
+
+ 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 = 1; i < 32; ++i) {
+ time.tm_year = i;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%g", &time);
+ char *result = spn.get_padded_num(i, 2);
+
+ ASSERT_STREQ(buffer, result);
+ ASSERT_EQ(written, spn.get_str_len());
+ }
+
+ // Test the harder to round cases
+
+ // not a leap year. Not relevant for the start-of-year tests, but it does
+ // matter for the end-of-year tests.
+ time.tm_year = 99;
+
+ // check the first days of the year
+ for (size_t wday = 0; wday < 7; ++wday) {
+ for (size_t yday = 1; yday < 5; ++yday) {
+ time.tm_wday = wday;
+ time.tm_yday = yday;
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%g", &time);
+ if (yday == 4) {
+ // Since the first ISO week must contain the 4th yday, this must always
+ // return the current year.
+ EXPECT_STREQ_LEN(written, buffer, "99");
+ } else if (yday == 3) {
+ // Only sunday the 3rd can be in a week that doesn't contain the 4th.
+ if (wday == 0) {
+ EXPECT_STREQ_LEN(written, buffer, "98");
+ } else {
+ EXPECT_STREQ_LEN(written, buffer, "99");
+ }
+ } else if (yday == 2) {
+ // Sunday or Monday the 2nd can be in a week that doesn't contain the
+ // 4th.
+ if (wday == 0 || wday == 6) {
+ EXPECT_STREQ_LEN(written, buffer, "98");
+ } else {
+ EXPECT_STREQ_LEN(written, buffer, "99");
+ }
+ } else {
+ // Sunday, Monday, or tuesday the 1st can be in a week that doesn't
+ // contain the 4th.
+ if (wday == 0 || wday == 6 || wday == 5) {
+ EXPECT_STREQ_LEN(written, buffer, "98");
+ } else {
+ EXPECT_STREQ_LEN(written, buffer, "99");
+ }
+ }
+ }
+ }
+
+
+ // check the last days of the year
+ for (size_t wday = 0; wday < 7; ++wday) {
+ for (size_t yday = 363; yday < 5; ++yday) {
+ time.tm_wday = wday;
+ time.tm_yday = yday;
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%g", &time);
+ if (yday == 4) {
+ // Since the first ISO week must contain the 4th yday, this must always
+ // return the current year.
+ EXPECT_STREQ_LEN(written, buffer, "99");
+ } else if (yday == 3) {
+ // Only sunday the 3rd can be in a week that doesn't contain the 4th.
+ if (wday == 0) {
+ EXPECT_STREQ_LEN(written, buffer, "98");
+ } else {
+ EXPECT_STREQ_LEN(written, buffer, "99");
+ }
+ } else if (yday == 2) {
+ // Sunday or Monday the 2nd can be in a week that doesn't contain the
+ // 4th.
+ if (wday == 0 || wday == 6) {
+ EXPECT_STREQ_LEN(written, buffer, "98");
+ } else {
+ EXPECT_STREQ_LEN(written, buffer, "99");
+ }
+ } else {
+ // Sunday, Monday, or tuesday the 1st can be in a week that doesn't
+ // contain the 4th.
+ if (wday == 0 || wday == 6 || wday == 5) {
+ EXPECT_STREQ_LEN(written, buffer, "98");
+ } else {
+ EXPECT_STREQ_LEN(written, buffer, "99");
+ }
+ }
+ }
+ }
+
+ // padding is technically undefined for this conversion, but we support it, so
+ // we need to test it.
+ time.tm_year = 5;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%01g", &time);
+ EXPECT_STREQ_LEN(written, buffer, "5");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%02g", &time);
+ EXPECT_STREQ_LEN(written, buffer, "05");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%05g", &time);
+ EXPECT_STREQ_LEN(written, buffer, "00005");
+
+ time.tm_year = 31;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%01g", &time);
+ EXPECT_STREQ_LEN(written, buffer, "31");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%02g", &time);
+ EXPECT_STREQ_LEN(written, buffer, "31");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%05g", &time);
+ EXPECT_STREQ_LEN(written, buffer, "00031");
+}
+
// TODO: tests for each other conversion.
TEST(LlvmLibcStrftimeTest, CompositeTests) {
>From 72f131567cc5c2bfe0dca28a2d92a52c74f36708 Mon Sep 17 00:00:00 2001
From: Michael Jones <michaelrj at google.com>
Date: Tue, 28 Jan 2025 16:49:54 -0800
Subject: [PATCH 4/4] finish %g tests and do all of %G tests
---
libc/test/src/time/strftime_test.cpp | 287 +++++++++++++++++++++------
1 file changed, 231 insertions(+), 56 deletions(-)
diff --git a/libc/test/src/time/strftime_test.cpp b/libc/test/src/time/strftime_test.cpp
index 627448a68ecb4a..c61c528d630e8e 100644
--- a/libc/test/src/time/strftime_test.cpp
+++ b/libc/test/src/time/strftime_test.cpp
@@ -421,7 +421,7 @@ TEST(LlvmLibcStrftimeTest, CenturyTests) {
// 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 = 8;
+ static constexpr size_t BUFF_LEN = 16;
char buff[BUFF_LEN];
size_t cur_len; // length of string currently in buff
@@ -434,7 +434,7 @@ class SimplePaddedNum {
public:
SimplePaddedNum() = default;
- // PRECONDITIONS: num < 999, min_width < 3
+ // PRECONDITIONS: num < 99999, min_width < 5
// 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) {
@@ -554,10 +554,10 @@ TEST(LlvmLibcStrftimeTest, ISOYearOfCentury) {
time.tm_yday = 100;
// Test the easy cases
- for (size_t i = 1; i < 32; ++i) {
+ for (size_t i = 0; i < 102; ++i) {
time.tm_year = i;
written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%g", &time);
- char *result = spn.get_padded_num(i, 2);
+ char *result = spn.get_padded_num(i % 100, 2);
ASSERT_STREQ(buffer, result);
ASSERT_EQ(written, spn.get_str_len());
@@ -569,36 +569,40 @@ TEST(LlvmLibcStrftimeTest, ISOYearOfCentury) {
// matter for the end-of-year tests.
time.tm_year = 99;
+ /*
+This table has an X for each day that should be in the previous year,
+everywhere else should be in the current year.
+
+ yday
+ 1234567
+ i 1 Monday
+ s 2 Tuesday
+ o 3 Wednesday
+ w 4 Thursday
+ d 5 X Friday
+ a 6 XX Saturday
+ y 7 XXX Sunday
+*/
+
// check the first days of the year
- for (size_t wday = 0; wday < 7; ++wday) {
- for (size_t yday = 1; yday < 5; ++yday) {
- time.tm_wday = wday;
+ for (size_t yday = 1; yday < 5; ++yday) {
+ for (size_t iso_wday = 1; iso_wday < 8; ++iso_wday) {
+ // start with monday, to match the ISO week.
+ time.tm_wday = iso_wday % 7;
time.tm_yday = yday;
written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%g", &time);
- if (yday == 4) {
- // Since the first ISO week must contain the 4th yday, this must always
- // return the current year.
+
+ if (iso_wday <= 4 || yday >= 4) {
+ // monday - thursday are never in the previous year, nor are the 4th and
+ // after.
EXPECT_STREQ_LEN(written, buffer, "99");
- } else if (yday == 3) {
- // Only sunday the 3rd can be in a week that doesn't contain the 4th.
- if (wday == 0) {
- EXPECT_STREQ_LEN(written, buffer, "98");
- } else {
- EXPECT_STREQ_LEN(written, buffer, "99");
- }
- } else if (yday == 2) {
- // Sunday or Monday the 2nd can be in a week that doesn't contain the
- // 4th.
- if (wday == 0 || wday == 6) {
- EXPECT_STREQ_LEN(written, buffer, "98");
- } else {
- EXPECT_STREQ_LEN(written, buffer, "99");
- }
} else {
- // Sunday, Monday, or tuesday the 1st can be in a week that doesn't
- // contain the 4th.
- if (wday == 0 || wday == 6 || wday == 5) {
+ // iso_wday is 5, 6, or 7 and yday is 1, 2, or 3.
+ // days_since_thursday is therefor 1, 2, or 3.
+ const size_t days_since_thursday = iso_wday - 4;
+
+ if (days_since_thursday >= yday) {
EXPECT_STREQ_LEN(written, buffer, "98");
} else {
EXPECT_STREQ_LEN(written, buffer, "99");
@@ -607,40 +611,73 @@ TEST(LlvmLibcStrftimeTest, ISOYearOfCentury) {
}
}
-
- // check the last days of the year
- for (size_t wday = 0; wday < 7; ++wday) {
- for (size_t yday = 363; yday < 5; ++yday) {
- time.tm_wday = wday;
- time.tm_yday = yday;
+ /*
+ Similar to above, but the Xs represent being in the NEXT year. Also the
+ top counts down until the end of the year.
+
+ year end - yday
+ 7654321
+ i 1 XXX Monday
+ s 2 XX Tuesday
+ o 3 X Wednesday
+ w 4 Thursday
+ d 5 Friday
+ a 6 Saturday
+ y 7 Sunday
+
+
+ If we place the charts next to each other, you can more easily see the
+ pattern:
+
+year end - yday yday
+ 6543210 1234567
+ i 1 XXX Monday
+ s 2 XX Tuesday
+ o 3 X Wednesday
+ w 4 Thursday
+ d 5 X Friday
+ a 6 XX Saturday
+ y 7 XXX Sunday
+
+ From this we can see that thursday is always in the same ISO and regular
+ year.
+ */
+
+ // set up all the extra stuff to cover leap years.
+ struct tm time_leap_year;
+ char buffer_leap_year[100];
+ size_t written_leap_year = 0;
+ time_leap_year = time;
+ time_leap_year.tm_year = 100; // 2000 is a leap year.
+
+ // check the last days of the year. Checking 5 to make sure all the leap year
+ // cases are covered as well.
+ for (size_t days_left = 0; days_left < 5; ++days_left) {
+ for (size_t iso_wday = 1; iso_wday < 8; ++iso_wday) {
+ // start with monday, to match the ISO week.
+ time.tm_wday = iso_wday % 7;
+ time.tm_yday = 365 - days_left;
+
+ time_leap_year.tm_wday = iso_wday % 7;
+ time_leap_year.tm_yday = 366 - days_left;
written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%g", &time);
- if (yday == 4) {
- // Since the first ISO week must contain the 4th yday, this must always
- // return the current year.
+ written_leap_year = LIBC_NAMESPACE::strftime(
+ buffer_leap_year, sizeof(buffer_leap_year), "%g", &time_leap_year);
+
+ if (iso_wday >= 4 || days_left >= 3) {
+ // thursday - sunday are never in the next year, nor are days more than
+ // 3 days before the end.
EXPECT_STREQ_LEN(written, buffer, "99");
- } else if (yday == 3) {
- // Only sunday the 3rd can be in a week that doesn't contain the 4th.
- if (wday == 0) {
- EXPECT_STREQ_LEN(written, buffer, "98");
- } else {
- EXPECT_STREQ_LEN(written, buffer, "99");
- }
- } else if (yday == 2) {
- // Sunday or Monday the 2nd can be in a week that doesn't contain the
- // 4th.
- if (wday == 0 || wday == 6) {
- EXPECT_STREQ_LEN(written, buffer, "98");
- } else {
- EXPECT_STREQ_LEN(written, buffer, "99");
- }
+ EXPECT_STREQ_LEN(written_leap_year, buffer_leap_year, "00");
} else {
- // Sunday, Monday, or tuesday the 1st can be in a week that doesn't
- // contain the 4th.
- if (wday == 0 || wday == 6 || wday == 5) {
- EXPECT_STREQ_LEN(written, buffer, "98");
+ // iso_wday is 1, 2 or 3 and days_left is 0, 1, or 2
+ if (iso_wday + days_left <= 3) {
+ EXPECT_STREQ_LEN(written, buffer, "00");
+ EXPECT_STREQ_LEN(written_leap_year, buffer_leap_year, "01");
} else {
EXPECT_STREQ_LEN(written, buffer, "99");
+ EXPECT_STREQ_LEN(written_leap_year, buffer_leap_year, "00");
}
}
}
@@ -669,6 +706,144 @@ TEST(LlvmLibcStrftimeTest, ISOYearOfCentury) {
EXPECT_STREQ_LEN(written, buffer, "00031");
}
+TEST(LlvmLibcStrftimeTest, ISOYear) {
+ // this tests %G, which reads: [tm_year, tm_wday, tm_yday]
+
+ // This stuff is all the same as above, but for brevity I'm not going to
+ // duplicate all the comments explaining exactly how ISO years work. The
+ // general comments are still here though.
+
+ 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 (int i = 1 - 1900; i < 10000 - 1900; ++i) {
+ time.tm_year = i;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%G", &time);
+ // apparently %G doesn't pad by default.
+ char *result = spn.get_padded_num(i + 1900, 0);
+
+ ASSERT_STREQ(buffer, result);
+ ASSERT_EQ(written, spn.get_str_len());
+ }
+
+ // also check it handles years with extra digits properly
+ time.tm_year = 12345 - 1900;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%G", &time);
+ EXPECT_STREQ_LEN(written, buffer, "12345");
+
+ // Test the harder to round cases
+
+ // not a leap year. Not relevant for the start-of-year tests, but it does
+ // matter for the end-of-year tests.
+ time.tm_year = 99;
+
+ // check the first days of the year
+ for (size_t yday = 1; yday < 5; ++yday) {
+ for (size_t iso_wday = 1; iso_wday < 8; ++iso_wday) {
+ // start with monday, to match the ISO week.
+ time.tm_wday = iso_wday % 7;
+ time.tm_yday = yday;
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%G", &time);
+
+ if (iso_wday <= 4 || yday >= 4) {
+ // monday - thursday are never in the previous year, nor are the 4th and
+ // after.
+ EXPECT_STREQ_LEN(written, buffer, "1999");
+ } else {
+ // iso_wday is 5, 6, or 7 and yday is 1, 2, or 3.
+ // days_since_thursday is therefor 1, 2, or 3.
+ const size_t days_since_thursday = iso_wday - 4;
+
+ if (days_since_thursday >= yday) {
+ EXPECT_STREQ_LEN(written, buffer, "1998");
+ } else {
+ EXPECT_STREQ_LEN(written, buffer, "1999");
+ }
+ }
+ }
+ }
+
+ // set up all the extra stuff to cover leap years.
+ struct tm time_leap_year;
+ char buffer_leap_year[100];
+ size_t written_leap_year = 0;
+ time_leap_year = time;
+ time_leap_year.tm_year = 100; // 2000 is a leap year.
+
+ // check the last days of the year. Checking 5 to make sure all the leap year
+ // cases are covered as well.
+ for (size_t days_left = 0; days_left < 5; ++days_left) {
+ for (size_t iso_wday = 1; iso_wday < 8; ++iso_wday) {
+ // start with monday, to match the ISO week.
+ time.tm_wday = iso_wday % 7;
+ time.tm_yday = 365 - days_left;
+
+ time_leap_year.tm_wday = iso_wday % 7;
+ time_leap_year.tm_yday = 366 - days_left;
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%G", &time);
+ written_leap_year = LIBC_NAMESPACE::strftime(
+ buffer_leap_year, sizeof(buffer_leap_year), "%G", &time_leap_year);
+
+ if (iso_wday >= 4 || days_left >= 3) {
+ // thursday - sunday are never in the next year, nor are days more than
+ // 3 days before the end.
+ EXPECT_STREQ_LEN(written, buffer, "1999");
+ EXPECT_STREQ_LEN(written_leap_year, buffer_leap_year, "2000");
+ } else {
+ // iso_wday is 1, 2 or 3 and days_left is 0, 1, or 2
+ if (iso_wday + days_left <= 3) {
+ EXPECT_STREQ_LEN(written, buffer, "2000");
+ EXPECT_STREQ_LEN(written_leap_year, buffer_leap_year, "2001");
+ } else {
+ EXPECT_STREQ_LEN(written, buffer, "1999");
+ EXPECT_STREQ_LEN(written_leap_year, buffer_leap_year, "2000");
+ }
+ }
+ }
+ }
+
+ // padding is technically undefined for this conversion, but we support it, so
+ // we need to test it.
+ time.tm_year = 5 - 1900;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%01G", &time);
+ EXPECT_STREQ_LEN(written, buffer, "5");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%02G", &time);
+ EXPECT_STREQ_LEN(written, buffer, "05");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%05G", &time);
+ EXPECT_STREQ_LEN(written, buffer, "00005");
+
+ time.tm_year = 31 - 1900;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%01G", &time);
+ EXPECT_STREQ_LEN(written, buffer, "31");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%02G", &time);
+ EXPECT_STREQ_LEN(written, buffer, "31");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%05G", &time);
+ EXPECT_STREQ_LEN(written, buffer, "00031");
+
+ time.tm_year = 2001 - 1900;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%01G", &time);
+ EXPECT_STREQ_LEN(written, buffer, "2001");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%02G", &time);
+ EXPECT_STREQ_LEN(written, buffer, "2001");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%05G", &time);
+ EXPECT_STREQ_LEN(written, buffer, "02001");
+}
+
// TODO: tests for each other conversion.
TEST(LlvmLibcStrftimeTest, CompositeTests) {
More information about the libc-commits
mailing list