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..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 @@ -99,7 +99,6 @@ add_entrypoint_object( HDRS mktime.h DEPENDS - .time_utils libc.include.time libc.src.errno.errno ) @@ -138,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 new file mode 100644 index 0000000000000..5035001e6fb6b --- /dev/null +++ b/libc/src/time/strftime.cpp @@ -0,0 +1,31 @@ +//===-- Implementation of strftime function -------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "src/time/strftime.h" +#include "src/__support/common.h" +#include "src/__support/macros/config.h" +#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 { + +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), + strftime_core::overflow_write_mock, nullptr); + printf_core::Writer writer(&wb); + int ret = strftime_core::strftime_main(&writer, format, timeptr); + if (buffsz > 0) // if the buffsz is 0 the buffer may be a null pointer. + wb.buff[wb.buff_cur] = '\0'; + return ret > 0 ? ret : 0; +} + +} // 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_core/CMakeLists.txt b/libc/src/time/strftime_core/CMakeLists.txt new file mode 100644 index 0000000000000..79e2eafc6a98d --- /dev/null +++ b/libc/src/time/strftime_core/CMakeLists.txt @@ -0,0 +1,57 @@ +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 + 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 + 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/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 new file mode 100644 index 0000000000000..a9bbf6ceca63c --- /dev/null +++ b/libc/src/time/strftime_core/converter.cpp @@ -0,0 +1,73 @@ +//===-- 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 "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 { + +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("%"); + 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': + case 'F': + case 'r': + case 'R': + case 'T': + case 'x': + case 'X': + return write_composite(writer, to_conv); + default: + return writer->write(to_conv.raw_string); + } + return 0; +} + +} // namespace strftime_core +} // namespace LIBC_NAMESPACE_DECL +#endif diff --git a/libc/src/time/strftime_core/converter.h b/libc/src/time/strftime_core/converter.h new file mode 100644 index 0000000000000..8809a8356e5c5 --- /dev/null +++ b/libc/src/time/strftime_core/converter.h @@ -0,0 +1,28 @@ +//===-- 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/stdio/printf_core/writer.h" +#include "src/time/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/time/strftime_core/core_structs.h b/libc/src/time/strftime_core/core_structs.h new file mode 100644 index 0000000000000..5c01638f132da --- /dev/null +++ b/libc/src/time/strftime_core/core_structs.h @@ -0,0 +1,39 @@ +//===-- 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; +}; + +#define RET_IF_RESULT_NEGATIVE(func) \ + { \ + int result = (func); \ + if (result < 0) \ + return result; \ + } + +} // namespace strftime_core +} // namespace LIBC_NAMESPACE_DECL + +#endif // LLVM_LIBC_SRC_STDIO_STRFTIME_CORE_CORE_STRUCTS_H diff --git a/libc/src/time/strftime_core/num_converter.h b/libc/src/time/strftime_core/num_converter.h new file mode 100644 index 0000000000000..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/parser.h b/libc/src/time/strftime_core/parser.h new file mode 100644 index 0000000000000..59bba59588a9e --- /dev/null +++ b/libc/src/time/strftime_core/parser.h @@ -0,0 +1,106 @@ +//===-- Format string parser for printf -------------------------*- C++ -*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#ifndef LLVM_LIBC_SRC_STDIO_STRFTIME_CORE_PARSER_H +#define LLVM_LIBC_SRC_STDIO_STRFTIME_CORE_PARSER_H + +#include "core_structs.h" +#include "src/__support/CPP/string_view.h" +#include "src/__support/macros/config.h" +#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 *str; + const struct tm &time; + size_t cur_pos = 0; + +public: + 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 + // 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; + ++cur_pos; + // locale-specific modifiers + if (str[cur_pos] == 'E') { + section.isE = true; + ++cur_pos; + } + if (str[cur_pos] == 'O') { + section.isO = true; + ++cur_pos; + } + section.conv_name = str[cur_pos]; + + // 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; + } + + 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') + ++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/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.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..eaa6d63bf61b2 --- /dev/null +++ b/libc/src/time/strftime_core/strftime_main.h @@ -0,0 +1,32 @@ +//===-- 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 { + +// 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); + +} // namespace strftime_core +} // namespace LIBC_NAMESPACE_DECL + +#endif // LLVM_LIBC_SRC_STDIO_STRFTIME_CORE_STRFTIME_MAIN_H diff --git a/libc/src/time/strftime_core/time_internal_def.h b/libc/src/time/strftime_core/time_internal_def.h new file mode 100644 index 0000000000000..66fd02f922978 --- /dev/null +++ b/libc/src/time/strftime_core/time_internal_def.h @@ -0,0 +1,77 @@ +//===-- 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 +// +//===----------------------------------------------------------------------===// + +#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" + +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"}; + +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]; +} + +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 new file mode 100644 index 0000000000000..3463d950bca19 --- /dev/null +++ b/libc/src/time/strftime_l.cpp @@ -0,0 +1,32 @@ +//===-- 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" + +#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 buffer, size_t buffsz, + const char *__restrict format, const struct tm *timeptr, + locale_t)) { + printf_core::WriteBuffer wb(buffer, (buffsz > 0 ? buffsz - 1 : 0), + strftime_core::overflow_write_mock, nullptr); + printf_core::Writer writer(&wb); + int ret = strftime_core::strftime_main(&writer, format, timeptr); + if (buffsz > 0) // if the buffsz is 0 the buffer may be a null pointer. + wb.buff[wb.buff_cur] = '\0'; + return ret > 0 ? ret : 0; +} + +} // 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 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/time/strftime_core/converter_test.cpp b/libc/test/src/time/strftime_core/converter_test.cpp new file mode 100644 index 0000000000000..7c4820bbd208b --- /dev/null +++ b/libc/test/src/time/strftime_core/converter_test.cpp @@ -0,0 +1,481 @@ +//===-- 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/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 {} + // 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; + 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; + 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); + + wb.buff[wb.buff_cur] = '\0'; + + ASSERT_STREQ(str, "Mon"); + ASSERT_EQ(writer.get_chars_written(), 3); +} +TEST_F(LlvmLibcStrftimeConverterTest, AbbreviatedMonthNameConversion) { + LIBC_NAMESPACE::strftime_core::FormatSection simple_conv; + 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; + 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); + + 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; + 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; + 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); + + 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; + 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; + 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); + + 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; + 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; + 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); + + 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; + 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; + 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); + + 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; + 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; + 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); + + 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; + 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; + 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); + + 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; + 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; + 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); + + 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; + 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; + 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); + + 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; + 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; + 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); + + 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; + 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; + 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); + + 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; + 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; + 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); + + 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; + 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; + 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); +} + +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; + 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); + + 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; + 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); + + 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; + 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); + + 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; + 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); + + 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; + 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); + + 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..39e642bcb510b --- /dev/null +++ b/libc/test/src/time/strftime_test.cpp @@ -0,0 +1,113 @@ + +//===-- 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), + strftime_core::overflow_write_mock, nullptr); + printf_core::Writer writer(&wb); + int ret = strftime_core::strftime_main(&writer, format, timeptr); + if (buffsz > 0) // if the buffsz is 0 the buffer may be a null pointer. + wb.buff[wb.buff_cur] = '\0'; + return ret > 0 ? ret : 0; +} + +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, ""); +} +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