0

When using at midnight next month and it is the 31 of January, how will it behave? Will it be the 4th of March or 28 of February? or the 31 of March?

ilkkachu
  • 138,973
  • 3
    this is something you could easily find out for yourself just by running something like echo ls | at midnight next month and then running atq. either wait for the last day of a month with 31 days (e.g. Aug 31, 3 days from today) that isn't July to try it or set your system clock to Jan 31 first (or in a VM). – cas Aug 27 '23 at 14:57

3 Answers3

3

This goal of this answer is to supplement the other (correct) answers you've already received and give some additional technical information.

When at commands gets the command NEXT <inc_dec_period>1, it takes the current date as a time structure (struct tm2), and adds "1" to the specified period.

So if at is instructed to run NEXT MONTH, it will take the current date, and add "1" to the month3, so that 0 (January) becomes 1 (February), 1 becomes 2 (March) and so on. This is equivalent to date -d 'now + 1 month' command.

Now, here's the important part: Both date and at commands then take the new time structure created after the manipulation, and use it as an argument to the mktime(3) library call that returns the new absolute time4. mktime(3) states at the documentation:

The mktime() function modifies the fields of the tm2 structure as follows: [...] if structure members are outside their valid interval, they will be normalized (so that, for example, 40 October is changed into 9 November);

So, for instance:

  • If the current date is 2023-08-31,
  • and the command is next month,
  • the new time structure would be 2023-09-31.

Since 31 is outside of the valid interval for September, then, as the documentation clarifies, it will be normalized to 2023-10-01.

$ date -d '2023-08-31 + 1 month' --rfc-3339=date
2023-10-01

And if the current date is 2023-01-31, the result of the same at command is a time structure of 2023-02-31, which is then normalized to 2023-03-03.

$ date -d '2023-01-31 + 1 month' --rfc-3339=date
2023-03-03
$ date -d '2024-01-31 + 1 month' --rfc-3339=date # leap year
2024-03-02

1 where inc_dec_period is one of the following:

MINUTE | HOUR | DAY | WEEK | MONTH | YEAR

2 The tm structure includes at least the following members:

int    tm_sec   seconds [0,61]
int    tm_min   minutes [0,59]
int    tm_hour  hour [0,23]
int    tm_mday  day of month [1,31]
int    tm_mon   month of year [0,11]
int    tm_year  years since 1900
int    tm_wday  day of week [0,6] (Sunday = 0)
int    tm_yday  day of year [0,365]
int    tm_isdst daylight savings flag

3 If the result of the month is > 11, it will also add 1 to the year field.

4 mktime returns data type time_t which represents calendar time. When interpreted as an absolute time value, it represents the number of seconds elapsed since the Epoch, 1970-01-01 00:00:00 +0000 (UTC).

aviro
  • 5,532
  • yes, indeed. The Linux and FreeBSD man pages at least seem to have that same example of October 40th, and that's what one logically gets by just adding days on top of the first of the month. But I had a hard time finding a source that would say that is indeed the/a standard behaviour. POSIX seems to defer to C, and at least cppreference.com only says that "The values in arg are not checked for being out of range." leaving it unsaid what an out-of-range value means. – ilkkachu Aug 28 '23 at 12:53
  • Right, but the example in this document shows otherwise: tm.tm_mon -= 100; // tm_mon is now outside its normal range, but then mktime(&tm) returns the correct date of 100 months ago. I think that "The values in arg are not checked for being out of range" actually means that "the original values of the other components are not restricted to their normal ranges", as the FreeBSD man pages (and POSIX) state. – aviro Aug 28 '23 at 13:03
  • @ilkkachu Also POSIX says: Upon successful completion, [...] the other components are set to represent the specified time since the Epoch, but with their values forced to the ranges indicated in the <time.h> entry; It doesn't specify how exactly it's forced, but I guess it's "normalized" like the Linux and FreeBSD man pages say. – aviro Aug 28 '23 at 13:11
  • ah I missed the example doing just that. And I don't really expect any system to do anything else either, but I was a bit surprised there was no explicit description of it anywhere. Technically I suppose saturation to the minimum or maximum value would also do to force the value to the range, as would even taking the modulus... – ilkkachu Aug 28 '23 at 15:29
2

March 3rd.

"Next month" means to go forward one month, to the day with the same number. So from Jan 31st, "next month" is Feb 31st, but that's equivalent to (counts with fingers...29, 30, 31) Mar 3rd. Or March 2nd if it's a leap year. Similarly, Mar 31st + one month would be May 1st, because Apr 31st only one day off of the end of April.

# date --set '2023-01-31 12:00'
Tue Jan 31 12:00:00 PM UTC 2023
# date
Tue Jan 31 12:00:00 PM UTC 2023
# at midnight next month
warning: commands will be executed using /bin/sh
at Fri Mar  3 00:00:00 2023
at> echo foo
at> <EOT>
job 2 at Fri Mar  3 00:00:00 2023

Similarly with date:

# date -d 'now + 1 month'
Fri Mar  3 12:01:32 PM UTC 2023

That's how it works on my Ubuntu system anyway.

ilkkachu
  • 138,973
  • I miss the logic (linux one), if its 31.01, midnight next month by logic is ANY day next month. For me if day of month is not explicitly defined it can be any date. So Linux receive one downvote from me :D – Romeo Ninov Aug 27 '23 at 15:01
  • BTW if the year is leap it will be 2 March. – Romeo Ninov Aug 27 '23 at 15:02
  • 1
    @RomeoNinov, yes, good point about leap years, thanks. – ilkkachu Aug 27 '23 at 15:06
  • In general, you don't have to set the time to the reference date if you want to perform such a check. You can provide the reference date in your date command, e.g date -d '2023-01-31 12:00 + 1 month' – aviro Aug 27 '23 at 15:34
  • @aviro, can you do that with at? – ilkkachu Aug 27 '23 at 15:45
  • @RomeoNinov next month is shorthand for now + 1 month, so it is not intended to be a random choice of day. But the issue of mapping 1..31 onto 1..28 exists in cron and awk, along with last Friday in month and similar business rules. I think cron should support negative days, -1 being the last day of the month etc, and also ISO week numbers. Shades of the French Republican calendar and Decimal Time. – Paul_Pedant Aug 27 '23 at 16:22
  • @Paul_Pedant, I do not speak well English, but in sentence "next month" I do not see ANY definition of date. So this approach (you mention) is accepted in UNIX/Linux, and it is NOT logical (for me). I know IT persons tend to shorten the expressions, so probably they accepted by default "same day of month". But for me if something is not defined explicitly is undefined by default. So the day of month can be any, only month should be the next one :) – Romeo Ninov Aug 27 '23 at 16:31
  • @Paul_Pedant, and if in cron the situation is same/similar record 0 0 2-3 1-31 *... must be executed two times on 1 March, 2 March and 3 March (if year is not leap) :D – Romeo Ninov Aug 27 '23 at 16:35
  • 2
    Agreeing with Romeo here, the documentation of at is really bad. Refers to POSIX.2 (not even referring to any section in that, instead of explaining what it does), then says it has "extensions to it", naming two very random examples, then refering to a syntax specification text file in /usr/share/doc/at without any semantics, but calling it "definition" (it's at best a declaration of syntax, and even that is questionable). It's honestly why I never use at: There's simply no reliable way to figure out how it works other than trying. – Marcus Müller Aug 27 '23 at 17:11
  • @MarcusMüller Agreed 95% of the syntax is vague enough to be considered random. The backstop is to do the complex stuff in date and then use the very specific styles at 17:45 2023-12-25 or at -t 202312251745. at now + 5 minutes and at now + 3 days seem reliable. – Paul_Pedant Aug 27 '23 at 22:16
  • 1
    @RomeoNinov No, cron does not wrap Feb 29, 30, 31st into March and run it twice, although I'm not prepared to hack my NTP to prove it. (I can set up an at job to email myself a reminder to check next February, I guess.) And pragmatically, at does carry forward any unspecified part of the current time when you advance by specified units, even if the man page does not specify that. It only scales for a single unit type, but at now + $(( 36 * 60 + 25 )) minutes works. – Paul_Pedant Aug 27 '23 at 22:47
  • 2
    @RomeoNinov, cron doesn't need to translate those dates into one that really exist, it only needs to check the current date against the numbers in the crontab – ilkkachu Aug 28 '23 at 05:54
2

To get the last day of the month after the current one, you can use date to force the current date away from the end date of this month (to avoid wrapping over an extra month), date again to jump two months, set the days to 01, and go back one day. It's a one-liner.

Edited: The first version posted failed on four dates where the next-but-one month was shorter than the current one: December 29-31st and July 31st.

$ date  # To show where we are now.
Tue 29 Aug 16:29:44 BST 2023
$ echo | at "$( date -d "$( date '+%Y-%m-15' ) + 2 months" '+%Y-%m-01') - 1 day"
warning: commands will be executed using /bin/sh
job 73 at Sat Sep 30 16:29:00 2023
$ atq
73  Sat Sep 30 16:29:00 2023 a paul

Some extension of this idea would be able to use the %w (day of week) option to shift the date to the last Friday of the month (for example).

Paul_Pedant
  • 8,679