HowardHinnant / date

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

Question about mutability of year_month_day and time_of_day (hh_mm_ss) #552

Open CKD4XXH opened 4 years ago

CKD4XXH commented 4 years ago

Given some year_month_day or hh_mm_ss variable,

year_month_day ymd{d};
hh_mm_ss<milliseconds> hms{l - d};

How can i do things like the following pseudo-code?

ymd.day() = 20_d;
ymd.day() += days{2};

ymd.month() = x3;

hms.hours() = x1;
hms.hours() += x2;

Are the year_month_day and hh_mm_ss objects mutable?

(Regarding the += method: does it mutates? Or somehow creates a new object? (What happens with the old object?))

HowardHinnant commented 4 years ago

ymd.day() = 20_d;

ymd = ymd.year()/ymd.month()/20;

or:

ymd = ymd.year()/ymd.month()/20_d;

ymd.day() += days{2};

ymd = ymd.year()/ymd.month()/(ymd.day() + days{2});

The above expression might create a day field that is beyond the end of the month for ymd.year()/ymd.month(). If that happens, ymd.ok() == false.

ymd.month() = x3;

ymd = ymd.year()/x3/ymd.day();

Ditto about the subsequent possibility that ymd.ok() == false.

hms.hours() = x1;

hms = hh_mm_ss{hms.to_duration() - hms.hours() + x1};

hms.hours() += x2;

hms = hh_mm_ss{hms.to_duration() + x2};

In summary, year_month_day is good for getting the year, month and day fields. It is also good for year and month arithmetic. It isn't that good at day arithmetic unless you know that you're not going to overflow the day field. sys_days is the right data structure for general purpose day arithmetic. And it is also good at converting year, month and day fields into a chrono::time_point.

In a nutshell, year_month_day is not your one-stop-shopping destination for all of your date needs. It is convenient only for those things that the underlying data structure is actually good at. Other data structures, such as sys_days are more convenient for other things, which they happen to be efficient for. And only with the union of all of the calendrical and chronological types, you get complete functionality.

hh_mm_ss is barely more than a formatting aid. It is a convenient way to break a duration down into human readable hours, minutes, seconds fields. Its most important contribution to functionality is taking an arbitrary precision duration and figuring out how many decimal digits of seconds will exactly represent that duration, if it is possible at all (which is non-trivial but necessary as part of the formatting process). For example:

using quarters = duration<int, ratio<1, 4>>;
hh_mm_ss hms{quarters{17}};
cout << "hms.fractional_width = " << hms.fractional_width << '\n';
cout << "hms.to_duration() = " << hms.to_duration() << '\n';
cout << "hms = " << hms << '\n';

Output:

hms.fractional_width = 2
hms.to_duration() = 425cs
hms = 00:00:04.25
CKD4XXH commented 4 years ago

sys_days is the right data structure for general purpose day arithmetic

Even when the ymd is "local"? I mean:

// 'zoned' is some zoned time
date:local_time<std::chrono::milliseconds> ltim = zoned.get_local_time();
date:year_month_day ymd{date:floor<date:days>(ltim)};

When to use sys_days and when to use local_days?

When I tried to test, did not found any difference. They give the same results:

auto ymd1 = date:year_month_day{date:sys_days{ymd}};
stdate:cout << ymd1 << (ymd1.ok() ? "\n" : " invalid date\n");
ymd = date:year_month_day{date:local_days{ymd}};
stdate:cout << ymd << (ymd.ok() ? "\n" : " invalid date\n");

(I do not thoroughly understand, why.)

HowardHinnant commented 4 years ago

local_days is also a good data structure to represent a date. It is just a count of days since 1970-01-01, just like sys_days. The only difference between sys_days and local_days is that sys_days represents a time point in UTC, and local_days represents a time point that has no associated time zone. Though a time zone can be paired with local_days to give it a precise meaning.

When converting among year_month_day <-> local_days, the exact same formulas are used as converting among year_month_day <-> sys_days. It's just that local_days and sys_days have subtly different meanings.

Example:

auto ymd = March/12/2020;
local_days ld{ymd};
sys_days   sd{ymd};

auto sydney_zone = locate_zone("Australia/Sydney");

zoned_time zt1{sydney_zone, ld};
zoned_time zt2{sydney_zone, sd};

cout << zt1.get_sys_time() << '\n';
cout << zt2.get_sys_time() << '\n';

Above we start with a year_month_day. Then we convert that to both local_days and sys_days. If we were to peek under the hood of both ld and sd we would see the exact same value (same count of days).

Next we create two zoned_times, both using "Australia/Sydney", but one using the local_days value and one using the sys_days value. These two zoned_times represent different instances in time. To see that, the UTC equivalent of each of these times is printed out:

2020-03-11 13:00:00
2020-03-12 00:00:00

The first does not even have the same date. The second is actually the same time that we constructed with. Equivalently we could have printed out the local time of both:

cout << zt1 << '\n';
cout << zt2 << '\n';

Output:

2020-03-12 00:00:00 AEDT
2020-03-12 11:00:00 AEDT

Now the first output is the identity operation with respect to the constructed time.


So for casual calendrical operations that need to convert between the {y, m, d} data structure, and the {count of days} data structure, it really doesn't make a lot of difference if you use local_days or sys_days. Whichever you find more readable is the winner.

But when dealing with time zones, and in particular zoned_time, it is critically important that you use local_days (local_time<Duration> in general) when you mean "local time", and sys_days (sys_time<Duration> in general) when you mean UTC.

sabelka commented 4 years ago

Howard, sorry for chiming in on this old thread, but I have a question regarding your last comment. I noticed that the conversions between sys_days and year_month_day are implicit, but they are explicit between local_days and year_month_day. So I wonder what was the design decision, to make it like this and not the other way round. Since both year_month_day and local_days have no relation to a particular clock or time zone, I would have thought it would be logical to allow direct conversions between those data types.

PS: Thank you for providing this great library - not only is it very useful, it also has taught me a lot on better c++ API design!

HowardHinnant commented 4 years ago

This is a good question.

First, the reason that the conversions between year_month_day and local_days/sys_days aren't both implicit is that when I tried that I got ambiguities in some inconvenient expressions. So only one of them can be implicit. And the reason sys_days was chosen as the implicit one is largely historical, as opposed to a solid technical reason: sys_days came first in the development cycle. local_days was invented only during the later development of the time zone library.

It would not be a bad design choice to make local_days the one with the implicit conversion. However I never came up with a sufficiently motivating use case to change the status quo.

sabelka commented 4 years ago

Thank you for your answer!

In my application I had to do lots of date operations with no particular time zone, like finding the start/end of business quarters, calculating deadlines, etc. So I started my implementation using sys_days. But later, occasionally, I had to determine if a certain time point (on the system_clock) is within a given day. There, I have to associate the date of the day with the current local time zone before I can do the actual comparison. Using sys_days here could lead to errors because it already implies UTC, and one could easily forget to convert it to the local time zone. So I changed my implementation to use local_days instead of sys_days, which, I think, matches my use case much better. But then, I hat to add explicit conversions between year_month_day and local_days all over my code, which makes it more verbose than it was before. So, for my application it would definitely be beneficial if local_days would be implicitly convertible. But I'm afraid that a change to the library at this point in time will no longer be so easy, since it would cause other programs depending on the current behavior to break.

HowardHinnant commented 4 years ago

Yes, that is a good use case. And you did exactly the right thing. Thanks for the feedback.