awk next month last date logic issue

Viewed 87

I searched SO, but could find answers using programming languages like Java, Python, JS, etc. I'm trying to find the next month last date when a relative date is given as input to the awk. Looking for awk implementation.

Here is my logic, but it breaks for different inputs.

$ export DT="2020-12-31"

$ awk -v dt=$DT ' BEGIN { gsub(/-/," ",dt); print dt;dt2=mktime(dt " 0 0 0"); 
while(c<2) { dd=strftime("%d",dt2+(i++)*86400);if(dd*1==1)c++;} ; print strftime("%F",dt2+(i-2)*86400) } '
2020 12 31
2021-01-31 # Correct

Below one is giving wrong answer

$ export DT="2020-01-01"

$ awk -v dt=$DT ' BEGIN { gsub(/-/," ",dt); print dt;dt2=mktime(dt " 0 0 0"); 
while(c<2) { dd=strftime("%d",dt2+(i++)*86400);if(dd*1==1)c++;} ; print strftime("%F",dt2+(i-2)*86400) } '
2020 01 01
2020-01-31 # Wrong - Should be 2020-02-29

what is wrong with the logic?. I welcome other awk solutions

In a similar way, I need to calculate prior-month start & end dates, relative month start and end date, next month's start and end date in a robust way using awk.

Below is the code for previous month end date

$ awk -v dt="2021-01-30" ' BEGIN { gsub(/-/," ",dt); print dt;dt2=mktime(dt " 0 0 0"); 
                dy=strftime("%d",dt2); print strftime("%F",dt2-dy*86400) } '
2021 01 30
2020-12-31
3 Answers
$ cat tst.awk
BEGIN {
    split(dt,curYMD,/-/)

    if ( curYMD[2] < 12 ) {
        tgtYear = curYMD[1]
        tgtMth  = curYMD[2] + 1
    }
    else {
        tgtYear = curYMD[1] + 1
        tgtMth  = 1
    }

    begSecs = mktime(tgtYear" "tgtMth" 1 12 0 0",1)
    begDate = strftime("%Y-%m-%d",begSecs)

    for (day=31; mth!=tgtMth; day--) {
        endSecs = mktime(tgtYear" "tgtMth" "day" 12 0 0",1)
        mth = strftime("%m",endSecs)+0
    }
    endDate = strftime("%Y-%m-%d",endSecs)

    print dt":\t" begDate, "->", endDate
}

$ awk -v dt='2020-12-31' -f tst.awk
2020-12-31:     2021-01-01 -> 2021-01-31
$ awk -v dt='2020-01-01' -f tst.awk
2020-01-01:     2020-02-01 -> 2020-02-29
$ awk -v dt='2021-01-30' -f tst.awk
2021-01-30:     2021-02-01 -> 2021-02-28

Best I can tell the problems in your code are:

  1. Trying to use the average number of seconds in a day as the actual duration in seconds of all days to count in days instead of just counting in days, and
  2. Using midnight as the time each day for mktime() as that causes problems with leap seconds and DST changes too, always use noon instead when you're just doing day calculations. It wouldn't hurt to add the UTC flag too to avoid DST concerns.

I used a loop to find the actual end day of the month because mktime() will try to guess at what you meant if you give it a date with too large a number of days for the given month so just see if the current month really supports 31 days and if not, decrement til it does (obvious will only go down to 28 max).

Hopefully the equivalent logic for the prior month start/end date is obvious but in case it's not, just change the first if statement to:

if ( curYMD[2] > 1 ) {
    tgtYear = curYMD[1]
    tgtMth  = curYMD[2] - 1
}
else {
    tgtYear = curYMD[1] - 1
    tgtMth  = 12
}

Abusing the GNU awk's mktime. Split the date to its components, add 2 months (for example 12+2=14) and subtract a day from the first day (01-1=0), so 2020-12-31 -> 2020-14-00 == 2021-01-31:

$ awk -v dt="2020-12-31" '
BEGIN {
    split(dt,a,/-/)                       # split to get the date components
    e=mktime(a[1] " " a[2]+2 " 0 0 0 0")  # add 2 months and subtract a day from 1st day
    print strftime("%F",e)                # output
}'

Output:

2021-01-31

I recommend to go with James answer. But here is the fix to my code, in case if someone is interested in the crude way of manipulating date using epoch calculations. If I do pre-increment the "i" and then subtract it when printing, it gets right answer.

$ export DT="2020-12-31"

$ awk -v dt=$DT ' BEGIN { gsub(/-/," ",dt); print dt;dt2=mktime(dt " 0 0 0"); 
while(c<2) { dd=strftime("%d",dt2+(++i)*86400);if(dd*1==1)c++;} ; print strftime("%F",dt2+(i-1)*86400) } '
2020 12 31
2021-01-31

Case-2:

$ export DT="2020-01-01"
$ awk -v dt=$DT ' BEGIN { gsub(/-/," ",dt); print dt;dt2=mktime(dt " 0 0 0");
while(c<2) { dd=strftime("%d",dt2+(++i)*86400);if(dd*1==1)c++;} ; print strftime("%F",dt2+(i-1)*86400) } '
2020 01 01
2020-02-29
Related