[libc-commits] [libc] 398f865 - [libc] Implement strftime (#122556)
via libc-commits
libc-commits at lists.llvm.org
Fri Feb 14 15:56:59 PST 2025
Author: Michael Jones
Date: 2025-02-14T15:56:55-08:00
New Revision: 398f865499e6d7c35df2496cdff29ed0239423de
URL: https://github.com/llvm/llvm-project/commit/398f865499e6d7c35df2496cdff29ed0239423de
DIFF: https://github.com/llvm/llvm-project/commit/398f865499e6d7c35df2496cdff29ed0239423de.diff
LOG: [libc] Implement strftime (#122556)
Implements the posix-specified strftime conversions for the default
locale, along with comprehensive unit tests. This reuses a lot of design
from printf, as well as the printf writer.
Roughly based on #111305, but with major rewrites.
Added:
libc/src/time/strftime.cpp
libc/src/time/strftime.h
libc/src/time/strftime_core/CMakeLists.txt
libc/src/time/strftime_core/composite_converter.h
libc/src/time/strftime_core/converter.cpp
libc/src/time/strftime_core/converter.h
libc/src/time/strftime_core/core_structs.h
libc/src/time/strftime_core/num_converter.h
libc/src/time/strftime_core/parser.h
libc/src/time/strftime_core/str_converter.h
libc/src/time/strftime_core/strftime_main.cpp
libc/src/time/strftime_core/strftime_main.h
libc/test/src/time/strftime_test.cpp
Modified:
libc/config/linux/x86_64/entrypoints.txt
libc/docs/dev/undefined_behavior.rst
libc/include/time.yaml
libc/src/time/CMakeLists.txt
libc/src/time/time_constants.h
libc/test/src/time/CMakeLists.txt
Removed:
################################################################################
diff --git a/libc/config/linux/x86_64/entrypoints.txt b/libc/config/linux/x86_64/entrypoints.txt
index 76e593296a4ea..a4f6671a59789 100644
--- a/libc/config/linux/x86_64/entrypoints.txt
+++ b/libc/config/linux/x86_64/entrypoints.txt
@@ -1127,6 +1127,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 60fda51e86452..b252832e6715c 100644
--- a/libc/docs/dev/undefined_behavior.rst
+++ b/libc/docs/dev/undefined_behavior.rst
@@ -106,3 +106,40 @@ 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. 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
+is unspecified." For LLVM-libc, the default padding character is ' ' (space)
+for all string-type conversions and '0' for integer-type conversions. Composite
+conversions pass the padding to the first (leftmost) conversion. In practice
+this is always a numeric conversion, so it pads with '0'. For the purposes of
+padding, composite conversions also assume the non-leading conversions have
+valid inputs and output their expected number of characters. For %c this means
+that the padding will be off if the year is outside of the range -999 to 9999.
+
+The %e conversion is padded with spaces by default, but pads with 0s if the '0'
+flag is set.
+
+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.
+Any composite conversion will pass along all flags to the component conversions.
diff --git a/libc/include/time.yaml b/libc/include/time.yaml
index b71b9ab72075b..37ee824678cda 100644
--- a/libc/include/time.yaml
+++ b/libc/include/time.yaml
@@ -91,6 +91,15 @@ 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: time
standard:
- stdc
diff --git a/libc/src/time/CMakeLists.txt b/libc/src/time/CMakeLists.txt
index dd28aa67280b7..8332e8ab66f97 100644
--- a/libc/src/time/CMakeLists.txt
+++ b/libc/src/time/CMakeLists.txt
@@ -135,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/strftime.cpp b/libc/src/time/strftime.cpp
new file mode 100644
index 0000000000000..4b89bf2ea3a70
--- /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 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 0000000000000..dadd046477305
--- /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 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 0000000000000..a12a26b2aee0f
--- /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/composite_converter.h b/libc/src/time/strftime_core/composite_converter.h
new file mode 100644
index 0000000000000..5c473f172c073
--- /dev/null
+++ b/libc/src/time/strftime_core/composite_converter.h
@@ -0,0 +1,237 @@
+//===-- Composite converter for strftime ------------------------*- 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_COMPOSITE_CONVERTER_H
+#define LLVM_LIBC_SRC_STDIO_STRFTIME_CORE_COMPOSITE_CONVERTER_H
+
+#include "hdr/types/struct_tm.h"
+#include "src/__support/CPP/string_view.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/strftime_core/num_converter.h"
+#include "src/time/strftime_core/str_converter.h"
+#include "src/time/time_constants.h"
+#include "src/time/time_utils.h"
+
+namespace LIBC_NAMESPACE_DECL {
+namespace strftime_core {
+
+LIBC_INLINE IntFormatSection
+get_specific_int_format(const tm *timeptr, const FormatSection &base_to_conv,
+ char new_conv_name, int TRAILING_CONV_LEN = -1) {
+ // a negative padding will be treated as the default
+ const int NEW_MIN_WIDTH =
+ TRAILING_CONV_LEN > 0 ? base_to_conv.min_width - TRAILING_CONV_LEN : 0;
+ FormatSection new_conv = base_to_conv;
+ new_conv.conv_name = new_conv_name;
+ new_conv.min_width = NEW_MIN_WIDTH;
+
+ IntFormatSection result = get_int_format(new_conv, timeptr);
+
+ // If the user set the padding, but it's below the width of the trailing
+ // conversions, then there should be no padding.
+ if (base_to_conv.min_width > 0 && NEW_MIN_WIDTH < 0)
+ result.pad_to_len = 0;
+
+ return result;
+}
+
+LIBC_INLINE int convert_date_us(printf_core::Writer *writer,
+ const FormatSection &to_conv,
+ const tm *timeptr) {
+ // format is %m/%d/%y (month/day/year)
+ // we only pad the first conversion, and we assume all the other values are in
+ // their valid ranges.
+ constexpr int TRAILING_CONV_LEN = 1 + 2 + 1 + 2; // sizeof("/01/02")
+ IntFormatSection year_conv;
+ IntFormatSection mon_conv;
+ IntFormatSection mday_conv;
+
+ mon_conv = get_specific_int_format(timeptr, to_conv, 'm', TRAILING_CONV_LEN);
+ mday_conv = get_specific_int_format(timeptr, to_conv, 'd');
+ year_conv = get_specific_int_format(timeptr, to_conv, 'y');
+
+ RET_IF_RESULT_NEGATIVE(write_padded_int(writer, mon_conv));
+ RET_IF_RESULT_NEGATIVE(writer->write('/'));
+ RET_IF_RESULT_NEGATIVE(write_padded_int(writer, mday_conv));
+ RET_IF_RESULT_NEGATIVE(writer->write('/'));
+ RET_IF_RESULT_NEGATIVE(write_padded_int(writer, year_conv));
+
+ return WRITE_OK;
+}
+
+LIBC_INLINE int convert_date_iso(printf_core::Writer *writer,
+ const FormatSection &to_conv,
+ const tm *timeptr) {
+ // format is "%Y-%m-%d" (year-month-day)
+ // we only pad the first conversion, and we assume all the other values are in
+ // their valid ranges.
+ constexpr int TRAILING_CONV_LEN = 1 + 2 + 1 + 2; // sizeof("-01-02")
+ IntFormatSection year_conv;
+ IntFormatSection mon_conv;
+ IntFormatSection mday_conv;
+
+ year_conv = get_specific_int_format(timeptr, to_conv, 'Y', TRAILING_CONV_LEN);
+ mon_conv = get_specific_int_format(timeptr, to_conv, 'm');
+ mday_conv = get_specific_int_format(timeptr, to_conv, 'd');
+
+ RET_IF_RESULT_NEGATIVE(write_padded_int(writer, year_conv));
+ RET_IF_RESULT_NEGATIVE(writer->write('-'));
+ RET_IF_RESULT_NEGATIVE(write_padded_int(writer, mon_conv));
+ RET_IF_RESULT_NEGATIVE(writer->write('-'));
+ RET_IF_RESULT_NEGATIVE(write_padded_int(writer, mday_conv));
+
+ return WRITE_OK;
+}
+
+LIBC_INLINE int convert_time_am_pm(printf_core::Writer *writer,
+ const FormatSection &to_conv,
+ const tm *timeptr) {
+ // format is "%I:%M:%S %p" (hour:minute:second AM/PM)
+ // we only pad the first conversion, and we assume all the other values are in
+ // their valid ranges.
+ constexpr int TRAILING_CONV_LEN =
+ 1 + 2 + 1 + 2 + 1 + 2; // sizeof(":01:02 AM")
+ IntFormatSection hour_conv;
+ IntFormatSection min_conv;
+ IntFormatSection sec_conv;
+
+ const time_utils::TMReader time_reader(timeptr);
+
+ hour_conv = get_specific_int_format(timeptr, to_conv, 'I', TRAILING_CONV_LEN);
+ min_conv = get_specific_int_format(timeptr, to_conv, 'M');
+ sec_conv = get_specific_int_format(timeptr, to_conv, 'S');
+
+ RET_IF_RESULT_NEGATIVE(write_padded_int(writer, hour_conv));
+ RET_IF_RESULT_NEGATIVE(writer->write(':'));
+ RET_IF_RESULT_NEGATIVE(write_padded_int(writer, min_conv));
+ RET_IF_RESULT_NEGATIVE(writer->write(':'));
+ RET_IF_RESULT_NEGATIVE(write_padded_int(writer, sec_conv));
+ RET_IF_RESULT_NEGATIVE(writer->write(' '));
+ RET_IF_RESULT_NEGATIVE(writer->write(time_reader.get_am_pm()));
+
+ return WRITE_OK;
+}
+
+LIBC_INLINE int convert_time_minute(printf_core::Writer *writer,
+ const FormatSection &to_conv,
+ const tm *timeptr) {
+ // format is "%H:%M" (hour:minute)
+ // we only pad the first conversion, and we assume all the other values are in
+ // their valid ranges.
+ constexpr int TRAILING_CONV_LEN = 1 + 2; // sizeof(":01")
+ IntFormatSection hour_conv;
+ IntFormatSection min_conv;
+
+ hour_conv = get_specific_int_format(timeptr, to_conv, 'H', TRAILING_CONV_LEN);
+ min_conv = get_specific_int_format(timeptr, to_conv, 'M');
+
+ RET_IF_RESULT_NEGATIVE(write_padded_int(writer, hour_conv));
+ RET_IF_RESULT_NEGATIVE(writer->write(':'));
+ RET_IF_RESULT_NEGATIVE(write_padded_int(writer, min_conv));
+
+ return WRITE_OK;
+}
+
+LIBC_INLINE int convert_time_second(printf_core::Writer *writer,
+ const FormatSection &to_conv,
+ const tm *timeptr) {
+ // format is "%H:%M:%S" (hour:minute:second)
+ // we only pad the first conversion, and we assume all the other values are in
+ // their valid ranges.
+ constexpr int TRAILING_CONV_LEN = 1 + 2 + 1 + 2; // sizeof(":01:02")
+ IntFormatSection hour_conv;
+ IntFormatSection min_conv;
+ IntFormatSection sec_conv;
+
+ hour_conv = get_specific_int_format(timeptr, to_conv, 'H', TRAILING_CONV_LEN);
+ min_conv = get_specific_int_format(timeptr, to_conv, 'M');
+ sec_conv = get_specific_int_format(timeptr, to_conv, 'S');
+
+ RET_IF_RESULT_NEGATIVE(write_padded_int(writer, hour_conv));
+ RET_IF_RESULT_NEGATIVE(writer->write(':'));
+ RET_IF_RESULT_NEGATIVE(write_padded_int(writer, min_conv));
+ RET_IF_RESULT_NEGATIVE(writer->write(':'));
+ RET_IF_RESULT_NEGATIVE(write_padded_int(writer, sec_conv));
+
+ return WRITE_OK;
+}
+
+LIBC_INLINE int convert_full_date_time(printf_core::Writer *writer,
+ const FormatSection &to_conv,
+ const tm *timeptr) {
+ const time_utils::TMReader time_reader(timeptr);
+ // format is "%a %b %e %T %Y" (weekday month mday [time] year)
+ // we only pad the first conversion, and we assume all the other values are in
+ // their valid ranges.
+ // sizeof("Sun Jan 12 03:45:06 2025")
+ const size_t full_conv_len = 3 + 1 + 3 + 1 + 2 + 1 + 8 + 1 + 4;
+ // use the full conv len because this isn't being passed to a proper converter
+ // that will handle the width of the leading conversion. Instead it has to be
+ // handled below.
+ const int requested_padding = to_conv.min_width - full_conv_len;
+
+ cpp::string_view wday_str = unwrap_opt(time_reader.get_weekday_short_name());
+ cpp::string_view month_str = unwrap_opt(time_reader.get_month_short_name());
+ IntFormatSection mday_conv;
+ IntFormatSection year_conv;
+
+ mday_conv = get_specific_int_format(timeptr, to_conv, 'e');
+ year_conv = get_specific_int_format(timeptr, to_conv, 'Y');
+
+ FormatSection raw_time_conv = to_conv;
+ raw_time_conv.conv_name = 'T';
+ raw_time_conv.min_width = 0;
+
+ if (requested_padding > 0)
+ RET_IF_RESULT_NEGATIVE(writer->write(' ', requested_padding));
+ RET_IF_RESULT_NEGATIVE(writer->write(wday_str));
+ RET_IF_RESULT_NEGATIVE(writer->write(' '));
+ RET_IF_RESULT_NEGATIVE(writer->write(month_str));
+ RET_IF_RESULT_NEGATIVE(writer->write(' '));
+ RET_IF_RESULT_NEGATIVE(write_padded_int(writer, mday_conv));
+ RET_IF_RESULT_NEGATIVE(writer->write(' '));
+ RET_IF_RESULT_NEGATIVE(convert_time_second(writer, raw_time_conv, timeptr));
+ RET_IF_RESULT_NEGATIVE(writer->write(' '));
+ RET_IF_RESULT_NEGATIVE(write_padded_int(writer, year_conv));
+
+ return WRITE_OK;
+}
+
+LIBC_INLINE int convert_composite(printf_core::Writer *writer,
+ const FormatSection &to_conv,
+ const tm *timeptr) {
+ switch (to_conv.conv_name) {
+ case 'c': // locale specified date and time
+ // in default locale Equivalent to %a %b %e %T %Y.
+ return convert_full_date_time(writer, to_conv, timeptr);
+ case 'D': // %m/%d/%y (month/day/year)
+ return convert_date_us(writer, to_conv, timeptr);
+ case 'F': // %Y-%m-%d (year-month-day)
+ return convert_date_iso(writer, to_conv, timeptr);
+ case 'r': // %I:%M:%S %p (hour:minute:second AM/PM)
+ return convert_time_am_pm(writer, to_conv, timeptr);
+ case 'R': // %H:%M (hour:minute)
+ return convert_time_minute(writer, to_conv, timeptr);
+ case 'T': // %H:%M:%S (hour:minute:second)
+ return convert_time_second(writer, to_conv, timeptr);
+ case 'x': // locale specified date
+ // in default locale Equivalent to %m/%d/%y. (same as %D)
+ return convert_date_us(writer, to_conv, timeptr);
+ case 'X': // locale specified time
+ // in default locale Equivalent to %T.
+ return convert_time_second(writer, to_conv, timeptr);
+ default:
+ __builtin_trap(); // this should be unreachable, but trap if you hit it.
+ }
+}
+} // namespace strftime_core
+} // namespace LIBC_NAMESPACE_DECL
+
+#endif // LLVM_LIBC_SRC_STDIO_STRFTIME_CORE_COMPOSITE_CONVERTER_H
diff --git a/libc/src/time/strftime_core/converter.cpp b/libc/src/time/strftime_core/converter.cpp
new file mode 100644
index 0000000000000..e9263af3d0e55
--- /dev/null
+++ b/libc/src/time/strftime_core/converter.cpp
@@ -0,0 +1,96 @@
+//===-- 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 "hdr/types/struct_tm.h"
+#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.
+ // Currently locale flags are ignored, as described by the posix standard for
+ // the default locale.
+
+ 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 [00-99]
+ case 'd': // Day of the month [01-31]
+ case 'e': // Day of the month [1-31]
+ case 'g': // last 2 digits of ISO year [00-99]
+ case 'G': // ISO year
+ case 'H': // 24-hour format [00-23]
+ case 'I': // 12-hour format [01-12]
+ case 'j': // Day of the year [001-366]
+ case 'm': // Month of the year [01-12]
+ case 'M': // Minute of the hour [00-59]
+ case 's': // Seconds since the epoch
+ case 'S': // Second of the minute [00-60]
+ 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 [00-99]
+ 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
+ return convert_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 convert_composite(writer, to_conv, timeptr);
+
+ // timezone conversions
+ case 'z': // Timezone offset (+/-hhmm) (num conv)
+ case 'Z': // Timezone name (string conv)
+ // the standard says if no time zone is determinable, write no characters.
+ // Leave this here until time zones are implemented.
+ return 0;
+ 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 0000000000000..154ee38d9f05a
--- /dev/null
+++ b/libc/src/time/strftime_core/converter.h
@@ -0,0 +1,28 @@
+//===-- 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 "hdr/types/struct_tm.h"
+#include "src/stdio/printf_core/writer.h"
+#include "src/time/strftime_core/core_structs.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 0000000000000..25bf5e616e0e2
--- /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 <stdint.h>
+
+namespace LIBC_NAMESPACE_DECL {
+namespace strftime_core {
+
+enum class ConvModifier { none, E, O };
+
+// These flags intentionally have
diff erent values from the ones used by printf.
+// They have
diff erent 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 = false;
+ cpp::string_view raw_string = {};
+
+ FormatFlags flags = FormatFlags(0);
+ ConvModifier modifier = ConvModifier::none;
+ char conv_name = '\0';
+ 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 0000000000000..aef9ddbf791b2
--- /dev/null
+++ b/libc/src/time/strftime_core/num_converter.h
@@ -0,0 +1,199 @@
+//===-- Numeric converter for strftime --------------------------*- 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 "hdr/types/struct_tm.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>;
+
+struct IntFormatSection {
+ uintmax_t num = 0;
+ char sign_char = '\0';
+ size_t pad_to_len = 0;
+ char padding_char = '0';
+};
+
+LIBC_INLINE int write_padded_int(printf_core::Writer *writer,
+ const IntFormatSection &num_info) {
+
+ DecFmt d(num_info.num);
+ auto str = d.view();
+
+ size_t digits_written = str.size();
+
+ // one less digit of padding if there's a sign char
+ int zeroes = static_cast<int>(num_info.pad_to_len - digits_written -
+ (num_info.sign_char == 0 ? 0 : 1));
+
+ // Format is (sign) (padding) digits
+ if (num_info.sign_char != 0)
+ RET_IF_RESULT_NEGATIVE(writer->write(num_info.sign_char));
+ if (zeroes > 0)
+ RET_IF_RESULT_NEGATIVE(writer->write(num_info.padding_char, zeroes))
+ RET_IF_RESULT_NEGATIVE(writer->write(str));
+
+ return WRITE_OK;
+}
+
+LIBC_INLINE IntFormatSection get_int_format(const FormatSection &to_conv,
+ const tm *timeptr) {
+ const time_utils::TMReader time_reader(timeptr);
+
+ intmax_t raw_num;
+
+ IntFormatSection result = {0, 0, 0, '0'};
+
+ // 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;
+ result.pad_to_len = 2;
+ break;
+ case 'd': // Day of the month [01-31]
+ raw_num = time_reader.get_mday(); // get_mday is 1 indexed
+ result.pad_to_len = 2;
+ break;
+ case 'e': // Day of the month [1-31]
+ raw_num = time_reader.get_mday(); // get_mday is 1 indexed
+ result.pad_to_len = 2;
+ result.padding_char = ' ';
+ break;
+ case 'g': // last 2 digits of ISO year [00-99]
+ raw_num = time_reader.get_iso_year() % 100;
+ result.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;
+ result.pad_to_len = 4;
+ break;
+ case 'H': // 24-hour format [00-23]
+ raw_num = time_reader.get_hour();
+ result.pad_to_len = 2;
+ break;
+ case 'I': // 12-hour format [01-12]
+ raw_num = ((time_reader.get_hour() + 11) % 12) + 1;
+ result.pad_to_len = 2;
+ break;
+ case 'j': // Day of the year [001-366]
+ raw_num = time_reader.get_yday() + 1; // get_yday is 0 indexed
+ result.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
+ result.pad_to_len = 2;
+ break;
+ case 'M': // Minute of the hour [00-59]
+ raw_num = time_reader.get_min();
+ result.pad_to_len = 2;
+ break;
+ case 's': // Seconds since the epoch
+ raw_num = time_reader.get_epoch();
+ result.pad_to_len = 0;
+ break;
+ case 'S': // Second of the minute [00-60]
+ raw_num = time_reader.get_sec();
+ result.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].
+ result.pad_to_len = 1;
+ break;
+ case 'U': // Week of the year ([00-53] week 1 starts on first *Sunday*)
+ // This doesn't actually end up using tm_year, despite the standard saying
+ // it's needed. The end of the current year doesn't really matter, so leap
+ // years aren't relevant. If this is wrong, please tell me what I'm missing.
+ raw_num = time_reader.get_week(time_constants::SUNDAY);
+ result.pad_to_len = 2;
+ break;
+ case 'V': // ISO week number ([01-53], 01 is first week majority in this year)
+ // This does need to know the year, since it may affect what the week of the
+ // previous year it underflows to.
+ raw_num = time_reader.get_iso_week();
+ result.pad_to_len = 2;
+ break;
+ case 'w': // Day of week ([0-6] starting Sunday)
+ raw_num = time_reader.get_wday();
+ result.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);
+ result.pad_to_len = 2;
+ break;
+ case 'y': // Year of the Century [00-99]
+ raw_num = time_reader.get_year() % 100;
+ result.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;
+ result.pad_to_len = 4;
+ break;
+ case 'z': // Timezone offset [+/-HHMM]
+ raw_num = time_reader.get_timezone_offset();
+ result.sign_char = '+'; // force the '+' sign iff raw_num is non-negative
+ result.pad_to_len = 5; // 4 + 1 for the sign
+ break;
+ default:
+ __builtin_trap(); // this should be unreachable, but trap if you hit it.
+ }
+
+ result.num = static_cast<uintmax_t>(raw_num < 0 ? -raw_num : raw_num);
+ const bool is_negative = raw_num < 0;
+
+ // TODO: Handle locale modifiers
+
+ if ((to_conv.flags & FormatFlags::LEADING_ZEROES) ==
+ FormatFlags::LEADING_ZEROES)
+ result.padding_char = '0';
+
+ if (is_negative)
+ result.sign_char = '-';
+ else if ((to_conv.flags & FormatFlags::FORCE_SIGN) ==
+ FormatFlags::FORCE_SIGN &&
+ gets_plus_sign)
+ result.sign_char = '+';
+
+ // sign isn't a problem because we're taking the max. The result is always
+ // non-negative. Also min_width can only be 0 if it's defaulted, since 0 is a
+ // flag.
+ if (to_conv.min_width > 0)
+ result.pad_to_len = to_conv.min_width;
+
+ return result;
+}
+
+LIBC_INLINE int convert_int(printf_core::Writer *writer,
+ const FormatSection &to_conv, const tm *timeptr) {
+
+ return write_padded_int(writer, get_int_format(to_conv, timeptr));
+}
+
+} // 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 0000000000000..659587a8c9d55
--- /dev/null
+++ b/libc/src/time/strftime_core/parser.h
@@ -0,0 +1,111 @@
+//===-- 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 "hdr/types/struct_tm.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/str_converter.h b/libc/src/time/strftime_core/str_converter.h
new file mode 100644
index 0000000000000..f0d5bf59102f3
--- /dev/null
+++ b/libc/src/time/strftime_core/str_converter.h
@@ -0,0 +1,76 @@
+//===-- String converter for strftime ---------------------------*- 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_STR_CONVERTER_H
+#define LLVM_LIBC_SRC_STDIO_STRFTIME_CORE_STR_CONVERTER_H
+
+#include "hdr/types/struct_tm.h"
+#include "src/__support/CPP/string_view.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 {
+
+static constexpr cpp::string_view OUT_OF_BOUNDS_STR = "?";
+
+LIBC_INLINE cpp::string_view
+unwrap_opt(cpp::optional<cpp::string_view> str_opt) {
+ return str_opt.has_value() ? *str_opt : OUT_OF_BOUNDS_STR;
+}
+
+LIBC_INLINE int convert_str(printf_core::Writer *writer,
+ const FormatSection &to_conv, const tm *timeptr) {
+ cpp::string_view str;
+ cpp::optional<cpp::string_view> str_opt;
+ const time_utils::TMReader time_reader(timeptr);
+
+ switch (to_conv.conv_name) {
+ case 'a': // Abbreviated weekday name
+ str_opt = time_reader.get_weekday_short_name();
+ str = unwrap_opt(str_opt);
+ break;
+ case 'A': // Full weekday name
+ str_opt = time_reader.get_weekday_full_name();
+ str = unwrap_opt(str_opt);
+ break;
+ case 'b': // Abbreviated month name
+ case 'h': // same as 'b'
+ str_opt = time_reader.get_month_short_name();
+ str = unwrap_opt(str_opt);
+ break;
+ case 'B': // Full month name
+ str_opt = time_reader.get_month_full_name();
+ str = unwrap_opt(str_opt);
+ break;
+ case 'p': // AM/PM designation
+ str = time_reader.get_am_pm();
+ break;
+ case 'Z': // Timezone name
+ // the standard says if no time zone is determinable, write no characters.
+ return WRITE_OK;
+ // str = time_reader.get_timezone_name();
+ break;
+ default:
+ __builtin_trap(); // this should be unreachable, but trap if you hit it.
+ }
+
+ int spaces = to_conv.min_width - static_cast<int>(str.size());
+ if (spaces > 0)
+ RET_IF_RESULT_NEGATIVE(writer->write(' ', spaces));
+ RET_IF_RESULT_NEGATIVE(writer->write(str));
+
+ return WRITE_OK;
+}
+
+} // namespace strftime_core
+} // namespace LIBC_NAMESPACE_DECL
+#endif // LLVM_LIBC_SRC_STDIO_STRFTIME_CORE_STR_CONVERTER_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 0000000000000..00839e5a3f4da
--- /dev/null
+++ b/libc/src/time/strftime_core/strftime_main.cpp
@@ -0,0 +1,40 @@
+//===-- 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 "hdr/types/struct_tm.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"
+
+namespace LIBC_NAMESPACE_DECL {
+namespace strftime_core {
+
+int strftime_main(printf_core::Writer *writer, const char *__restrict str,
+ const 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 0000000000000..ae706828df44c
--- /dev/null
+++ b/libc/src/time/strftime_core/strftime_main.h
@@ -0,0 +1,25 @@
+//===-- 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 "hdr/types/struct_tm.h"
+#include "src/__support/macros/config.h"
+#include "src/stdio/printf_core/writer.h"
+
+namespace LIBC_NAMESPACE_DECL {
+namespace strftime_core {
+
+int strftime_main(printf_core::Writer *writer, const char *__restrict str,
+ const 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 bcf19ff5f193e..0fcb7ffbf13b0 100644
--- a/libc/src/time/time_constants.h
+++ b/libc/src/time/time_constants.h
@@ -48,6 +48,7 @@ constexpr int HOURS_PER_DAY = 24;
constexpr int DAYS_PER_WEEK = 7;
constexpr int WEEKS_PER_YEAR = 52;
constexpr int MONTHS_PER_YEAR = 12;
+constexpr int MAX_DAYS_PER_MONTH = 31;
constexpr int DAYS_PER_NON_LEAP_YEAR = 365;
constexpr int DAYS_PER_LEAP_YEAR = 366;
diff --git a/libc/test/src/time/CMakeLists.txt b/libc/test/src/time/CMakeLists.txt
index 12add224f386a..618812fd8eee5 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 0000000000000..4fbd1154e3807
--- /dev/null
+++ b/libc/test/src/time/strftime_test.cpp
@@ -0,0 +1,2330 @@
+//===-- 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, e = str.size(); str_cur < e; ++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
diff erence 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) {
+ using LIBC_NAMESPACE::time_constants::MAX_DAYS_PER_MONTH;
+ // 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 <= MAX_DAYS_PER_MONTH; ++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) {
+ time.tm_year = i;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%g", &time);
+ char *result = spn.get_padded_num(i % 100, 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;
+
+ /*
+This table has an X for each day that should be in the previous year,
+everywhere else should be in the current year.
+
+ yday
+ 0123456
+ 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 yday = 0; yday < 5; ++yday) {
+ for (size_t iso_wday = LIBC_NAMESPACE::time_constants::MONDAY; iso_wday < 8;
+ ++iso_wday) {
+ // start with monday, to match the ISO week.
+ time.tm_wday = iso_wday % LIBC_NAMESPACE::time_constants::DAYS_PER_WEEK;
+ time.tm_yday = yday;
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%g", &time);
+
+ if (iso_wday <= LIBC_NAMESPACE::time_constants::THURSDAY || yday >= 3) {
+ // monday - thursday are never in the previous year, nor are the 4th and
+ // after.
+ EXPECT_STREQ_LEN(written, buffer, "99");
+ } else {
+ // iso_wday is 5, 6, or 7 and yday is 0, 1, or 2.
+ // days_since_thursday is therefor 1, 2, or 3.
+ const size_t days_since_thursday =
+ iso_wday - LIBC_NAMESPACE::time_constants::THURSDAY;
+
+ if (days_since_thursday > yday) {
+ EXPECT_STREQ_LEN(written, buffer, "98");
+ } else {
+ EXPECT_STREQ_LEN(written, buffer, "99");
+ }
+ }
+ }
+ }
+
+ /*
+ 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
+ 6543210
+ 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 0123456
+ 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, because the ISO year starts on the week with the 4th. Since Thursday
+ is at least 3 days from either edge of the ISO week, the first thursday of
+ the year is always in the first ISO week of the 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 = LIBC_NAMESPACE::time_constants::MONDAY; iso_wday < 8;
+ ++iso_wday) {
+ // start with monday, to match the ISO week.
+ time.tm_wday = iso_wday % LIBC_NAMESPACE::time_constants::DAYS_PER_WEEK;
+ // subtract 1 from the max yday to handle yday being 0-indexed.
+ time.tm_yday = LIBC_NAMESPACE::time_constants::DAYS_PER_NON_LEAP_YEAR -
+ 1 - days_left;
+
+ time_leap_year.tm_wday =
+ iso_wday % LIBC_NAMESPACE::time_constants::DAYS_PER_WEEK;
+ time_leap_year.tm_yday =
+ LIBC_NAMESPACE::time_constants::LAST_DAY_OF_LEAP_YEAR - 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 >= LIBC_NAMESPACE::time_constants::THURSDAY ||
+ 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");
+ EXPECT_STREQ_LEN(written_leap_year, buffer_leap_year, "00");
+ } 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, "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");
+ }
+ }
+ }
+ }
+
+ // 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");
+}
+
+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; i < 10000; ++i) {
+ time.tm_year = get_adjusted_year(i);
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%G", &time);
+ char *result = spn.get_padded_num(i, 4);
+
+ ASSERT_STREQ(buffer, result);
+ ASSERT_EQ(written, spn.get_str_len());
+ }
+
+ // also check it handles years with extra digits properly
+ time.tm_year = get_adjusted_year(12345);
+ 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 = get_adjusted_year(1999);
+
+ // check the first days of the year
+ for (size_t yday = 0; 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 % LIBC_NAMESPACE::time_constants::DAYS_PER_WEEK;
+ time.tm_yday = yday;
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%G", &time);
+
+ if (iso_wday <= LIBC_NAMESPACE::time_constants::THURSDAY || 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 0, 1, or 2.
+ // days_since_thursday is therefor 1, 2, or 3.
+ const size_t days_since_thursday =
+ iso_wday - LIBC_NAMESPACE::time_constants::THURSDAY;
+
+ 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 % LIBC_NAMESPACE::time_constants::DAYS_PER_WEEK;
+ // subtract 1 from the max yday to handle yday being 0-indexed.
+ time.tm_yday =
+ LIBC_NAMESPACE::time_constants::LAST_DAY_OF_NON_LEAP_YEAR - days_left;
+
+ time_leap_year.tm_wday =
+ iso_wday % LIBC_NAMESPACE::time_constants::DAYS_PER_WEEK;
+ time_leap_year.tm_yday =
+ LIBC_NAMESPACE::time_constants::LAST_DAY_OF_LEAP_YEAR - 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 = get_adjusted_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 = get_adjusted_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");
+
+ time.tm_year = get_adjusted_year(2001);
+ 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");
+}
+
+TEST(LlvmLibcStrftimeTest, TwentyFourHour) {
+ // this tests %H, which reads: [tm_hour]
+ struct tm time;
+ char buffer[100];
+ size_t written = 0;
+ SimplePaddedNum spn;
+
+ // Tests on all the well defined values
+ for (size_t i = 0; i < 24; ++i) {
+ time.tm_hour = i;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%H", &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_hour = 5;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%01H", &time);
+ EXPECT_STREQ_LEN(written, buffer, "5");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%02H", &time);
+ EXPECT_STREQ_LEN(written, buffer, "05");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%05H", &time);
+ EXPECT_STREQ_LEN(written, buffer, "00005");
+
+ time.tm_hour = 23;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%01H", &time);
+ EXPECT_STREQ_LEN(written, buffer, "23");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%02H", &time);
+ EXPECT_STREQ_LEN(written, buffer, "23");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%05H", &time);
+ EXPECT_STREQ_LEN(written, buffer, "00023");
+}
+
+TEST(LlvmLibcStrftimeTest, TwelveHour) {
+ // this tests %I, which reads: [tm_hour]
+ struct tm time;
+ char buffer[100];
+ size_t written = 0;
+ SimplePaddedNum spn;
+
+ time.tm_hour = 0;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%I", &time);
+ EXPECT_STREQ_LEN(written, buffer, "12");
+
+ // Tests on all the well defined values, except 0 since it was easier to
+ // special case it.
+ for (size_t i = 1; i <= 12; ++i) {
+ char *result = spn.get_padded_num(i, 2);
+
+ time.tm_hour = i;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%I", &time);
+ ASSERT_STREQ(buffer, result);
+ ASSERT_EQ(written, spn.get_str_len());
+
+ // hour + 12 should give the same result
+ time.tm_hour = i + 12;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%I", &time);
+ 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_hour = 5;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%01I", &time);
+ EXPECT_STREQ_LEN(written, buffer, "5");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%02I", &time);
+ EXPECT_STREQ_LEN(written, buffer, "05");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%05I", &time);
+ EXPECT_STREQ_LEN(written, buffer, "00005");
+
+ time.tm_hour = 23;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%01I", &time);
+ EXPECT_STREQ_LEN(written, buffer, "11");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%02I", &time);
+ EXPECT_STREQ_LEN(written, buffer, "11");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%05I", &time);
+ EXPECT_STREQ_LEN(written, buffer, "00011");
+}
+
+TEST(LlvmLibcStrftimeTest, DayOfYear) {
+ // this tests %j, which reads: [tm_yday]
+ struct tm time;
+ char buffer[100];
+ size_t written = 0;
+ SimplePaddedNum spn;
+
+ // Tests on all the well defined values
+ for (size_t i = 0; i < LIBC_NAMESPACE::time_constants::DAYS_PER_LEAP_YEAR;
+ ++i) {
+ time.tm_yday = i;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%j", &time);
+ char *result = spn.get_padded_num(i + 1, 3);
+
+ 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_yday = 5 - 1;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%01j", &time);
+ EXPECT_STREQ_LEN(written, buffer, "5");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%02j", &time);
+ EXPECT_STREQ_LEN(written, buffer, "05");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%05j", &time);
+ EXPECT_STREQ_LEN(written, buffer, "00005");
+
+ time.tm_yday = 123 - 1;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%01j", &time);
+ EXPECT_STREQ_LEN(written, buffer, "123");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%02j", &time);
+ EXPECT_STREQ_LEN(written, buffer, "123");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%05j", &time);
+ EXPECT_STREQ_LEN(written, buffer, "00123");
+}
+
+TEST(LlvmLibcStrftimeTest, MonthOfYear) {
+ // this tests %m, which reads: [tm_mon]
+ struct tm time;
+ char buffer[100];
+ size_t written = 0;
+ SimplePaddedNum spn;
+
+ // Tests on all the well defined values
+ for (size_t i = 0; i < LIBC_NAMESPACE::time_constants::MONTHS_PER_YEAR; ++i) {
+ time.tm_mon = i;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%m", &time);
+ // %m is 1 indexed, so add 1 to the number we're comparing to.
+ char *result = spn.get_padded_num(i + 1, 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_mon = 5 - 1;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%01m", &time);
+ EXPECT_STREQ_LEN(written, buffer, "5");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%02m", &time);
+ EXPECT_STREQ_LEN(written, buffer, "05");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%05m", &time);
+ EXPECT_STREQ_LEN(written, buffer, "00005");
+
+ time.tm_mon = 11 - 1;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%01m", &time);
+ EXPECT_STREQ_LEN(written, buffer, "11");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%02m", &time);
+ EXPECT_STREQ_LEN(written, buffer, "11");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%05m", &time);
+ EXPECT_STREQ_LEN(written, buffer, "00011");
+}
+
+TEST(LlvmLibcStrftimeTest, MinuteOfHour) {
+ // this tests %M, which reads: [tm_min]
+ struct tm time;
+ char buffer[100];
+ size_t written = 0;
+ SimplePaddedNum spn;
+
+ // Tests on all the well defined values
+ for (size_t i = 0; i < LIBC_NAMESPACE::time_constants::MINUTES_PER_HOUR;
+ ++i) {
+ time.tm_min = i;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%M", &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_min = 5;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%01M", &time);
+ EXPECT_STREQ_LEN(written, buffer, "5");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%02M", &time);
+ EXPECT_STREQ_LEN(written, buffer, "05");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%05M", &time);
+ EXPECT_STREQ_LEN(written, buffer, "00005");
+
+ time.tm_min = 11;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%01M", &time);
+ EXPECT_STREQ_LEN(written, buffer, "11");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%02M", &time);
+ EXPECT_STREQ_LEN(written, buffer, "11");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%05M", &time);
+ EXPECT_STREQ_LEN(written, buffer, "00011");
+}
+
+TEST(LlvmLibcStrftimeTest, SecondsSinceEpoch) {
+ // this tests %s, which reads: [tm_year, tm_mon, tm_mday, tm_hour, tm_min,
+ // tm_sec, tm_isdst]
+ struct tm time;
+ char buffer[100];
+ size_t written = 0;
+
+ time.tm_year = get_adjusted_year(1970);
+ // yday is not used, the day of the year is calculated from the month and mday
+ time.tm_mon = 0;
+ time.tm_mday = 1; // the only 1-indexed member
+ time.tm_hour = 0;
+ time.tm_min = 0;
+ time.tm_sec = 1;
+ time.tm_isdst = 0;
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%s", &time);
+ EXPECT_STREQ_LEN(written, buffer, "1");
+
+ // The time as of writing this test
+ time.tm_year = get_adjusted_year(2025);
+ time.tm_mon = 1;
+ time.tm_mday = 4;
+ time.tm_hour = 11;
+ time.tm_min = 8;
+ time.tm_sec = 41;
+ time.tm_isdst = 0;
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%s", &time);
+ // if you run your system's strftime to compare you will likely get a slightly
+ //
diff erent result because it's supposed to respect timezones.
+ EXPECT_STREQ_LEN(written, buffer, "1738667321");
+
+ // Thorough testing of the mktime mechanism is done in the mktime tests, so
+ // they aren't duplicated here.
+
+ // padding is technically undefined for this conversion, but we support it, so
+ // we need to test it.
+ time.tm_year = get_adjusted_year(1970);
+ time.tm_mon = 0;
+ time.tm_mday = 1;
+ time.tm_hour = 0;
+ time.tm_min = 0;
+ time.tm_sec = 5;
+ time.tm_isdst = 0;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%01s", &time);
+ EXPECT_STREQ_LEN(written, buffer, "5");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%02s", &time);
+ EXPECT_STREQ_LEN(written, buffer, "05");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%05s", &time);
+ EXPECT_STREQ_LEN(written, buffer, "00005");
+
+ time.tm_min = 11;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%01s", &time);
+ EXPECT_STREQ_LEN(written, buffer, "665");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%02s", &time);
+ EXPECT_STREQ_LEN(written, buffer, "665");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%05s", &time);
+ EXPECT_STREQ_LEN(written, buffer, "00665");
+}
+
+TEST(LlvmLibcStrftimeTest, SecondOfMinute) {
+ // this tests %S, which reads: [tm_sec]
+ struct tm time;
+ char buffer[100];
+ size_t written = 0;
+ SimplePaddedNum spn;
+
+ // Tests on all the well defined values
+ for (size_t i = 0; i < LIBC_NAMESPACE::time_constants::SECONDS_PER_MIN; ++i) {
+ time.tm_sec = i;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%S", &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_sec = 5;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%01S", &time);
+ EXPECT_STREQ_LEN(written, buffer, "5");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%02S", &time);
+ EXPECT_STREQ_LEN(written, buffer, "05");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%05S", &time);
+ EXPECT_STREQ_LEN(written, buffer, "00005");
+
+ time.tm_sec = 11;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%01S", &time);
+ EXPECT_STREQ_LEN(written, buffer, "11");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%02S", &time);
+ EXPECT_STREQ_LEN(written, buffer, "11");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%05S", &time);
+ EXPECT_STREQ_LEN(written, buffer, "00011");
+}
+
+TEST(LlvmLibcStrftimeTest, ISODayOfWeek) {
+ // this tests %u, which reads: [tm_wday]
+ struct tm time;
+ char buffer[100];
+ size_t written = 0;
+ SimplePaddedNum spn;
+
+ time.tm_wday = LIBC_NAMESPACE::time_constants::SUNDAY;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%u", &time);
+ EXPECT_STREQ_LEN(written, buffer, "7");
+
+ // Tests on all the well defined values except for sunday, which is 0 in
+ // normal weekdays but 7 here.
+ for (size_t i = LIBC_NAMESPACE::time_constants::MONDAY;
+ i <= LIBC_NAMESPACE::time_constants::SATURDAY; ++i) {
+ time.tm_wday = i;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%u", &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_wday = 5;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%01u", &time);
+ EXPECT_STREQ_LEN(written, buffer, "5");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%02u", &time);
+ EXPECT_STREQ_LEN(written, buffer, "05");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%05u", &time);
+ EXPECT_STREQ_LEN(written, buffer, "00005");
+}
+
+TEST(LlvmLibcStrftimeTest, WeekOfYearStartingSunday) {
+ // this tests %U, which reads: [tm_year, tm_wday, tm_yday]
+ struct tm time;
+ char buffer[100];
+ size_t written = 0;
+ SimplePaddedNum spn;
+
+ // setting the year to a leap year, but it doesn't actually matter. This
+ // conversion doesn't end up checking the year at all.
+ time.tm_year = get_adjusted_year(2000);
+
+ const int WEEK_START = LIBC_NAMESPACE::time_constants::SUNDAY;
+
+ for (size_t first_weekday = LIBC_NAMESPACE::time_constants::SUNDAY;
+ first_weekday <= LIBC_NAMESPACE::time_constants::SATURDAY;
+ ++first_weekday) {
+ time.tm_wday = first_weekday;
+ size_t cur_week = 0;
+
+ // iterate through the year, starting on first_weekday.
+ for (size_t yday = 0;
+ yday < LIBC_NAMESPACE::time_constants::DAYS_PER_LEAP_YEAR; ++yday) {
+ time.tm_yday = yday;
+ // If the week just ended, move to the next week.
+ if (time.tm_wday == WEEK_START)
+ ++cur_week;
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%U", &time);
+ char *result = spn.get_padded_num(cur_week, 2);
+
+ ASSERT_STREQ(buffer, result);
+ ASSERT_EQ(written, spn.get_str_len());
+
+ // a day has passed, move to the next weekday, looping as necessary.
+ time.tm_wday =
+ (time.tm_wday + 1) % LIBC_NAMESPACE::time_constants::DAYS_PER_WEEK;
+ }
+ }
+
+ // padding is technically undefined for this conversion, but we support it, so
+ // we need to test it.
+ time.tm_wday = LIBC_NAMESPACE::time_constants::SUNDAY;
+ time.tm_yday = 22;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%01U", &time);
+ EXPECT_STREQ_LEN(written, buffer, "4");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%02U", &time);
+ EXPECT_STREQ_LEN(written, buffer, "04");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%05U", &time);
+ EXPECT_STREQ_LEN(written, buffer, "00004");
+
+ time.tm_wday = LIBC_NAMESPACE::time_constants::SUNDAY;
+ time.tm_yday = 78;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%01U", &time);
+ EXPECT_STREQ_LEN(written, buffer, "12");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%02U", &time);
+ EXPECT_STREQ_LEN(written, buffer, "12");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%05U", &time);
+ EXPECT_STREQ_LEN(written, buffer, "00012");
+}
+
+TEST(LlvmLibcStrftimeTest, ISOWeekOfYear) {
+ // this tests %V, which reads: [tm_year, tm_wday, tm_yday]
+ struct tm time;
+ char buffer[100];
+ size_t written = 0;
+ SimplePaddedNum spn;
+
+ const int starting_year = get_adjusted_year(1999);
+
+ // we're going to check the days from 1999 to 2001 to cover all the
+ // transitions to and from leap years and non-leap years (the start of 1999
+ // and end of 2001 cover the non-leap years).
+ const int days_to_check = // 1096
+ LIBC_NAMESPACE::time_constants::DAYS_PER_NON_LEAP_YEAR +
+ LIBC_NAMESPACE::time_constants::DAYS_PER_LEAP_YEAR +
+ LIBC_NAMESPACE::time_constants::DAYS_PER_NON_LEAP_YEAR;
+
+ const int WEEK_START = LIBC_NAMESPACE::time_constants::MONDAY;
+
+ for (size_t first_weekday = LIBC_NAMESPACE::time_constants::SUNDAY;
+ first_weekday <= LIBC_NAMESPACE::time_constants::SATURDAY;
+ ++first_weekday) {
+ time.tm_year = starting_year;
+ time.tm_wday = first_weekday;
+ time.tm_yday = 0;
+ size_t cur_week = 1;
+ if (first_weekday == LIBC_NAMESPACE::time_constants::SUNDAY ||
+ first_weekday == LIBC_NAMESPACE::time_constants::SATURDAY)
+ cur_week = 52;
+ else if (first_weekday == LIBC_NAMESPACE::time_constants::FRIDAY)
+ cur_week = 53;
+
+ // iterate through the year, starting on first_weekday.
+ for (size_t cur_day = 0; cur_day < days_to_check; ++cur_day) {
+ // If the week just ended, move to the next week.
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%V", &time);
+ char *result = spn.get_padded_num(cur_week, 2);
+
+ ASSERT_STREQ(buffer, result);
+ ASSERT_EQ(written, spn.get_str_len());
+
+ // a day has passed, increment the counters.
+ ++time.tm_yday;
+ if (time.tm_yday ==
+ (time.tm_year == get_adjusted_year(2000)
+ ? LIBC_NAMESPACE::time_constants::DAYS_PER_LEAP_YEAR
+ : LIBC_NAMESPACE::time_constants::DAYS_PER_NON_LEAP_YEAR)) {
+ time.tm_yday = 0;
+ ++time.tm_year;
+ }
+
+ time.tm_wday =
+ (time.tm_wday + 1) % LIBC_NAMESPACE::time_constants::DAYS_PER_WEEK;
+ if (time.tm_wday == WEEK_START) {
+ ++cur_week;
+ const int days_left_in_year =
+ (time.tm_year == get_adjusted_year(2000)
+ ? LIBC_NAMESPACE::time_constants::LAST_DAY_OF_LEAP_YEAR
+ : LIBC_NAMESPACE::time_constants::LAST_DAY_OF_NON_LEAP_YEAR) -
+ time.tm_yday;
+
+ // if the week we're currently in is in the next year, or if the year
+ // has turned over, reset the week.
+ if (days_left_in_year < 3 || (cur_week > 51 && time.tm_yday < 10))
+ cur_week = 1;
+ }
+ }
+ }
+
+ // padding is technically undefined for this conversion, but we support it, so
+ // we need to test it.
+ time.tm_wday = LIBC_NAMESPACE::time_constants::SUNDAY;
+ time.tm_yday = 22;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%01U", &time);
+ EXPECT_STREQ_LEN(written, buffer, "4");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%02U", &time);
+ EXPECT_STREQ_LEN(written, buffer, "04");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%05U", &time);
+ EXPECT_STREQ_LEN(written, buffer, "00004");
+
+ time.tm_wday = LIBC_NAMESPACE::time_constants::SUNDAY;
+ time.tm_yday = 78;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%01U", &time);
+ EXPECT_STREQ_LEN(written, buffer, "12");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%02U", &time);
+ EXPECT_STREQ_LEN(written, buffer, "12");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%05U", &time);
+ EXPECT_STREQ_LEN(written, buffer, "00012");
+}
+
+TEST(LlvmLibcStrftimeTest, DayOfWeek) {
+ // this tests %w, which reads: [tm_wday]
+ struct tm time;
+ char buffer[100];
+ size_t written = 0;
+ SimplePaddedNum spn;
+
+ // Tests on all the well defined values.
+ for (size_t i = LIBC_NAMESPACE::time_constants::SUNDAY;
+ i <= LIBC_NAMESPACE::time_constants::SATURDAY; ++i) {
+ time.tm_wday = i;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%w", &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_wday = 5;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%01w", &time);
+ EXPECT_STREQ_LEN(written, buffer, "5");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%02w", &time);
+ EXPECT_STREQ_LEN(written, buffer, "05");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%05w", &time);
+ EXPECT_STREQ_LEN(written, buffer, "00005");
+}
+
+TEST(LlvmLibcStrftimeTest, WeekOfYearStartingMonday) {
+ // this tests %W, which reads: [tm_year, tm_wday, tm_yday]
+ struct tm time;
+ char buffer[100];
+ size_t written = 0;
+ SimplePaddedNum spn;
+
+ // setting the year to a leap year, but it doesn't actually matter. This
+ // conversion doesn't end up checking the year at all.
+ time.tm_year = get_adjusted_year(2000);
+
+ const int WEEK_START = LIBC_NAMESPACE::time_constants::MONDAY;
+
+ for (size_t first_weekday = LIBC_NAMESPACE::time_constants::SUNDAY;
+ first_weekday <= LIBC_NAMESPACE::time_constants::SATURDAY;
+ ++first_weekday) {
+ time.tm_wday = first_weekday;
+ size_t cur_week = 0;
+
+ // iterate through the year, starting on first_weekday.
+ for (size_t yday = 0;
+ yday < LIBC_NAMESPACE::time_constants::DAYS_PER_LEAP_YEAR; ++yday) {
+ time.tm_yday = yday;
+ // If the week just ended, move to the next week.
+ if (time.tm_wday == WEEK_START)
+ ++cur_week;
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%W", &time);
+ char *result = spn.get_padded_num(cur_week, 2);
+
+ ASSERT_STREQ(buffer, result);
+ ASSERT_EQ(written, spn.get_str_len());
+
+ // a day has passed, move to the next weekday, looping as necessary.
+ time.tm_wday =
+ (time.tm_wday + 1) % LIBC_NAMESPACE::time_constants::DAYS_PER_WEEK;
+ }
+ }
+
+ // padding is technically undefined for this conversion, but we support it, so
+ // we need to test it.
+ time.tm_wday = LIBC_NAMESPACE::time_constants::MONDAY;
+ time.tm_yday = 22;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%01W", &time);
+ EXPECT_STREQ_LEN(written, buffer, "4");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%02W", &time);
+ EXPECT_STREQ_LEN(written, buffer, "04");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%05W", &time);
+ EXPECT_STREQ_LEN(written, buffer, "00004");
+
+ time.tm_wday = LIBC_NAMESPACE::time_constants::MONDAY;
+ time.tm_yday = 78;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%01W", &time);
+ EXPECT_STREQ_LEN(written, buffer, "12");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%02W", &time);
+ EXPECT_STREQ_LEN(written, buffer, "12");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%05W", &time);
+ EXPECT_STREQ_LEN(written, buffer, "00012");
+}
+
+TEST(LlvmLibcStrftimeTest, YearOfCentury) {
+ // this tests %y, which reads: [tm_year]
+ struct tm time;
+ char buffer[100];
+ size_t written = 0;
+ SimplePaddedNum spn;
+
+ time.tm_year = get_adjusted_year(2000);
+
+ // iterate through the year, starting on first_weekday.
+ for (size_t year = 1900; year < 2001; ++year) {
+ time.tm_year = get_adjusted_year(year);
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%y", &time);
+ char *result = spn.get_padded_num(year % 100, 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_year = get_adjusted_year(2004);
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%01y", &time);
+ EXPECT_STREQ_LEN(written, buffer, "4");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%02y", &time);
+ EXPECT_STREQ_LEN(written, buffer, "04");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%05y", &time);
+ EXPECT_STREQ_LEN(written, buffer, "00004");
+
+ time.tm_year = get_adjusted_year(12345);
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%01y", &time);
+ EXPECT_STREQ_LEN(written, buffer, "45");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%02y", &time);
+ EXPECT_STREQ_LEN(written, buffer, "45");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%05y", &time);
+ EXPECT_STREQ_LEN(written, buffer, "00045");
+}
+
+TEST(LlvmLibcStrftimeTest, FullYearTests) {
+ // this tests %Y, which reads: [tm_year]
+ struct tm time;
+ char buffer[100];
+ size_t written = 0;
+ SimplePaddedNum spn;
+
+ // Test the easy cases
+ for (int i = 1; i < 10000; ++i) {
+ time.tm_year = get_adjusted_year(i);
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%Y", &time);
+ char *result = spn.get_padded_num(i, 4);
+
+ ASSERT_STREQ(buffer, result);
+ ASSERT_EQ(written, spn.get_str_len());
+ }
+
+ time.tm_year = get_adjusted_year(11900);
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%Y", &time);
+ EXPECT_STREQ_LEN(written, buffer, "11900");
+
+ time.tm_year = get_adjusted_year(0);
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%Y", &time);
+ EXPECT_STREQ_LEN(written, buffer, "0000");
+
+ time.tm_year = get_adjusted_year(-1);
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%Y", &time);
+ // TODO: should this be what we standardize? Posix doesn't specify what to do
+ // about negative numbers
+ EXPECT_STREQ_LEN(written, buffer, "-001");
+
+ time.tm_year = get_adjusted_year(-9001);
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%Y", &time);
+ EXPECT_STREQ_LEN(written, buffer, "-9001");
+
+ time.tm_year = get_adjusted_year(-10001);
+ 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 = get_adjusted_year(2023);
+ 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 = get_adjusted_year(900);
+ 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 = get_adjusted_year(12345);
+ 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 = get_adjusted_year(-123);
+ 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 = get_adjusted_year(2023);
+ 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 = get_adjusted_year(900);
+ 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 = get_adjusted_year(12345);
+ 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 = get_adjusted_year(-123);
+ 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 = get_adjusted_year(1970);
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%Y", &time);
+ EXPECT_STREQ_LEN(written, buffer, "1970");
+
+ time.tm_year = get_adjusted_year(1970);
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%+4Y", &time);
+ EXPECT_STREQ_LEN(written, buffer, "1970");
+
+ time.tm_year = get_adjusted_year(27);
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%Y", &time);
+ EXPECT_STREQ_LEN(written, buffer, "0027");
+
+ time.tm_year = get_adjusted_year(270);
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%Y", &time);
+ EXPECT_STREQ_LEN(written, buffer, "0270");
+
+ time.tm_year = get_adjusted_year(270);
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%+4Y", &time);
+ EXPECT_STREQ_LEN(written, buffer, "0270");
+
+ time.tm_year = get_adjusted_year(12345);
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%Y", &time);
+ EXPECT_STREQ_LEN(written, buffer, "12345");
+
+ time.tm_year = get_adjusted_year(12345);
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%+4Y", &time);
+ EXPECT_STREQ_LEN(written, buffer, "+12345");
+
+ time.tm_year = get_adjusted_year(12345);
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%05Y", &time);
+ EXPECT_STREQ_LEN(written, buffer, "12345");
+
+ time.tm_year = get_adjusted_year(270);
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%+5Y", &time);
+ EXPECT_STREQ_LEN(written, buffer, "+0270");
+
+ time.tm_year = get_adjusted_year(12345);
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%+5Y", &time);
+ EXPECT_STREQ_LEN(written, buffer, "+12345");
+
+ time.tm_year = get_adjusted_year(12345);
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%06Y", &time);
+ EXPECT_STREQ_LEN(written, buffer, "012345");
+
+ time.tm_year = get_adjusted_year(12345);
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%+6Y", &time);
+ EXPECT_STREQ_LEN(written, buffer, "+12345");
+
+ time.tm_year = get_adjusted_year(123456);
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%08Y", &time);
+ EXPECT_STREQ_LEN(written, buffer, "00123456");
+
+ time.tm_year = get_adjusted_year(123456);
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%+8Y", &time);
+ EXPECT_STREQ_LEN(written, buffer, "+0123456");
+}
+
+// String conversions
+
+struct num_str_pair {
+ int num;
+ LIBC_NAMESPACE::cpp::string_view str;
+};
+
+TEST(LlvmLibcStrftimeTest, ShortWeekdayName) {
+ // this tests %a, which reads: [tm_wday]
+ struct tm time;
+ char buffer[100];
+ size_t written = 0;
+
+ constexpr LIBC_NAMESPACE::cpp::array<
+ num_str_pair, LIBC_NAMESPACE::time_constants::DAYS_PER_WEEK>
+ WEEKDAY_PAIRS = {{
+ {LIBC_NAMESPACE::time_constants::SUNDAY, "Sun"},
+ {LIBC_NAMESPACE::time_constants::MONDAY, "Mon"},
+ {LIBC_NAMESPACE::time_constants::TUESDAY, "Tue"},
+ {LIBC_NAMESPACE::time_constants::WEDNESDAY, "Wed"},
+ {LIBC_NAMESPACE::time_constants::THURSDAY, "Thu"},
+ {LIBC_NAMESPACE::time_constants::FRIDAY, "Fri"},
+ {LIBC_NAMESPACE::time_constants::SATURDAY, "Sat"},
+ }};
+
+ for (size_t i = 0; i < WEEKDAY_PAIRS.size(); ++i) {
+ time.tm_wday = WEEKDAY_PAIRS[i].num;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%a", &time);
+ EXPECT_STREQ(buffer, WEEKDAY_PAIRS[i].str.data());
+ EXPECT_EQ(written, WEEKDAY_PAIRS[i].str.size());
+ }
+
+ // check invalid weekdays
+ time.tm_wday = -1;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%a", &time);
+ EXPECT_STREQ_LEN(written, buffer, "?");
+
+ time.tm_wday = LIBC_NAMESPACE::time_constants::SATURDAY + 1;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%a", &time);
+ EXPECT_STREQ_LEN(written, buffer, "?");
+
+ // padding is technically undefined for this conversion, but we support it, so
+ // we need to test it.
+ time.tm_wday = LIBC_NAMESPACE::time_constants::THURSDAY;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%1a", &time);
+ EXPECT_STREQ_LEN(written, buffer, "Thu");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%3a", &time);
+ EXPECT_STREQ_LEN(written, buffer, "Thu");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%10a", &time);
+ EXPECT_STREQ_LEN(written, buffer, " Thu");
+
+ time.tm_wday = -1;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%1a", &time);
+ EXPECT_STREQ_LEN(written, buffer, "?");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%3a", &time);
+ EXPECT_STREQ_LEN(written, buffer, " ?");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%10a", &time);
+ EXPECT_STREQ_LEN(written, buffer, " ?");
+}
+
+TEST(LlvmLibcStrftimeTest, FullWeekdayName) {
+ // this tests %a, which reads: [tm_wday]
+ struct tm time;
+ char buffer[100];
+ size_t written = 0;
+
+ constexpr LIBC_NAMESPACE::cpp::array<
+ num_str_pair, LIBC_NAMESPACE::time_constants::DAYS_PER_WEEK>
+ WEEKDAY_PAIRS = {{
+ {LIBC_NAMESPACE::time_constants::SUNDAY, "Sunday"},
+ {LIBC_NAMESPACE::time_constants::MONDAY, "Monday"},
+ {LIBC_NAMESPACE::time_constants::TUESDAY, "Tuesday"},
+ {LIBC_NAMESPACE::time_constants::WEDNESDAY, "Wednesday"},
+ {LIBC_NAMESPACE::time_constants::THURSDAY, "Thursday"},
+ {LIBC_NAMESPACE::time_constants::FRIDAY, "Friday"},
+ {LIBC_NAMESPACE::time_constants::SATURDAY, "Saturday"},
+ }};
+
+ for (size_t i = 0; i < WEEKDAY_PAIRS.size(); ++i) {
+ time.tm_wday = WEEKDAY_PAIRS[i].num;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%A", &time);
+ EXPECT_STREQ(buffer, WEEKDAY_PAIRS[i].str.data());
+ EXPECT_EQ(written, WEEKDAY_PAIRS[i].str.size());
+ }
+
+ // check invalid weekdays
+ time.tm_wday = -1;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%A", &time);
+ EXPECT_STREQ_LEN(written, buffer, "?");
+
+ time.tm_wday = LIBC_NAMESPACE::time_constants::SATURDAY + 1;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%A", &time);
+ EXPECT_STREQ_LEN(written, buffer, "?");
+
+ // padding is technically undefined for this conversion, but we support it, so
+ // we need to test it.
+ time.tm_wday = LIBC_NAMESPACE::time_constants::THURSDAY;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%1A", &time);
+ EXPECT_STREQ_LEN(written, buffer, "Thursday");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%3A", &time);
+ EXPECT_STREQ_LEN(written, buffer, "Thursday");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%10A", &time);
+ EXPECT_STREQ_LEN(written, buffer, " Thursday");
+
+ time.tm_wday = -1;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%1A", &time);
+ EXPECT_STREQ_LEN(written, buffer, "?");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%3A", &time);
+ EXPECT_STREQ_LEN(written, buffer, " ?");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%10A", &time);
+ EXPECT_STREQ_LEN(written, buffer, " ?");
+}
+
+TEST(LlvmLibcStrftimeTest, ShortMonthName) {
+ // this tests %b, which reads: [tm_mon]
+ struct tm time;
+ char buffer[100];
+ size_t written = 0;
+
+ constexpr LIBC_NAMESPACE::cpp::array<
+ num_str_pair, LIBC_NAMESPACE::time_constants::MONTHS_PER_YEAR>
+ MONTH_PAIRS = {{
+ {LIBC_NAMESPACE::time_constants::JANUARY, "Jan"},
+ {LIBC_NAMESPACE::time_constants::FEBRUARY, "Feb"},
+ {LIBC_NAMESPACE::time_constants::MARCH, "Mar"},
+ {LIBC_NAMESPACE::time_constants::APRIL, "Apr"},
+ {LIBC_NAMESPACE::time_constants::MAY, "May"},
+ {LIBC_NAMESPACE::time_constants::JUNE, "Jun"},
+ {LIBC_NAMESPACE::time_constants::JULY, "Jul"},
+ {LIBC_NAMESPACE::time_constants::AUGUST, "Aug"},
+ {LIBC_NAMESPACE::time_constants::SEPTEMBER, "Sep"},
+ {LIBC_NAMESPACE::time_constants::OCTOBER, "Oct"},
+ {LIBC_NAMESPACE::time_constants::NOVEMBER, "Nov"},
+ {LIBC_NAMESPACE::time_constants::DECEMBER, "Dec"},
+ }};
+
+ for (size_t i = 0; i < MONTH_PAIRS.size(); ++i) {
+ time.tm_mon = MONTH_PAIRS[i].num;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%b", &time);
+ EXPECT_STREQ(buffer, MONTH_PAIRS[i].str.data());
+ EXPECT_EQ(written, MONTH_PAIRS[i].str.size());
+ }
+
+ // check invalid weekdays
+ time.tm_mon = -1;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%b", &time);
+ EXPECT_STREQ_LEN(written, buffer, "?");
+
+ time.tm_mon = LIBC_NAMESPACE::time_constants::DECEMBER + 1;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%b", &time);
+ EXPECT_STREQ_LEN(written, buffer, "?");
+
+ // Also test %h, which is identical to %b
+ time.tm_mon = LIBC_NAMESPACE::time_constants::OCTOBER;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%h", &time);
+ EXPECT_STREQ_LEN(written, buffer, "Oct");
+
+ // padding is technically undefined for this conversion, but we support it, so
+ // we need to test it.
+ time.tm_mon = LIBC_NAMESPACE::time_constants::OCTOBER;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%1b", &time);
+ EXPECT_STREQ_LEN(written, buffer, "Oct");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%3b", &time);
+ EXPECT_STREQ_LEN(written, buffer, "Oct");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%10b", &time);
+ EXPECT_STREQ_LEN(written, buffer, " Oct");
+
+ time.tm_mon = -1;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%1b", &time);
+ EXPECT_STREQ_LEN(written, buffer, "?");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%3b", &time);
+ EXPECT_STREQ_LEN(written, buffer, " ?");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%10b", &time);
+ EXPECT_STREQ_LEN(written, buffer, " ?");
+}
+
+TEST(LlvmLibcStrftimeTest, FullMonthName) {
+ // this tests %B, which reads: [tm_mon]
+ struct tm time;
+ char buffer[100];
+ size_t written = 0;
+
+ constexpr LIBC_NAMESPACE::cpp::array<
+ num_str_pair, LIBC_NAMESPACE::time_constants::MONTHS_PER_YEAR>
+ MONTH_PAIRS = {{
+ {LIBC_NAMESPACE::time_constants::JANUARY, "January"},
+ {LIBC_NAMESPACE::time_constants::FEBRUARY, "February"},
+ {LIBC_NAMESPACE::time_constants::MARCH, "March"},
+ {LIBC_NAMESPACE::time_constants::APRIL, "April"},
+ {LIBC_NAMESPACE::time_constants::MAY, "May"},
+ {LIBC_NAMESPACE::time_constants::JUNE, "June"},
+ {LIBC_NAMESPACE::time_constants::JULY, "July"},
+ {LIBC_NAMESPACE::time_constants::AUGUST, "August"},
+ {LIBC_NAMESPACE::time_constants::SEPTEMBER, "September"},
+ {LIBC_NAMESPACE::time_constants::OCTOBER, "October"},
+ {LIBC_NAMESPACE::time_constants::NOVEMBER, "November"},
+ {LIBC_NAMESPACE::time_constants::DECEMBER, "December"},
+ }};
+
+ for (size_t i = 0; i < MONTH_PAIRS.size(); ++i) {
+ time.tm_mon = MONTH_PAIRS[i].num;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%B", &time);
+ EXPECT_STREQ(buffer, MONTH_PAIRS[i].str.data());
+ EXPECT_EQ(written, MONTH_PAIRS[i].str.size());
+ }
+
+ // check invalid weekdays
+ time.tm_mon = -1;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%B", &time);
+ EXPECT_STREQ_LEN(written, buffer, "?");
+
+ time.tm_mon = LIBC_NAMESPACE::time_constants::DECEMBER + 1;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%B", &time);
+ EXPECT_STREQ_LEN(written, buffer, "?");
+
+ // padding is technically undefined for this conversion, but we support it, so
+ // we need to test it.
+ time.tm_mon = LIBC_NAMESPACE::time_constants::OCTOBER;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%1B", &time);
+ EXPECT_STREQ_LEN(written, buffer, "October");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%3B", &time);
+ EXPECT_STREQ_LEN(written, buffer, "October");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%10B", &time);
+ EXPECT_STREQ_LEN(written, buffer, " October");
+
+ time.tm_mon = -1;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%1B", &time);
+ EXPECT_STREQ_LEN(written, buffer, "?");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%3B", &time);
+ EXPECT_STREQ_LEN(written, buffer, " ?");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%10B", &time);
+ EXPECT_STREQ_LEN(written, buffer, " ?");
+}
+
+TEST(LlvmLibcStrftimeTest, AM_PM) {
+ // this tests %p, which reads: [tm_hour]
+ struct tm time;
+ char buffer[100];
+ size_t written = 0;
+
+ time.tm_hour = 0;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%p", &time);
+ EXPECT_STREQ_LEN(written, buffer, "AM");
+
+ time.tm_hour = 6;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%p", &time);
+ EXPECT_STREQ_LEN(written, buffer, "AM");
+
+ time.tm_hour = 12;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%p", &time);
+ EXPECT_STREQ_LEN(written, buffer, "PM");
+
+ time.tm_hour = 18;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%p", &time);
+ EXPECT_STREQ_LEN(written, buffer, "PM");
+
+ // padding is technically undefined for this conversion, but we support it, so
+ // we need to test it.
+ time.tm_hour = 6;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%1p", &time);
+ EXPECT_STREQ_LEN(written, buffer, "AM");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%2p", &time);
+ EXPECT_STREQ_LEN(written, buffer, "AM");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%10p", &time);
+ EXPECT_STREQ_LEN(written, buffer, " AM");
+
+ time.tm_hour = 18;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%1p", &time);
+ EXPECT_STREQ_LEN(written, buffer, "PM");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%2p", &time);
+ EXPECT_STREQ_LEN(written, buffer, "PM");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%10p", &time);
+ EXPECT_STREQ_LEN(written, buffer, " PM");
+}
+
+TEST(LlvmLibcStrftimeTest, DateFormatUS) {
+ // this tests %D, which reads: [tm_mon, tm_mday, tm_year]
+ // This is equivalent to "%m/%d/%y"
+ struct tm time;
+ char buffer[100];
+ size_t written = 0;
+
+ // each of %m, %d, and %y have their own tests, so this test won't cover all
+ // values of those. Instead it will do basic tests and focus on the specific
+ // padding behavior.
+
+ time.tm_mon = 0; // 0 indexed, so 0 is january
+ time.tm_mday = 2; // 1 indexed, so 2 is the 2nd
+ time.tm_year = get_adjusted_year(1903);
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%D", &time);
+ EXPECT_STREQ_LEN(written, buffer, "01/02/03");
+
+ time.tm_mon = 11;
+ time.tm_mday = 31;
+ time.tm_year = get_adjusted_year(1999);
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%D", &time);
+ EXPECT_STREQ_LEN(written, buffer, "12/31/99");
+
+ // The day LLVM-libc started
+ time.tm_mon = 8;
+ time.tm_mday = 16;
+ time.tm_year = get_adjusted_year(2019);
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%D", &time);
+ EXPECT_STREQ_LEN(written, buffer, "09/16/19");
+
+ // %x is equivalent to %D in default locale
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%x", &time);
+ EXPECT_STREQ_LEN(written, buffer, "09/16/19");
+
+ // padding is technically undefined for this conversion, but we support it, so
+ // we need to test it.
+ // Padding is handled in the same way as POSIX describes for %F
+ time.tm_mon = 1;
+ time.tm_mday = 5;
+ time.tm_year = get_adjusted_year(2025);
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%01D", &time);
+ EXPECT_STREQ_LEN(written, buffer, "2/05/25");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%07D", &time);
+ EXPECT_STREQ_LEN(written, buffer, "2/05/25");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%010D", &time);
+ EXPECT_STREQ_LEN(written, buffer, "0002/05/25");
+
+ time.tm_mon = 9;
+ time.tm_mday = 2;
+ time.tm_year = get_adjusted_year(2000);
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%01D", &time);
+ EXPECT_STREQ_LEN(written, buffer, "10/02/00");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%07D", &time);
+ EXPECT_STREQ_LEN(written, buffer, "10/02/00");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%010D", &time);
+ EXPECT_STREQ_LEN(written, buffer, "0010/02/00");
+}
+
+TEST(LlvmLibcStrftimeTest, DateFormatISO) {
+ // this tests %F, which reads: [tm_year, tm_mon, tm_mday]
+ // This is equivalent to "%Y-%m-%d"
+ struct tm time;
+ char buffer[100];
+ size_t written = 0;
+
+ // each of %Y, %m, and %d have their own tests, so this test won't cover all
+ // values of those. Instead it will do basic tests and focus on the specific
+ // padding behavior.
+
+ time.tm_year = get_adjusted_year(1901);
+ time.tm_mon = 1; // 0 indexed, so 1 is february
+ time.tm_mday = 3; // 1 indexed, so 2 is the 2nd
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%F", &time);
+ EXPECT_STREQ_LEN(written, buffer, "1901-02-03");
+
+ time.tm_year = get_adjusted_year(1999);
+ time.tm_mon = 11;
+ time.tm_mday = 31;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%F", &time);
+ EXPECT_STREQ_LEN(written, buffer, "1999-12-31");
+
+ time.tm_year = get_adjusted_year(2019);
+ time.tm_mon = 8;
+ time.tm_mday = 16;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%F", &time);
+ EXPECT_STREQ_LEN(written, buffer, "2019-09-16");
+
+ time.tm_year = get_adjusted_year(123);
+ time.tm_mon = 3;
+ time.tm_mday = 5;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%F", &time);
+ EXPECT_STREQ_LEN(written, buffer, "0123-04-05");
+
+ time.tm_year = get_adjusted_year(67);
+ time.tm_mon = 7;
+ time.tm_mday = 9;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%F", &time);
+ EXPECT_STREQ_LEN(written, buffer, "0067-08-09");
+
+ time.tm_year = get_adjusted_year(2);
+ time.tm_mon = 1;
+ time.tm_mday = 14;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%F", &time);
+ EXPECT_STREQ_LEN(written, buffer, "0002-02-14");
+
+ time.tm_year = get_adjusted_year(-543);
+ time.tm_mon = 1;
+ time.tm_mday = 1;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%F", &time);
+ EXPECT_STREQ_LEN(written, buffer, "-543-02-01");
+
+ // padding tests
+ time.tm_year = get_adjusted_year(2025);
+ time.tm_mon = 1;
+ time.tm_mday = 5;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%01F", &time);
+ EXPECT_STREQ_LEN(written, buffer, "2025-02-05");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%010F", &time);
+ EXPECT_STREQ_LEN(written, buffer, "2025-02-05");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%012F", &time);
+ EXPECT_STREQ_LEN(written, buffer, "002025-02-05");
+
+ time.tm_year = get_adjusted_year(12345);
+ time.tm_mon = 11;
+ time.tm_mday = 25;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%01F", &time);
+ EXPECT_STREQ_LEN(written, buffer, "12345-12-25");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%010F", &time);
+ EXPECT_STREQ_LEN(written, buffer, "12345-12-25");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%012F", &time);
+ EXPECT_STREQ_LEN(written, buffer, "012345-12-25");
+
+ time.tm_year = get_adjusted_year(476);
+ time.tm_mon = 8;
+ time.tm_mday = 4;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%01F", &time);
+ EXPECT_STREQ_LEN(written, buffer, "476-09-04");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%010F", &time);
+ EXPECT_STREQ_LEN(written, buffer, "0476-09-04");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%012F", &time);
+ EXPECT_STREQ_LEN(written, buffer, "000476-09-04");
+
+ time.tm_year = get_adjusted_year(-100);
+ time.tm_mon = 9;
+ time.tm_mday = 31;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%01F", &time);
+ EXPECT_STREQ_LEN(written, buffer, "-100-10-31");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%010F", &time);
+ EXPECT_STREQ_LEN(written, buffer, "-100-10-31");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%012F", &time);
+ EXPECT_STREQ_LEN(written, buffer, "-00100-10-31");
+
+ // '+' flag tests
+ time.tm_year = get_adjusted_year(2025);
+ time.tm_mon = 1;
+ time.tm_mday = 5;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%+1F", &time);
+ EXPECT_STREQ_LEN(written, buffer, "2025-02-05");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%+10F", &time);
+ EXPECT_STREQ_LEN(written, buffer, "2025-02-05");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%+12F", &time);
+ EXPECT_STREQ_LEN(written, buffer, "+02025-02-05");
+
+ time.tm_year = get_adjusted_year(12345);
+ time.tm_mon = 11;
+ time.tm_mday = 25;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%+1F", &time);
+ EXPECT_STREQ_LEN(written, buffer, "+12345-12-25");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%+10F", &time);
+ EXPECT_STREQ_LEN(written, buffer, "+12345-12-25");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%+12F", &time);
+ EXPECT_STREQ_LEN(written, buffer, "+12345-12-25");
+
+ time.tm_year = get_adjusted_year(476);
+ time.tm_mon = 8;
+ time.tm_mday = 4;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%+1F", &time);
+ EXPECT_STREQ_LEN(written, buffer, "476-09-04");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%+10F", &time);
+ EXPECT_STREQ_LEN(written, buffer, "0476-09-04");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%+12F", &time);
+ EXPECT_STREQ_LEN(written, buffer, "+00476-09-04");
+
+ time.tm_year = get_adjusted_year(-100);
+ time.tm_mon = 9;
+ time.tm_mday = 31;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%+1F", &time);
+ EXPECT_STREQ_LEN(written, buffer, "-100-10-31");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%+10F", &time);
+ EXPECT_STREQ_LEN(written, buffer, "-100-10-31");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%+12F", &time);
+ EXPECT_STREQ_LEN(written, buffer, "-00100-10-31");
+}
+
+TEST(LlvmLibcStrftimeTest, TimeFormatAMPM) {
+ // this tests %r, which reads: [tm_hour, tm_min, tm_sec]
+ // This is equivalent to "%I:%M:%S %p"
+ struct tm time;
+ char buffer[100];
+ size_t written = 0;
+
+ // each of %I, %M, %S, and %p have their own tests, so this test won't cover
+ // all values of those. Instead it will do basic tests and focus on the
+ // specific padding behavior.
+
+ time.tm_hour = 0;
+ time.tm_min = 0;
+ time.tm_sec = 0;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%r", &time);
+ EXPECT_STREQ_LEN(written, buffer, "12:00:00 AM");
+
+ time.tm_hour = 1;
+ time.tm_min = 23;
+ time.tm_sec = 45;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%r", &time);
+ EXPECT_STREQ_LEN(written, buffer, "01:23:45 AM");
+
+ time.tm_hour = 18;
+ time.tm_min = 6;
+ time.tm_sec = 2;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%r", &time);
+ EXPECT_STREQ_LEN(written, buffer, "06:06:02 PM");
+
+ // padding is technically undefined for this conversion, but we support it, so
+ // we need to test it.
+ // Padding is handled in the same way as POSIX describes for %F
+ time.tm_hour = 10;
+ time.tm_min = 9;
+ time.tm_sec = 59;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%01r", &time);
+ EXPECT_STREQ_LEN(written, buffer, "10:09:59 AM");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%011r", &time);
+ EXPECT_STREQ_LEN(written, buffer, "10:09:59 AM");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%013r", &time);
+ EXPECT_STREQ_LEN(written, buffer, "0010:09:59 AM");
+
+ time.tm_hour = 16;
+ time.tm_min = 56;
+ time.tm_sec = 9;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%01r", &time);
+ EXPECT_STREQ_LEN(written, buffer, "4:56:09 PM");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%011r", &time);
+ EXPECT_STREQ_LEN(written, buffer, "04:56:09 PM");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%013r", &time);
+ EXPECT_STREQ_LEN(written, buffer, "0004:56:09 PM");
+}
+
+TEST(LlvmLibcStrftimeTest, TimeFormatMinute) {
+ // this tests %R, which reads: [tm_hour, tm_min]
+ // This is equivalent to "%H:%M"
+ struct tm time;
+ char buffer[100];
+ size_t written = 0;
+
+ // each of %H and %M have their own tests, so this test won't cover
+ // all values of those. Instead it will do basic tests and focus on the
+ // specific padding behavior.
+
+ time.tm_hour = 0;
+ time.tm_min = 0;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%R", &time);
+ EXPECT_STREQ_LEN(written, buffer, "00:00");
+
+ time.tm_hour = 1;
+ time.tm_min = 23;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%R", &time);
+ EXPECT_STREQ_LEN(written, buffer, "01:23");
+
+ time.tm_hour = 18;
+ time.tm_min = 6;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%R", &time);
+ EXPECT_STREQ_LEN(written, buffer, "18:06");
+
+ // padding is technically undefined for this conversion, but we support it, so
+ // we need to test it.
+ // Padding is handled in the same way as POSIX describes for %F
+ time.tm_hour = 10;
+ time.tm_min = 9;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%01R", &time);
+ EXPECT_STREQ_LEN(written, buffer, "10:09");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%05R", &time);
+ EXPECT_STREQ_LEN(written, buffer, "10:09");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%07R", &time);
+ EXPECT_STREQ_LEN(written, buffer, "0010:09");
+
+ time.tm_hour = 4;
+ time.tm_min = 56;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%01R", &time);
+ EXPECT_STREQ_LEN(written, buffer, "4:56");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%05R", &time);
+ EXPECT_STREQ_LEN(written, buffer, "04:56");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%07R", &time);
+ EXPECT_STREQ_LEN(written, buffer, "0004:56");
+}
+
+TEST(LlvmLibcStrftimeTest, TimeFormatSecond) {
+ // this tests %T, which reads: [tm_hour, tm_min, tm_sec]
+ // This is equivalent to "%H:%M:%S"
+ struct tm time;
+ char buffer[100];
+ size_t written = 0;
+
+ // each of %H, %M, and %S have their own tests, so this test won't cover
+ // all values of those. Instead it will do basic tests and focus on the
+ // specific padding behavior.
+
+ time.tm_hour = 0;
+ time.tm_min = 0;
+ time.tm_sec = 0;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%T", &time);
+ EXPECT_STREQ_LEN(written, buffer, "00:00:00");
+
+ time.tm_hour = 1;
+ time.tm_min = 23;
+ time.tm_sec = 45;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%T", &time);
+ EXPECT_STREQ_LEN(written, buffer, "01:23:45");
+
+ time.tm_hour = 18;
+ time.tm_min = 6;
+ time.tm_sec = 2;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%T", &time);
+ EXPECT_STREQ_LEN(written, buffer, "18:06:02");
+
+ // %X is equivalent to %T in default locale
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%X", &time);
+ EXPECT_STREQ_LEN(written, buffer, "18:06:02");
+
+ // padding is technically undefined for this conversion, but we support it, so
+ // we need to test it.
+ // Padding is handled in the same way as POSIX describes for %F
+ time.tm_hour = 10;
+ time.tm_min = 9;
+ time.tm_sec = 59;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%01T", &time);
+ EXPECT_STREQ_LEN(written, buffer, "10:09:59");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%08T", &time);
+ EXPECT_STREQ_LEN(written, buffer, "10:09:59");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%010T", &time);
+ EXPECT_STREQ_LEN(written, buffer, "0010:09:59");
+
+ time.tm_hour = 4;
+ time.tm_min = 56;
+ time.tm_sec = 9;
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%01T", &time);
+ EXPECT_STREQ_LEN(written, buffer, "4:56:09");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%08T", &time);
+ EXPECT_STREQ_LEN(written, buffer, "04:56:09");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%010T", &time);
+ EXPECT_STREQ_LEN(written, buffer, "0004:56:09");
+}
+
+TEST(LlvmLibcStrftimeTest, TimeFormatFullDateTime) {
+ // this tests %c, which reads:
+ // [tm_wday, tm_mon, tm_mday, tm_hour, tm_min, tm_sec, tm_year]
+ // This is equivalent to "%a %b %e %T %Y"
+ struct tm time;
+ char buffer[100];
+ size_t written = 0;
+
+ // each of the individual conversions have their own tests, so this test won't
+ // cover all values of those. Instead it will do basic tests and focus on the
+ // specific padding behavior.
+
+ time.tm_wday = 0;
+ time.tm_mon = 0;
+ time.tm_mday = 1;
+ time.tm_hour = 0;
+ time.tm_min = 0;
+ time.tm_sec = 0;
+ time.tm_year = get_adjusted_year(1900);
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%c", &time);
+ EXPECT_STREQ_LEN(written, buffer, "Sun Jan 1 00:00:00 1900");
+
+ time.tm_wday = 3;
+ time.tm_mon = 5;
+ time.tm_mday = 15;
+ time.tm_hour = 14;
+ time.tm_min = 13;
+ time.tm_sec = 12;
+ time.tm_year = get_adjusted_year(2011);
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%c", &time);
+ EXPECT_STREQ_LEN(written, buffer, "Wed Jun 15 14:13:12 2011");
+
+ // now, as of the writing of this test
+ time.tm_wday = 4;
+ time.tm_mon = 1;
+ time.tm_mday = 6;
+ time.tm_hour = 12;
+ time.tm_min = 57;
+ time.tm_sec = 50;
+ time.tm_year = get_adjusted_year(2025);
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%c", &time);
+ EXPECT_STREQ_LEN(written, buffer, "Thu Feb 6 12:57:50 2025");
+
+ time.tm_wday = 5;
+ time.tm_mon = 8;
+ time.tm_mday = 4;
+ time.tm_hour = 16;
+ time.tm_min = 57;
+ time.tm_sec = 18;
+ time.tm_year = get_adjusted_year(476);
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%c", &time);
+ EXPECT_STREQ_LEN(written, buffer, "Fri Sep 4 16:57:18 0476");
+
+ // padding is technically undefined for this conversion, but we support it, so
+ // we need to test it.
+ // Padding is handled in the same way as POSIX describes for %F.
+ // This includes assuming the trailing conversions are of a fixed width, which
+ // isn't true for years. For simplicity, we format years (%Y) to be padded to
+ // 4 digits when possible, which means padding will work as expected for years
+ // -999 to 9999. If the current year is large enough to trigger this bug,
+ // congrats on making it another ~8000 years!
+ time.tm_wday = 5;
+ time.tm_mon = 8;
+ time.tm_mday = 4;
+ time.tm_hour = 16;
+ time.tm_min = 57;
+ time.tm_sec = 18;
+ time.tm_year = get_adjusted_year(476);
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%1c", &time);
+ EXPECT_STREQ_LEN(written, buffer, "Fri Sep 4 16:57:18 0476");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%24c", &time);
+ EXPECT_STREQ_LEN(written, buffer, "Fri Sep 4 16:57:18 0476");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%26c", &time);
+ EXPECT_STREQ_LEN(written, buffer, " Fri Sep 4 16:57:18 0476");
+
+ // '0' flag has no effect on the string part of the conversion, only the
+ // numbers, and the only one of those that defaults to spaces is day of month.
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%026c", &time);
+ EXPECT_STREQ_LEN(written, buffer, " Fri Sep 04 16:57:18 0476");
+
+ time.tm_wday = 3;
+ time.tm_mon = 5;
+ time.tm_mday = 15;
+ time.tm_hour = 14;
+ time.tm_min = 13;
+ time.tm_sec = 12;
+ time.tm_year = get_adjusted_year(2011);
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%1c", &time);
+ EXPECT_STREQ_LEN(written, buffer, "Wed Jun 15 14:13:12 2011");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%24c", &time);
+ EXPECT_STREQ_LEN(written, buffer, "Wed Jun 15 14:13:12 2011");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%26c", &time);
+ EXPECT_STREQ_LEN(written, buffer, " Wed Jun 15 14:13:12 2011");
+
+ written = LIBC_NAMESPACE::strftime(buffer, sizeof(buffer), "%026c", &time);
+ EXPECT_STREQ_LEN(written, buffer, " Wed Jun 15 14:13:12 2011");
+}
+
+// TODO: implement %z and %Z when timezones are implemented.
+// TEST(LlvmLibcStrftimeTest, TimezoneOffset) {
+// // this tests %z, which reads: [tm_isdst, tm_zone]
+// struct tm time;
+// char buffer[100];
+// size_t written = 0;
+// SimplePaddedNum spn;
+// }
More information about the libc-commits
mailing list