Jack
Jack

Reputation: 1693

How to use bash to get the last day of each month for the current year without using if else or switch or while loop?

As we know that each year have the following max day in each month as follows:

Jan - 31 days
Feb - 28 days / 29 days (leap year)
Mar - 31 days
Apr - 30 days
May - 31 days
Jun - 30 days
Jul - 31 days
Aug - 31 days
Sep - 30 days
Oct - 31 days
Nov - 30 days
Dec - 31 days

How to I get bash to return the value (last day of each month) for the current year without using if else or switch or while loop?

Upvotes: 21

Views: 52187

Answers (15)

RARE Kpop Manifesto
RARE Kpop Manifesto

Reputation: 2805

UPDATE 1 : Function code further streamlined to minimize redundant calculations.


Unless you insist on shell-only purity, here's a fully POSIX-compliant awk-based solution that has built-in leap year calculation while COMPLETELY avoiding the need of any lookup tables or reference strings.

Unlike other suggested solutions, for months other than February, modulo op is only performed once instead of twice by replacing

(mm - 1) % 7 % 2

with

(odd month) XNOR (before August)

jot -w '2023 %d' 12 | awk '!_; ++$!_' | gsort -k 1,1n -k 2,2n  |

function monyear2numdays(__, ___, _) {

    return ((__ = +__) % (_ += _ ^= _<_)  == (__++ < _^++_)) + _^_ \
           + (_ - __ ? _ :  --__^(___  == "" || (___ = +___) % ++_ \
           ? !_ : ___ % (_ *= _ * ++_) || ___ % (__ * _ * __) == !_))
}

awk '$++NF = monyear2numdays($2, $1)'

    2023  1 31        2024  1 31
    2023  2 28        2024  2 29
    2023  3 31        2024  3 31
    2023  4 30        2024  4 30

    2023  5 31        2024  5 31
    2023  6 30        2024  6 30
    2023  7 31        2024  7 31
    2023  8 31        2024  8 31

    2023  9 30        2024  9 30
    2023 10 31        2024 10 31
    2023 11 30        2024 11 30
    2023 12 31        2024 12 31

Month field (__) values beyond [1, 12] yield meaningless results. The year field (___) is optional. Empty year inputs defaults to common year values. Zero-padded YYYY values are accepted but completely optional.

The function only requires 1 extra temp variable on top of the 2 mandatory ones for accepting user input to calculate all necessary constants, thresholds, offsets, and leap-year logic on-the-fly.

If, instead, you have a pre-made leap year indicator boolean flag ("y"), then this tiny tiny expression is all you need to calculate number of days

27+(m-2?(m<8~m%2)+3:m^y)m^y is exponentiation

Upvotes: 0

Ren&#233; Roth
Ren&#233; Roth

Reputation: 2106

A solution that should work across all unix systems including OSX, using cal and string parsing instead of relying on a certain version of date:

# fetches the current year
year=$(date +%Y)
# iterates over the months
for month in $(seq 1 12);
do
  # `cal` gets a shell text calendar for the given month and year,
  # then `awk` parses out the last day by going over every cell and
  # storing the last one in `days`.
  days=$(cal $month $year | awk 'NF{days = $NF};END{print days}')
  # This adds a leading zero.
  # You won't need this in bash v4, zsh and some others,
  # where you can use leading zeroes in the `for` declaration:
  # `for month in {01..12}`
  month=$(printf "%02d" $month)
  # Change this to whichever output format you need.
  echo "$month/$year: $days days"
done

Will print something like this:

01/2024: 31 days
02/2024: 29 days
03/2024: 31 days
04/2024: 30 days
05/2024: 31 days
06/2024: 30 days
07/2024: 31 days
08/2024: 31 days
09/2024: 30 days
10/2024: 31 days
11/2024: 30 days
12/2024: 31 days

Can be a oneliner:

year=$(date +%Y);for month in $(seq 1 12);do days=$(cal $month $year | awk 'NF{days = $NF};END{print days}');echo "$(printf "%02d" $month)/$year: $days days";done

Upvotes: 0

FoxBuster
FoxBuster

Reputation: 1

Following the same answer of glenn jackman, but with an update; his answer originally used the "+ 1 month - 1 day" configuration, however I've attempted his code but it didn't work properly - for some unknown reason, my date command returned the date + 29 days (in my test event, it was a December date and it returned Dec 30, instead of Dec 31).

My recommendation is that you switch the "1 month" for "next month", which will return the correct results properly.

In my case, I had to collect from another variable the year and month, and so it ended up like this:

date -d "$AnalysisYear-$AnalysisMonth-01 00:00:00 + next month - 1 second" "+%d"

Upvotes: 0

Josiah DeWitt
Josiah DeWitt

Reputation: 1802

Returns the number of days in the month compensating for February changes in leap years without looping, using an if statement or forking to a binary.

This code tests date to see if Feb 29th of the requested year is valid, if so then it updates the second character in the day offset string. The month argument selects the respective substring and adds the month difference to 28.

function daysin()
{
   s=303232332323                                        # normal year
   ((!($2%4)&&($2%100||!($2%400)))) && s=313232332323      # leap year
   echo $[ ${s:$[$1-1]:1} + 28 ]
}

daysin $1 $2                                               #daysin [1-12] [YYYY]

Upvotes: 4

Aeronautix
Aeronautix

Reputation: 316

Building on patm's answer using BSD date for macOS (patm's answer left out December):

for i in {1..12}; do date -v1m -v1d -v+"$i"m -v-1d "+%b - %d days"; done
Explanation:

-v, when using BSD date, means adjust date to:

-v1m means go to first month (January of current year).

-v1d means go to first day (so now we are in January 1).

-v+"$i"m means go to next month.

-v-1d means subtract one day. This gets the last day of the previous month.

"+%b - %d days" is whatever format you want the output to be in.

This will output all the months of the current year and the number of days in each month. The output below is for the as-of-now current year 2022:

Jan - 31 days
Feb - 28 days
Mar - 31 days
Apr - 30 days
May - 31 days
Jun - 30 days
Jul - 31 days
Aug - 31 days
Sep - 30 days
Oct - 31 days
Nov - 30 days
Dec - 31 days

Upvotes: 1

I needed this few times, so when in PHP comes with easy in bash is not, so I used this till throw me error "invalid arithemtic operator" and even with warrings in spellcheck ( "mt" stands for month, "yr" for year )

last=$(echo $(cal ${mt} ${yr}) | awk '{print $NF}')

so this works fine...

### get last day of month
#
# implement from PHP
# src: https://www.php.net/manual/en/function.cal-days-in-month.php
#
if [ $mt -eq 2 ];then
    if [[ $(bc <<< "${yr} % 4") -gt 0 ]];then
        last=28
    else
        if [[ $(bc <<< "${yr} % 100") -gt 0 ]];then
            last=29
        else
            [[ $(bc <<< "${yr} % 400") -gt 0 ]] && last=28 || last=29
        fi
    fi
else
    [[ $(bc <<< "(${mt}-1) % 7 % 2") -gt 0 ]] && last=30 || last=31
fi

Upvotes: 0

crazyswissie
crazyswissie

Reputation: 181

cal $(date +"%m %Y") |
awk 'NF {DAYS = $NF}; END {print DAYS}'

This uses the standard cal utility to display the specified month, then runs a simple Awk script to pull out just the last day's number.

Upvotes: 8

Sharuzzaman Ahmat Raslan
Sharuzzaman Ahmat Raslan

Reputation: 1657

A variation for the accepted answer to show the use of "yesterday"

$ for m in {1..12}; do date -d "yesterday $m/1 + 1 month" "+%b - %d days"; done
Jan - 31 days
Feb - 28 days
Mar - 31 days
Apr - 30 days
May - 31 days
Jun - 30 days
Jul - 31 days
Aug - 31 days
Sep - 30 days
Oct - 31 days
Nov - 30 days
Dec - 31 days

How it works?

Show the date of yesterday for the date "month/1" after adding 1 month

Upvotes: 0

patm
patm

Reputation: 1486

On a Mac which features BSD date you can just do:

for i in {2..12}; do date -v1d -v"$i"m -v-1d "+%d"; done

Quick Explanation

-v stands for adjust. We are adjusting the date to:

-v1d stands for first day of the month

-v"$i"m defined the month e.g. (-v2m for Feb)

-v-1d minus one day (so we're getting the last day of the previous month)

"+%d" print the day of the month

for i in {2..12}; do date -v1d -v"$i"m -v-1d "+%d"; done
31
28
31
30
31
30
31
31
30
31
30

You can add year of course. See examples in the manpage (link above).

Upvotes: 2

Walk
Walk

Reputation: 1649

Try using this code

date -d "-$(date +%d) days  month" +%Y-%m-%d

Upvotes: 5

Valentin H
Valentin H

Reputation: 7448

for m in $(seq 1 12); do cal  $(date +"$m %Y") | grep -v "^$" |tail -1|grep -o "..$"; done
  • iterate from 1 to 12 (for...)
  • print calendar table for each month (cal...)
  • remove empty lines from output (grep -v...)
  • print last number in the table (tail...)

There is no sense, to avoid using cal, because it is required by POSIX, so should be there

Upvotes: 0

glenn jackman
glenn jackman

Reputation: 246799

my take:

for m in {1..12}; do
  date -d "$m/1 + 1 month - 1 day" "+%b - %d days"; 
done

To explain: for the first iteration when m=1 the -d argument is "1/1 + 1 month - 1 day" and "1/1" is interpreted as Jan 1st. So Jan 1 + 1 month - 1 day is Jan 31. Next iteration "2/1" is Feb 1st, add a month subtract a day to get Feb 28 or 29. And so on.

Upvotes: 54

amdn
amdn

Reputation: 11582

Assuming you allow "for", then the following in bash

for m in {1..12}; do
    echo $(date -d $m/1/1 +%b) - $(date -d "$(($m%12+1))/1 - 1 days" +%d) days
done

produces this

 Jan - 31 days
 Feb - 29 days
 Mar - 31 days
 Apr - 30 days
 May - 31 days
 Jun - 30 days
 Jul - 31 days
 Aug - 31 days
 Sep - 30 days
 Oct - 31 days
 Nov - 30 days
 Dec - 31 days

Note: I removed the need for cal

For those that enjoy trivia:

Number months from 1 to 12 and look at the binary representation in four
bits {b3,b2,b1,b0}.  A month has 31 days if and only if b3 differs from b0.
All other months have 30 days except for February.

So with the exception of February this works:

for m in {1..12}; do
    echo $(date -d $m/1/1 +%b) - $((30+($m>>3^$m&1))) days
done

Result:

Jan - 31 days
Feb - 30 days (wrong)
Mar - 31 days
Apr - 30 days
May - 31 days
Jun - 30 days
Jul - 31 days
Aug - 31 days
Sep - 30 days
Oct - 31 days
Nov - 30 days
Dec - 31 days

Upvotes: 7

Steve
Steve

Reputation: 54392

Contents of script.sh:

#!/bin/bash
begin="-$(date +'%-m') + 2"
end="10+$begin"

for ((i=$begin; i<=$end; i++)); do
    echo $(date -d "$i month -$(date +%d) days" | awk '{ printf "%s - %s days", $2, $3 }')
done

Results:

Jan - 31 days
Feb - 29 days
Mar - 31 days
Apr - 30 days
May - 31 days
Jun - 30 days
Jul - 31 days
Aug - 31 days
Sep - 30 days
Oct - 31 days
Nov - 30 days

Upvotes: 1

clyfish
clyfish

Reputation: 10450

cat <<EOF
Jan - 31 days
Feb - `date -d "yesterday 3/1" +"%d"` days
Mar - 31 days
Apr - 30 days
May - 31 days
Jun - 30 days
Jul - 31 days
Aug - 31 days
Sep - 30 days
Oct - 31 days
Nov - 30 days
Dec - 31 days
EOF

Upvotes: 21

Related Questions