mirror of
https://github.com/fmtlib/fmt.git
synced 2026-10-29 02:17:28 +08:00
Simplify pre-epoch local_time formatting
Floor the seconds before calling gmtime instead of adjusting tm_sec, and keep the subsecond remainder in the common duration type, which fixes overflow for narrow reps such as duration<int16_t, std::micro>.
This commit is contained in:
parent
6ba294f5a6
commit
4ded41f186
@ -2233,27 +2233,25 @@ struct formatter<local_time<Duration>, Char>
|
||||
auto format(local_time<Duration> val, FormatContext& ctx) const
|
||||
-> decltype(ctx.out()) {
|
||||
auto time_since_epoch = val.time_since_epoch();
|
||||
auto seconds_since_epoch =
|
||||
detail::duration_cast<std::chrono::seconds>(time_since_epoch);
|
||||
auto secs = detail::duration_cast<std::chrono::seconds>(time_since_epoch);
|
||||
// duration_cast truncates towards zero, so the remainder is negative
|
||||
// before the epoch. Compute it before flooring the seconds because
|
||||
// converting floored seconds back overflows at the limits, e.g.
|
||||
// nanoseconds(INT64_MIN). Keep it in the common duration type as well
|
||||
// since Duration may be too narrow, e.g. duration<int16_t, std::micro>.
|
||||
auto subsecs = time_since_epoch - secs;
|
||||
if (subsecs.count() < 0) {
|
||||
secs -= std::chrono::seconds(1);
|
||||
subsecs += std::chrono::seconds(1);
|
||||
}
|
||||
// Use gmtime to prevent time zone conversion since local_time has an
|
||||
// unspecified time zone.
|
||||
std::tm t = gmtime(seconds_since_epoch.count());
|
||||
std::tm t = gmtime(secs.count());
|
||||
using period = typename Duration::period;
|
||||
if (period::num == 1 && period::den == 1 &&
|
||||
!std::is_floating_point<typename Duration::rep>::value) {
|
||||
return formatter<std::tm, Char>::format(t, ctx);
|
||||
}
|
||||
auto subsecs =
|
||||
detail::duration_cast<Duration>(time_since_epoch - seconds_since_epoch);
|
||||
if (subsecs.count() < 0) {
|
||||
auto second = detail::duration_cast<Duration>(std::chrono::seconds(1));
|
||||
if (t.tm_sec != 0) {
|
||||
--t.tm_sec;
|
||||
} else {
|
||||
t = gmtime(seconds_since_epoch.count() - 1);
|
||||
}
|
||||
subsecs += second;
|
||||
}
|
||||
return formatter<std::tm, Char>::do_format(t, ctx, &subsecs);
|
||||
}
|
||||
};
|
||||
|
||||
@ -342,6 +342,44 @@ TEST(chrono_test, local_time) {
|
||||
EXPECT_EQ(fmt::format("{:%S}", pre_epoch), "01.123");
|
||||
}
|
||||
|
||||
TEST(chrono_test, local_time_before_epoch) {
|
||||
using std::chrono::milliseconds;
|
||||
using std::chrono::nanoseconds;
|
||||
using std::chrono::seconds;
|
||||
auto ms = [](long long n) {
|
||||
return fmt::local_time<milliseconds>(milliseconds(n));
|
||||
};
|
||||
// Subseconds are a remainder of a floored second, not a negative offset.
|
||||
EXPECT_EQ(fmt::format("{}", ms(-500)), "1969-12-31 23:59:59.500");
|
||||
EXPECT_EQ(fmt::format("{}", ms(-1000)), "1969-12-31 23:59:59.000");
|
||||
EXPECT_EQ(fmt::format("{}", ms(-1500)), "1969-12-31 23:59:58.500");
|
||||
// Flooring the second also moves to the previous minute.
|
||||
EXPECT_EQ(fmt::format("{}", ms(-61500)), "1969-12-31 23:58:58.500");
|
||||
EXPECT_EQ(fmt::format("{}", fmt::local_time<std::chrono::duration<double>>(
|
||||
std::chrono::duration<double>(-1.5))),
|
||||
"1969-12-31 23:59:58.500000");
|
||||
// A whole-second duration has no fractional part to format.
|
||||
EXPECT_EQ(fmt::format("{}", fmt::local_time<seconds>(seconds(-1))),
|
||||
"1969-12-31 23:59:59");
|
||||
// Flooring the seconds must not convert them back to the input precision,
|
||||
// which would overflow. Only a sanitizer detects this, so the result is
|
||||
// irrelevant and gmtime may reject the value on platforms with a narrower
|
||||
// supported range.
|
||||
auto ns_min = nanoseconds((std::numeric_limits<nanoseconds::rep>::min)());
|
||||
try {
|
||||
(void)fmt::format("{}", fmt::local_time<nanoseconds>(ns_min));
|
||||
} catch (const fmt::format_error&) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST(chrono_test, local_time_narrow_duration) {
|
||||
// int16_t microseconds hold less than a second, so the subsecond remainder
|
||||
// does not fit in the duration itself.
|
||||
using narrow = std::chrono::duration<int16_t, std::micro>;
|
||||
EXPECT_EQ(fmt::format("{}", fmt::local_time<narrow>(narrow(-30000))),
|
||||
"1969-12-31 23:59:59.970000");
|
||||
}
|
||||
|
||||
template <typename T, FMT_ENABLE_IF(fmt::detail::has_tm_gmtoff<T>::value)>
|
||||
auto set_tm_gmtoff(T& time, long offset) -> bool {
|
||||
time.tm_gmtoff = offset;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user