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