20

https://www.timeanddate.com/date/weekday.html computes various facts about a day of the year, for example:

https://i.stack.imgur.com/WPWuO.png

Given an arbitrary date, how can these numbers be computed with the C++20 chrono specification?

Howard Hinnant
  • 206,506
  • 52
  • 449
  • 577

1 Answers1

28

This is remarkably easy with the C++20 chrono specification. Below I show a function which inputs an arbitrary date, and prints this information to cout. Though at the time of this writing, the C++20 chrono specification isn't yet shipping, it is approximated by a free, open-source library. So you can experiment with it today, and even include it in shipping applications as long as you adopt C++11 or later.

This answer will take the form of a function:

void info(std::chrono::sys_days sd);

sys_days is a day-precision time_point in the system_clock family. That means it is simply a count of days since 1970-01-01 00:00:00 UTC. The type alias sys_days is new with C++20, but the underlying type has been available since C++11 (time_point<system_clock, duration<int, ratio<86400>>>). If you use the open-source C++20 preview library, sys_days is in namespace date.

The code below assumes function-local:

using namespace std;
using namespace std::chrono;

to reduce verbosity. If you are experimenting with the open-source C++20 preview library, also assume:

using namespace date;

Heading

To output the first two lines is simple:

cout << format("{:%d %B %Y is a %A}\n", sd)
     << "\nAdditional facts\n";

Just take the date sd and use format with the familiar strftime/put_time flags to print out the date and text. The open-source C++20 preview library hasn't yet integrated the fmt library, and so uses the slightly altered format string "%d %B %Y is a %A\n".

This will output (for example):

26 December 2019 is a Thursday

Additional facts

Common intermediate results computed once

This section of the function is written last, because one doesn't yet know what computations will be needed multiple times. But once you know, here is how to compute them:

year_month_day ymd = sd;
auto y = ymd.year();
auto m = ymd.month();
weekday wd{sd};
sys_days NewYears = y/1/1;
sys_days LastDayOfYear = y/12/31;

We will need the year and month fields of sd, and the weekday (day of the week). It is efficient to compute them once and for all in this manner. We will also need (multiple times) the first and last days of the current year. It is hard to tell at this point, but it is efficient to store these values as type sys_days as their subsequent use is only with day-oriented arithmetic which sys_days is very efficient at (sub-nanosecond speeds).

Fact 1: day number of year, and number of days left in year

auto dn = sd - NewYears + days{1};
auto dl = LastDayOfYear - sd;
cout << "* It is day number " << dn/days{1} << " of the year, "
     << dl/days{1} << " days left.\n";

This prints out the day number of the year, with January 1 being day 1, and then also prints out the number of days remaining in the year, not including sd. The computation to do this is trivial. Dividing each result by days{1} is a way to extract the number of days in dn and dl into an integral type for formatting purposes.

Fact 2: Number of this weekday and total number of weekdays in year

sys_days first_wd = y/1/wd[1];
sys_days last_wd = y/12/wd[last];
auto total_wd = (last_wd - first_wd)/weeks{1} + 1;
auto n_wd = (sd - first_wd)/weeks{1} + 1;
cout << format("* It is {:%A} number ", wd) << n_wd << " out of "
     << total_wd << format(" in {:%Y}.\n}", y);

wd is the day of the week (Monday thru Sunday) computed at the top of this article. To perform this computation we first need the dates of the first and last wd's in the year y. y/1/wd[1] is the first wd in January, and y/12/wd[last] is the last wd in December.

The total number of wds in the year is just the number of weeks between these two dates (plus 1). The sub-expression last_wd - first_wd is the number of days between the two dates. Dividing this result by 1 week results in an integral type holding the number of weeks between the two dates.

The week number is done the same way as the total number of weeks except one starts with the current day instead of the last wd of the year: sd - first_wd.

Fact 3: Number of this weekday and total number of weekdays in month

first_wd = y/m/wd[1];
last_wd = y/m/wd[last];
total_wd = (last_wd - first_wd)/weeks{1} + 1;
n_wd = (sd - first_wd)/weeks{1} + 1;
cout << format("* It is {:%A} number }", wd) << n_wd << " out of "
     << total_wd << format(" in {:%B %Y}.\n", y/m);

This works just like Fact 2, except we start with the first and last wds of the year-month pair y/m instead of the entire year.

Fact 4: Number of days in year

auto total_days = LastDayOfYear - NewYears + days{1};
cout << format("* Year {:%Y} has ", y) << total_days/days{1} << " days.\n";

The code pretty much speaks for itself.

Fact 5 Number of days in month

total_days = sys_days{y/m/last} - sys_days{y/m/1} + days{1};
cout << format("* {:%B %Y} has ", y/m) << total_days/days{1} << " days.\n";

The expression y/m/last is the last day of the year-month pair y/m, and of course y/m/1 is the first day of the month. Both are converted to sys_days so that they can be subtracted to get the number of days between them. Add 1 for the 1-based count.

Use

info can be used like this:

info(December/26/2019);

or like this:

info(floor<days>(system_clock::now()));

Here is example output:

26 December 2019 is a Thursday

Additional facts
* It is day number 360 of the year, 5 days left.
* It is Thursday number 52 out of 52 in 2019.
* It is Thursday number 4 out of 4 in December 2019.
* Year 2019 has 365 days.
* December 2019 has 31 days.

Edit

For those who are not fond of the "conventional syntax", there is a complete "constructor syntax" that can be used instead.

For example:

sys_days NewYears = y/1/1;
sys_days first_wd = y/1/wd[1];
sys_days last_wd = y/12/wd[last];

can be replaced by:

sys_days NewYears = year_month_day{y, month{1}, day{1}};
sys_days first_wd = year_month_weekday{y, month{1}, weekday_indexed{wd, 1}};
sys_days last_wd = year_month_weekday_last{y, month{12}, weekday_last{wd}};
Howard Hinnant
  • 206,506
  • 52
  • 449
  • 577
  • 8
    This new abuse of the division operator is even worse than the old abuse of the bitshift operators. It makes me sad :( – Dave Dec 20 '19 at 13:02
  • 3
    On a more serious note, can I suggest that you move some of your pre-computed variables down to the sections which use them? It's a bit awkward to follow when having to scroll up and down to see where values come from and how they were generated. And you can de-clutter your day-of-year stuff a bit by doing the division first, like you did for the weeks. – Dave Dec 20 '19 at 13:04
  • 2
    Disagree completely. It looks good, it's easy to understand and, notably, it's easier to read than the more verbose version. – Not a real meerkat Jan 06 '20 at 23:53
  • @CássioRenan might be, but remember that syntax abuse quite often comes with unexpected behavior. With the aforementioned bit shifts, e.g., note the behavior of `std::cout << "a*b = " << a*b << "; a^b = " << a^b << '\n';` (which is, fortunately, almost always caught at compile time, but still is an annoyance). So I'd be cautious when using this new division operator abuse. – Ruslan Jan 13 '20 at 21:08
  • 3
    @Ruslan Caution is always warranted with any new library. That's why this one has been freely and publicly tested since 2015. The feedback from clients has been incorporated back into the design. It was not proposed for standardization until it had a solid foundation of years of positive field experience. In particular, the use of operators has been designed with operator precedence in mind, field tested widely, and comes with an equivalent "constructor API". See https://star-history.t9t.io/#HowardHinnant/date&google/cctz and https://www.youtube.com/watch?v=tzyGjOm8AKo . – Howard Hinnant Jan 13 '20 at 23:37
  • Regarding computation of the day of the year, would an expression like `floor(sd - floor(sd))` also be a [correct] alternative? And would it be a viable approach, again compared to the one in the answer? – Armen Michaeli Apr 23 '23 at 18:28
  • 1
    It would not give you exactly the same answer. I've given a "calendrical computation", and your suggestion is a "chronological computation". The difference is described for months in more detail here: https://stackoverflow.com/a/43018120/576911. The same analysis applies for `years` as it does for `months`. To do calendrical arithmetic with `years` you must be using calendrical types such as `year_month_day`. And `sys_days` is a chronological type. – Howard Hinnant Apr 23 '23 at 20:38