Reputation: 1693
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
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
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
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
Reputation: 1802
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
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
-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
Reputation: 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
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
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
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
Reputation: 7448
for m in $(seq 1 12); do cal $(date +"$m %Y") | grep -v "^$" |tail -1|grep -o "..$"; done
There is no sense, to avoid using cal, because it is required by POSIX, so should be there
Upvotes: 0
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
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
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
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