Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added support for subsecond resolution for time_point #2292

Closed
wants to merge 7 commits into from

Conversation

brcha
Copy link

@brcha brcha commented May 18, 2021

Fixes #2207

This code:

#include "fmt/include/fmt/chrono.h"

int main () {
	auto now = std::chrono::high_resolution_clock::now();
	fmt::print("Time is {:%T}\n", now);
	fmt::print("Time (HH:MM) is {:%H:%M}\n", now);

	std::chrono::system_clock::time_point t{std::chrono::milliseconds{1234}};
	fmt::print("Seconds are {:%S}\n", t.time_since_epoch());
}

now outputs:

tmp/c++11tests/chrono ❯ g++ -std=c++11 -o fmt_test fmt_test.cpp -L ./fmt-build -lfmt                                                                                                                                                           уто 18 14:41:59

tmp/c++11tests/chrono ❯ ./fmt_test                                                                                                                                                                                                             уто 18 14:43:17
Time is 14:43:18.21308033
Time (HH:MM) is 14:43
Seconds are 01.234

@@ -412,7 +412,11 @@ struct formatter<std::chrono::time_point<std::chrono::system_clock, Duration>,
auto format(std::chrono::time_point<std::chrono::system_clock> val,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be a template accepting any std::chrono::time_point<std::chrono::system_clock, Period>

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or more precisely, the type of the first parameter should be std::chrono::time_point<std::chrono::system_clock, std::chrono::duration<Rep, Period>>.

@@ -412,7 +412,11 @@ struct formatter<std::chrono::time_point<std::chrono::system_clock, Duration>,
auto format(std::chrono::time_point<std::chrono::system_clock> val,
FormatContext& ctx) -> decltype(ctx.out()) {
std::tm time = localtime(val);
return formatter<std::tm, Char>::format(time, ctx);
auto epoch = val.time_since_epoch();
auto seconds = std::chrono::duration_cast<std::chrono::milliseconds>(epoch).count() / 1000;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can just duration_cast<std::chrono::seconds>

auto epoch = val.time_since_epoch();
auto seconds = std::chrono::duration_cast<std::chrono::milliseconds>(epoch).count() / 1000;
auto today = std::chrono::high_resolution_clock::duration(std::chrono::milliseconds(seconds * 1000));
auto subseconds = std::chrono::duration_cast<std::chrono::nanoseconds>(epoch - today);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just do subseconds = epoch - seconds.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Times preceding the epoch (1970-01-01) need to be considered so that subseconds is never negative.

auto hasT = std::find(tm_format.begin(), tm_format.end(), 'T') != tm_format.end();
auto writeSubseconds = (hasS || hasT) && (subseconds != 0);
if (writeSubseconds)
tm_format.push_back('.');
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to check locale an insert the preferred decimal separator?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should check, but I don't know how to get the locale in the chrono formatter. I'll try to grep for anything related to locale.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Locale can be addressed separately, the default is locale-independent anyway.

@@ -449,6 +459,11 @@ template <typename Char> struct formatter<std::tm, Char> {
const size_t MIN_GROWTH = 10;
buf.reserve(buf.capacity() + (size > MIN_GROWTH ? size : MIN_GROWTH));
}

if (writeSubseconds) {
buf.append(std::to_string(subseconds));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does not add leading zeroes.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the comments. I did notice I've overlooked some of those, but I was out. Should be fixed now.

@brcha
Copy link
Author

brcha commented May 18, 2021

Should also test with time preceding the epoch, as well as whole seconds (zero subseconds).

It does test for that, and it works. I've tested it like:

  auto past = std::chrono::system_clock::time_point{-std::chrono::hours{15}};
  fmt::print("15 hours before the start of time is: {:%Y:%m:%d %H:%M:%S}\n", past);

and I get

15 hours before the start of time is: 1969:12:31 10:00:00

@ecorm
Copy link

ecorm commented May 18, 2021

Should also test with time preceding the epoch, as well as whole seconds (zero subseconds).

It does test for that, and it works. I've tested it like:

  auto past = std::chrono::system_clock::time_point{-std::chrono::hours{15}};
  fmt::print("15 hours before the start of time is: {:%Y:%m:%d %H:%M:%S}\n", past);

and I get

15 hours before the start of time is: 1969:12:31 10:00:00

I think it would have failed if you tried a time with non-zero subseconds.

@ecorm
Copy link

ecorm commented May 18, 2021

See here for a possible implementation of std::chrono::floor: https://en.cppreference.com/w/cpp/chrono/duration/floor

@brcha
Copy link
Author

brcha commented May 18, 2021

Should also test with time preceding the epoch, as well as whole seconds (zero subseconds).

It does test for that, and it works. I've tested it like:

  auto past = std::chrono::system_clock::time_point{-std::chrono::hours{15}};
  fmt::print("15 hours before the start of time is: {:%Y:%m:%d %H:%M:%S}\n", past);

and I get

15 hours before the start of time is: 1969:12:31 10:00:00

I think it would have failed if you tried a time with non-zero subseconds.

You were correct. I've updated the std::tm localtime as well, and set subseconds to be 1s - whatever value they had. Works now for epoch -15h - 15ns as "1969:12:31 09:59:59.999999985".

@brcha
Copy link
Author

brcha commented May 18, 2021

This only works when Period::num is 1, which is not guaranteed. This can be computed at compile time. See how Hinnant does it (it's quite complex, though).

Thanks for the links. I've got some friends coming over soon, so I'll take a look tomorrow (or later). Also a way to handle fractional time.

@brcha
Copy link
Author

brcha commented May 18, 2021

This is computed needlessly when the given time precedes the epoch (it is recomputed again on line 420).

It has to be separately computed because localtime simply truncates the subseconds and returns whichever second was before that. But it shouldn't be computed twice, that's true.

Copy link
Contributor

@vitaut vitaut left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR.

@@ -412,7 +412,11 @@ struct formatter<std::chrono::time_point<std::chrono::system_clock, Duration>,
auto format(std::chrono::time_point<std::chrono::system_clock> val,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or more precisely, the type of the first parameter should be std::chrono::time_point<std::chrono::system_clock, std::chrono::duration<Rep, Period>>.


if (subseconds < subseconds.zero()) {
time = localtime(val - std::chrono::seconds{1});
subseconds = std::chrono::seconds{1} + subseconds;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: std::chrono::seconds{1} -> std::chrono::seconds(1) in two places for consistency with the rest of the library.

Comment on lines +427 to +432
auto width = std::to_string(Period::den).size() - 1;
std::basic_stringstream<Char> os;
os.fill('0');
os.width(width);
os << subseconds.count();
auto formatted_ss = os.str();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use format instead of stringstream, something like

auto formatted_ss format("{:0{}}", subseconds.count(), width);
return formatter<std::tm, Char>::format(time, ctx, bytes(formatted_ss));

@@ -449,8 +476,15 @@ template <typename Char> struct formatter<std::tm, Char> {
const size_t MIN_GROWTH = 10;
buf.reserve(buf.capacity() + (size > MIN_GROWTH ? size : MIN_GROWTH));
}

auto buf_end = buf.end() - 1;
if (writeSubseconds) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

writeSubseconds -> write_subseconds (naming convention).

@vitaut
Copy link
Contributor

vitaut commented Jun 1, 2021

@brcha, do you plan to continue working on this PR or shall we close it for now?

@brcha
Copy link
Author

brcha commented Jun 1, 2021

@brcha, do you plan to continue working on this PR or shall we close it for now?

Sorry, I was very busy the last week or so. I'll continue working on this PR ASAP, given that now I have a lot more free time.

@vitaut
Copy link
Contributor

vitaut commented Jun 23, 2021

Closing for now but feel free to submit another PR if and whenever you have time. Thanks!

@vitaut vitaut closed this Jun 23, 2021
@vitaut
Copy link
Contributor

vitaut commented Jun 23, 2021

Or let me know and I'll reopen this one.

@kkm000
Copy link

kkm000 commented Jul 18, 2021

@brcha, @ecorm, if you ever get back to this, I think this hasn't been noticed in the discussion, unless I miss it: the test is one hour off, epoch‒15 hours is 09:00:00, not 10:00:00. At sharp hours, the hour and –(offset) must sum up to 0 mod 24, for both pre- and post-epoch times. For example, epoch‒1 hour is 23:00, ‒(‒1)+23 = 24 ≡ 0 mod 24; epoch+25 hours is 01:00, ‒(+1) + 25 = 24 ≡ 0 mod 24).

@zeroxia
Copy link

zeroxia commented Feb 3, 2023

I think making %S to output seconds including fractional part is a breaking change. (strftime defines %S to be decimal number, range is 00 to 60)

Is it possible to make %S accept parameters like floating point, e.g. %.06S would output fractional part in nanoseconds, while %.03S will output milliseconds.

Or keep %S to always output integral seconds, and introduce another formatter char to output subseconds value.

@vitaut
Copy link
Contributor

vitaut commented Feb 3, 2023

{fmt} follows the C++ standard (#2207) but you can discard the fractional using std::chrono::floor (https://en.cppreference.com/w/cpp/chrono/duration/floor) or other method.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Printed seconds precision does not match C++20 spec
5 participants