Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I find the Nth weekday of the month using chrono?

There are lots of Stack Overflow Q&A's solving this problem in other languages, but not so much in C++.

How do I find the Nth weekday of the month using <chrono>? For example, what is the day of the month for the second Sunday in June, 2025?

What if I ask for the 5th weekday and there isn't one for that year and month combination? Do I get an error?

How can I ask for the last weekday of a month without knowing or testing whether it has 4 or 5 weekdays in that month/year?

How do I schedule repeating events? For example, I want to set up a meeting for the second Sunday of every month.

Given that different parts of the world are on different days depending on the time of day, how do I localize this to a time zone? For example I want my meeting to be on the second Sunday of every month as defined by Sydney Australia. But Sydney is one of the first places on Earth to have a Sunday. If it is Sunday morning in Sydney, most of the rest of the planet is still on Saturday.

Can the second Sunday morning (say, 9 am) meeting in Sydney be translated into local times in other places?

like image 772
Howard Hinnant Avatar asked Sep 06 '25 03:09

Howard Hinnant


1 Answers

<chrono> has a dedicated calendrical type just for this purpose.

First a refresher: In <chrono> you can specify a date in the civil calendar using year, month and day in one of three ways:

  • year/month/day
  • month/day/year
  • day/month/year

All of these expressions produce a type known as std::chrono::year_month_day.

Anywhere above that you can specify the day, you can also specify the day by using a std::chrono::weekday_indexed which is conveniently constructed with the syntax wd[n] where wd is a std::chrono::weekday and n is unsigned and in the range [1, 5].

For example, Canadian Rivers Day is the second Sunday in June of every year. In <chrono> this would look like June/Sunday[2], or Sunday[2]/June. In either expression one could replace June with 6, or a variable of type std::chrono::month with the value June, and still get the same result.

And one can pair this with a year simply as:

auto date = 2025y/June/Sunday[2];
auto date = June/Sunday[2]/2025;
auto date = Sunday[2]/June/2025;

I.e. You can still use any of the three orderings: y/m/d, m/d/y or d/m/y. And anywhere you want to specify a day, you can use a weekday_indexed in that position. The type of each of these expressions is std::chrono::year_month_weekday.

When you stream it out, it will output the exact information you put into it:

using namespace std::chrono;
auto date = Sunday[2]/June/2025;
std::cout << date << '\n';  // 2025/Jun/Sun[2]

If you want this in a {year, month, day} data structure, simply explicitly convert to it:

std::cout << year_month_day{date} << '\n';  // 2025-06-08

Essentially year_month_weekday is yet another calendar supplied by <chrono>. And you can convert to and from year_month_day, or any other calendar that interoperates with the <chrono> calendrical system. The conversion happens by implicitly converting to the canonical calendar sys_days under the hood as an intermediate step.


What if I ask for the 5th weekday and there isn't one for that year and month combination?

For example, what if one asks for the fifth Friday of June in 2025:

auto date = Friday[5]/June/2025;
std::cout << date << '\n';
std::cout << date.ok() << '\n';

This outputs:

2025/Jun/Fri[5]
0

That is, the construction works just fine, and you can print it out and it gives you exactly what you put in. But every calendrical type in <chrono> has a .ok() member function that prints out true if it might be ok, and otherwise false. Since there are only four Fridays in June 2025, the output in this example is false. But you can still convert it to a year_month_day and it will simply roll over to the first Friday in July:

std::cout << year_month_day{date} << '\n';  // 2025-07-04

You can depend on an invariant that wd[n] is always seven days after wd[n-1]. Though the implementation of weekday_indexed is only required to hold the least significant 3 bits of the index. So don't get carried away with arbitrarily large indices.

The rationale for this design is to make year and month arithmetic on year_month_weekday "regular". That is, no matter what the value of the year_month_weekday is, if you add years and/or months to it, and then subtract the same amount, you are guaranteed to get back the original value.

Example:

auto print = [](auto date)
{
    std::cout << date << " is ";
    if (date.ok())
        std::cout << "valid\n";
    else
        std::cout << "not valid\n";
};
using namespace std::chrono;
auto date = Sunday[5]/June/2025;
print(date);
date += months{1};
print(date);
date -= months{1};
print(date);

Output:

2025/Jun/Sun[5] is valid
2025/Jul/Sun[5] is not valid
2025/Jun/Sun[5] is valid

In this example the fifth Sunday of July is merely an intermediate result, and not an error. But you can query each result for validity and take whatever action is appropriate for your application (ignore, throw an exception, wrap to the end of the month, print out "not valid", etc.).


How can I ask for the last weekday of a month without knowing or testing whether it has 4 or 5 weekdays in that month/year?

You can index a weekday with last to get the last weekday of the year and month:

Example:

auto print = [](auto date)
{
    std::cout << date << " is ";
    if (date.ok())
        std::cout << "valid\n";
    else
        std::cout << "not valid\n";
};
using namespace std::chrono;
auto date = Sunday[last]/June/2025;
print(date);
date += months{1};
print(date);
date -= months{1};
print(date);

Output:

2025/Jun/Sun[last] is valid
2025/Jul/Sun[last] is valid
2025/Jun/Sun[last] is valid

When you perform year and month arithmetic on this value, the last sticks with it even if the meaning of last jumps between 4 and 5. And at any stage you can still explicitly convert such a date to a year_month_day:

date += months{1};                  // date is 2025/Jul/Sun[last]
std::cout << year_month_day{date};  // 2025-07-27

How do I schedule repeating events?

So it should now be clear that if you need to schedule an event on the second Sunday of every month, you can just choose a start date, e.g. Sunday[2]/January/2025, and add months{1} to get the next event, and so on.

Ditto if it is the last weekday of the month: Sunday[last]/January/2025.


How do I localize this to a time zone?

You can convert the year_month_weekday to a local_days, optionally add a time of day to it, and pair it with a time_zone to create a repeating meeting on the local Nth weekday of the month.

std::chrono::zoned_time (used in the example below) is simply a convenience wrapper around a time_zone and sys_time time point. The zoned_time constructor's 2nd argument is a time point that can be specified in one of 3 ways:

  1. A sys_time (UTC).
  2. A local_time in terms of the time_zone supplied as the 1st argument.
  3. Another zoned_time in which case the newly constructed zoned_time will have the same UTC equivalent as the argument zoned_time. That is, both zoned_times will refer to the same instant in time.

Example:

#include <chrono>
#include <iostream>

int
main()
{
    // Meet at 9 am on the second Sunday of every month of 2025 in Sydney
    // Send notice of the equivalent time in both UTC and Los Angeles local time
    using namespace std::chrono;
    auto tz_sy = locate_zone("Australia/Sydney");
    auto tz_la = locate_zone("America/Los_Angeles");
    for (auto date = Sunday[2]/January/2025; date.year() == 2025y; date += months{1})
    {
        zoned_time zt{tz_sy, local_days{date} + 9h};
        std::cout << zt << " == "
                  << zt.get_sys_time() << " UTC == "
                  << zoned_time{tz_la, zt} << '\n';
    }
}

Output:

2025-01-12 09:00:00 AEDT == 2025-01-11 22:00:00 UTC == 2025-01-11 14:00:00 PST
2025-02-09 09:00:00 AEDT == 2025-02-08 22:00:00 UTC == 2025-02-08 14:00:00 PST
2025-03-09 09:00:00 AEDT == 2025-03-08 22:00:00 UTC == 2025-03-08 14:00:00 PST
2025-04-13 09:00:00 AEST == 2025-04-12 23:00:00 UTC == 2025-04-12 16:00:00 PDT
2025-05-11 09:00:00 AEST == 2025-05-10 23:00:00 UTC == 2025-05-10 16:00:00 PDT
2025-06-08 09:00:00 AEST == 2025-06-07 23:00:00 UTC == 2025-06-07 16:00:00 PDT
2025-07-13 09:00:00 AEST == 2025-07-12 23:00:00 UTC == 2025-07-12 16:00:00 PDT
2025-08-10 09:00:00 AEST == 2025-08-09 23:00:00 UTC == 2025-08-09 16:00:00 PDT
2025-09-14 09:00:00 AEST == 2025-09-13 23:00:00 UTC == 2025-09-13 16:00:00 PDT
2025-10-12 09:00:00 AEDT == 2025-10-11 22:00:00 UTC == 2025-10-11 15:00:00 PDT
2025-11-09 09:00:00 AEDT == 2025-11-08 22:00:00 UTC == 2025-11-08 14:00:00 PST
2025-12-14 09:00:00 AEDT == 2025-12-13 22:00:00 UTC == 2025-12-13 14:00:00 PST

Demo.

Notes:

  • The Sydney local time is always 9 am, and the date is always the second Sunday.
  • The time zone abbreviation changes between AEDT and AEST.
  • The equivalent UTC time of day changes between 10 pm and 11 pm, and is always the previous date.
  • The equivalent Los Angeles time of day changes between 2 pm, 3 pm and 4 pm, and is always the previous date. It's always extra fun when different time zones change between standard time and daylight saving time on different dates and in different directions. ;-) That is, sometimes Sydney is ahead of LA by 17h, sometimes by 18h, and sometimes by 19h. But <chrono> allows you to compute at a higher level of abstraction than dealing with UTC offsets directly.
like image 159
Howard Hinnant Avatar answered Sep 07 '25 22:09

Howard Hinnant