HowardHinnant / date

A date and time library based on the C++11/14/17 <chrono> header
Other
3.09k stars 674 forks source link

full time of day support? #655

Open frostius opened 3 years ago

frostius commented 3 years ago

Is there a reason why there isn't a single class that contains all date & time fields? Also, why no support for time of day literals hh:mm:ss.ms and timezone? Its clumsy trying to do something as simple as initialize a time point with a known date & time.

auto tp = 30 Mar 2021 17:30:00.123 -5Z; //set date & time
tp += 1d 05:15:00; //add time
HowardHinnant commented 3 years ago

Field types are relatively inefficient data structures for much anything beyond retrieving values for the fields. They are good for formatting as that is when the fields are needed. However for storage, arithmetic, comparison, etc. the field types are inefficient both in size and speed.

If one really wants one, it is easy enough to build a {year, month, day, hour, minute, seconds, milliseconds} struct, and use the existing facilities to translate between that and sys_time<milliseconds>.

As far as I know, C++ doesn't provide a way to create a literal with the syntax 30 Mar 2021 17:30:00.123 -5Z or 05:15:00. Creating such literals would require new features in the language, and language changes are out of scope for this library project.

frostius commented 3 years ago

From the description of date.h:

"date.h" is a header-only library which builds upon <chrono>. It adds some new duration types, and new time_point types. It also adds "field" types such as year_month_day which is a struct {year, month, day}. And it provides convenient means to convert between the "field" types and the time_point types.

Then you say:

Field types are relatively inefficient data structures

I'm confused. <chrono> and time_t already provide efficient time handling. If date.h does not exists to "provide convenient means to convert between the "field" types and the time_point types." what does it do? And if so, why not support time of day? Not trying to be antagonistic, but it just feels like date.h is half-baked w/o support for time. Either that, or it's already well supported and I'm just missing something?

And yeah, I can roll my own helper classes (and I have) but why not solve the problem once for everyone?

HowardHinnant commented 3 years ago

For just time, here does exist hh_mm_ss. The documentation for this was obsoleted late in the standardization process, resulting in the best documentation is the std spec itself: http://eel.is/c++draft/time.hms.

hh_mm_ss is a template struct of the form {hours, minutes, seconds, subseconds} where subseconds is a function of the class template parameter. It is best used as a formatting aid. It takes a general chrono::duration and divides it into the various fields. subseconds is constrained to be a unit with a precision that is a non-positive power of 10. This facilitates formatting the correct number of decimal digits to represent the fractions of a second.

And there are various date field types (e.g. year_month_day) which have conversions to and from the type std::chrono::time_point<system_clock, duration<int, ratio<86400>>> (a count of days since 1970-01-01).

One can construct a system_clock-based time_point like this:

auto tp = sys_days{30_d/March/2021} + 17h + 30min + 123ms;  // 2021-03-30 17:30:00.123 UTC
frostius commented 3 years ago

Thanks for the example of full time construction - I was missing that I had to wrap it in a sys_days{} to get it work. That helps a lot!

------ Original Message ------ From: "Howard Hinnant" @.> To: "HowardHinnant/date" @.> Cc: "Matt Frost" @.>; "Author" @.> Sent: 3/30/2021 11:56:12 AM Subject: Re: [HowardHinnant/date] full time of day support? (#655)

For just time, here does exist hh_mm_ss. The documentation for this was obsoleted late in the standardization process, resulting in the best documentation is the std spec itself: http://eel.is/c++draft/time.hms.

hh_mm_ss is a template struct of the form {hours, minutes, seconds, subseconds} where subseconds is a function of the class template parameter. It is best used as a formatting aid. It takes a general chrono::duration and divides it into the various fields. subseconds is constrained to be a unit with a precision that is a non-positive power of 10. This facilitates formatting the correct number of decimal digits to represent the fractions of a second.

And there are various date field types (e.g. year_month_day) which have conversions to and from the type std::chrono::time_point<system_clock, duration<int, ratio<86400>>> (a count of days since 1970-01-01).

One can construct a system_clock-based time_point like this:

auto tp = sys_days{30_d/March/2021} + 17h + 30min + 123ms; // 2021-03-30 17:30:00.123 UTC — You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub https://github.com/HowardHinnant/date/issues/655#issuecomment-810421551, or unsubscribe https://github.com/notifications/unsubscribe-auth/AA5EK4W2ALQVZJOHR24QNTDTGH7CZANCNFSM42CME35Q.

HowardHinnant commented 3 years ago

As we speak, Nicolai Josuttis is working on a new book that will better document this library (in its std form). No doubt his documentation will put mine to shame. ;-)

HowardHinnant commented 3 years ago

Given tp above, one can deconstruct it back into fields with:

auto td = floor<days>(tp);
year_month_day ymd = td;  // ymd has year(), month(), day() getters
hh_mm_ss tod{tp-td};  // tod has hours(), minutes(), seconds(), subseconds() getters
frostius commented 3 years ago

In case this is helpful to anyone else

  /// Decompose system time_point to millisecond resolution fields
  struct sys_ms_fields {
    date::year_month_day ymd;
    date::hh_mm_ss<std::chrono::milliseconds> hms;

    sys_ms_fields(std::chrono::system_clock::time_point tp) {
      auto td = floor<days>(tp);
      ymd = date::year_month_day(td);
      hms = date::hh_mm_ss(floor<std::chrono::milliseconds>(tp-td));
    }
    int            year()    { return static_cast<int>                     (ymd.year()); }
    unsigned short month()   { return (unsigned short)static_cast<unsigned>(ymd.month()); }
    unsigned short day()     { return (unsigned short)static_cast<unsigned>(ymd.day()); }
    unsigned short hours()   { return static_cast<unsigned short>          (hms.hours().count()); }
    unsigned short minutes() { return static_cast<unsigned short>          (hms.minutes().count()); }
    unsigned short seconds() { return static_cast<unsigned short>          (hms.seconds().count()); }
    unsigned short ms()      { return static_cast<unsigned short>          (hms.subseconds().count()); }
  };
sweihub commented 1 year ago

Is there a straight-forward way to make a hh-mm-ss, it's really clumsy to use the suffix, if not constants?

Why there's no such kind of easy stuff?

    int hours = 24;
    int minutes = 12;
    int seconds = 59;
    auto t = date::time_of_day<int> { hours, minutes, seconds};
HowardHinnant commented 1 year ago

This will do it:

    using namespace date;
    using namespace std::chrono;

    int h = 24;
    int m = 12;
    int s = 59;
    hh_mm_ss tod{hours{h} + minutes{m} + seconds{s}};
sweihub commented 1 year ago

Thanks for the quick reply, Now I am able to make date { year, month, day}, time { hours, mimutes, seconds} in std::chrono way, would you update your documentation for those who just need a straightforward example?

BTW, another question, why the date::year with int operaor, date::month & date::day with unsigned int operator? why not std::chrono::hours::count() convention?

#include <date/date.h>
#include <gtest/gtest.h>

TEST(DateTest, Basic)
{
    using namespace date;
    using namespace std::chrono;

    auto year = date::year(2023);
    auto month = date::month(12);
    auto day = date::day(13);
    date::year_month_day x(year, month, day);

    ASSERT_TRUE(x.year() == year && x.month() == month && x.day() == day);
    int y = (int)year;
    int m = (unsigned int)month;
    int d = (unsigned int)day;

    ASSERT_TRUE(y == 2023 && m == 12 && d == 13);

    int hh = 23;
    int mm = 12;
    int ss = 59;
    auto tod = date::time_of_day<std::chrono::seconds>{hours(hh) + hours(mm) + seconds(ss)};
}
HowardHinnant commented 1 year ago

I was working on the theory that a generic spelling for explicit type conversion would reduce the "apparent API".

In order to explicitly convert a value u of type U to a type T, this syntax often works:

T t{u};

For example, to convert an int to a year:

int i = 2023;
year y{i};

And so I made it symmetrical:

year y = 2023_y;
int i{y};

This was done in the hopes that the apparent API would become smaller because one no longer has to remember the spelling of a named getter. But that will only happen if this pattern becomes more widely spread.

I should also emphasize that the conversions in both directions (year to int and int to year) are explicit because the two types are not semantically equivalent. Though the conversion is necessary on a practical level, it is also a dangerous conversion in that the result has a different meaning than the source (even though the value is the same).

In general I encourage people to minimize conversions into and out of the chrono type system.

With all that in mind, here is alternate syntax for your test for your consideration:

TEST(DateTest, Basic)
{
    using namespace date;
    using namespace std::chrono;

    auto year = 2023_y;
    auto month = December;
    auto day = 13_d;
    auto x = year/month/day;

    ASSERT_TRUE(x.year() == 2023_y && x.month() == December && x.day() == 13_d);
    auto y = int{year};
    auto m = unsigned{month};
    auto d = unsigned{day};

    ASSERT_TRUE(y == 2023 && m == 12 && d == 13);

    int hh = 23;
    int mm = 12;
    int ss = 59;
    auto tod = hours{hh} + hours{mm} + seconds{ss};
}
sweihub commented 1 year ago

Thanks for the thoughtful & detailed explanation, thanks for the contributions!