From 560e57a27fe968b9435e0b26baf6d550fca00e27 Mon Sep 17 00:00:00 2001 From: Tsz Chan Date: Sun, 13 Oct 2024 21:59:37 +0800 Subject: [PATCH 1/4] [libc] Implement strftime --- libc/src/stdio/CMakeLists.txt | 1 + libc/src/stdio/strftime_core/CMakeLists.txt | 52 +++ libc/src/stdio/strftime_core/converter.cpp | 264 +++++++++++++++ libc/src/stdio/strftime_core/converter.h | 29 ++ libc/src/stdio/strftime_core/core_structs.h | 97 ++++++ libc/src/stdio/strftime_core/parser.h | 125 +++++++ .../stdio/strftime_core/time_internal_def.h | 43 +++ libc/test/src/stdio/CMakeLists.txt | 1 + .../src/stdio/strftime_core/CMakeLists.txt | 11 + .../stdio/strftime_core/converter_test.cpp | 320 ++++++++++++++++++ .../src/stdio/strftime_core/parser_test.cpp | 0 11 files changed, 943 insertions(+) create mode 100644 libc/src/stdio/strftime_core/CMakeLists.txt create mode 100644 libc/src/stdio/strftime_core/converter.cpp create mode 100644 libc/src/stdio/strftime_core/converter.h create mode 100644 libc/src/stdio/strftime_core/core_structs.h create mode 100644 libc/src/stdio/strftime_core/parser.h create mode 100644 libc/src/stdio/strftime_core/time_internal_def.h create mode 100644 libc/test/src/stdio/strftime_core/CMakeLists.txt create mode 100644 libc/test/src/stdio/strftime_core/converter_test.cpp create mode 100644 libc/test/src/stdio/strftime_core/parser_test.cpp diff --git a/libc/src/stdio/CMakeLists.txt b/libc/src/stdio/CMakeLists.txt index b9bc904471df9..5338c17d6232b 100644 --- a/libc/src/stdio/CMakeLists.txt +++ b/libc/src/stdio/CMakeLists.txt @@ -239,6 +239,7 @@ add_entrypoint_object( add_subdirectory(printf_core) add_subdirectory(scanf_core) +add_subdirectory(strftime_core) add_entrypoint_object( remove diff --git a/libc/src/stdio/strftime_core/CMakeLists.txt b/libc/src/stdio/strftime_core/CMakeLists.txt new file mode 100644 index 0000000000000..e234d8d064e15 --- /dev/null +++ b/libc/src/stdio/strftime_core/CMakeLists.txt @@ -0,0 +1,52 @@ + +add_header_library( + core_structs + HDRS + core_structs.h + DEPENDS + libc.src.__support.CPP.string_view + libc.src.__support.FPUtil.fp_bits +) + +add_header_library( + parser + HDRS + parser.h + DEPENDS + .core_structs + libc.src.__support.arg_list + libc.src.__support.ctype_utils + libc.src.__support.str_to_integer + libc.src.__support.CPP.algorithm + libc.src.__support.CPP.bit + libc.src.__support.CPP.optional + libc.src.__support.CPP.string_view + libc.src.__support.CPP.type_traits + libc.src.__support.common +) + + +add_object_library( + converter + SRCS + converter.cpp + HDRS + converter.h + DEPENDS + .core_structs + libc.src.stdio.printf_core.writer + libc.src.__support.big_int + libc.src.math.log10 + libc.src.__support.common + libc.src.__support.CPP.limits + libc.src.__support.CPP.span + libc.src.__support.CPP.string_view + libc.src.__support.float_to_string + libc.src.__support.FPUtil.fenv_impl + libc.src.__support.FPUtil.fp_bits + libc.src.__support.FPUtil.rounding_mode + libc.src.__support.integer_to_string + libc.src.__support.libc_assert + libc.src.__support.uint128 + libc.src.__support.StringUtil.error_to_string +) \ No newline at end of file diff --git a/libc/src/stdio/strftime_core/converter.cpp b/libc/src/stdio/strftime_core/converter.cpp new file mode 100644 index 0000000000000..79e3727cc88f5 --- /dev/null +++ b/libc/src/stdio/strftime_core/converter.cpp @@ -0,0 +1,264 @@ +//===-- 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_CONVERTER_H +#define LLVM_LIBC_SRC_STDIO_STRFTIME_CORE_CONVERTER_H + +#include "src/__support/CPP/string.h" +#include "src/__support/CPP/string_view.h" +#include "src/__support/integer_to_string.h" +#include "src/__support/macros/config.h" +#include "src/math/log10.h" +#include "src/stdio/printf_core/writer.h" +#include "src/stdio/strftime_core/core_structs.h" +#include "src/stdio/strftime_core/time_internal_def.h" +#include + +namespace LIBC_NAMESPACE_DECL { +namespace strftime_core { + +namespace details { + +LIBC_INLINE cpp::optional +num_to_strview(uintmax_t num, cpp::span bufref) { + return IntegerToString::format_to(bufref, num); +} + +template +LIBC_INLINE int write_num(uintmax_t num, printf_core::Writer *writer) { + cpp::array buf; + return writer->write(*num_to_strview(num, buf)); +} + +template +LIBC_INLINE int write_num_with_padding(uintmax_t num, + printf_core::Writer *writer) { + cpp::array buf; + auto digits = log10(num) + 1; + auto padding_needed = width - digits; + int char_written = 0; + for (int _ = 0; _ < padding_needed; _++) { + char_written += writer->write(padding); + } + char_written += writer->write(*num_to_strview(num, buf)); + return char_written; +} + +} // namespace details + +/* Nonzero if YEAR is a leap year (every 4 years, + except every 100th isn't, and every 400th is). */ +LIBC_INLINE bool is_leap(int year) { + return ((year) % 4 == 0 && ((year) % 100 != 0 || (year) % 400 == 0)); +} + +LIBC_INLINE int convert_weekday(printf_core::Writer *writer, + const FormatSection &to_conv) { + return writer->write(day_names[to_conv.time->tm_wday]); +} + +LIBC_INLINE int convert_zero_padded_day_of_year(printf_core::Writer *writer, + const FormatSection &to_conv) { + return details::write_num_with_padding<3, '0'>(to_conv.time->tm_yday + 1, + writer); +} + +LIBC_INLINE int convert_zero_padded_day_of_month(printf_core::Writer *writer, + const FormatSection &to_conv) { + return details::write_num_with_padding<2, '0'>(to_conv.time->tm_mday, writer); +} + +LIBC_INLINE int +convert_space_padded_day_of_month(printf_core::Writer *writer, + const FormatSection &to_conv) { + return details::write_num_with_padding<2, ' '>(to_conv.time->tm_mday, writer); +} + +LIBC_INLINE int convert_decimal_weekday(printf_core::Writer *writer, + const FormatSection &to_conv) { + return details::write_num<1>( + to_conv.time->tm_wday == 0 ? 7 : to_conv.time->tm_wday, writer); +} + +LIBC_INLINE int convert_decimal_weekday_iso(printf_core::Writer *writer, + const FormatSection &to_conv) { + return details::write_num<1>(to_conv.time->tm_wday, writer); +} + +LIBC_INLINE int convert_week_number_sunday(printf_core::Writer *writer, + const FormatSection &to_conv) { + int wday = to_conv.time->tm_wday; + int yday = to_conv.time->tm_yday; + int week = (yday - wday + 7) / 7; + return details::write_num_with_padding<2, '0'>(week, writer); +} + +LIBC_INLINE int convert_week_number_monday(printf_core::Writer *writer, + const FormatSection &to_conv) { + int wday = (to_conv.time->tm_wday + 6) % 7; + int yday = to_conv.time->tm_yday; + int week = (yday - wday + 7) / 7; + return details::write_num_with_padding<2, '0'>(week, writer); +} + +LIBC_INLINE int convert_full_month(printf_core::Writer *writer, + const FormatSection &to_conv) { + return writer->write(month_names[to_conv.time->tm_mon]); +} + +LIBC_INLINE int convert_abbreviated_month(printf_core::Writer *writer, + const FormatSection &to_conv) { + return writer->write(abbreviated_month_names[to_conv.time->tm_mon]); +} + +LIBC_INLINE int convert_zero_padded_month(printf_core::Writer *writer, + const FormatSection &to_conv) { + return details::write_num_with_padding<2, '0'>(to_conv.time->tm_mon + 1, + writer); +} + +LIBC_INLINE int convert_full_year(printf_core::Writer *writer, + const FormatSection &to_conv) { + return details::write_num_with_padding<4, '0'>( + to_conv.time->tm_year + YEAR_BASE, writer); +} + +LIBC_INLINE int convert_two_digit_year(printf_core::Writer *writer, + const FormatSection &to_conv) { + return details::write_num_with_padding<2, '0'>( + (to_conv.time->tm_year + YEAR_BASE) % 100, writer); +} + +LIBC_INLINE int convert_century(printf_core::Writer *writer, + const FormatSection &to_conv) { + return details::write_num_with_padding<2, '0'>( + (to_conv.time->tm_year + YEAR_BASE) / 100, writer); +} + +static int iso_week_days(int yday, int wday) { + /* Add enough to the first operand of % to make it nonnegative. */ + int big_enough_multiple_of_7 = (-YDAY_MINIMUM / 7 + 2) * 7; + return (yday - (yday - wday + ISO_WEEK1_WDAY + big_enough_multiple_of_7) % 7 + + ISO_WEEK1_WDAY - ISO_WEEK_START_WDAY); +} + +LIBC_INLINE int convert_iso_year(printf_core::Writer *writer, + const FormatSection &to_conv) { + int year = to_conv.time->tm_year + YEAR_BASE; + int days = iso_week_days(to_conv.time->tm_yday, to_conv.time->tm_wday); + + if (days < 0) { + /* This ISO week belongs to the previous year. */ + year--; + days = iso_week_days(to_conv.time->tm_yday + (365 + is_leap(year)), + to_conv.time->tm_wday); + } else { + int d = iso_week_days(to_conv.time->tm_yday - (365 + is_leap(year)), + to_conv.time->tm_wday); + if (0 <= d) { + /* This ISO week belongs to the next year. */ + year++; + days = d; + } + } + + return details::write_num<4>(year, writer); +} + +LIBC_INLINE int convert_hour_24(printf_core::Writer *writer, + const FormatSection &to_conv) { + return details::write_num_with_padding<2, '0'>(to_conv.time->tm_hour, writer); +} + +LIBC_INLINE int convert_pm(printf_core::Writer *writer, + const FormatSection &to_conv) { + static const cpp::string_view AM = "AM"; + static const cpp::string_view PM = "PM"; + return writer->write(to_conv.time->tm_hour >= 12 ? PM : AM); +} + +LIBC_INLINE int convert_hour_12(printf_core::Writer *writer, + const FormatSection &to_conv) { + int hour = to_conv.time->tm_hour % 12; // Convert to 12-hour format + if (hour == 0) + hour = 12; // Adjust for midnight + return details::write_num_with_padding<2, '0'>(hour, writer); +} + +LIBC_INLINE int convert_minute(printf_core::Writer *writer, + const FormatSection &to_conv) { + return details::write_num_with_padding<2, '0'>(to_conv.time->tm_min, writer); +} + +LIBC_INLINE int convert_second(printf_core::Writer *writer, + const FormatSection &to_conv) { + return details::write_num_with_padding<2, '0'>(to_conv.time->tm_sec, writer); +} + +int convert(printf_core::Writer *writer, const FormatSection &to_conv) { + if (!to_conv.has_conv) + return writer->write(to_conv.raw_string); + switch (to_conv.conv_name) { + // day of the week + case 'a': + return convert_weekday(writer, to_conv); + case 'w': + return convert_decimal_weekday(writer, to_conv); + case 'u': + return convert_decimal_weekday_iso(writer, to_conv); + // day of the year/ month + case 'j': + return convert_zero_padded_day_of_year(writer, to_conv); + case 'd': + return convert_zero_padded_day_of_month(writer, to_conv); + case 'e': + return convert_space_padded_day_of_month(writer, to_conv); + // week + case 'U': + return convert_week_number_sunday(writer, to_conv); + case 'W': + return convert_week_number_monday(writer, to_conv); + case 'V': // TODO: ISO 8061 + // month + case 'B': + return convert_full_month(writer, to_conv); + case 'b': + case 'h': + return convert_abbreviated_month(writer, to_conv); + case 'm': + return convert_zero_padded_month(writer, to_conv); + // year + case 'Y': + return convert_full_year(writer, to_conv); + case 'y': + return convert_two_digit_year(writer, to_conv); + case 'C': + return convert_century(writer, to_conv); + case 'G': + // TODO + return convert_iso_year(writer, to_conv); + // hours + case 'p': + return convert_pm(writer, to_conv); + case 'H': + return convert_hour_24(writer, to_conv); + case 'I': + return convert_hour_12(writer, to_conv); + // minutes + case 'M': + return convert_minute(writer, to_conv); + // seconds + case 'S': + return convert_second(writer, to_conv); + } + return 0; +} + +} // namespace strftime_core +} // namespace LIBC_NAMESPACE_DECL +#endif diff --git a/libc/src/stdio/strftime_core/converter.h b/libc/src/stdio/strftime_core/converter.h new file mode 100644 index 0000000000000..57801b8e8e126 --- /dev/null +++ b/libc/src/stdio/strftime_core/converter.h @@ -0,0 +1,29 @@ +//===-- Format specifier converter 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_CONVERTER_H +#define LLVM_LIBC_SRC_STDIO_STRFTIME_CORE_CONVERTER_H + +#include "src/__support/macros/config.h" +#include "src/stdio/printf_core/writer.h" +#include "src/stdio/strftime_core/core_structs.h" + +#include + +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); + +} // namespace strftime_core +} // namespace LIBC_NAMESPACE_DECL + +#endif // LLVM_LIBC_SRC_STDIO_STRFTIME_CORE_CONVERTER_H diff --git a/libc/src/stdio/strftime_core/core_structs.h b/libc/src/stdio/strftime_core/core_structs.h new file mode 100644 index 0000000000000..404902bcfbf6f --- /dev/null +++ b/libc/src/stdio/strftime_core/core_structs.h @@ -0,0 +1,97 @@ +//===-- Core Structures 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_CORE_STRUCTS_H +#define LLVM_LIBC_SRC_STDIO_STRFTIME_CORE_CORE_STRUCTS_H + +#include "src/__support/macros/config.h" + +#include "src/__support/CPP/string_view.h" +#include "src/__support/CPP/type_traits.h" +#include "src/__support/FPUtil/FPBits.h" + +#include +#include + +namespace LIBC_NAMESPACE_DECL { +namespace strftime_core { + +struct tm { + int tm_sec; /* seconds after the minute [0-60] */ + int tm_min; /* minutes after the hour [0-59] */ + int tm_hour; /* hours since midnight [0-23] */ + int tm_mday; /* day of the month [1-31] */ + int tm_mon; /* months since January [0-11] */ + int tm_year; /* years since 1900 */ + int tm_wday; /* days since Sunday [0-6] */ + int tm_yday; /* days since January 1 [0-365] */ + int tm_isdst; /* Daylight Savings Time flag */ + long tm_gmtoff; /* offset from UTC in seconds */ + char *tm_zone; /* timezone abbreviation */ +}; + +struct FormatSection { + bool has_conv{false}; + bool isE{false}; + bool isO{false}; + cpp::string_view raw_string; + char conv_name; + const struct tm *time; +}; + +enum PrimaryType : uint8_t { + Unknown = 0, + Float = 1, + Pointer = 2, + Integer = 3, + FixedPoint = 4, +}; + +// TypeDesc stores the information about a type that is relevant to printf in +// a relatively compact manner. +struct TypeDesc { + uint8_t size; + PrimaryType primary_type; + LIBC_INLINE constexpr bool operator==(const TypeDesc &other) const { + return (size == other.size) && (primary_type == other.primary_type); + } +}; + +template LIBC_INLINE constexpr TypeDesc type_desc_from_type() { + if constexpr (cpp::is_same_v) { + return TypeDesc{0, PrimaryType::Unknown}; + } else { + constexpr bool IS_POINTER = cpp::is_pointer_v; + constexpr bool IS_FLOAT = cpp::is_floating_point_v; +#ifdef LIBC_INTERNAL_STRFTIME_HAS_FIXED_POINT + constexpr bool IS_FIXED_POINT = cpp::is_fixed_point_v; +#else + constexpr bool IS_FIXED_POINT = false; +#endif // LIBC_INTERNAL_STRFTIME_HAS_FIXED_POINT + + return TypeDesc{sizeof(T), IS_POINTER ? PrimaryType::Pointer + : IS_FLOAT ? PrimaryType::Float + : IS_FIXED_POINT ? PrimaryType::FixedPoint + : PrimaryType::Integer}; + } +} + +// This is the value to be returned by conversions when no error has occurred. +constexpr int WRITE_OK = 0; +// These are the printf return values for when an error has occurred. They are +// all negative, and should be distinct. +constexpr int FILE_WRITE_ERROR = -1; +constexpr int FILE_STATUS_ERROR = -2; +constexpr int NULLPTR_WRITE_ERROR = -3; +constexpr int INT_CONVERSION_ERROR = -4; +constexpr int FIXED_POINT_CONVERSION_ERROR = -5; +constexpr int ALLOCATION_ERROR = -6; +} // namespace strftime_core +} // namespace LIBC_NAMESPACE_DECL + +#endif // LLVM_LIBC_SRC_STDIO_STRFTIME_CORE_CORE_STRUCTS_H diff --git a/libc/src/stdio/strftime_core/parser.h b/libc/src/stdio/strftime_core/parser.h new file mode 100644 index 0000000000000..046f5b1095ad0 --- /dev/null +++ b/libc/src/stdio/strftime_core/parser.h @@ -0,0 +1,125 @@ +//===-- 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 "include/llvm-libc-macros/stdfix-macros.h" +#include "src/__support/CPP/algorithm.h" // max +#include "src/__support/CPP/limits.h" +#include "src/__support/CPP/optional.h" +#include "src/__support/CPP/type_traits.h" +#include "src/__support/macros/config.h" +#include "src/__support/str_to_integer.h" +#include "src/stdio/strftime_core/core_structs.h" +// #include "src/stdio/strftime_core/printf_config.h" + +#include + +#ifdef LIBC_INTERNAL_PRINTF_HAS_FIXED_POINT +#include "src/__support/fixed_point/fx_rep.h" +#endif // LIBC_INTERNAL_PRINTF_HAS_FIXED_POINT +#ifndef LIBC_COPT_PRINTF_DISABLE_STRERROR +#include "src/errno/libc_errno.h" +#endif // LIBC_COPT_PRINTF_DISABLE_STRERROR + +namespace LIBC_NAMESPACE_DECL { +namespace strftime_core { + +class Parser { + const char *__restrict str; + const struct tm &time; + size_t cur_pos = 0; + +public: + LIBC_INLINE Parser(const char *__restrict new_str, const tm &time) + : str(new_str), time(time) {} + + // 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; + } else { + // format section + section.has_conv = true; + section.time = &time; + // locale-specific modifiers + if (str[cur_pos] == 'E') + section.isE = true; + if (str[cur_pos] == 'O') + section.isO = true; + ++cur_pos; + section.conv_name = str[cur_pos]; + + switch (str[cur_pos]) { + case ('%'): + case ('a'): + case ('A'): + case ('b'): + case ('B'): + case ('c'): + case ('C'): + case ('d'): + case ('D'): + case ('e'): + case ('F'): + case ('g'): + case ('G'): + case ('h'): + case ('H'): + case ('I'): + case ('j'): + case ('m'): + case ('M'): + case ('n'): + case ('p'): + case ('r'): + case ('R'): + case ('S'): + case ('t'): + case ('T'): + case ('u'): + case ('U'): + case ('V'): + case ('w'): + case ('W'): + case ('x'): + case ('X'): + case ('y'): + case ('Y'): + case ('z'): + case ('Z'): + section.has_conv = true; + break; + default: + // if the conversion is undefined, change this to a raw section. + section.has_conv = false; + break; + } + // 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; + } +}; + +} // namespace strftime_core +} // namespace LIBC_NAMESPACE_DECL + +#endif // LLVM_LIBC_SRC_STDIO_STRFTIME_CORE_PARSER_H diff --git a/libc/src/stdio/strftime_core/time_internal_def.h b/libc/src/stdio/strftime_core/time_internal_def.h new file mode 100644 index 0000000000000..a7672ad43704e --- /dev/null +++ b/libc/src/stdio/strftime_core/time_internal_def.h @@ -0,0 +1,43 @@ +//===-- Strftime related internals -------------------------*- 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 +// +//===----------------------------------------------------------------------===// + +#include "src/__support/CPP/array.h" +#include "src/__support/CPP/string_view.h" + +namespace LIBC_NAMESPACE_DECL { +namespace strftime_core { + +static constexpr int NUM_DAYS = 7; +static constexpr int NUM_MONTHS = 12; +static constexpr int YEAR_BASE = 1900; + +/* The number of days from the first day of the first ISO week of this + year to the year day YDAY with week day WDAY. ISO weeks start on + Monday; the first ISO week has the year's first Thursday. YDAY may + be as small as YDAY_MINIMUM. */ +static constexpr int ISO_WEEK_START_WDAY = 1; /* Monday */ +static constexpr int ISO_WEEK1_WDAY = 4; /* Thursday */ +static constexpr int YDAY_MINIMUM = -366; + +static constexpr cpp::array day_names = { + "Sunday", "Monday", "Tuesday", "Wednesday", + "Thursday", "Friday", "Saturday"}; + +static constexpr cpp::array month_names = { + "January", "February", "March", "April", "May", "June", + "July", "August", "September", "October", "November", "December"}; + +static constexpr cpp::array abbreviated_day_names = + {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}; + +static constexpr cpp::array + abbreviated_month_names = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; + +} // namespace strftime_core +} // namespace LIBC_NAMESPACE_DECL diff --git a/libc/test/src/stdio/CMakeLists.txt b/libc/test/src/stdio/CMakeLists.txt index e970e4cf01405..c43c24dc3459a 100644 --- a/libc/test/src/stdio/CMakeLists.txt +++ b/libc/test/src/stdio/CMakeLists.txt @@ -542,4 +542,5 @@ endif() add_subdirectory(printf_core) add_subdirectory(scanf_core) +add_subdirectory(strftime_core) add_subdirectory(testdata) diff --git a/libc/test/src/stdio/strftime_core/CMakeLists.txt b/libc/test/src/stdio/strftime_core/CMakeLists.txt new file mode 100644 index 0000000000000..e47cd0f8bb2f8 --- /dev/null +++ b/libc/test/src/stdio/strftime_core/CMakeLists.txt @@ -0,0 +1,11 @@ +add_libc_unittest( + converter_test + SUITE + libc_stdio_unittests + SRCS + converter_test.cpp + DEPENDS + libc.src.stdio.strftime_core.converter + libc.src.stdio.printf_core.writer + libc.src.stdio.strftime_core.core_structs +) diff --git a/libc/test/src/stdio/strftime_core/converter_test.cpp b/libc/test/src/stdio/strftime_core/converter_test.cpp new file mode 100644 index 0000000000000..53ea3694227ef --- /dev/null +++ b/libc/test/src/stdio/strftime_core/converter_test.cpp @@ -0,0 +1,320 @@ +//===-- Unittests for the printf Converter --------------------------------===// +// +// 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/stdio/printf_core/writer.h" +#include "src/stdio/strftime_core/converter.h" +#include "src/stdio/strftime_core/core_structs.h" + +#include "test/UnitTest/Test.h" + +class LlvmLibcStrftimeConverterTest : public LIBC_NAMESPACE::testing::Test { +protected: + // void SetUp() override {} + // void TearDown() override {} + + char str[60]; + LIBC_NAMESPACE::printf_core::WriteBuffer wb = + LIBC_NAMESPACE::printf_core::WriteBuffer(str, sizeof(str) - 1); + LIBC_NAMESPACE::printf_core::Writer writer = + LIBC_NAMESPACE::printf_core::Writer(&wb); +}; + +TEST_F(LlvmLibcStrftimeConverterTest, SimpleRawConversion) { + LIBC_NAMESPACE::strftime_core::FormatSection raw_section; + raw_section.has_conv = false; + raw_section.raw_string = "abc"; + + LIBC_NAMESPACE::strftime_core::convert(&writer, raw_section); + + wb.buff[wb.buff_cur] = '\0'; + + ASSERT_STREQ(str, "abc"); + ASSERT_EQ(writer.get_chars_written(), 3); +} + +TEST_F(LlvmLibcStrftimeConverterTest, PercentConversion) { + LIBC_NAMESPACE::strftime_core::FormatSection simple_conv; + simple_conv.has_conv = true; + simple_conv.raw_string = "%%"; + simple_conv.conv_name = '%'; + + LIBC_NAMESPACE::strftime_core::convert(&writer, simple_conv); + + wb.buff[wb.buff_cur] = '\0'; + + ASSERT_STREQ(str, "%"); + ASSERT_EQ(writer.get_chars_written(), 1); +} + +TEST_F(LlvmLibcStrftimeConverterTest, WeekdayConversion) { + LIBC_NAMESPACE::strftime_core::FormatSection simple_conv; + LIBC_NAMESPACE::strftime_core::tm time; + time.tm_wday = 1; + simple_conv.has_conv = true; + simple_conv.raw_string = "%a"; + simple_conv.conv_name = 'a'; + simple_conv.time = &time; + + LIBC_NAMESPACE::strftime_core::convert(&writer, simple_conv); + + wb.buff[wb.buff_cur] = '\0'; + + ASSERT_STREQ(str, "Monday"); + ASSERT_EQ(writer.get_chars_written(), 6); +} +TEST_F(LlvmLibcStrftimeConverterTest, AbbreviatedMonthNameConversion) { + LIBC_NAMESPACE::strftime_core::FormatSection simple_conv; + LIBC_NAMESPACE::strftime_core::tm time; + time.tm_mon = 4; // May + simple_conv.has_conv = true; + simple_conv.raw_string = "%b"; + simple_conv.conv_name = 'b'; + simple_conv.time = &time; + + LIBC_NAMESPACE::strftime_core::convert(&writer, simple_conv); + + wb.buff[wb.buff_cur] = '\0'; + + ASSERT_STREQ(str, "May"); + ASSERT_EQ(writer.get_chars_written(), 3); +} + +TEST_F(LlvmLibcStrftimeConverterTest, CenturyConversion) { + LIBC_NAMESPACE::strftime_core::FormatSection simple_conv; + LIBC_NAMESPACE::strftime_core::tm time; + time.tm_year = 122; // Represents 2022 + simple_conv.has_conv = true; + simple_conv.raw_string = "%C"; + simple_conv.conv_name = 'C'; + simple_conv.time = &time; + + LIBC_NAMESPACE::strftime_core::convert(&writer, simple_conv); + + wb.buff[wb.buff_cur] = '\0'; + + ASSERT_STREQ(str, "20"); + ASSERT_EQ(writer.get_chars_written(), 2); +} + +TEST_F(LlvmLibcStrftimeConverterTest, DayOfMonthZeroPaddedConversion) { + LIBC_NAMESPACE::strftime_core::FormatSection simple_conv; + LIBC_NAMESPACE::strftime_core::tm time; + time.tm_mday = 7; + simple_conv.has_conv = true; + simple_conv.raw_string = "%d"; + simple_conv.conv_name = 'd'; + simple_conv.time = &time; + + LIBC_NAMESPACE::strftime_core::convert(&writer, simple_conv); + + wb.buff[wb.buff_cur] = '\0'; + + ASSERT_STREQ(str, "07"); + ASSERT_EQ(writer.get_chars_written(), 2); +} + +TEST_F(LlvmLibcStrftimeConverterTest, DayOfMonthSpacePaddedConversion) { + LIBC_NAMESPACE::strftime_core::FormatSection simple_conv; + LIBC_NAMESPACE::strftime_core::tm time; + time.tm_mday = 7; + simple_conv.has_conv = true; + simple_conv.raw_string = "%e"; + simple_conv.conv_name = 'e'; + simple_conv.time = &time; + + LIBC_NAMESPACE::strftime_core::convert(&writer, simple_conv); + + wb.buff[wb.buff_cur] = '\0'; + + ASSERT_STREQ(str, " 7"); + ASSERT_EQ(writer.get_chars_written(), 2); +} + +TEST_F(LlvmLibcStrftimeConverterTest, FullMonthNameConversion) { + LIBC_NAMESPACE::strftime_core::FormatSection simple_conv; + LIBC_NAMESPACE::strftime_core::tm time; + time.tm_mon = 4; // May + simple_conv.has_conv = true; + simple_conv.raw_string = "%B"; + simple_conv.conv_name = 'B'; + simple_conv.time = &time; + + LIBC_NAMESPACE::strftime_core::convert(&writer, simple_conv); + + wb.buff[wb.buff_cur] = '\0'; + + ASSERT_STREQ(str, "May"); + ASSERT_EQ(writer.get_chars_written(), 3); +} + +TEST_F(LlvmLibcStrftimeConverterTest, Hour12Conversion) { + LIBC_NAMESPACE::strftime_core::FormatSection simple_conv; + LIBC_NAMESPACE::strftime_core::tm time; + time.tm_hour = 14; + simple_conv.has_conv = true; + simple_conv.raw_string = "%I"; + simple_conv.conv_name = 'I'; + simple_conv.time = &time; + + LIBC_NAMESPACE::strftime_core::convert(&writer, simple_conv); + + wb.buff[wb.buff_cur] = '\0'; + + ASSERT_STREQ(str, "02"); + ASSERT_EQ(writer.get_chars_written(), 2); +} + +TEST_F(LlvmLibcStrftimeConverterTest, Hour24PaddedConversion) { + LIBC_NAMESPACE::strftime_core::FormatSection simple_conv; + LIBC_NAMESPACE::strftime_core::tm time; + time.tm_hour = 9; + simple_conv.has_conv = true; + simple_conv.raw_string = "%H"; + simple_conv.conv_name = 'H'; + simple_conv.time = &time; + + LIBC_NAMESPACE::strftime_core::convert(&writer, simple_conv); + + wb.buff[wb.buff_cur] = '\0'; + + ASSERT_STREQ(str, "09"); + ASSERT_EQ(writer.get_chars_written(), 2); +} + +TEST_F(LlvmLibcStrftimeConverterTest, MinuteConversion) { + LIBC_NAMESPACE::strftime_core::FormatSection simple_conv; + LIBC_NAMESPACE::strftime_core::tm time; + time.tm_min = 45; + simple_conv.has_conv = true; + simple_conv.raw_string = "%M"; + simple_conv.conv_name = 'M'; + simple_conv.time = &time; + + LIBC_NAMESPACE::strftime_core::convert(&writer, simple_conv); + + wb.buff[wb.buff_cur] = '\0'; + + ASSERT_STREQ(str, "45"); + ASSERT_EQ(writer.get_chars_written(), 2); +} + +TEST_F(LlvmLibcStrftimeConverterTest, AMPMConversion) { + LIBC_NAMESPACE::strftime_core::FormatSection simple_conv; + LIBC_NAMESPACE::strftime_core::tm time; + time.tm_hour = 14; // 2 PM + simple_conv.has_conv = true; + simple_conv.raw_string = "%p"; + simple_conv.conv_name = 'p'; + simple_conv.time = &time; + + LIBC_NAMESPACE::strftime_core::convert(&writer, simple_conv); + + wb.buff[wb.buff_cur] = '\0'; + + ASSERT_STREQ(str, "PM"); + ASSERT_EQ(writer.get_chars_written(), 2); +} + +TEST_F(LlvmLibcStrftimeConverterTest, SecondsConversion) { + LIBC_NAMESPACE::strftime_core::FormatSection simple_conv; + LIBC_NAMESPACE::strftime_core::tm time; + time.tm_sec = 30; + simple_conv.has_conv = true; + simple_conv.raw_string = "%S"; + simple_conv.conv_name = 'S'; + simple_conv.time = &time; + + LIBC_NAMESPACE::strftime_core::convert(&writer, simple_conv); + + wb.buff[wb.buff_cur] = '\0'; + + ASSERT_STREQ(str, "30"); + ASSERT_EQ(writer.get_chars_written(), 2); +} + +TEST_F(LlvmLibcStrftimeConverterTest, FullYearConversion) { + LIBC_NAMESPACE::strftime_core::FormatSection simple_conv; + LIBC_NAMESPACE::strftime_core::tm time; + time.tm_year = 122; // Represents 2022 (1900 + 122) + simple_conv.has_conv = true; + simple_conv.raw_string = "%Y"; + simple_conv.conv_name = 'Y'; + simple_conv.time = &time; + + LIBC_NAMESPACE::strftime_core::convert(&writer, simple_conv); + + wb.buff[wb.buff_cur] = '\0'; + + ASSERT_STREQ(str, "2022"); + ASSERT_EQ(writer.get_chars_written(), 4); +} + +TEST_F(LlvmLibcStrftimeConverterTest, TwoDigitYearConversion) { + LIBC_NAMESPACE::strftime_core::FormatSection simple_conv; + LIBC_NAMESPACE::strftime_core::tm time; + time.tm_year = 122; // Represents 2022 (1900 + 122) + simple_conv.has_conv = true; + simple_conv.raw_string = "%y"; + simple_conv.conv_name = 'y'; + simple_conv.time = &time; + + LIBC_NAMESPACE::strftime_core::convert(&writer, simple_conv); + + wb.buff[wb.buff_cur] = '\0'; + + ASSERT_STREQ(str, "22"); + ASSERT_EQ(writer.get_chars_written(), 2); +} + +TEST_F(LlvmLibcStrftimeConverterTest, IsoYearEdgeCaseConversionEndOfYear) { + LIBC_NAMESPACE::strftime_core::FormatSection iso_year; + LIBC_NAMESPACE::strftime_core::tm time; + + // Set up the time for December 31, 2022 (a Saturday) + time.tm_year = 122; // Represents 2022 + time.tm_mon = 11; // December (0-based) + time.tm_mday = 31; // 31st day of the month + time.tm_wday = 6; // Saturday (0-based, 6 is Saturday) + + iso_year.has_conv = true; + iso_year.raw_string = "%G"; + iso_year.conv_name = 'G'; + iso_year.time = &time; + + LIBC_NAMESPACE::strftime_core::convert(&writer, iso_year); + + wb.buff[wb.buff_cur] = '\0'; + + // The ISO year for this date is 2021 + ASSERT_STREQ(str, "2021"); + ASSERT_EQ(writer.get_chars_written(), 4); +} + +TEST_F(LlvmLibcStrftimeConverterTest, IsoYearEdgeCaseConversionStartOfYear) { + LIBC_NAMESPACE::strftime_core::FormatSection iso_year; + LIBC_NAMESPACE::strftime_core::tm time; + + // Set up the time for January 1, 2023 (a Sunday) + time.tm_year = 123; // Represents 2023 + time.tm_mon = 0; // January (0-based) + time.tm_mday = 1; // 1st day of the month + time.tm_wday = 0; // Sunday (0-based, 0 is Sunday) + + iso_year.has_conv = true; + iso_year.raw_string = "%G"; + iso_year.conv_name = 'G'; + iso_year.time = &time; + + LIBC_NAMESPACE::strftime_core::convert(&writer, iso_year); + + wb.buff[wb.buff_cur] = '\0'; + + // The ISO year for this date is 2022, not 2023 + ASSERT_STREQ(str, "2022"); + ASSERT_EQ(writer.get_chars_written(), 4); +} \ No newline at end of file diff --git a/libc/test/src/stdio/strftime_core/parser_test.cpp b/libc/test/src/stdio/strftime_core/parser_test.cpp new file mode 100644 index 0000000000000..e69de29bb2d1d From 8ea3b4c06bcbb107e481721cabd02d05d8aa85d0 Mon Sep 17 00:00:00 2001 From: Joseph Huber Date: Sun, 11 Aug 2024 09:26:00 -0500 Subject: [PATCH 2/4] [libc] Add stubs for 'strftime' function Summary: This patch does not actually implement the function, but provides the interface. This partially resovles https://github.com/llvm/llvm-project/issues/106630 as it will allow us to build libc++ with locale support (so long as you don't use it). --- libc/config/gpu/entrypoints.txt | 2 ++ libc/newhdrgen/yaml/time.yaml | 20 ++++++++++++++++++++ libc/spec/stdc.td | 21 +++++++++++++++++++++ libc/src/time/CMakeLists.txt | 23 ++++++++++++++++++++++- libc/src/time/strftime.cpp | 26 ++++++++++++++++++++++++++ libc/src/time/strftime.h | 23 +++++++++++++++++++++++ libc/src/time/strftime_l.cpp | 24 ++++++++++++++++++++++++ libc/src/time/strftime_l.h | 24 ++++++++++++++++++++++++ 8 files changed, 162 insertions(+), 1 deletion(-) create mode 100644 libc/src/time/strftime.cpp create mode 100644 libc/src/time/strftime.h create mode 100644 libc/src/time/strftime_l.cpp create mode 100644 libc/src/time/strftime_l.h diff --git a/libc/config/gpu/entrypoints.txt b/libc/config/gpu/entrypoints.txt index b4cfe47f4505f..af05dc57023f0 100644 --- a/libc/config/gpu/entrypoints.txt +++ b/libc/config/gpu/entrypoints.txt @@ -256,6 +256,8 @@ set(TARGET_LIBC_ENTRYPOINTS libc.src.time.clock libc.src.time.clock_gettime libc.src.time.nanosleep + libc.src.time.strftime + libc.src.time.strftime_l # wchar.h entrypoints libc.src.wchar.wctob diff --git a/libc/newhdrgen/yaml/time.yaml b/libc/newhdrgen/yaml/time.yaml index 69b40bef3160d..593b52b2d5a70 100644 --- a/libc/newhdrgen/yaml/time.yaml +++ b/libc/newhdrgen/yaml/time.yaml @@ -8,6 +8,7 @@ types: - type_name: time_t - type_name: clock_t - type_name: size_t + - type_name: locale_t enums: [] objects: [] functions: @@ -96,3 +97,22 @@ functions: return_type: time_t arguments: - type: time_t * + - 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 diff --git a/libc/spec/stdc.td b/libc/spec/stdc.td index 7caf543748151..f673e0e3bc246 100644 --- a/libc/spec/stdc.td +++ b/libc/spec/stdc.td @@ -1586,6 +1586,7 @@ def StdC : StandardSpec<"stdc"> { StructTimeSpec, TimeTType, SizeTType, + LocaleT, ], [], // Enumerations [ @@ -1651,6 +1652,26 @@ def StdC : StandardSpec<"stdc"> { RetValSpec, [ArgSpec] >, + FunctionSpec< + "strftime", + RetValSpec, + [ + ArgSpec, + ArgSpec, + ArgSpec, + ArgSpec + ] + FunctionSpec< + "strftime_l", + RetValSpec, + [ + ArgSpec, + ArgSpec, + ArgSpec, + ArgSpec, + ArgSpec + ] + >, ] >; diff --git a/libc/src/time/CMakeLists.txt b/libc/src/time/CMakeLists.txt index b3318e7ca87fa..c45c50e2e9d7c 100644 --- a/libc/src/time/CMakeLists.txt +++ b/libc/src/time/CMakeLists.txt @@ -99,11 +99,32 @@ add_entrypoint_object( HDRS mktime.h DEPENDS - .time_utils libc.include.time libc.src.errno.errno ) + +add_entrypoint_object( + strftime + SRCS + strftime.cpp + HDRS + strftime.h + DEPENDS + libc.include.time +) + +add_entrypoint_object( + strftime_l + SRCS + strftime_l.cpp + HDRS + strftime_l.h + DEPENDS + libc.include.time + libc.include.locale +) + add_entrypoint_object( time ALIAS diff --git a/libc/src/time/strftime.cpp b/libc/src/time/strftime.cpp new file mode 100644 index 0000000000000..4fa8a7fc3ec7d --- /dev/null +++ b/libc/src/time/strftime.cpp @@ -0,0 +1,26 @@ +//===-- 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 "src/__support/common.h" +#include "src/__support/macros/config.h" +#include "src/errno/libc_errno.h" +#include "src/time/time_utils.h" + +namespace LIBC_NAMESPACE_DECL { + +using LIBC_NAMESPACE::time_utils::TimeConstants; + +LLVM_LIBC_FUNCTION(size_t, strftime, + (char *__restrict, size_t, const char *__restrict, + const struct tm *)) { + // TODO: Implement this for the default locale. + return -1; +} + +} // namespace LIBC_NAMESPACE_DECL diff --git a/libc/src/time/strftime.h b/libc/src/time/strftime.h new file mode 100644 index 0000000000000..aa30336d8957d --- /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 "src/__support/macros/config.h" +#include +#include + +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_l.cpp b/libc/src/time/strftime_l.cpp new file mode 100644 index 0000000000000..c25103b58e836 --- /dev/null +++ b/libc/src/time/strftime_l.cpp @@ -0,0 +1,24 @@ +//===-- Implementation of strftime_l 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_l.h" +#include "src/__support/common.h" +#include "src/__support/macros/config.h" +#include "src/errno/libc_errno.h" +#include "src/time/time_utils.h" + +namespace LIBC_NAMESPACE_DECL { + +LLVM_LIBC_FUNCTION(size_t, strftime_l, + (char *__restrict, size_t, const char *__restrict, + const struct tm *, locale_t)) { + // TODO: Implement this for the default locale. + return -1; +} + +} // namespace LIBC_NAMESPACE_DECL diff --git a/libc/src/time/strftime_l.h b/libc/src/time/strftime_l.h new file mode 100644 index 0000000000000..68c48007e6ad8 --- /dev/null +++ b/libc/src/time/strftime_l.h @@ -0,0 +1,24 @@ +//===-- Implementation header of strftime_l ---------------------*- 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_L_H +#define LLVM_LIBC_SRC_TIME_STRFTIME_L_H + +#include "hdr/types/locale_t.h" +#include "src/__support/macros/config.h" +#include +#include + +namespace LIBC_NAMESPACE_DECL { + +size_t strftime_l(char *__restrict, size_t max, const char *__restrict format, + const struct tm *timeptr, locale_t locale); + +} // namespace LIBC_NAMESPACE_DECL + +#endif // LLVM_LIBC_SRC_TIME_STRFTIME_L_H From 097d459969b1d60ba44a607548b09c19b64eb6f3 Mon Sep 17 00:00:00 2001 From: Tsz Chan Date: Sun, 20 Oct 2024 00:09:14 +0800 Subject: [PATCH 3/4] [libc] Implement strftime and strftime_l --- libc/src/stdio/CMakeLists.txt | 1 - libc/src/stdio/strftime_core/CMakeLists.txt | 52 ----- libc/src/stdio/strftime_core/core_structs.h | 97 -------- libc/src/time/CMakeLists.txt | 55 +++-- libc/src/time/strftime.cpp | 14 +- libc/src/time/strftime_core/CMakeLists.txt | 52 +++++ .../strftime_core/converter.cpp | 214 ++++++++++++++---- .../{stdio => time}/strftime_core/converter.h | 3 +- libc/src/time/strftime_core/core_structs.h | 42 ++++ .../{stdio => time}/strftime_core/parser.h | 111 ++++----- libc/src/time/strftime_core/strftime_main.cpp | 43 ++++ libc/src/time/strftime_core/strftime_main.h | 28 +++ .../strftime_core/time_internal_def.h | 20 ++ libc/src/time/strftime_l.cpp | 13 +- libc/test/src/stdio/CMakeLists.txt | 1 - .../src/stdio/strftime_core/CMakeLists.txt | 11 - .../src/stdio/strftime_core/parser_test.cpp | 0 libc/test/src/time/CMakeLists.txt | 12 + .../src/time/strftime_core/CMakeLists.txt | 23 ++ .../strftime_core/converter_test.cpp | 164 ++++++++++++-- .../src/time/strftime_core/parser_test.cpp | 102 +++++++++ libc/test/src/time/strftime_test.cpp | 98 ++++++++ 22 files changed, 824 insertions(+), 332 deletions(-) delete mode 100644 libc/src/stdio/strftime_core/CMakeLists.txt delete mode 100644 libc/src/stdio/strftime_core/core_structs.h create mode 100644 libc/src/time/strftime_core/CMakeLists.txt rename libc/src/{stdio => time}/strftime_core/converter.cpp (60%) rename libc/src/{stdio => time}/strftime_core/converter.h (91%) create mode 100644 libc/src/time/strftime_core/core_structs.h rename libc/src/{stdio => time}/strftime_core/parser.h (53%) create mode 100644 libc/src/time/strftime_core/strftime_main.cpp create mode 100644 libc/src/time/strftime_core/strftime_main.h rename libc/src/{stdio => time}/strftime_core/time_internal_def.h (71%) delete mode 100644 libc/test/src/stdio/strftime_core/CMakeLists.txt delete mode 100644 libc/test/src/stdio/strftime_core/parser_test.cpp create mode 100644 libc/test/src/time/strftime_core/CMakeLists.txt rename libc/test/src/{stdio => time}/strftime_core/converter_test.cpp (66%) create mode 100644 libc/test/src/time/strftime_core/parser_test.cpp create mode 100644 libc/test/src/time/strftime_test.cpp diff --git a/libc/src/stdio/CMakeLists.txt b/libc/src/stdio/CMakeLists.txt index 5338c17d6232b..b9bc904471df9 100644 --- a/libc/src/stdio/CMakeLists.txt +++ b/libc/src/stdio/CMakeLists.txt @@ -239,7 +239,6 @@ add_entrypoint_object( add_subdirectory(printf_core) add_subdirectory(scanf_core) -add_subdirectory(strftime_core) add_entrypoint_object( remove diff --git a/libc/src/stdio/strftime_core/CMakeLists.txt b/libc/src/stdio/strftime_core/CMakeLists.txt deleted file mode 100644 index e234d8d064e15..0000000000000 --- a/libc/src/stdio/strftime_core/CMakeLists.txt +++ /dev/null @@ -1,52 +0,0 @@ - -add_header_library( - core_structs - HDRS - core_structs.h - DEPENDS - libc.src.__support.CPP.string_view - libc.src.__support.FPUtil.fp_bits -) - -add_header_library( - parser - HDRS - parser.h - DEPENDS - .core_structs - libc.src.__support.arg_list - libc.src.__support.ctype_utils - libc.src.__support.str_to_integer - libc.src.__support.CPP.algorithm - libc.src.__support.CPP.bit - libc.src.__support.CPP.optional - libc.src.__support.CPP.string_view - libc.src.__support.CPP.type_traits - libc.src.__support.common -) - - -add_object_library( - converter - SRCS - converter.cpp - HDRS - converter.h - DEPENDS - .core_structs - libc.src.stdio.printf_core.writer - libc.src.__support.big_int - libc.src.math.log10 - libc.src.__support.common - libc.src.__support.CPP.limits - libc.src.__support.CPP.span - libc.src.__support.CPP.string_view - libc.src.__support.float_to_string - libc.src.__support.FPUtil.fenv_impl - libc.src.__support.FPUtil.fp_bits - libc.src.__support.FPUtil.rounding_mode - libc.src.__support.integer_to_string - libc.src.__support.libc_assert - libc.src.__support.uint128 - libc.src.__support.StringUtil.error_to_string -) \ No newline at end of file diff --git a/libc/src/stdio/strftime_core/core_structs.h b/libc/src/stdio/strftime_core/core_structs.h deleted file mode 100644 index 404902bcfbf6f..0000000000000 --- a/libc/src/stdio/strftime_core/core_structs.h +++ /dev/null @@ -1,97 +0,0 @@ -//===-- Core Structures 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_CORE_STRUCTS_H -#define LLVM_LIBC_SRC_STDIO_STRFTIME_CORE_CORE_STRUCTS_H - -#include "src/__support/macros/config.h" - -#include "src/__support/CPP/string_view.h" -#include "src/__support/CPP/type_traits.h" -#include "src/__support/FPUtil/FPBits.h" - -#include -#include - -namespace LIBC_NAMESPACE_DECL { -namespace strftime_core { - -struct tm { - int tm_sec; /* seconds after the minute [0-60] */ - int tm_min; /* minutes after the hour [0-59] */ - int tm_hour; /* hours since midnight [0-23] */ - int tm_mday; /* day of the month [1-31] */ - int tm_mon; /* months since January [0-11] */ - int tm_year; /* years since 1900 */ - int tm_wday; /* days since Sunday [0-6] */ - int tm_yday; /* days since January 1 [0-365] */ - int tm_isdst; /* Daylight Savings Time flag */ - long tm_gmtoff; /* offset from UTC in seconds */ - char *tm_zone; /* timezone abbreviation */ -}; - -struct FormatSection { - bool has_conv{false}; - bool isE{false}; - bool isO{false}; - cpp::string_view raw_string; - char conv_name; - const struct tm *time; -}; - -enum PrimaryType : uint8_t { - Unknown = 0, - Float = 1, - Pointer = 2, - Integer = 3, - FixedPoint = 4, -}; - -// TypeDesc stores the information about a type that is relevant to printf in -// a relatively compact manner. -struct TypeDesc { - uint8_t size; - PrimaryType primary_type; - LIBC_INLINE constexpr bool operator==(const TypeDesc &other) const { - return (size == other.size) && (primary_type == other.primary_type); - } -}; - -template LIBC_INLINE constexpr TypeDesc type_desc_from_type() { - if constexpr (cpp::is_same_v) { - return TypeDesc{0, PrimaryType::Unknown}; - } else { - constexpr bool IS_POINTER = cpp::is_pointer_v; - constexpr bool IS_FLOAT = cpp::is_floating_point_v; -#ifdef LIBC_INTERNAL_STRFTIME_HAS_FIXED_POINT - constexpr bool IS_FIXED_POINT = cpp::is_fixed_point_v; -#else - constexpr bool IS_FIXED_POINT = false; -#endif // LIBC_INTERNAL_STRFTIME_HAS_FIXED_POINT - - return TypeDesc{sizeof(T), IS_POINTER ? PrimaryType::Pointer - : IS_FLOAT ? PrimaryType::Float - : IS_FIXED_POINT ? PrimaryType::FixedPoint - : PrimaryType::Integer}; - } -} - -// This is the value to be returned by conversions when no error has occurred. -constexpr int WRITE_OK = 0; -// These are the printf return values for when an error has occurred. They are -// all negative, and should be distinct. -constexpr int FILE_WRITE_ERROR = -1; -constexpr int FILE_STATUS_ERROR = -2; -constexpr int NULLPTR_WRITE_ERROR = -3; -constexpr int INT_CONVERSION_ERROR = -4; -constexpr int FIXED_POINT_CONVERSION_ERROR = -5; -constexpr int ALLOCATION_ERROR = -6; -} // namespace strftime_core -} // namespace LIBC_NAMESPACE_DECL - -#endif // LLVM_LIBC_SRC_STDIO_STRFTIME_CORE_CORE_STRUCTS_H diff --git a/libc/src/time/CMakeLists.txt b/libc/src/time/CMakeLists.txt index c45c50e2e9d7c..6e6c21bc65738 100644 --- a/libc/src/time/CMakeLists.txt +++ b/libc/src/time/CMakeLists.txt @@ -1,6 +1,6 @@ -if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/${LIBC_TARGET_OS}) - add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/${LIBC_TARGET_OS}) -endif() +# if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/${LIBC_TARGET_OS}) +# add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/${LIBC_TARGET_OS}) +# endif() add_object_library( time_utils @@ -103,28 +103,6 @@ add_entrypoint_object( libc.src.errno.errno ) - -add_entrypoint_object( - strftime - SRCS - strftime.cpp - HDRS - strftime.h - DEPENDS - libc.include.time -) - -add_entrypoint_object( - strftime_l - SRCS - strftime_l.cpp - HDRS - strftime_l.h - DEPENDS - libc.include.time - libc.include.locale -) - add_entrypoint_object( time ALIAS @@ -159,3 +137,30 @@ add_entrypoint_object( DEPENDS .${LIBC_TARGET_OS}.gettimeofday ) + +add_subdirectory(strftime_core) + +add_entrypoint_object( + strftime + SRCS + strftime.cpp + HDRS + strftime.h + DEPENDS + libc.include.time + libc.src.time.strftime_core.strftime_main + libc.src.stdio.printf_core.writer +) + +add_entrypoint_object( + strftime_l + SRCS + strftime_l.cpp + HDRS + strftime_l.h + DEPENDS + libc.include.time + libc.include.locale + libc.src.time.strftime_core.strftime_main + libc.src.stdio.printf_core.writer +) \ No newline at end of file diff --git a/libc/src/time/strftime.cpp b/libc/src/time/strftime.cpp index 4fa8a7fc3ec7d..7c20268cec9a3 100644 --- a/libc/src/time/strftime.cpp +++ b/libc/src/time/strftime.cpp @@ -12,15 +12,17 @@ #include "src/errno/libc_errno.h" #include "src/time/time_utils.h" +#include "src/stdio/printf_core/writer.h" +#include "src/time/strftime_core/strftime_main.h" namespace LIBC_NAMESPACE_DECL { -using LIBC_NAMESPACE::time_utils::TimeConstants; +size_t strftime(char *__restrict buffer, size_t buffsz, + const char *__restrict format, const struct tm *timeptr) { -LLVM_LIBC_FUNCTION(size_t, strftime, - (char *__restrict, size_t, const char *__restrict, - const struct tm *)) { - // TODO: Implement this for the default locale. - return -1; + printf_core::WriteBuffer wb(buffer, (buffsz > 0 ? buffsz - 1 : 0)); + printf_core::Writer writer(&wb); + strftime_core::strftime_main(&writer, format, timeptr); + return writer.get_chars_written(); } } // namespace LIBC_NAMESPACE_DECL diff --git a/libc/src/time/strftime_core/CMakeLists.txt b/libc/src/time/strftime_core/CMakeLists.txt new file mode 100644 index 0000000000000..78f37ea3ef707 --- /dev/null +++ b/libc/src/time/strftime_core/CMakeLists.txt @@ -0,0 +1,52 @@ +add_header_library( + core_structs + HDRS + core_structs.h + DEPENDS + libc.src.__support.CPP.string_view + libc.include.time +) + +add_header_library( + parser + HDRS + parser.h + DEPENDS + .core_structs + libc.src.string.string_utils + libc.include.time + +) + +add_object_library( + converter + SRCS + converter.cpp + HDRS + converter.h + DEPENDS + .core_structs + libc.src.stdio.printf_core.writer + libc.src.__support.big_int + libc.src.__support.CPP.string_view + libc.src.__support.float_to_string + libc.src.__support.integer_to_string + libc.src.__support.uint128 + libc.src.__support.StringUtil.error_to_string + libc.include.time + +) + +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.include.time +) \ No newline at end of file diff --git a/libc/src/stdio/strftime_core/converter.cpp b/libc/src/time/strftime_core/converter.cpp similarity index 60% rename from libc/src/stdio/strftime_core/converter.cpp rename to libc/src/time/strftime_core/converter.cpp index 79e3727cc88f5..d7b3e57f19ce8 100644 --- a/libc/src/stdio/strftime_core/converter.cpp +++ b/libc/src/time/strftime_core/converter.cpp @@ -13,15 +13,20 @@ #include "src/__support/CPP/string_view.h" #include "src/__support/integer_to_string.h" #include "src/__support/macros/config.h" -#include "src/math/log10.h" #include "src/stdio/printf_core/writer.h" -#include "src/stdio/strftime_core/core_structs.h" -#include "src/stdio/strftime_core/time_internal_def.h" -#include +#include "src/time/strftime_core/core_structs.h" +#include "src/time/strftime_core/time_internal_def.h" namespace LIBC_NAMESPACE_DECL { namespace strftime_core { +#define RET_IF_RESULT_NEGATIVE(func) \ + { \ + int result = (func); \ + if (result < 0) \ + return result; \ + } + namespace details { LIBC_INLINE cpp::optional @@ -35,18 +40,29 @@ LIBC_INLINE int write_num(uintmax_t num, printf_core::Writer *writer) { return writer->write(*num_to_strview(num, buf)); } +template int count_digits(T num) { + if (num == 0) + return 1; + int digits = 0; + while (num > 0) { + num /= 10; + digits++; + } + return digits; +} + template LIBC_INLINE int write_num_with_padding(uintmax_t num, printf_core::Writer *writer) { cpp::array buf; - auto digits = log10(num) + 1; - auto padding_needed = width - digits; - int char_written = 0; + int digits = count_digits(num); + int padding_needed = width - digits; + for (int _ = 0; _ < padding_needed; _++) { - char_written += writer->write(padding); + RET_IF_RESULT_NEGATIVE(writer->write(padding)); } - char_written += writer->write(*num_to_strview(num, buf)); - return char_written; + + return writer->write(*num_to_strview(num, buf)); } } // namespace details @@ -54,12 +70,19 @@ LIBC_INLINE int write_num_with_padding(uintmax_t num, /* Nonzero if YEAR is a leap year (every 4 years, except every 100th isn't, and every 400th is). */ LIBC_INLINE bool is_leap(int year) { - return ((year) % 4 == 0 && ((year) % 100 != 0 || (year) % 400 == 0)); + return ((year % 4 == 0) && (year % 100 != 0 || year % 400 == 0)); } +// Conversion functions + LIBC_INLINE int convert_weekday(printf_core::Writer *writer, const FormatSection &to_conv) { - return writer->write(day_names[to_conv.time->tm_wday]); + return writer->write(safe_day_name(to_conv.time->tm_wday)); +} + +LIBC_INLINE int convert_abbreviated_weekday(printf_core::Writer *writer, + const FormatSection &to_conv) { + return writer->write(safe_abbreviated_day_name(to_conv.time->tm_wday)); } LIBC_INLINE int convert_zero_padded_day_of_year(printf_core::Writer *writer, @@ -108,12 +131,12 @@ LIBC_INLINE int convert_week_number_monday(printf_core::Writer *writer, LIBC_INLINE int convert_full_month(printf_core::Writer *writer, const FormatSection &to_conv) { - return writer->write(month_names[to_conv.time->tm_mon]); + return writer->write(safe_month_name(to_conv.time->tm_mon)); } LIBC_INLINE int convert_abbreviated_month(printf_core::Writer *writer, const FormatSection &to_conv) { - return writer->write(abbreviated_month_names[to_conv.time->tm_mon]); + return writer->write(safe_abbreviated_month_name(to_conv.time->tm_mon)); } LIBC_INLINE int convert_zero_padded_month(printf_core::Writer *writer, @@ -124,20 +147,100 @@ LIBC_INLINE int convert_zero_padded_month(printf_core::Writer *writer, LIBC_INLINE int convert_full_year(printf_core::Writer *writer, const FormatSection &to_conv) { - return details::write_num_with_padding<4, '0'>( - to_conv.time->tm_year + YEAR_BASE, writer); + return details::write_num_with_padding<4, '0'>(to_conv.time->tm_year + 1900, + writer); } LIBC_INLINE int convert_two_digit_year(printf_core::Writer *writer, const FormatSection &to_conv) { return details::write_num_with_padding<2, '0'>( - (to_conv.time->tm_year + YEAR_BASE) % 100, writer); + (to_conv.time->tm_year + 1900) % 100, writer); } LIBC_INLINE int convert_century(printf_core::Writer *writer, const FormatSection &to_conv) { return details::write_num_with_padding<2, '0'>( - (to_conv.time->tm_year + YEAR_BASE) / 100, writer); + (to_conv.time->tm_year + 1900) / 100, writer); +} + +LIBC_INLINE int convert_hour_24(printf_core::Writer *writer, + const FormatSection &to_conv) { + return details::write_num_with_padding<2, '0'>(to_conv.time->tm_hour, writer); +} + +LIBC_INLINE int convert_pm(printf_core::Writer *writer, + const FormatSection &to_conv) { + static const cpp::string_view AM = "AM"; + static const cpp::string_view PM = "PM"; + return writer->write(to_conv.time->tm_hour >= 12 ? PM : AM); +} + +LIBC_INLINE int convert_hour_12(printf_core::Writer *writer, + const FormatSection &to_conv) { + int hour = to_conv.time->tm_hour % 12; + if (hour == 0) + hour = 12; + return details::write_num_with_padding<2, '0'>(hour, writer); +} + +LIBC_INLINE int convert_minute(printf_core::Writer *writer, + const FormatSection &to_conv) { + return details::write_num_with_padding<2, '0'>(to_conv.time->tm_min, writer); +} + +LIBC_INLINE int convert_second(printf_core::Writer *writer, + const FormatSection &to_conv) { + return details::write_num_with_padding<2, '0'>(to_conv.time->tm_sec, writer); +} + +LIBC_INLINE int convert_MM_DD_YY(printf_core::Writer *writer, + const FormatSection &to_conv) { + RET_IF_RESULT_NEGATIVE(convert_zero_padded_month(writer, to_conv)); + RET_IF_RESULT_NEGATIVE(writer->write("/")); + RET_IF_RESULT_NEGATIVE(convert_zero_padded_day_of_month(writer, to_conv)); + RET_IF_RESULT_NEGATIVE(writer->write("/")); + return convert_two_digit_year(writer, to_conv); +} + +LIBC_INLINE int convert_YYYY_MM_DD(printf_core::Writer *writer, + const FormatSection &to_conv) { + RET_IF_RESULT_NEGATIVE(convert_full_year(writer, to_conv)); + RET_IF_RESULT_NEGATIVE(writer->write("-")); + RET_IF_RESULT_NEGATIVE(convert_zero_padded_month(writer, to_conv)); + RET_IF_RESULT_NEGATIVE(writer->write("-")); + return convert_zero_padded_day_of_month(writer, to_conv); +} + +LIBC_INLINE int convert_hour_minute(printf_core::Writer *writer, + const FormatSection &to_conv) { + RET_IF_RESULT_NEGATIVE(convert_hour_24(writer, to_conv)); + RET_IF_RESULT_NEGATIVE(writer->write(":")); + return convert_minute(writer, to_conv); +} + +LIBC_INLINE int convert_date_time(printf_core::Writer *writer, + const FormatSection &to_conv) { + RET_IF_RESULT_NEGATIVE(convert_abbreviated_weekday(writer, to_conv)); + RET_IF_RESULT_NEGATIVE(writer->write(" ")); + RET_IF_RESULT_NEGATIVE(convert_abbreviated_month(writer, to_conv)); + RET_IF_RESULT_NEGATIVE(writer->write(" ")); + RET_IF_RESULT_NEGATIVE(convert_space_padded_day_of_month(writer, to_conv)); + RET_IF_RESULT_NEGATIVE(writer->write(" ")); + RET_IF_RESULT_NEGATIVE(convert_full_year(writer, to_conv)); + RET_IF_RESULT_NEGATIVE(writer->write(" ")); + RET_IF_RESULT_NEGATIVE(convert_hour_24(writer, to_conv)); + RET_IF_RESULT_NEGATIVE(writer->write(":")); + RET_IF_RESULT_NEGATIVE(convert_minute(writer, to_conv)); + RET_IF_RESULT_NEGATIVE(writer->write(":")); + return convert_second(writer, to_conv); +} + +LIBC_INLINE int convert_time_zone(printf_core::Writer *writer) { + return writer->write("UTC"); +} + +LIBC_INLINE int convert_time_zone_offset(printf_core::Writer *writer) { + return writer->write("+0000"); } static int iso_week_days(int yday, int wday) { @@ -170,43 +273,39 @@ LIBC_INLINE int convert_iso_year(printf_core::Writer *writer, return details::write_num<4>(year, writer); } -LIBC_INLINE int convert_hour_24(printf_core::Writer *writer, +LIBC_INLINE int convert_iso_day(printf_core::Writer *writer, const FormatSection &to_conv) { - return details::write_num_with_padding<2, '0'>(to_conv.time->tm_hour, writer); -} - -LIBC_INLINE int convert_pm(printf_core::Writer *writer, - const FormatSection &to_conv) { - static const cpp::string_view AM = "AM"; - static const cpp::string_view PM = "PM"; - return writer->write(to_conv.time->tm_hour >= 12 ? PM : AM); -} - -LIBC_INLINE int convert_hour_12(printf_core::Writer *writer, - const FormatSection &to_conv) { - int hour = to_conv.time->tm_hour % 12; // Convert to 12-hour format - if (hour == 0) - hour = 12; // Adjust for midnight - return details::write_num_with_padding<2, '0'>(hour, writer); -} - -LIBC_INLINE int convert_minute(printf_core::Writer *writer, - const FormatSection &to_conv) { - return details::write_num_with_padding<2, '0'>(to_conv.time->tm_min, writer); -} + int year = to_conv.time->tm_year + YEAR_BASE; + int days = iso_week_days(to_conv.time->tm_yday, to_conv.time->tm_wday); -LIBC_INLINE int convert_second(printf_core::Writer *writer, - const FormatSection &to_conv) { - return details::write_num_with_padding<2, '0'>(to_conv.time->tm_sec, writer); + if (days < 0) { + /* This ISO week belongs to the previous year. */ + year--; + days = iso_week_days(to_conv.time->tm_yday + (365 + is_leap(year)), + to_conv.time->tm_wday); + } else { + int d = iso_week_days(to_conv.time->tm_yday - (365 + is_leap(year)), + to_conv.time->tm_wday); + if (0 <= d) { + /* This ISO week belongs to the next year. */ + year++; + days = d; + } + } + return details::write_num_with_padding<2, '0'>(days / 7 + 1, writer); } int convert(printf_core::Writer *writer, const FormatSection &to_conv) { if (!to_conv.has_conv) return writer->write(to_conv.raw_string); switch (to_conv.conv_name) { + case '%': + return writer->write("%"); // day of the week - case 'a': + case 'A': return convert_weekday(writer, to_conv); + case 'a': + return convert_abbreviated_weekday(writer, to_conv); case 'w': return convert_decimal_weekday(writer, to_conv); case 'u': @@ -223,7 +322,8 @@ int convert(printf_core::Writer *writer, const FormatSection &to_conv) { return convert_week_number_sunday(writer, to_conv); case 'W': return convert_week_number_monday(writer, to_conv); - case 'V': // TODO: ISO 8061 + case 'V': + return convert_iso_day(writer, to_conv); // month case 'B': return convert_full_month(writer, to_conv); @@ -240,7 +340,6 @@ int convert(printf_core::Writer *writer, const FormatSection &to_conv) { case 'C': return convert_century(writer, to_conv); case 'G': - // TODO return convert_iso_year(writer, to_conv); // hours case 'p': @@ -255,6 +354,27 @@ int convert(printf_core::Writer *writer, const FormatSection &to_conv) { // seconds case 'S': return convert_second(writer, to_conv); + // Date and Time + case 'c': + return convert_date_time(writer, to_conv); + + // Timezone + case 'Z': + return convert_time_zone(writer); + case 'z': + return convert_time_zone_offset(writer); + + // Custom formats: MM/DD/YY, YYYY-MM-DD + case 'D': + return convert_MM_DD_YY(writer, to_conv); // Equivalent to %m/%d/%y + case 'F': + return convert_YYYY_MM_DD(writer, to_conv); // Equivalent to %Y-%m-%d + + // 24-hour time without seconds (HH:MM) + case 'R': + return convert_hour_minute(writer, to_conv); // Equivalent to %H:%M + default: + writer->write(to_conv.raw_string); } return 0; } diff --git a/libc/src/stdio/strftime_core/converter.h b/libc/src/time/strftime_core/converter.h similarity index 91% rename from libc/src/stdio/strftime_core/converter.h rename to libc/src/time/strftime_core/converter.h index 57801b8e8e126..8809a8356e5c5 100644 --- a/libc/src/stdio/strftime_core/converter.h +++ b/libc/src/time/strftime_core/converter.h @@ -9,9 +9,8 @@ #ifndef LLVM_LIBC_SRC_STDIO_STRFTIME_CORE_CONVERTER_H #define LLVM_LIBC_SRC_STDIO_STRFTIME_CORE_CONVERTER_H -#include "src/__support/macros/config.h" #include "src/stdio/printf_core/writer.h" -#include "src/stdio/strftime_core/core_structs.h" +#include "src/time/strftime_core/core_structs.h" #include diff --git a/libc/src/time/strftime_core/core_structs.h b/libc/src/time/strftime_core/core_structs.h new file mode 100644 index 0000000000000..d6cd91884b8a6 --- /dev/null +++ b/libc/src/time/strftime_core/core_structs.h @@ -0,0 +1,42 @@ +//===-- Core Structures 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_CORE_STRUCTS_H +#define LLVM_LIBC_SRC_STDIO_STRFTIME_CORE_CORE_STRUCTS_H + +#include "src/__support/CPP/string_view.h" +#include + +namespace LIBC_NAMESPACE_DECL { +namespace strftime_core { + +struct FormatSection { + bool has_conv{false}; + bool isE{false}; + bool isO{false}; + cpp::string_view raw_string{}; + char conv_name; + const struct tm *time; + int min_width{0}; + char padding; +}; + +// This is the value to be returned by conversions when no error has occurred. +constexpr int WRITE_OK = 0; +// These are the printf return values for when an error has occurred. They are +// all negative, and should be distinct. +constexpr int FILE_WRITE_ERROR = -1; +constexpr int FILE_STATUS_ERROR = -2; +constexpr int NULLPTR_WRITE_ERROR = -3; +constexpr int INT_CONVERSION_ERROR = -4; +constexpr int FIXED_POINT_CONVERSION_ERROR = -5; +constexpr int ALLOCATION_ERROR = -6; +} // namespace strftime_core +} // namespace LIBC_NAMESPACE_DECL + +#endif // LLVM_LIBC_SRC_STDIO_STRFTIME_CORE_CORE_STRUCTS_H diff --git a/libc/src/stdio/strftime_core/parser.h b/libc/src/time/strftime_core/parser.h similarity index 53% rename from libc/src/stdio/strftime_core/parser.h rename to libc/src/time/strftime_core/parser.h index 046f5b1095ad0..59bba59588a9e 100644 --- a/libc/src/stdio/strftime_core/parser.h +++ b/libc/src/time/strftime_core/parser.h @@ -9,35 +9,42 @@ #ifndef LLVM_LIBC_SRC_STDIO_STRFTIME_CORE_PARSER_H #define LLVM_LIBC_SRC_STDIO_STRFTIME_CORE_PARSER_H -#include "include/llvm-libc-macros/stdfix-macros.h" -#include "src/__support/CPP/algorithm.h" // max -#include "src/__support/CPP/limits.h" -#include "src/__support/CPP/optional.h" -#include "src/__support/CPP/type_traits.h" +#include "core_structs.h" +#include "src/__support/CPP/string_view.h" #include "src/__support/macros/config.h" -#include "src/__support/str_to_integer.h" -#include "src/stdio/strftime_core/core_structs.h" -// #include "src/stdio/strftime_core/printf_config.h" - -#include - -#ifdef LIBC_INTERNAL_PRINTF_HAS_FIXED_POINT -#include "src/__support/fixed_point/fx_rep.h" -#endif // LIBC_INTERNAL_PRINTF_HAS_FIXED_POINT -#ifndef LIBC_COPT_PRINTF_DISABLE_STRERROR -#include "src/errno/libc_errno.h" -#endif // LIBC_COPT_PRINTF_DISABLE_STRERROR +#include "src/string/string_utils.h" +#include namespace LIBC_NAMESPACE_DECL { namespace strftime_core { +static constexpr cpp::string_view valid_conversions_after_E = "cCxXyY"; +static constexpr cpp::string_view valid_conversions_after_O = + "dHeHIOmMSuUVwWyY"; +static constexpr cpp::string_view all_valid_conversions = + "%aAbBcCdDeFgGhHIjmMnprRSuUVwWxXyYzZ"; + +int min_width(char conv) { + if (internal::strchr_implementation("CdegHImMSUVWy", conv)) + return 2; + if (conv == 'j') + return 3; + return 0; +} + +char get_padding(char conv) { + if (internal::strchr_implementation("CdgHIjmMSUVWy", conv)) + return '0'; + return ' '; +} + class Parser { - const char *__restrict str; + const char *str; const struct tm &time; size_t cur_pos = 0; public: - LIBC_INLINE Parser(const char *__restrict new_str, const tm &time) + LIBC_INLINE Parser(const char *new_str, const struct tm &time) : str(new_str), time(time) {} // get_next_section will parse the format string until it has a fully @@ -56,59 +63,33 @@ class Parser { // format section section.has_conv = true; section.time = &time; + ++cur_pos; // locale-specific modifiers - if (str[cur_pos] == 'E') + if (str[cur_pos] == 'E') { section.isE = true; - if (str[cur_pos] == 'O') + ++cur_pos; + } + if (str[cur_pos] == 'O') { section.isO = true; - ++cur_pos; + ++cur_pos; + } section.conv_name = str[cur_pos]; - switch (str[cur_pos]) { - case ('%'): - case ('a'): - case ('A'): - case ('b'): - case ('B'): - case ('c'): - case ('C'): - case ('d'): - case ('D'): - case ('e'): - case ('F'): - case ('g'): - case ('G'): - case ('h'): - case ('H'): - case ('I'): - case ('j'): - case ('m'): - case ('M'): - case ('n'): - case ('p'): - case ('r'): - case ('R'): - case ('S'): - case ('t'): - case ('T'): - case ('u'): - case ('U'): - case ('V'): - case ('w'): - case ('W'): - case ('x'): - case ('X'): - case ('y'): - case ('Y'): - case ('z'): - case ('Z'): - section.has_conv = true; - break; - default: - // if the conversion is undefined, change this to a raw section. + // Check if modifiers are valid + if ((section.isE && + !internal::strchr_implementation(valid_conversions_after_E.data(), + str[cur_pos])) || + (section.isO && + !internal::strchr_implementation(valid_conversions_after_O.data(), + str[cur_pos])) || + (!internal::strchr_implementation(all_valid_conversions.data(), + str[cur_pos]))) { section.has_conv = false; - break; } + + section.min_width = min_width(str[cur_pos]); + section.padding = get_padding(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') diff --git a/libc/src/time/strftime_core/strftime_main.cpp b/libc/src/time/strftime_core/strftime_main.cpp new file mode 100644 index 0000000000000..ab79d0a8ce5de --- /dev/null +++ b/libc/src/time/strftime_core/strftime_main.cpp @@ -0,0 +1,43 @@ +//===-- 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 +// +//===----------------------------------------------------------------------===// + +#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 +#include + +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, *timeptr); + 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); + else + result = writer->write(cur_section.raw_string); + + if (result < 0) + return result; + } + + return writer->get_chars_written(); +} + +} // namespace strftime_core +} // namespace LIBC_NAMESPACE_DECL diff --git a/libc/src/time/strftime_core/strftime_main.h b/libc/src/time/strftime_core/strftime_main.h new file mode 100644 index 0000000000000..fa1d0ad1a4e2d --- /dev/null +++ b/libc/src/time/strftime_core/strftime_main.h @@ -0,0 +1,28 @@ +//===-- 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 +#include + +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/stdio/strftime_core/time_internal_def.h b/libc/src/time/strftime_core/time_internal_def.h similarity index 71% rename from libc/src/stdio/strftime_core/time_internal_def.h rename to libc/src/time/strftime_core/time_internal_def.h index a7672ad43704e..198897ef9186b 100644 --- a/libc/src/stdio/strftime_core/time_internal_def.h +++ b/libc/src/time/strftime_core/time_internal_def.h @@ -39,5 +39,25 @@ static constexpr cpp::array abbreviated_month_names = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; +static constexpr cpp::string_view out_of_bound_str = + "?"; // From glibc output ? when days out of range + +LIBC_INLINE cpp::string_view safe_day_name(int day) { + return (day < 0 || day > 6) ? out_of_bound_str : day_names[day]; +} + +LIBC_INLINE cpp::string_view safe_abbreviated_day_name(int day) { + return (day < 0 || day > 6) ? out_of_bound_str : abbreviated_day_names[day]; +} + +LIBC_INLINE cpp::string_view safe_month_name(int month) { + return (month < 0 || month > 11) ? out_of_bound_str : month_names[month]; +} + +LIBC_INLINE cpp::string_view safe_abbreviated_month_name(int month) { + return (month < 0 || month > 11) ? out_of_bound_str + : abbreviated_month_names[month]; +} + } // namespace strftime_core } // namespace LIBC_NAMESPACE_DECL diff --git a/libc/src/time/strftime_l.cpp b/libc/src/time/strftime_l.cpp index c25103b58e836..a6dc28dd62e37 100644 --- a/libc/src/time/strftime_l.cpp +++ b/libc/src/time/strftime_l.cpp @@ -12,13 +12,18 @@ #include "src/errno/libc_errno.h" #include "src/time/time_utils.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_l, - (char *__restrict, size_t, const char *__restrict, - const struct tm *, locale_t)) { - // TODO: Implement this for the default locale. - return -1; + (char *__restrict buffer, size_t buffsz, + const char *__restrict format, const struct tm *timeptr, + locale_t)) { + printf_core::WriteBuffer wb(buffer, (buffsz > 0 ? buffsz - 1 : 0)); + printf_core::Writer writer(&wb); + strftime_core::strftime_main(&writer, format, timeptr); + return writer.get_chars_written(); } } // namespace LIBC_NAMESPACE_DECL diff --git a/libc/test/src/stdio/CMakeLists.txt b/libc/test/src/stdio/CMakeLists.txt index c43c24dc3459a..e970e4cf01405 100644 --- a/libc/test/src/stdio/CMakeLists.txt +++ b/libc/test/src/stdio/CMakeLists.txt @@ -542,5 +542,4 @@ endif() add_subdirectory(printf_core) add_subdirectory(scanf_core) -add_subdirectory(strftime_core) add_subdirectory(testdata) diff --git a/libc/test/src/stdio/strftime_core/CMakeLists.txt b/libc/test/src/stdio/strftime_core/CMakeLists.txt deleted file mode 100644 index e47cd0f8bb2f8..0000000000000 --- a/libc/test/src/stdio/strftime_core/CMakeLists.txt +++ /dev/null @@ -1,11 +0,0 @@ -add_libc_unittest( - converter_test - SUITE - libc_stdio_unittests - SRCS - converter_test.cpp - DEPENDS - libc.src.stdio.strftime_core.converter - libc.src.stdio.printf_core.writer - libc.src.stdio.strftime_core.core_structs -) diff --git a/libc/test/src/stdio/strftime_core/parser_test.cpp b/libc/test/src/stdio/strftime_core/parser_test.cpp deleted file mode 100644 index e69de29bb2d1d..0000000000000 diff --git a/libc/test/src/time/CMakeLists.txt b/libc/test/src/time/CMakeLists.txt index bba01f063fed2..ac6f88e354155 100644 --- a/libc/test/src/time/CMakeLists.txt +++ b/libc/test/src/time/CMakeLists.txt @@ -173,3 +173,15 @@ add_libc_test( libc.src.time.clock libc.src.errno.errno ) + +add_subdirectory(strftime_core) + +add_libc_test( + strftime_test + SUITE + libc_time_unittests + SRCS + strftime_test.cpp + DEPENDS + libc.src.time.strftime_core.strftime_main +) \ No newline at end of file diff --git a/libc/test/src/time/strftime_core/CMakeLists.txt b/libc/test/src/time/strftime_core/CMakeLists.txt new file mode 100644 index 0000000000000..d07f2806ab383 --- /dev/null +++ b/libc/test/src/time/strftime_core/CMakeLists.txt @@ -0,0 +1,23 @@ +add_libc_unittest( + converter_test + SUITE + libc_time_unittests + SRCS + converter_test.cpp + DEPENDS + libc.src.time.strftime_core.converter + libc.src.stdio.printf_core.writer + libc.src.time.strftime_core.core_structs +) + +add_libc_unittest( + parser_test + SUITE + libc_time_unittests + SRCS + parser_test.cpp + DEPENDS + libc.src.time.strftime_core.parser + libc.src.time.strftime_core.core_structs + +) diff --git a/libc/test/src/stdio/strftime_core/converter_test.cpp b/libc/test/src/time/strftime_core/converter_test.cpp similarity index 66% rename from libc/test/src/stdio/strftime_core/converter_test.cpp rename to libc/test/src/time/strftime_core/converter_test.cpp index 53ea3694227ef..9be44ef39fce7 100644 --- a/libc/test/src/stdio/strftime_core/converter_test.cpp +++ b/libc/test/src/time/strftime_core/converter_test.cpp @@ -7,10 +7,10 @@ //===----------------------------------------------------------------------===// #include "src/stdio/printf_core/writer.h" -#include "src/stdio/strftime_core/converter.h" -#include "src/stdio/strftime_core/core_structs.h" - +#include "src/time/strftime_core/converter.h" +#include "src/time/strftime_core/core_structs.h" #include "test/UnitTest/Test.h" +#include class LlvmLibcStrftimeConverterTest : public LIBC_NAMESPACE::testing::Test { protected: @@ -53,7 +53,7 @@ TEST_F(LlvmLibcStrftimeConverterTest, PercentConversion) { TEST_F(LlvmLibcStrftimeConverterTest, WeekdayConversion) { LIBC_NAMESPACE::strftime_core::FormatSection simple_conv; - LIBC_NAMESPACE::strftime_core::tm time; + tm time; time.tm_wday = 1; simple_conv.has_conv = true; simple_conv.raw_string = "%a"; @@ -64,12 +64,12 @@ TEST_F(LlvmLibcStrftimeConverterTest, WeekdayConversion) { wb.buff[wb.buff_cur] = '\0'; - ASSERT_STREQ(str, "Monday"); - ASSERT_EQ(writer.get_chars_written(), 6); + ASSERT_STREQ(str, "Mon"); + ASSERT_EQ(writer.get_chars_written(), 3); } TEST_F(LlvmLibcStrftimeConverterTest, AbbreviatedMonthNameConversion) { LIBC_NAMESPACE::strftime_core::FormatSection simple_conv; - LIBC_NAMESPACE::strftime_core::tm time; + tm time; time.tm_mon = 4; // May simple_conv.has_conv = true; simple_conv.raw_string = "%b"; @@ -86,7 +86,7 @@ TEST_F(LlvmLibcStrftimeConverterTest, AbbreviatedMonthNameConversion) { TEST_F(LlvmLibcStrftimeConverterTest, CenturyConversion) { LIBC_NAMESPACE::strftime_core::FormatSection simple_conv; - LIBC_NAMESPACE::strftime_core::tm time; + tm time; time.tm_year = 122; // Represents 2022 simple_conv.has_conv = true; simple_conv.raw_string = "%C"; @@ -103,7 +103,7 @@ TEST_F(LlvmLibcStrftimeConverterTest, CenturyConversion) { TEST_F(LlvmLibcStrftimeConverterTest, DayOfMonthZeroPaddedConversion) { LIBC_NAMESPACE::strftime_core::FormatSection simple_conv; - LIBC_NAMESPACE::strftime_core::tm time; + tm time; time.tm_mday = 7; simple_conv.has_conv = true; simple_conv.raw_string = "%d"; @@ -120,7 +120,7 @@ TEST_F(LlvmLibcStrftimeConverterTest, DayOfMonthZeroPaddedConversion) { TEST_F(LlvmLibcStrftimeConverterTest, DayOfMonthSpacePaddedConversion) { LIBC_NAMESPACE::strftime_core::FormatSection simple_conv; - LIBC_NAMESPACE::strftime_core::tm time; + tm time; time.tm_mday = 7; simple_conv.has_conv = true; simple_conv.raw_string = "%e"; @@ -137,7 +137,7 @@ TEST_F(LlvmLibcStrftimeConverterTest, DayOfMonthSpacePaddedConversion) { TEST_F(LlvmLibcStrftimeConverterTest, FullMonthNameConversion) { LIBC_NAMESPACE::strftime_core::FormatSection simple_conv; - LIBC_NAMESPACE::strftime_core::tm time; + tm time; time.tm_mon = 4; // May simple_conv.has_conv = true; simple_conv.raw_string = "%B"; @@ -154,7 +154,7 @@ TEST_F(LlvmLibcStrftimeConverterTest, FullMonthNameConversion) { TEST_F(LlvmLibcStrftimeConverterTest, Hour12Conversion) { LIBC_NAMESPACE::strftime_core::FormatSection simple_conv; - LIBC_NAMESPACE::strftime_core::tm time; + tm time; time.tm_hour = 14; simple_conv.has_conv = true; simple_conv.raw_string = "%I"; @@ -171,7 +171,7 @@ TEST_F(LlvmLibcStrftimeConverterTest, Hour12Conversion) { TEST_F(LlvmLibcStrftimeConverterTest, Hour24PaddedConversion) { LIBC_NAMESPACE::strftime_core::FormatSection simple_conv; - LIBC_NAMESPACE::strftime_core::tm time; + tm time; time.tm_hour = 9; simple_conv.has_conv = true; simple_conv.raw_string = "%H"; @@ -188,7 +188,7 @@ TEST_F(LlvmLibcStrftimeConverterTest, Hour24PaddedConversion) { TEST_F(LlvmLibcStrftimeConverterTest, MinuteConversion) { LIBC_NAMESPACE::strftime_core::FormatSection simple_conv; - LIBC_NAMESPACE::strftime_core::tm time; + tm time; time.tm_min = 45; simple_conv.has_conv = true; simple_conv.raw_string = "%M"; @@ -205,7 +205,7 @@ TEST_F(LlvmLibcStrftimeConverterTest, MinuteConversion) { TEST_F(LlvmLibcStrftimeConverterTest, AMPMConversion) { LIBC_NAMESPACE::strftime_core::FormatSection simple_conv; - LIBC_NAMESPACE::strftime_core::tm time; + tm time; time.tm_hour = 14; // 2 PM simple_conv.has_conv = true; simple_conv.raw_string = "%p"; @@ -222,7 +222,7 @@ TEST_F(LlvmLibcStrftimeConverterTest, AMPMConversion) { TEST_F(LlvmLibcStrftimeConverterTest, SecondsConversion) { LIBC_NAMESPACE::strftime_core::FormatSection simple_conv; - LIBC_NAMESPACE::strftime_core::tm time; + tm time; time.tm_sec = 30; simple_conv.has_conv = true; simple_conv.raw_string = "%S"; @@ -239,7 +239,7 @@ TEST_F(LlvmLibcStrftimeConverterTest, SecondsConversion) { TEST_F(LlvmLibcStrftimeConverterTest, FullYearConversion) { LIBC_NAMESPACE::strftime_core::FormatSection simple_conv; - LIBC_NAMESPACE::strftime_core::tm time; + tm time; time.tm_year = 122; // Represents 2022 (1900 + 122) simple_conv.has_conv = true; simple_conv.raw_string = "%Y"; @@ -256,7 +256,7 @@ TEST_F(LlvmLibcStrftimeConverterTest, FullYearConversion) { TEST_F(LlvmLibcStrftimeConverterTest, TwoDigitYearConversion) { LIBC_NAMESPACE::strftime_core::FormatSection simple_conv; - LIBC_NAMESPACE::strftime_core::tm time; + tm time; time.tm_year = 122; // Represents 2022 (1900 + 122) simple_conv.has_conv = true; simple_conv.raw_string = "%y"; @@ -273,7 +273,7 @@ TEST_F(LlvmLibcStrftimeConverterTest, TwoDigitYearConversion) { TEST_F(LlvmLibcStrftimeConverterTest, IsoYearEdgeCaseConversionEndOfYear) { LIBC_NAMESPACE::strftime_core::FormatSection iso_year; - LIBC_NAMESPACE::strftime_core::tm time; + tm time; // Set up the time for December 31, 2022 (a Saturday) time.tm_year = 122; // Represents 2022 @@ -297,7 +297,7 @@ TEST_F(LlvmLibcStrftimeConverterTest, IsoYearEdgeCaseConversionEndOfYear) { TEST_F(LlvmLibcStrftimeConverterTest, IsoYearEdgeCaseConversionStartOfYear) { LIBC_NAMESPACE::strftime_core::FormatSection iso_year; - LIBC_NAMESPACE::strftime_core::tm time; + tm time; // Set up the time for January 1, 2023 (a Sunday) time.tm_year = 123; // Represents 2023 @@ -317,4 +317,126 @@ TEST_F(LlvmLibcStrftimeConverterTest, IsoYearEdgeCaseConversionStartOfYear) { // The ISO year for this date is 2022, not 2023 ASSERT_STREQ(str, "2022"); ASSERT_EQ(writer.get_chars_written(), 4); -} \ No newline at end of file +} + +TEST_F(LlvmLibcStrftimeConverterTest, WeekNumberSundayFirstDayConversion) { + LIBC_NAMESPACE::strftime_core::FormatSection simple_conv; + tm time; + + // Set the time for a known Sunday (2023-05-07, which is a Sunday) + time.tm_year = 123; // Represents 2023 + time.tm_mon = 4; // May (0-based) + time.tm_mday = 7; // 7th day of the month + time.tm_wday = 0; // Sunday (0-based, 0 is Sunday) + time.tm_yday = 126; // 126th day of the year + + simple_conv.has_conv = true; + simple_conv.raw_string = "%U"; // Week number (Sunday is first day of week) + simple_conv.conv_name = 'U'; + simple_conv.time = &time; + + LIBC_NAMESPACE::strftime_core::convert(&writer, simple_conv); + + wb.buff[wb.buff_cur] = '\0'; + + // The week number for May 7, 2023 (Sunday as first day) should be 19 + ASSERT_STREQ(str, "19"); + ASSERT_EQ(writer.get_chars_written(), 2); +} + +TEST_F(LlvmLibcStrftimeConverterTest, WeekNumberMondayFirstDayConversion) { + LIBC_NAMESPACE::strftime_core::FormatSection simple_conv; + tm time; + + // Set the time for a known Monday (2023-05-08, which is a Monday) + time.tm_year = 123; // Represents 2023 + time.tm_mon = 4; // May (0-based) + time.tm_mday = 8; // 8th day of the month + time.tm_wday = 1; // Monday (0-based, 1 is Monday) + time.tm_yday = 127; // 127th day of the year + + simple_conv.has_conv = true; + simple_conv.raw_string = "%W"; // Week number (Monday is first day of week) + simple_conv.conv_name = 'W'; + simple_conv.time = &time; + + LIBC_NAMESPACE::strftime_core::convert(&writer, simple_conv); + + wb.buff[wb.buff_cur] = '\0'; + + // The week number for May 8, 2023 (Monday as first day) should be 19 + ASSERT_STREQ(str, "19"); + ASSERT_EQ(writer.get_chars_written(), 2); +} + +TEST_F(LlvmLibcStrftimeConverterTest, ISO8601WeekNumberConversion) { + LIBC_NAMESPACE::strftime_core::FormatSection simple_conv; + tm time; + + // Set the time for a known date (2023-05-10) + time.tm_year = 123; // Represents 2023 + time.tm_mon = 4; // May (0-based) + time.tm_mday = 10; // 10th day of the month + time.tm_wday = 3; // Wednesday (0-based, 3 is Wednesday) + time.tm_yday = 129; // 129th day of the year + + simple_conv.has_conv = true; + simple_conv.raw_string = "%V"; // ISO 8601 week number + simple_conv.conv_name = 'V'; + simple_conv.time = &time; + + LIBC_NAMESPACE::strftime_core::convert(&writer, simple_conv); + + wb.buff[wb.buff_cur] = '\0'; + + // The ISO week number for May 10, 2023 should be 19 + ASSERT_STREQ(str, "19"); + ASSERT_EQ(writer.get_chars_written(), 2); +} + +TEST_F(LlvmLibcStrftimeConverterTest, DayOfYearConversion) { + LIBC_NAMESPACE::strftime_core::FormatSection simple_conv; + tm time; + + // Set the time for a known date (2023-02-25) + time.tm_year = 123; // Represents 2023 + time.tm_mon = 1; // February (0-based) + time.tm_mday = 25; // 25th day of the month + time.tm_yday = 55; // 55th day of the year + + simple_conv.has_conv = true; + simple_conv.raw_string = "%j"; // Day of the year + simple_conv.conv_name = 'j'; + simple_conv.time = &time; + + LIBC_NAMESPACE::strftime_core::convert(&writer, simple_conv); + + wb.buff[wb.buff_cur] = '\0'; + + // The day of the year for February 25, 2023 should be 056 (padded) + ASSERT_STREQ(str, "056"); + ASSERT_EQ(writer.get_chars_written(), 3); +} + +TEST_F(LlvmLibcStrftimeConverterTest, ISO8601DateConversion) { + LIBC_NAMESPACE::strftime_core::FormatSection simple_conv; + tm time; + + // Set the time for a known date (2023-05-10) + time.tm_year = 123; // Represents 2023 + time.tm_mon = 4; // May (0-based) + time.tm_mday = 10; // 10th day of the month + + simple_conv.has_conv = true; + simple_conv.raw_string = "%F"; // ISO 8601 date format + simple_conv.conv_name = 'F'; + simple_conv.time = &time; + + LIBC_NAMESPACE::strftime_core::convert(&writer, simple_conv); + + wb.buff[wb.buff_cur] = '\0'; + + // The ISO date format for May 10, 2023 should be 2023-05-10 + ASSERT_STREQ(str, "2023-05-10"); + ASSERT_EQ(writer.get_chars_written(), 10); +} diff --git a/libc/test/src/time/strftime_core/parser_test.cpp b/libc/test/src/time/strftime_core/parser_test.cpp new file mode 100644 index 0000000000000..bb18702fb57d0 --- /dev/null +++ b/libc/test/src/time/strftime_core/parser_test.cpp @@ -0,0 +1,102 @@ +//===-- Unittests for the printf Converter --------------------------------===// +// + +// +//===----------------------------------------------------------------------===// + +#include "src/time/strftime_core/core_structs.h" +#include "src/time/strftime_core/parser.h" + +#include "test/UnitTest/Test.h" + +namespace LIBC_NAMESPACE_DECL { + +using namespace strftime_core; + +class LlvmLibcStrftimeParserTest : public LIBC_NAMESPACE::testing::Test { +protected: +protected: + struct tm test_time; + + void SetUp() override { + test_time = {}; + test_time.tm_year = 123; + test_time.tm_mon = 4; + test_time.tm_mday = 10; + test_time.tm_hour = 14; + test_time.tm_min = 30; + test_time.tm_sec = 0; + test_time.tm_isdst = 0; + } +}; + +TEST_F(LlvmLibcStrftimeParserTest, ParseRawSection) { + const char *format = "Today is %Y-%m-%d"; + Parser parser(format, test_time); + + FormatSection section = parser.get_next_section(); + + ASSERT_FALSE(section.has_conv); + ASSERT_EQ(section.raw_string, cpp::string_view("Today is ")); +} + +TEST_F(LlvmLibcStrftimeParserTest, ParseConversionSection) { + const char *format = "%Y is the year"; + Parser parser(format, test_time); + + FormatSection section = parser.get_next_section(); + + ASSERT_TRUE(section.has_conv); + ASSERT_EQ(section.conv_name, 'Y'); + ASSERT_EQ(section.time->tm_year, test_time.tm_year); + ASSERT_EQ(section.raw_string, cpp::string_view("%Y")); +} + +TEST_F(LlvmLibcStrftimeParserTest, ParseConversionSectionWithModifiers) { + const char *format = "%Od"; + Parser parser(format, test_time); + + FormatSection section = parser.get_next_section(); + + ASSERT_TRUE(section.has_conv); + ASSERT_EQ(section.conv_name, 'd'); + ASSERT_TRUE(section.isO); + ASSERT_FALSE(section.isE); +} + +TEST_F(LlvmLibcStrftimeParserTest, HandleInvalidConversion) { + const char *format = "%k"; + Parser parser(format, test_time); + + FormatSection section = parser.get_next_section(); + + ASSERT_FALSE(section.has_conv); + ASSERT_EQ(section.raw_string, cpp::string_view("%k")); +} + +TEST_F(LlvmLibcStrftimeParserTest, HandleMultipleSections) { + const char *format = "%Y-%m-%d %H:%M:%S"; + Parser parser(format, test_time); + + FormatSection section1 = parser.get_next_section(); + ASSERT_TRUE(section1.has_conv); + ASSERT_EQ(section1.conv_name, 'Y'); + + FormatSection section2 = parser.get_next_section(); + ASSERT_FALSE(section2.has_conv); + ASSERT_EQ(section2.raw_string, cpp::string_view("-")); + + FormatSection section3 = parser.get_next_section(); + ASSERT_TRUE(section3.has_conv); + ASSERT_EQ(section3.conv_name, 'm'); + + FormatSection section4 = parser.get_next_section(); + ASSERT_FALSE(section4.has_conv); + ASSERT_EQ(section4.raw_string, cpp::string_view("-")); + + FormatSection section5 = parser.get_next_section(); + ASSERT_TRUE(section5.has_conv); + ASSERT_EQ(section5.conv_name, 'd'); +} + +} // namespace LIBC_NAMESPACE_DECL diff --git a/libc/test/src/time/strftime_test.cpp b/libc/test/src/time/strftime_test.cpp new file mode 100644 index 0000000000000..6565c6e737307 --- /dev/null +++ b/libc/test/src/time/strftime_test.cpp @@ -0,0 +1,98 @@ + +//===-- Unittests for ctime -----------------------------------------------===// +// +// 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/errno/libc_errno.h" +#include "src/stdio/printf_core/writer.h" +#include "src/time/strftime_core/core_structs.h" +#include "src/time/strftime_core/strftime_main.h" +#include "test/UnitTest/Test.h" +#include + +namespace LIBC_NAMESPACE_DECL { + +using namespace strftime_core; +size_t call_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); + strftime_core::strftime_main(&writer, format, timeptr); + return writer.get_chars_written(); +} + +TEST(LlvmLibcStrftimeTest, FormatsYearMonthDayCorrectly) { + 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]; + call_strftime(buffer, sizeof(buffer), "%Y-%m-%d", &time); + EXPECT_STREQ(buffer, "2022-10-15"); +} + +TEST(LlvmLibcStrftimeTest, FormatsTimeCorrectly) { + struct tm time; + time.tm_hour = 14; // 2:00 PM + time.tm_min = 30; // 30 minutes + time.tm_sec = 45; // 45 seconds + + char buffer[100]; + call_strftime(buffer, sizeof(buffer), "%H:%M:%S", &time); + EXPECT_STREQ(buffer, "14:30:45"); +} + +TEST(LlvmLibcStrftimeTest, FormatsAmPmCorrectly) { + struct tm time; + time.tm_hour = 13; // 1:00 PM + time.tm_min = 0; + + char buffer[100]; + call_strftime(buffer, sizeof(buffer), "%I:%M %p", &time); + EXPECT_STREQ(buffer, "01:00 PM"); +} + +TEST(LlvmLibcStrftimeTest, HandlesLeapYear) { + struct tm time; + time.tm_year = 120; // Year 2020 + time.tm_mon = 1; // February + time.tm_mday = 29; // 29th day + + char buffer[100]; + call_strftime(buffer, sizeof(buffer), "%Y-%m-%d", &time); + EXPECT_STREQ(buffer, "2020-02-29"); +} + +TEST(LlvmLibcStrftimeTest, HandlesEndOfYear) { + struct tm time; + time.tm_year = 121; // Year 2021 + time.tm_mon = 11; // December + time.tm_mday = 31; // 31st day + + char buffer[100]; + call_strftime(buffer, sizeof(buffer), "%Y-%m-%d", &time); + EXPECT_STREQ(buffer, "2021-12-31"); +} + +TEST(LlvmLibcStrftimeTest, FormatsTimezoneCorrectly) { + struct tm time; + time.tm_year = 122; // Year 2022 + time.tm_mon = 9; // October + time.tm_mday = 15; + time.tm_hour = 12; + time.tm_min = 0; + time.tm_sec = 0; + time.tm_isdst = -1; // Use system's daylight saving time information + + char buffer[100]; + call_strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S %Z", &time); + EXPECT_STRNE(buffer, ""); +} + +} // namespace LIBC_NAMESPACE_DECL From c4f57285256ca2d385fb4edc33e48c69d52dacd4 Mon Sep 17 00:00:00 2001 From: Tsz Chan Date: Sun, 20 Oct 2024 02:02:27 +0800 Subject: [PATCH 4/4] [libc] Refactor converter.cpp --- libc/src/time/strftime.cpp | 9 +- libc/src/time/strftime_core/CMakeLists.txt | 5 + .../time/strftime_core/composite_converter.h | 124 ++++++ libc/src/time/strftime_core/converter.cpp | 381 ++---------------- libc/src/time/strftime_core/core_structs.h | 17 +- libc/src/time/strftime_core/num_converter.h | 208 ++++++++++ libc/src/time/strftime_core/str_converter.h | 55 +++ libc/src/time/strftime_core/strftime_main.h | 4 + .../time/strftime_core/time_internal_def.h | 14 + libc/src/time/strftime_l.cpp | 9 +- .../src/time/strftime_core/converter_test.cpp | 39 ++ libc/test/src/time/strftime_test.cpp | 23 +- 12 files changed, 522 insertions(+), 366 deletions(-) create mode 100644 libc/src/time/strftime_core/composite_converter.h create mode 100644 libc/src/time/strftime_core/num_converter.h create mode 100644 libc/src/time/strftime_core/str_converter.h diff --git a/libc/src/time/strftime.cpp b/libc/src/time/strftime.cpp index 7c20268cec9a3..5035001e6fb6b 100644 --- a/libc/src/time/strftime.cpp +++ b/libc/src/time/strftime.cpp @@ -19,10 +19,13 @@ namespace LIBC_NAMESPACE_DECL { 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::WriteBuffer wb(buffer, (buffsz > 0 ? buffsz - 1 : 0), + strftime_core::overflow_write_mock, nullptr); printf_core::Writer writer(&wb); - strftime_core::strftime_main(&writer, format, timeptr); - return writer.get_chars_written(); + 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 ? ret : 0; } } // namespace LIBC_NAMESPACE_DECL diff --git a/libc/src/time/strftime_core/CMakeLists.txt b/libc/src/time/strftime_core/CMakeLists.txt index 78f37ea3ef707..79e2eafc6a98d 100644 --- a/libc/src/time/strftime_core/CMakeLists.txt +++ b/libc/src/time/strftime_core/CMakeLists.txt @@ -24,9 +24,14 @@ add_object_library( converter.cpp HDRS converter.h + num_converter.h + str_converter.h + composite_converter.h DEPENDS .core_structs + libc.src.__support.arg_list libc.src.stdio.printf_core.writer + libc.src.stdio.printf_core.printf_main libc.src.__support.big_int libc.src.__support.CPP.string_view libc.src.__support.float_to_string diff --git a/libc/src/time/strftime_core/composite_converter.h b/libc/src/time/strftime_core/composite_converter.h new file mode 100644 index 0000000000000..cf317988de075 --- /dev/null +++ b/libc/src/time/strftime_core/composite_converter.h @@ -0,0 +1,124 @@ +//===-- 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_COMPOSITE_CONVERTER_H +#define LLVM_LIBC_SRC_STDIO_STRFTIME_CORE_COMPOSITE_CONVERTER_H + +#include "src/__support/CPP/string_view.h" +#include "src/__support/arg_list.h" +#include "src/__support/integer_to_string.h" +#include "src/__support/macros/config.h" +#include "src/stdio/printf_core/printf_main.h" +#include "src/stdio/printf_core/writer.h" +#include "src/time/strftime_core/core_structs.h" +#include "src/time/strftime_core/time_internal_def.h" + +namespace LIBC_NAMESPACE_DECL { +namespace strftime_core { + +namespace details { +int snprintf_impl(char *__restrict buffer, size_t buffsz, + const char *__restrict format, ...) { + va_list vlist; + va_start(vlist, format); + internal::ArgList args(vlist); + va_end(vlist); + printf_core::WriteBuffer wb(buffer, (buffsz > 0 ? buffsz - 1 : 0)); + printf_core::Writer writer(&wb); + + int ret_val = printf_core::printf_main(&writer, format, args); + if (buffsz > 0) + wb.buff[wb.buff_cur] = '\0'; + return ret_val; +} +} // namespace details + +int write_composite(printf_core::Writer *writer, const FormatSection &to_conv) { + char buffer[100]; + auto &time = *to_conv.time; + + switch (to_conv.conv_name) { + // Full date and time representation (e.g., equivalent to %a %b %e %T %Y) + case 'c': { + RET_IF_RESULT_NEGATIVE(details::snprintf_impl( + buffer, sizeof(buffer), "%s %s %02d %02d:%02d:%02d %d", + safe_abbreviated_day_name(time.tm_wday), + safe_abbreviated_month_name(time.tm_mon), time.tm_mday, time.tm_hour, + time.tm_min, time.tm_sec, time.tm_year + 1900)); + break; + } + + // Zero-padded day of the month (equivalent to %m/%d/%y) + case 'D': { + RET_IF_RESULT_NEGATIVE(details::snprintf_impl( + buffer, sizeof(buffer), "%02d/%02d/%02d", time.tm_mon + 1, time.tm_mday, + (time.tm_year + 1900) % 100)); + break; + } + + // ISO 8601 date representation in YYYY-MM-DD (equivalent to %Y-%m-%d) + case 'F': { + RET_IF_RESULT_NEGATIVE(details::snprintf_impl( + buffer, sizeof(buffer), "%04d-%02d-%02d", time.tm_year + 1900, + time.tm_mon + 1, time.tm_mday)); + break; + } + + // 12-hour clock time with seconds and AM/PM (equivalent to %I:%M:%S %p) + case 'r': { + int hour12 = time.tm_hour % 12; + if (hour12 == 0) + hour12 = 12; + RET_IF_RESULT_NEGATIVE(details::snprintf_impl( + buffer, sizeof(buffer), "%02d:%02d:%02d %s", hour12, time.tm_min, + time.tm_sec, + to_conv.time->tm_hour >= 12 ? default_PM_str : default_AM_str)); + break; + } + + // 24-hour time without seconds (equivalent to %H:%M) + case 'R': { + RET_IF_RESULT_NEGATIVE(details::snprintf_impl( + buffer, sizeof(buffer), "%02d:%02d", time.tm_hour, time.tm_min)); + break; + } + + // Time with seconds (equivalent to %H:%M:%S) + case 'T': { + RET_IF_RESULT_NEGATIVE( + details::snprintf_impl(buffer, sizeof(buffer), "%02d:%02d:%02d", + time.tm_hour, time.tm_min, time.tm_sec)); + break; + } + + // Locale's date representation (often equivalent to %m/%d/%y) + case 'x': { + RET_IF_RESULT_NEGATIVE(details::snprintf_impl( + buffer, sizeof(buffer), "%02d/%02d/%02d", time.tm_mon + 1, time.tm_mday, + (time.tm_year + 1900) % 100)); + break; + } + + // Locale's time representation (equivalent to %H:%M:%S) + case 'X': { + RET_IF_RESULT_NEGATIVE( + details::snprintf_impl(buffer, sizeof(buffer), "%02d:%02d:%02d", + time.tm_hour, time.tm_min, time.tm_sec)); + break; + } + + default: + return writer->write(to_conv.raw_string); + } + return writer->write(buffer); +} + +} // namespace strftime_core +} // namespace LIBC_NAMESPACE_DECL + +#endif diff --git a/libc/src/time/strftime_core/converter.cpp b/libc/src/time/strftime_core/converter.cpp index d7b3e57f19ce8..a9bbf6ceca63c 100644 --- a/libc/src/time/strftime_core/converter.cpp +++ b/libc/src/time/strftime_core/converter.cpp @@ -9,372 +9,61 @@ #ifndef LLVM_LIBC_SRC_STDIO_STRFTIME_CORE_CONVERTER_H #define LLVM_LIBC_SRC_STDIO_STRFTIME_CORE_CONVERTER_H -#include "src/__support/CPP/string.h" +#include "composite_converter.h" +#include "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/strftime_core/time_internal_def.h" +#include "str_converter.h" namespace LIBC_NAMESPACE_DECL { namespace strftime_core { -#define RET_IF_RESULT_NEGATIVE(func) \ - { \ - int result = (func); \ - if (result < 0) \ - return result; \ - } - -namespace details { - -LIBC_INLINE cpp::optional -num_to_strview(uintmax_t num, cpp::span bufref) { - return IntegerToString::format_to(bufref, num); -} - -template -LIBC_INLINE int write_num(uintmax_t num, printf_core::Writer *writer) { - cpp::array buf; - return writer->write(*num_to_strview(num, buf)); -} - -template int count_digits(T num) { - if (num == 0) - return 1; - int digits = 0; - while (num > 0) { - num /= 10; - digits++; - } - return digits; -} - -template -LIBC_INLINE int write_num_with_padding(uintmax_t num, - printf_core::Writer *writer) { - cpp::array buf; - int digits = count_digits(num); - int padding_needed = width - digits; - - for (int _ = 0; _ < padding_needed; _++) { - RET_IF_RESULT_NEGATIVE(writer->write(padding)); - } - - return writer->write(*num_to_strview(num, buf)); -} - -} // namespace details - -/* Nonzero if YEAR is a leap year (every 4 years, - except every 100th isn't, and every 400th is). */ -LIBC_INLINE bool is_leap(int year) { - return ((year % 4 == 0) && (year % 100 != 0 || year % 400 == 0)); -} - -// Conversion functions - -LIBC_INLINE int convert_weekday(printf_core::Writer *writer, - const FormatSection &to_conv) { - return writer->write(safe_day_name(to_conv.time->tm_wday)); -} - -LIBC_INLINE int convert_abbreviated_weekday(printf_core::Writer *writer, - const FormatSection &to_conv) { - return writer->write(safe_abbreviated_day_name(to_conv.time->tm_wday)); -} - -LIBC_INLINE int convert_zero_padded_day_of_year(printf_core::Writer *writer, - const FormatSection &to_conv) { - return details::write_num_with_padding<3, '0'>(to_conv.time->tm_yday + 1, - writer); -} - -LIBC_INLINE int convert_zero_padded_day_of_month(printf_core::Writer *writer, - const FormatSection &to_conv) { - return details::write_num_with_padding<2, '0'>(to_conv.time->tm_mday, writer); -} - -LIBC_INLINE int -convert_space_padded_day_of_month(printf_core::Writer *writer, - const FormatSection &to_conv) { - return details::write_num_with_padding<2, ' '>(to_conv.time->tm_mday, writer); -} - -LIBC_INLINE int convert_decimal_weekday(printf_core::Writer *writer, - const FormatSection &to_conv) { - return details::write_num<1>( - to_conv.time->tm_wday == 0 ? 7 : to_conv.time->tm_wday, writer); -} - -LIBC_INLINE int convert_decimal_weekday_iso(printf_core::Writer *writer, - const FormatSection &to_conv) { - return details::write_num<1>(to_conv.time->tm_wday, writer); -} - -LIBC_INLINE int convert_week_number_sunday(printf_core::Writer *writer, - const FormatSection &to_conv) { - int wday = to_conv.time->tm_wday; - int yday = to_conv.time->tm_yday; - int week = (yday - wday + 7) / 7; - return details::write_num_with_padding<2, '0'>(week, writer); -} - -LIBC_INLINE int convert_week_number_monday(printf_core::Writer *writer, - const FormatSection &to_conv) { - int wday = (to_conv.time->tm_wday + 6) % 7; - int yday = to_conv.time->tm_yday; - int week = (yday - wday + 7) / 7; - return details::write_num_with_padding<2, '0'>(week, writer); -} - -LIBC_INLINE int convert_full_month(printf_core::Writer *writer, - const FormatSection &to_conv) { - return writer->write(safe_month_name(to_conv.time->tm_mon)); -} - -LIBC_INLINE int convert_abbreviated_month(printf_core::Writer *writer, - const FormatSection &to_conv) { - return writer->write(safe_abbreviated_month_name(to_conv.time->tm_mon)); -} - -LIBC_INLINE int convert_zero_padded_month(printf_core::Writer *writer, - const FormatSection &to_conv) { - return details::write_num_with_padding<2, '0'>(to_conv.time->tm_mon + 1, - writer); -} - -LIBC_INLINE int convert_full_year(printf_core::Writer *writer, - const FormatSection &to_conv) { - return details::write_num_with_padding<4, '0'>(to_conv.time->tm_year + 1900, - writer); -} - -LIBC_INLINE int convert_two_digit_year(printf_core::Writer *writer, - const FormatSection &to_conv) { - return details::write_num_with_padding<2, '0'>( - (to_conv.time->tm_year + 1900) % 100, writer); -} - -LIBC_INLINE int convert_century(printf_core::Writer *writer, - const FormatSection &to_conv) { - return details::write_num_with_padding<2, '0'>( - (to_conv.time->tm_year + 1900) / 100, writer); -} - -LIBC_INLINE int convert_hour_24(printf_core::Writer *writer, - const FormatSection &to_conv) { - return details::write_num_with_padding<2, '0'>(to_conv.time->tm_hour, writer); -} - -LIBC_INLINE int convert_pm(printf_core::Writer *writer, - const FormatSection &to_conv) { - static const cpp::string_view AM = "AM"; - static const cpp::string_view PM = "PM"; - return writer->write(to_conv.time->tm_hour >= 12 ? PM : AM); -} - -LIBC_INLINE int convert_hour_12(printf_core::Writer *writer, - const FormatSection &to_conv) { - int hour = to_conv.time->tm_hour % 12; - if (hour == 0) - hour = 12; - return details::write_num_with_padding<2, '0'>(hour, writer); -} - -LIBC_INLINE int convert_minute(printf_core::Writer *writer, - const FormatSection &to_conv) { - return details::write_num_with_padding<2, '0'>(to_conv.time->tm_min, writer); -} - -LIBC_INLINE int convert_second(printf_core::Writer *writer, - const FormatSection &to_conv) { - return details::write_num_with_padding<2, '0'>(to_conv.time->tm_sec, writer); -} - -LIBC_INLINE int convert_MM_DD_YY(printf_core::Writer *writer, - const FormatSection &to_conv) { - RET_IF_RESULT_NEGATIVE(convert_zero_padded_month(writer, to_conv)); - RET_IF_RESULT_NEGATIVE(writer->write("/")); - RET_IF_RESULT_NEGATIVE(convert_zero_padded_day_of_month(writer, to_conv)); - RET_IF_RESULT_NEGATIVE(writer->write("/")); - return convert_two_digit_year(writer, to_conv); -} - -LIBC_INLINE int convert_YYYY_MM_DD(printf_core::Writer *writer, - const FormatSection &to_conv) { - RET_IF_RESULT_NEGATIVE(convert_full_year(writer, to_conv)); - RET_IF_RESULT_NEGATIVE(writer->write("-")); - RET_IF_RESULT_NEGATIVE(convert_zero_padded_month(writer, to_conv)); - RET_IF_RESULT_NEGATIVE(writer->write("-")); - return convert_zero_padded_day_of_month(writer, to_conv); -} - -LIBC_INLINE int convert_hour_minute(printf_core::Writer *writer, - const FormatSection &to_conv) { - RET_IF_RESULT_NEGATIVE(convert_hour_24(writer, to_conv)); - RET_IF_RESULT_NEGATIVE(writer->write(":")); - return convert_minute(writer, to_conv); -} - -LIBC_INLINE int convert_date_time(printf_core::Writer *writer, - const FormatSection &to_conv) { - RET_IF_RESULT_NEGATIVE(convert_abbreviated_weekday(writer, to_conv)); - RET_IF_RESULT_NEGATIVE(writer->write(" ")); - RET_IF_RESULT_NEGATIVE(convert_abbreviated_month(writer, to_conv)); - RET_IF_RESULT_NEGATIVE(writer->write(" ")); - RET_IF_RESULT_NEGATIVE(convert_space_padded_day_of_month(writer, to_conv)); - RET_IF_RESULT_NEGATIVE(writer->write(" ")); - RET_IF_RESULT_NEGATIVE(convert_full_year(writer, to_conv)); - RET_IF_RESULT_NEGATIVE(writer->write(" ")); - RET_IF_RESULT_NEGATIVE(convert_hour_24(writer, to_conv)); - RET_IF_RESULT_NEGATIVE(writer->write(":")); - RET_IF_RESULT_NEGATIVE(convert_minute(writer, to_conv)); - RET_IF_RESULT_NEGATIVE(writer->write(":")); - return convert_second(writer, to_conv); -} - -LIBC_INLINE int convert_time_zone(printf_core::Writer *writer) { - return writer->write("UTC"); -} - -LIBC_INLINE int convert_time_zone_offset(printf_core::Writer *writer) { - return writer->write("+0000"); -} - -static int iso_week_days(int yday, int wday) { - /* Add enough to the first operand of % to make it nonnegative. */ - int big_enough_multiple_of_7 = (-YDAY_MINIMUM / 7 + 2) * 7; - return (yday - (yday - wday + ISO_WEEK1_WDAY + big_enough_multiple_of_7) % 7 + - ISO_WEEK1_WDAY - ISO_WEEK_START_WDAY); -} - -LIBC_INLINE int convert_iso_year(printf_core::Writer *writer, - const FormatSection &to_conv) { - int year = to_conv.time->tm_year + YEAR_BASE; - int days = iso_week_days(to_conv.time->tm_yday, to_conv.time->tm_wday); - - if (days < 0) { - /* This ISO week belongs to the previous year. */ - year--; - days = iso_week_days(to_conv.time->tm_yday + (365 + is_leap(year)), - to_conv.time->tm_wday); - } else { - int d = iso_week_days(to_conv.time->tm_yday - (365 + is_leap(year)), - to_conv.time->tm_wday); - if (0 <= d) { - /* This ISO week belongs to the next year. */ - year++; - days = d; - } - } - - return details::write_num<4>(year, writer); -} - -LIBC_INLINE int convert_iso_day(printf_core::Writer *writer, - const FormatSection &to_conv) { - int year = to_conv.time->tm_year + YEAR_BASE; - int days = iso_week_days(to_conv.time->tm_yday, to_conv.time->tm_wday); - - if (days < 0) { - /* This ISO week belongs to the previous year. */ - year--; - days = iso_week_days(to_conv.time->tm_yday + (365 + is_leap(year)), - to_conv.time->tm_wday); - } else { - int d = iso_week_days(to_conv.time->tm_yday - (365 + is_leap(year)), - to_conv.time->tm_wday); - if (0 <= d) { - /* This ISO week belongs to the next year. */ - year++; - days = d; - } - } - return details::write_num_with_padding<2, '0'>(days / 7 + 1, writer); -} - int convert(printf_core::Writer *writer, const FormatSection &to_conv) { if (!to_conv.has_conv) return writer->write(to_conv.raw_string); switch (to_conv.conv_name) { case '%': return writer->write("%"); - // day of the week - case 'A': - return convert_weekday(writer, to_conv); - case 'a': - return convert_abbreviated_weekday(writer, to_conv); - case 'w': - return convert_decimal_weekday(writer, to_conv); - case 'u': - return convert_decimal_weekday_iso(writer, to_conv); - // day of the year/ month - case 'j': - return convert_zero_padded_day_of_year(writer, to_conv); - case 'd': - return convert_zero_padded_day_of_month(writer, to_conv); - case 'e': - return convert_space_padded_day_of_month(writer, to_conv); - // week - case 'U': - return convert_week_number_sunday(writer, to_conv); - case 'W': - return convert_week_number_monday(writer, to_conv); - case 'V': - return convert_iso_day(writer, to_conv); - // month - case 'B': - return convert_full_month(writer, to_conv); - case 'b': - case 'h': - return convert_abbreviated_month(writer, to_conv); - case 'm': - return convert_zero_padded_month(writer, to_conv); - // year - case 'Y': - return convert_full_year(writer, to_conv); - case 'y': - return convert_two_digit_year(writer, to_conv); - case 'C': - return convert_century(writer, to_conv); - case 'G': - return convert_iso_year(writer, to_conv); - // hours - case 'p': - return convert_pm(writer, to_conv); - case 'H': - return convert_hour_24(writer, to_conv); - case 'I': - return convert_hour_12(writer, to_conv); - // minutes - case 'M': - return convert_minute(writer, to_conv); - // seconds - case 'S': - return convert_second(writer, to_conv); - // Date and Time + case 'C': // Century (C) + case 'Y': // Full year (Y) + case 'y': // Two-digit year (y) + case 'j': // Day of the year (j) + case 'm': // Month (m) + case 'd': // Day of the month (d) + case 'e': // Day of the month (e) + case 'H': // 24-hour format (H) + case 'I': // 12-hour format (I) + case 'M': // Minute (M) + case 'S': // Second (S) + case 'U': // Week number starting on Sunday (U) + case 'W': // Week number starting on Monday (W) + case 'V': // ISO week number (V) + case 'G': // ISO year (G) + case 'w': // Decimal weekday (w) + case 'u': // ISO weekday (u) + return write_num(writer, to_conv); + case 'a': // Abbreviated weekday name (a) + case 'A': // Full weekday name (A) + case 'b': // Abbreviated month name (b) + case 'B': // Full month name (B) + case 'p': // AM/PM designation (p) + case 'z': // Timezone offset (z) + case 'Z': // Timezone name (Z) + return write_str(writer, to_conv); case 'c': - return convert_date_time(writer, to_conv); - - // Timezone - case 'Z': - return convert_time_zone(writer); - case 'z': - return convert_time_zone_offset(writer); - - // Custom formats: MM/DD/YY, YYYY-MM-DD - case 'D': - return convert_MM_DD_YY(writer, to_conv); // Equivalent to %m/%d/%y case 'F': - return convert_YYYY_MM_DD(writer, to_conv); // Equivalent to %Y-%m-%d - - // 24-hour time without seconds (HH:MM) + case 'r': case 'R': - return convert_hour_minute(writer, to_conv); // Equivalent to %H:%M + case 'T': + case 'x': + case 'X': + return write_composite(writer, to_conv); default: - writer->write(to_conv.raw_string); + return writer->write(to_conv.raw_string); } return 0; } diff --git a/libc/src/time/strftime_core/core_structs.h b/libc/src/time/strftime_core/core_structs.h index d6cd91884b8a6..5c01638f132da 100644 --- a/libc/src/time/strftime_core/core_structs.h +++ b/libc/src/time/strftime_core/core_structs.h @@ -26,16 +26,13 @@ struct FormatSection { char padding; }; -// This is the value to be returned by conversions when no error has occurred. -constexpr int WRITE_OK = 0; -// These are the printf return values for when an error has occurred. They are -// all negative, and should be distinct. -constexpr int FILE_WRITE_ERROR = -1; -constexpr int FILE_STATUS_ERROR = -2; -constexpr int NULLPTR_WRITE_ERROR = -3; -constexpr int INT_CONVERSION_ERROR = -4; -constexpr int FIXED_POINT_CONVERSION_ERROR = -5; -constexpr int ALLOCATION_ERROR = -6; +#define RET_IF_RESULT_NEGATIVE(func) \ + { \ + int result = (func); \ + if (result < 0) \ + return result; \ + } + } // namespace strftime_core } // namespace LIBC_NAMESPACE_DECL diff --git a/libc/src/time/strftime_core/num_converter.h b/libc/src/time/strftime_core/num_converter.h new file mode 100644 index 0000000000000..baad78a281b65 --- /dev/null +++ b/libc/src/time/strftime_core/num_converter.h @@ -0,0 +1,208 @@ +//===-- 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/strftime_core/time_internal_def.h" + +namespace LIBC_NAMESPACE_DECL { +namespace strftime_core { + +namespace details { + +LIBC_INLINE cpp::optional +num_to_strview(uintmax_t num, cpp::span bufref) { + return IntegerToString::format_to(bufref, num); +} + +template int count_digits(T num) { + if (num == 0) + return 1; + int digits = 0; + while (num > 0) { + num /= 10; + digits++; + } + return digits; +} + +LIBC_INLINE int write_num_with_padding(int width, char padding, uintmax_t num, + printf_core::Writer *writer) { + cpp::array::buffer_size()> buf; + int digits = count_digits(num); + int padding_needed = width - digits; + + for (int _ = 0; _ < padding_needed; _++) { + RET_IF_RESULT_NEGATIVE(writer->write(padding)); + } + + return writer->write(*num_to_strview(num, buf)); +} + +} // namespace details + +namespace iso { + +/* Nonzero if YEAR is a leap year (every 4 years, + except every 100th isn't, and every 400th is). */ +LIBC_INLINE bool is_leap(int year) { + return ((year % 4 == 0) && (year % 100 != 0 || year % 400 == 0)); +} + +static int iso_week_days(int yday, int wday) { + /* Add enough to the first operand of % to make it nonnegative. */ + int big_enough_multiple_of_7 = (-YDAY_MINIMUM / 7 + 2) * 7; + return (yday - (yday - wday + ISO_WEEK1_WDAY + big_enough_multiple_of_7) % 7 + + ISO_WEEK1_WDAY - ISO_WEEK_START_WDAY); +} + +enum class IsoData { + GET_DATE, + GET_YEAR, +}; + +template +LIBC_INLINE int convert_iso(const FormatSection &to_conv) { + int year = to_conv.time->tm_year + YEAR_BASE; + int days = iso_week_days(to_conv.time->tm_yday, to_conv.time->tm_wday); + + if (days < 0) { + /* This ISO week belongs to the previous year. */ + year--; + days = iso_week_days(to_conv.time->tm_yday + (365 + is_leap(year)), + to_conv.time->tm_wday); + } else { + int d = iso_week_days(to_conv.time->tm_yday - (365 + is_leap(year)), + to_conv.time->tm_wday); + if (0 <= d) { + /* This ISO week belongs to the next year. */ + year++; + days = d; + } + } + + if constexpr (get_date_or_year == IsoData::GET_YEAR) { + return year; + } else { + return days / 7 + 1; + } +} +} // namespace iso + +int write_num(printf_core::Writer *writer, const FormatSection &to_conv) { + int num = 0; + auto &time = *to_conv.time; + + // Handle numeric conversions based on the format specifier (conv_name) + switch (to_conv.conv_name) { + // Century (C) - the first two digits of the year + case 'C': + num = (time.tm_year + 1900) / 100; + break; + + // Full year (Y) - the full four-digit year + case 'Y': + num = time.tm_year + 1900; + break; + + // Two-digit year (y) - the last two digits of the year + case 'y': + num = (time.tm_year + 1900) % 100; + break; + + // Day of the year (j) - the day number within the year (1-366) + case 'j': + num = time.tm_yday + 1; + break; + + // Zero-padded month (m) - month as a zero-padded number (01-12) + case 'm': + num = time.tm_mon + 1; + break; + + // Day of the month (d) - zero-padded day of the month (01-31) + case 'd': + case 'e': + num = time.tm_mday; + break; + + // 24-hour format (H) - zero-padded hour (00-23) + case 'H': + num = time.tm_hour; + break; + + // 12-hour format (I) - zero-padded hour (01-12) + case 'I': + num = time.tm_hour % 12; + if (num == 0) + num = 12; // Convert 0 to 12 for 12-hour format + break; + + // Minute (M) - zero-padded minute (00-59) + case 'M': + num = time.tm_min; + break; + + // Second (S) - zero-padded second (00-59) + case 'S': + num = time.tm_sec; + break; + + // Week number starting on Sunday (U) - week number of the year (Sunday as the + // start of the week) + case 'U': { + int wday = time.tm_wday; + num = (time.tm_yday - wday + 7) / 7; + break; + } + + // Week number starting on Monday (W) - week number of the year (Monday as the + // start of the week) + case 'W': { + int wday = (time.tm_wday + 6) % 7; // Adjust to Monday as the first day + num = (time.tm_yday - wday + 7) / 7; + break; + } + + // ISO week day (V) - week number following ISO 8601 + case 'V': + num = iso::convert_iso(to_conv); + break; + + case 'G': + num = iso::convert_iso(to_conv); + break; + // Decimal weekday (w) - day of the week (Sunday = 0, Monday = 1, etc.) + case 'w': + num = time.tm_wday; + break; + + // ISO weekday (u) - day of the week (Monday = 1, Sunday = 7) + case 'u': + num = (time.tm_wday == 0) ? 7 : time.tm_wday; + break; + + default: + return writer->write( + to_conv.raw_string); // Default: write raw string if no match + } + + return details::write_num_with_padding(to_conv.min_width, to_conv.padding, + num, writer); +} + +} // namespace strftime_core +} // namespace LIBC_NAMESPACE_DECL + +#endif diff --git a/libc/src/time/strftime_core/str_converter.h b/libc/src/time/strftime_core/str_converter.h new file mode 100644 index 0000000000000..1221650b3f2ab --- /dev/null +++ b/libc/src/time/strftime_core/str_converter.h @@ -0,0 +1,55 @@ +//===-- 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_STR_CONVERTER_H +#define LLVM_LIBC_SRC_STDIO_STRFTIME_CORE_STR_CONVERTER_H + +#include "src/__support/CPP/string_view.h" +#include "src/__support/integer_to_string.h" +#include "src/stdio/printf_core/writer.h" +#include "src/time/strftime_core/core_structs.h" +#include "src/time/strftime_core/time_internal_def.h" + +namespace LIBC_NAMESPACE_DECL { +namespace strftime_core { + +int write_str(printf_core::Writer *writer, const FormatSection &to_conv) { + cpp::string_view str; + auto &time = *to_conv.time; + switch (to_conv.conv_name) { + case 'a': + str = safe_abbreviated_day_name(time.tm_wday); + break; + case 'A': + str = safe_day_name(time.tm_wday); + break; + case 'b': + str = safe_abbreviated_month_name(time.tm_mon); + break; + case 'B': + str = safe_month_name(time.tm_mon); + break; + case 'p': + str = to_conv.time->tm_hour >= 12 ? default_PM_str : default_AM_str; + break; + case 'z': + str = default_timezone_offset; + break; + case 'Z': + str = default_timezone_name; + break; + default: + return writer->write(to_conv.raw_string); + } + return writer->write(str); +} + +} // namespace strftime_core +} // namespace LIBC_NAMESPACE_DECL + +#endif diff --git a/libc/src/time/strftime_core/strftime_main.h b/libc/src/time/strftime_core/strftime_main.h index fa1d0ad1a4e2d..eaa6d63bf61b2 100644 --- a/libc/src/time/strftime_core/strftime_main.h +++ b/libc/src/time/strftime_core/strftime_main.h @@ -19,6 +19,10 @@ namespace LIBC_NAMESPACE_DECL { namespace strftime_core { +// Passed to writeBuffer so error is returned if there is not enough buffer +// space. +LIBC_INLINE int overflow_write_mock(cpp::string_view, void *) { return -1; } + int strftime_main(printf_core::Writer *writer, const char *__restrict str, const struct tm *timeptr); diff --git a/libc/src/time/strftime_core/time_internal_def.h b/libc/src/time/strftime_core/time_internal_def.h index 198897ef9186b..66fd02f922978 100644 --- a/libc/src/time/strftime_core/time_internal_def.h +++ b/libc/src/time/strftime_core/time_internal_def.h @@ -6,6 +6,9 @@ // //===----------------------------------------------------------------------===// +#ifndef LLVM_LIBC_SRC_STDIO_STRFTIME_CORE_TIME_DEF_H +#define LLVM_LIBC_SRC_STDIO_STRFTIME_CORE_TIME_DEF_H + #include "src/__support/CPP/array.h" #include "src/__support/CPP/string_view.h" @@ -59,5 +62,16 @@ LIBC_INLINE cpp::string_view safe_abbreviated_month_name(int month) { : abbreviated_month_names[month]; } +static constexpr cpp::string_view default_timezone_name = "UTC"; + +// TODO +static constexpr cpp::string_view default_timezone_offset = "+0000"; + +static constexpr cpp::string_view default_PM_str = "PM"; + +static constexpr cpp::string_view default_AM_str = "AM"; + } // namespace strftime_core } // namespace LIBC_NAMESPACE_DECL + +#endif diff --git a/libc/src/time/strftime_l.cpp b/libc/src/time/strftime_l.cpp index a6dc28dd62e37..3463d950bca19 100644 --- a/libc/src/time/strftime_l.cpp +++ b/libc/src/time/strftime_l.cpp @@ -20,10 +20,13 @@ LLVM_LIBC_FUNCTION(size_t, strftime_l, (char *__restrict buffer, size_t buffsz, const char *__restrict format, const struct tm *timeptr, locale_t)) { - printf_core::WriteBuffer wb(buffer, (buffsz > 0 ? buffsz - 1 : 0)); + printf_core::WriteBuffer wb(buffer, (buffsz > 0 ? buffsz - 1 : 0), + strftime_core::overflow_write_mock, nullptr); printf_core::Writer writer(&wb); - strftime_core::strftime_main(&writer, format, timeptr); - return writer.get_chars_written(); + 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 ? ret : 0; } } // namespace LIBC_NAMESPACE_DECL diff --git a/libc/test/src/time/strftime_core/converter_test.cpp b/libc/test/src/time/strftime_core/converter_test.cpp index 9be44ef39fce7..7c4820bbd208b 100644 --- a/libc/test/src/time/strftime_core/converter_test.cpp +++ b/libc/test/src/time/strftime_core/converter_test.cpp @@ -9,9 +9,12 @@ #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 "test/UnitTest/Test.h" #include +using namespace LIBC_NAMESPACE; + class LlvmLibcStrftimeConverterTest : public LIBC_NAMESPACE::testing::Test { protected: // void SetUp() override {} @@ -59,6 +62,8 @@ TEST_F(LlvmLibcStrftimeConverterTest, WeekdayConversion) { simple_conv.raw_string = "%a"; simple_conv.conv_name = 'a'; simple_conv.time = &time; + simple_conv.min_width = strftime_core::min_width(simple_conv.conv_name); + simple_conv.padding = strftime_core::get_padding(simple_conv.conv_name); LIBC_NAMESPACE::strftime_core::convert(&writer, simple_conv); @@ -75,6 +80,8 @@ TEST_F(LlvmLibcStrftimeConverterTest, AbbreviatedMonthNameConversion) { simple_conv.raw_string = "%b"; simple_conv.conv_name = 'b'; simple_conv.time = &time; + simple_conv.min_width = strftime_core::min_width(simple_conv.conv_name); + simple_conv.padding = strftime_core::get_padding(simple_conv.conv_name); LIBC_NAMESPACE::strftime_core::convert(&writer, simple_conv); @@ -92,6 +99,8 @@ TEST_F(LlvmLibcStrftimeConverterTest, CenturyConversion) { simple_conv.raw_string = "%C"; simple_conv.conv_name = 'C'; simple_conv.time = &time; + simple_conv.min_width = strftime_core::min_width(simple_conv.conv_name); + simple_conv.padding = strftime_core::get_padding(simple_conv.conv_name); LIBC_NAMESPACE::strftime_core::convert(&writer, simple_conv); @@ -109,6 +118,8 @@ TEST_F(LlvmLibcStrftimeConverterTest, DayOfMonthZeroPaddedConversion) { simple_conv.raw_string = "%d"; simple_conv.conv_name = 'd'; simple_conv.time = &time; + simple_conv.min_width = strftime_core::min_width(simple_conv.conv_name); + simple_conv.padding = strftime_core::get_padding(simple_conv.conv_name); LIBC_NAMESPACE::strftime_core::convert(&writer, simple_conv); @@ -126,6 +137,8 @@ TEST_F(LlvmLibcStrftimeConverterTest, DayOfMonthSpacePaddedConversion) { simple_conv.raw_string = "%e"; simple_conv.conv_name = 'e'; simple_conv.time = &time; + simple_conv.min_width = strftime_core::min_width(simple_conv.conv_name); + simple_conv.padding = strftime_core::get_padding(simple_conv.conv_name); LIBC_NAMESPACE::strftime_core::convert(&writer, simple_conv); @@ -143,6 +156,8 @@ TEST_F(LlvmLibcStrftimeConverterTest, FullMonthNameConversion) { simple_conv.raw_string = "%B"; simple_conv.conv_name = 'B'; simple_conv.time = &time; + simple_conv.min_width = strftime_core::min_width(simple_conv.conv_name); + simple_conv.padding = strftime_core::get_padding(simple_conv.conv_name); LIBC_NAMESPACE::strftime_core::convert(&writer, simple_conv); @@ -160,6 +175,8 @@ TEST_F(LlvmLibcStrftimeConverterTest, Hour12Conversion) { simple_conv.raw_string = "%I"; simple_conv.conv_name = 'I'; simple_conv.time = &time; + simple_conv.min_width = strftime_core::min_width(simple_conv.conv_name); + simple_conv.padding = strftime_core::get_padding(simple_conv.conv_name); LIBC_NAMESPACE::strftime_core::convert(&writer, simple_conv); @@ -177,6 +194,8 @@ TEST_F(LlvmLibcStrftimeConverterTest, Hour24PaddedConversion) { simple_conv.raw_string = "%H"; simple_conv.conv_name = 'H'; simple_conv.time = &time; + simple_conv.min_width = strftime_core::min_width(simple_conv.conv_name); + simple_conv.padding = strftime_core::get_padding(simple_conv.conv_name); LIBC_NAMESPACE::strftime_core::convert(&writer, simple_conv); @@ -194,6 +213,8 @@ TEST_F(LlvmLibcStrftimeConverterTest, MinuteConversion) { simple_conv.raw_string = "%M"; simple_conv.conv_name = 'M'; simple_conv.time = &time; + simple_conv.min_width = strftime_core::min_width(simple_conv.conv_name); + simple_conv.padding = strftime_core::get_padding(simple_conv.conv_name); LIBC_NAMESPACE::strftime_core::convert(&writer, simple_conv); @@ -211,6 +232,8 @@ TEST_F(LlvmLibcStrftimeConverterTest, AMPMConversion) { simple_conv.raw_string = "%p"; simple_conv.conv_name = 'p'; simple_conv.time = &time; + simple_conv.min_width = strftime_core::min_width(simple_conv.conv_name); + simple_conv.padding = strftime_core::get_padding(simple_conv.conv_name); LIBC_NAMESPACE::strftime_core::convert(&writer, simple_conv); @@ -228,6 +251,8 @@ TEST_F(LlvmLibcStrftimeConverterTest, SecondsConversion) { simple_conv.raw_string = "%S"; simple_conv.conv_name = 'S'; simple_conv.time = &time; + simple_conv.min_width = strftime_core::min_width(simple_conv.conv_name); + simple_conv.padding = strftime_core::get_padding(simple_conv.conv_name); LIBC_NAMESPACE::strftime_core::convert(&writer, simple_conv); @@ -245,6 +270,8 @@ TEST_F(LlvmLibcStrftimeConverterTest, FullYearConversion) { simple_conv.raw_string = "%Y"; simple_conv.conv_name = 'Y'; simple_conv.time = &time; + simple_conv.min_width = strftime_core::min_width(simple_conv.conv_name); + simple_conv.padding = strftime_core::get_padding(simple_conv.conv_name); LIBC_NAMESPACE::strftime_core::convert(&writer, simple_conv); @@ -262,6 +289,8 @@ TEST_F(LlvmLibcStrftimeConverterTest, TwoDigitYearConversion) { simple_conv.raw_string = "%y"; simple_conv.conv_name = 'y'; simple_conv.time = &time; + simple_conv.min_width = strftime_core::min_width(simple_conv.conv_name); + simple_conv.padding = strftime_core::get_padding(simple_conv.conv_name); LIBC_NAMESPACE::strftime_core::convert(&writer, simple_conv); @@ -334,6 +363,8 @@ TEST_F(LlvmLibcStrftimeConverterTest, WeekNumberSundayFirstDayConversion) { simple_conv.raw_string = "%U"; // Week number (Sunday is first day of week) simple_conv.conv_name = 'U'; simple_conv.time = &time; + simple_conv.min_width = strftime_core::min_width(simple_conv.conv_name); + simple_conv.padding = strftime_core::get_padding(simple_conv.conv_name); LIBC_NAMESPACE::strftime_core::convert(&writer, simple_conv); @@ -359,6 +390,8 @@ TEST_F(LlvmLibcStrftimeConverterTest, WeekNumberMondayFirstDayConversion) { simple_conv.raw_string = "%W"; // Week number (Monday is first day of week) simple_conv.conv_name = 'W'; simple_conv.time = &time; + simple_conv.min_width = strftime_core::min_width(simple_conv.conv_name); + simple_conv.padding = strftime_core::get_padding(simple_conv.conv_name); LIBC_NAMESPACE::strftime_core::convert(&writer, simple_conv); @@ -384,6 +417,8 @@ TEST_F(LlvmLibcStrftimeConverterTest, ISO8601WeekNumberConversion) { simple_conv.raw_string = "%V"; // ISO 8601 week number simple_conv.conv_name = 'V'; simple_conv.time = &time; + simple_conv.min_width = strftime_core::min_width(simple_conv.conv_name); + simple_conv.padding = strftime_core::get_padding(simple_conv.conv_name); LIBC_NAMESPACE::strftime_core::convert(&writer, simple_conv); @@ -408,6 +443,8 @@ TEST_F(LlvmLibcStrftimeConverterTest, DayOfYearConversion) { simple_conv.raw_string = "%j"; // Day of the year simple_conv.conv_name = 'j'; simple_conv.time = &time; + simple_conv.min_width = strftime_core::min_width(simple_conv.conv_name); + simple_conv.padding = strftime_core::get_padding(simple_conv.conv_name); LIBC_NAMESPACE::strftime_core::convert(&writer, simple_conv); @@ -431,6 +468,8 @@ TEST_F(LlvmLibcStrftimeConverterTest, ISO8601DateConversion) { simple_conv.raw_string = "%F"; // ISO 8601 date format simple_conv.conv_name = 'F'; simple_conv.time = &time; + simple_conv.min_width = strftime_core::min_width(simple_conv.conv_name); + simple_conv.padding = strftime_core::get_padding(simple_conv.conv_name); LIBC_NAMESPACE::strftime_core::convert(&writer, simple_conv); diff --git a/libc/test/src/time/strftime_test.cpp b/libc/test/src/time/strftime_test.cpp index 6565c6e737307..39e642bcb510b 100644 --- a/libc/test/src/time/strftime_test.cpp +++ b/libc/test/src/time/strftime_test.cpp @@ -19,11 +19,13 @@ namespace LIBC_NAMESPACE_DECL { using namespace strftime_core; size_t call_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::WriteBuffer wb(buffer, (buffsz > 0 ? buffsz - 1 : 0), + strftime_core::overflow_write_mock, nullptr); printf_core::Writer writer(&wb); - strftime_core::strftime_main(&writer, format, timeptr); - return writer.get_chars_written(); + 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 ? ret : 0; } TEST(LlvmLibcStrftimeTest, FormatsYearMonthDayCorrectly) { @@ -94,5 +96,18 @@ TEST(LlvmLibcStrftimeTest, FormatsTimezoneCorrectly) { call_strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S %Z", &time); EXPECT_STRNE(buffer, ""); } +TEST(LlvmLibcStrftimeTest, TooManyCharacters) { + struct tm time; + time.tm_year = 122; // Year 2022 + time.tm_mon = 9; // October + time.tm_mday = 15; + time.tm_hour = 12; + time.tm_min = 0; + time.tm_sec = 0; + time.tm_isdst = -1; // Use system's daylight saving time information + char buffer[10]; + int ret = call_strftime(buffer, sizeof(buffer), "Abcdefghijklmnopq", &time); + EXPECT_EQ(ret, 0); +} } // namespace LIBC_NAMESPACE_DECL