Home > Back-end >  How do I represent a 24-hour clock with the STL?
How do I represent a 24-hour clock with the STL?

Time:11-24

I'm looking for a solution from the STL, for dealing with "time of day". I am working on a simple unit test exercise, with behavior depending on whether current time is in the morning, evening or night.

For a first iteration I used a humble integer as a stand-in for some "time object":

using TimeOfDay = int;
constexpr bool isBetween(TimeOfDay in, TimeOfDay min, TimeOfDay max) noexcept {
  return in >= min && in <= max; 
}
constexpr bool isMorning(TimeOfDay in) noexcept { 
  return isBetween(in, 6, 12); }
constexpr bool isEvening(TimeOfDay in) noexcept {
  return isBetween(in, 18, 22);
}
constexpr bool isNight(TimeOfDay in) noexcept {
  return isBetween(in, 22, 24) || isBetween(in, 0, 6);
}

constexpr string_view getGreetingFor(TimeOfDay time) noexcept {
  if (isMorning(time)) {
    return "Good morning "sv;
  }
  if (isEvening(time)) {
    return "Good evening "sv;
  }
  if (isNight(time)) {
    return "Good night "sv;
  }
  return "Hello "sv;
}

This works but has a couple of smells:

  • the int simply isn't the right type to represent a 24 hour clock
  • isNight() requires an unnecessarily complicated comparison, due to wrapping (22-06)
  • ideally I would like to be able actually use the system clock for some of my tests.
  • std::chrono::system_clock::now() returns a std::chrono::time_point, so my ideal type should probably be something that can be compared to a time_point, or easily constructed from a time_point.

Any pointers would be very appreciated!

(I am working in Visual Studio with C Latest (preview of the C working draft, so roughly C 23))

CodePudding user response:

I'm going to start with the assumption that you're looking for the local time of day. chrono::system_clock represents UTC. So I recommend a helper function that takes std::chrono::system_clock::time_point and returns a std::chrono::system_clock::duration which represents the time elapsed since the most recent local midnight:

using TimeOfDay = std::chrono::system_clock::duration;

TimeOfDay
get_local_time_of_day(std::chrono::system_clock::time_point tp) noexcept
{
  using namespace std::chrono;
  auto time = current_zone()->to_local(tp);
  return time - floor<days>(time);
}

chrono::current_zone() returns a pointer to the chrono::time_zone that your computer is currently set to. This is used to convert the system_clock::time_point to a local time_point. This local time_point is a chrono::time_point but with a different clock that means "local time".

The expression floor<days>(time) truncates the time_point to precision days, which effectively gives you a pointer to the most recently passed local midnight. Subtracting that from time gives you a chrono::duration since the local midnight. This duration will have the same precision as tp.

This function can not be made constexpr because the rules for transforming UTC to local time is in the hands of your politicians and is subject to change (in many countries twice a year!).

Now you can write isMorning (et al) like this:

bool isMorning(std::chrono::system_clock::time_point in) noexcept { 
  using namespace std::literals;
  return isBetween(get_local_time_of_day(in), 6h, 12h); }

And call it like this:

if (isMorning(system_clock::now())) ...

Your logic for isNight looks fine to me. And your code for isBetween can stay like it is (picking up the new definition of TimeOfDay).

CodePudding user response:

If you are talking about time within a day, detached from a date, use a std::chrono::duration. For integer hours, there is the alias std::chrono::hours.

See also std::chrono::hh_mm_ss

Something like this:

#include <iostream>
#include <chrono>

using namespace std::chrono;

int main()
{
    auto now = system_clock::now().time_since_epoch();
    auto today = duration_cast<days>(now);
    hh_mm_ss time { now - today };
    std::cout << time.hours() << time.minutes() << time.seconds();
}

See it on coliru

CodePudding user response:

The "time of day" should be stored in a std::chrono::duration. To detach only the h/m/s information of any duration, you can do:

auto time_info = my_duration - std::chrono::round<std::chrono::days>(my_duration);

Similarly, you can detach the h/m/s information of a time_point with the same interface:

auto now = std::chrono::system_clock::now();
auto time_info = now - std::chrono::round<std::chrono::days>(now);

Then you can use the helper class hh_mm_ss to examine the information:

auto hms = std::chrono::hh_mm_ss{ time_info };
std::cout << hms.hours() << hms.minutes() << hms.seconds();

To check if a duration is between two other time, you can do:

using TimeOfDay = std::chrono::seconds;

constexpr bool isBetween(TimeOfDay in, TimeOfDay min, TimeOfDay max) noexcept {
    return in >= min && in <= max; 
}

constexpr bool isMorning(TimeOfDay in) noexcept { 
    return isBetween(in, std::chrono::hours{6}, std::chrono::hours{12}); 
}

You can also enable the literals by using namespace std::chrono_literals or using namespace std::literals:

constexpr bool isMorning(TimeOfDay in) noexcept { 
    return isBetween(in, 6h, 12h); 
}

For the wrapping in isEvening, you can potentially do something like: isBetween(in 12h - days{1}, 10h, 18h), but I think what you have right now have a much cleaner and easier to follow logic.


And this can be easily turned back to a time_point if needed:

auto today = std::chrono::round<std::chrono::days>(std::chrono::system_clock::now());
auto now = today   time_info;
  • Related