Skip to content

Commit 085ac4f

Browse files
[libc] Implement strftime
Implements the posix-specified strftime conversions for the default locale, along with comprehensive unit tests. This reuses a lot of design from printf, as well as the printf writer. Roughly based on #111305, but with major rewrites.
1 parent ae7f7c4 commit 085ac4f

23 files changed

+3865
-94
lines changed

libc/config/linux/x86_64/entrypoints.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1109,6 +1109,7 @@ if(LLVM_LIBC_FULL_BUILD)
11091109
libc.src.time.gmtime_r
11101110
libc.src.time.mktime
11111111
libc.src.time.nanosleep
1112+
libc.src.time.strftime
11121113
libc.src.time.time
11131114
libc.src.time.timespec_get
11141115

libc/docs/dev/undefined_behavior.rst

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,3 +106,40 @@ uninitialized spinlock and invalid spinlock is left undefined. We follow the rec
106106
POSIX.1-2024, where EINVAL is returned if the spinlock is invalid (here we only check for null pointers) or
107107
EBUSY is returned if the spinlock is currently locked. The lock is poisoned after a successful destroy. That is,
108108
subsequent operations on the lock object without any reinitialization will return EINVAL.
109+
110+
Strftime
111+
--------
112+
In the C Standard, it provides a list of modifiers, and the conversions these
113+
are valid on. It also says that a modifier on an unspecified conversion is
114+
undefined. For LLVM-libc, the conversion is treated as if the modifier isn't
115+
there.
116+
117+
If a struct tm with values out of the normal range is passed, the standard says
118+
the result is undefined. For LLVM-libc, the result may be either the normalized
119+
value (e.g. weekday % 7) or the actual, out of range value. For any numeric
120+
conversion where the result is just printing a value out of the struct
121+
(e.g. "%w" prints the day of the week), no normalization occurs ("%w" on a
122+
tm_wday of 32 prints "32"). For any numeric conversion where the value is
123+
calculated (e.g. "%u" prints the day of the week, starting on monday), the
124+
value is normalized (e.g. "%u" on a tm_wday of 32 prints "4"). For conversions
125+
that result in strings, passing an out of range value will result in "?".
126+
127+
Posix adds padding support to strftime, but says "the default padding character
128+
is unspecified." For LLVM-libc, the default padding character is ' ' (space)
129+
for all string-type conversions and '0' for integer-type conversions. Composite
130+
conversions pass the padding to the first (leftmost) conversion. In practice
131+
this is always a numeric conversion, so it pads with '0'. For the purposes of
132+
padding, composite conversions also assume the non-leading conversions have
133+
valid inputs and output their expected number of characters. For %c this means
134+
that the padding will be off if the year is outside of the range -999 to 9999.
135+
136+
The %e conversion is padded with spaces by default, but pads with 0s if the '0'
137+
flag is set.
138+
139+
Posix also adds flags and a minimum field width, but leaves unspecified what
140+
happens for most combinations of these. For LLVM-libc:
141+
An unspecified minimum field width defaults to 0.
142+
More specific flags take precedence over less specific flags (i.e. '+' takes precedence over '0')
143+
Any conversion with a minimum width is padded with the padding character until it is at least as long as the minimum width.
144+
Modifiers are applied, then the result is padded if necessary.
145+
Any composite conversion will pass along all flags to the component conversions.

libc/include/llvm-libc-types/struct_tm.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ struct tm {
1919
int tm_wday; // days since Sunday
2020
int tm_yday; // days since January
2121
int tm_isdst; // Daylight Saving Time flag
22+
// TODO: add tm_gmtoff and tm_zone? (posix extensions)
2223
};
2324

2425
#endif // LLVM_LIBC_TYPES_STRUCT_TM_H

libc/include/time.yaml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,25 @@ functions:
9191
arguments:
9292
- type: const struct timespec *
9393
- type: struct timespec *
94+
- name: strftime
95+
standard:
96+
- stdc
97+
return_type: size_t
98+
arguments:
99+
- type: char *__restrict
100+
- type: size_t
101+
- type: const char *__restrict
102+
- type: const struct tm *__restrict
103+
- name: strftime_l
104+
standard:
105+
- stdc
106+
return_type: size_t
107+
arguments:
108+
- type: char *__restrict
109+
- type: size_t
110+
- type: const char *__restrict
111+
- type: const struct tm *__restrict
112+
- type: locale_t
94113
- name: time
95114
standard:
96115
- stdc

libc/src/time/CMakeLists.txt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ add_object_library(
2222
DEPENDS
2323
libc.include.time
2424
libc.src.__support.CPP.limits
25+
libc.src.__support.CPP.string_view
26+
libc.src.__support.CPP.optional
2527
libc.src.errno.errno
2628
.time_constants
2729
libc.hdr.types.time_t
@@ -133,6 +135,21 @@ add_entrypoint_object(
133135
libc.hdr.types.struct_tm
134136
)
135137

138+
add_subdirectory(strftime_core) #TODO: Move to top
139+
140+
add_entrypoint_object(
141+
strftime
142+
SRCS
143+
strftime.cpp
144+
HDRS
145+
strftime.h
146+
DEPENDS
147+
libc.hdr.types.size_t
148+
libc.hdr.types.struct_tm
149+
libc.src.stdio.printf_core.writer
150+
libc.src.time.strftime_core.strftime_main
151+
)
152+
136153
add_entrypoint_object(
137154
time
138155
SRCS

libc/src/time/mktime.cpp

Lines changed: 1 addition & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -14,100 +14,8 @@
1414

1515
namespace LIBC_NAMESPACE_DECL {
1616

17-
// Returns number of years from (1, year).
18-
static constexpr int64_t get_num_of_leap_years_before(int64_t year) {
19-
return (year / 4) - (year / 100) + (year / 400);
20-
}
21-
22-
// Returns True if year is a leap year.
23-
static constexpr bool is_leap_year(const int64_t year) {
24-
return (((year) % 4) == 0 && (((year) % 100) != 0 || ((year) % 400) == 0));
25-
}
26-
2717
LLVM_LIBC_FUNCTION(time_t, mktime, (struct tm * tm_out)) {
28-
// Unlike most C Library functions, mktime doesn't just die on bad input.
29-
// TODO(rtenneti); Handle leap seconds.
30-
int64_t tm_year_from_base = tm_out->tm_year + time_constants::TIME_YEAR_BASE;
31-
32-
// 32-bit end-of-the-world is 03:14:07 UTC on 19 January 2038.
33-
if (sizeof(time_t) == 4 &&
34-
tm_year_from_base >= time_constants::END_OF32_BIT_EPOCH_YEAR) {
35-
if (tm_year_from_base > time_constants::END_OF32_BIT_EPOCH_YEAR)
36-
return time_utils::out_of_range();
37-
if (tm_out->tm_mon > 0)
38-
return time_utils::out_of_range();
39-
if (tm_out->tm_mday > 19)
40-
return time_utils::out_of_range();
41-
else if (tm_out->tm_mday == 19) {
42-
if (tm_out->tm_hour > 3)
43-
return time_utils::out_of_range();
44-
else if (tm_out->tm_hour == 3) {
45-
if (tm_out->tm_min > 14)
46-
return time_utils::out_of_range();
47-
else if (tm_out->tm_min == 14) {
48-
if (tm_out->tm_sec > 7)
49-
return time_utils::out_of_range();
50-
}
51-
}
52-
}
53-
}
54-
55-
// Years are ints. A 32-bit year will fit into a 64-bit time_t.
56-
// A 64-bit year will not.
57-
static_assert(
58-
sizeof(int) == 4,
59-
"ILP64 is unimplemented. This implementation requires 32-bit integers.");
60-
61-
// Calculate number of months and years from tm_mon.
62-
int64_t month = tm_out->tm_mon;
63-
if (month < 0 || month >= time_constants::MONTHS_PER_YEAR - 1) {
64-
int64_t years = month / 12;
65-
month %= 12;
66-
if (month < 0) {
67-
years--;
68-
month += 12;
69-
}
70-
tm_year_from_base += years;
71-
}
72-
bool tm_year_is_leap = is_leap_year(tm_year_from_base);
73-
74-
// Calculate total number of days based on the month and the day (tm_mday).
75-
int64_t total_days = tm_out->tm_mday - 1;
76-
for (int64_t i = 0; i < month; ++i)
77-
total_days += time_constants::NON_LEAP_YEAR_DAYS_IN_MONTH[i];
78-
// Add one day if it is a leap year and the month is after February.
79-
if (tm_year_is_leap && month > 1)
80-
total_days++;
81-
82-
// Calculate total numbers of days based on the year.
83-
total_days += (tm_year_from_base - time_constants::EPOCH_YEAR) *
84-
time_constants::DAYS_PER_NON_LEAP_YEAR;
85-
if (tm_year_from_base >= time_constants::EPOCH_YEAR) {
86-
total_days += get_num_of_leap_years_before(tm_year_from_base - 1) -
87-
get_num_of_leap_years_before(time_constants::EPOCH_YEAR);
88-
} else if (tm_year_from_base >= 1) {
89-
total_days -= get_num_of_leap_years_before(time_constants::EPOCH_YEAR) -
90-
get_num_of_leap_years_before(tm_year_from_base - 1);
91-
} else {
92-
// Calculate number of leap years until 0th year.
93-
total_days -= get_num_of_leap_years_before(time_constants::EPOCH_YEAR) -
94-
get_num_of_leap_years_before(0);
95-
if (tm_year_from_base <= 0) {
96-
total_days -= 1; // Subtract 1 for 0th year.
97-
// Calculate number of leap years until -1 year
98-
if (tm_year_from_base < 0) {
99-
total_days -= get_num_of_leap_years_before(-tm_year_from_base) -
100-
get_num_of_leap_years_before(1);
101-
}
102-
}
103-
}
104-
105-
// TODO: https://github.com/llvm/llvm-project/issues/121962
106-
// Need to handle timezone and update of tm_isdst.
107-
int64_t seconds = tm_out->tm_sec +
108-
tm_out->tm_min * time_constants::SECONDS_PER_MIN +
109-
tm_out->tm_hour * time_constants::SECONDS_PER_HOUR +
110-
total_days * time_constants::SECONDS_PER_DAY;
18+
int64_t seconds = time_utils::mktime_internal(tm_out);
11119

11220
// Update the tm structure's year, month, day, etc. from seconds.
11321
if (time_utils::update_from_seconds(seconds, tm_out) < 0)

libc/src/time/strftime.cpp

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
//===-- Implementation of strftime function -------------------------------===//
2+
//
3+
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4+
// See https://llvm.org/LICENSE.txt for license information.
5+
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6+
//
7+
//===----------------------------------------------------------------------===//
8+
9+
#include "src/time/strftime.h"
10+
#include "hdr/types/size_t.h"
11+
#include "hdr/types/struct_tm.h"
12+
#include "src/__support/common.h"
13+
#include "src/__support/macros/config.h"
14+
#include "src/stdio/printf_core/writer.h"
15+
#include "src/time/strftime_core/strftime_main.h"
16+
17+
namespace LIBC_NAMESPACE_DECL {
18+
19+
LLVM_LIBC_FUNCTION(size_t, strftime,
20+
(char *__restrict buffer, size_t buffsz,
21+
const char *__restrict format, const struct tm *timeptr)) {
22+
23+
printf_core::WriteBuffer wb(buffer, (buffsz > 0 ? buffsz - 1 : 0));
24+
printf_core::Writer writer(&wb);
25+
int ret = strftime_core::strftime_main(&writer, format, timeptr);
26+
if (buffsz > 0) // if the buffsz is 0 the buffer may be a null pointer.
27+
wb.buff[wb.buff_cur] = '\0';
28+
return (ret < 0 || static_cast<size_t>(ret) > buffsz) ? 0 : ret;
29+
}
30+
31+
} // namespace LIBC_NAMESPACE_DECL

libc/src/time/strftime.h

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
//===-- Implementation header of strftime -----------------------*- C++ -*-===//
2+
//
3+
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4+
// See https://llvm.org/LICENSE.txt for license information.
5+
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6+
//
7+
//===----------------------------------------------------------------------===//
8+
9+
#ifndef LLVM_LIBC_SRC_TIME_STRFTIME_H
10+
#define LLVM_LIBC_SRC_TIME_STRFTIME_H
11+
12+
#include "hdr/types/size_t.h"
13+
#include "hdr/types/struct_tm.h"
14+
#include "src/__support/macros/config.h"
15+
16+
namespace LIBC_NAMESPACE_DECL {
17+
18+
size_t strftime(char *__restrict, size_t max, const char *__restrict format,
19+
const struct tm *timeptr);
20+
21+
} // namespace LIBC_NAMESPACE_DECL
22+
23+
#endif // LLVM_LIBC_SRC_TIME_STRFTIME_H
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
add_header_library(
2+
core_structs
3+
HDRS
4+
core_structs.h
5+
DEPENDS
6+
libc.src.__support.CPP.string_view
7+
libc.hdr.types.struct_tm
8+
)
9+
10+
add_header_library(
11+
parser
12+
HDRS
13+
parser.h
14+
DEPENDS
15+
.core_structs
16+
libc.src.__support.CPP.string_view
17+
libc.src.__support.ctype_utils
18+
libc.src.__support.str_to_integer
19+
)
20+
21+
add_object_library(
22+
converter
23+
SRCS
24+
converter.cpp
25+
HDRS
26+
converter.h
27+
num_converter.h
28+
str_converter.h
29+
composite_converter.h
30+
DEPENDS
31+
.core_structs
32+
libc.src.time.time_utils
33+
libc.src.time.time_constants
34+
libc.src.stdio.printf_core.writer
35+
libc.src.__support.CPP.string_view
36+
libc.src.__support.integer_to_string
37+
)
38+
39+
add_object_library(
40+
strftime_main
41+
SRCS
42+
strftime_main.cpp
43+
HDRS
44+
strftime_main.h
45+
DEPENDS
46+
.core_structs
47+
.parser
48+
.converter
49+
libc.src.stdio.printf_core.writer
50+
libc.hdr.types.struct_tm
51+
)

0 commit comments

Comments
 (0)