- 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?
- 12 Answers 12
- 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.
- First and last day of a month
- 5 Answers 5
- Date, find out the number of days in a month
- 1 Answer 1
- How can I get the 1st and last date of the previous month in a Bash script?
- 7 Answers 7
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?
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?
@KeithThompson Nope, this is not a homework. I am more interested in optimize speed and using lesser command to accomplish tasks and was wondering if bash can get the last day of a month without telling bash to go through a loop (e.g. if a certain month value, then display this value) or performing hard-coding.
If speed optimization is your concern, then I suggest saying so rather than imposing specific restrictions. If speed is that important, a C program would probably be a better choice.
12 Answers 12
for m in ; 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.
@amdn: this may be better code, but without your explanation (below), I’d have no clue what this is doing 🙂
ah, yes, comprehension: I first thought $m/1 was a division, but it’s actually the base date for the calculation. I added the year, so it’s more flexible and maybe less confusing: y=2000; m=2; date —date=»$y/$m/1 + 1 month — 1 day» «+%b %Y — %d days»;
Like several of the other answers here, this requires GNU date , which is generally not the version installed on non-Linux systems.
cal $(date +"%m %Y") | awk 'NF ; END '
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.
While this code snippet may solve the question, including an explanation really helps to improve the quality of your post. Remember that you are answering the question for readers in the future, and those people might not know the reasons for your code suggestion.
Assuming you allow «for», then the following in bash
for m in ; do echo $(date -d $m/1/1 +%b) - $(date -d "$(($m%12+1))/1 - 1 days" +%d) days done
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 . 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 ; do echo $(date -d $m/1/1 +%b) - $((30+($m>>3^$m&1))) days done
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
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 $[ $+ 28 ] > daysin $1 $2 #daysin 1 [YYYY]
Note: your comments # leap year and # normal need to be swapped to be associated with the correct line. s=»303232332323″ is the normal month encoding for the addition in the echo line.
date -d "-$(date +%d) days month" +%Y-%m-%d
The inner date +%d call returns the current day of this month. The value for the -d flag becomes (for example): «-8 days month», which, according to the docs means «subtract 8 days from the current date and add one month». Subtracting the current number of days takes you back to the end of the previous month, and adding one month takes you to the end of the current month. GNU date also has a —debug switch which can help to explain what’s happening.
On a Mac which features BSD date you can just do:
for i in ; 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 ; 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).
#!/bin/bash begin="-$(date +'%-m') + 2" end="10+$begin" for ((i=$begin; i') done
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
@Jack: Thanks, yes end=»10+$begin» should actually be end=»11+$begin» . Glad you chose Glenn’s answer, it is far superior.
Building on patm’s answer using BSD date for macOS (patm’s answer left out December):
for i in ; 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
First and last day of a month
I’m looking for a solution using GNU date or BSD date (on OS X).
5 Answers 5
# Last month: l_first_date=$(date -d "`date +%Y%m01` -1 month" +%Y-%m-%d) l_last_date=$(date -d "`date +%Y%m01` -1 day" +%Y-%m-%d) # This month: t_first_date=$(date +%Y-%m-01) t_last_date=$(date -d "`date +%Y%m01` +1 month -1 day" +%Y-%m-%d) # Next month: n_first_date=$(date -d "`date +%Y%m01` +1 month" +%Y-%m-%d) n_last_date=$(date -d "`date +%Y%m01` +2 month -1 day" +%Y-%m-%d) # Print everything echo "Last month: $l_first_date to $l_last_date" echo "This month: $t_first_date to $t_last_date" echo "Next month: $n_first_date to $n_last_date"
Pure gold, the nested date is perfect in cron one-liners. thanks! Please note also the nested $() will work e.g. d=$(date -d «$(date +%Y%m01) -3 month» +%Y-%m-%d
I’ll be honest; from the way you’re asking the question I get the sense you’ve been assigned some homework, so I’ll leave a few steps out of the answer as an exercise for the reader:
You’ll want to take a good look at the date manual page; especially the -d flag, which allows you to examine any given day. The first day of month M in year Y would be «M/01/Y»
Getting the last day of the month, your best bet is to add 1 to the number of the month you were given, then deduct one day in the date.
Hint: date can actually accept some extensive arithmetic; I can, for instance, say date -d «01/07/2012 + 1 month — 1 day» and it will give me the correct answer.
You can find out how to display the output you want in 2) and 3) by studying the «format» section of the date manpage.
Date, find out the number of days in a month
Why does this need to be done in date ? Except for leap years the number of days in each month is fixed.
I haven’t found any other utilities, but did find mention that this date can do it. If you can suggest another way that doesn’t force me to install something on the system, and the structures for these operations will be small.
My question is: why does this need any utility to do it for you? This is a trivial lookup against month numbers with a check for leap years: jan=31, if (leap year) then (feb = 29) else (feb = 28), mar = 31, apr = 30 . etc
What have you tried so far? Where are you stuck? What is the actual problem you are trying to solve? Why do you believe that date is the correct tool for this task?
1 Answer 1
Brute force: generate the next 31 & 62 days and take the biggest number:
month=$(date +%m) maxDayThisMonth=$(( for d in ; do date -d "+$d days" +%m-%d ; done ; ) | grep -Po "(?<=$month-)\d+" | sort -rn | head -1) nextMonth=$(printf "%02d" $(( ($month+1) % 12 )) ) maxDayNextMonth=$(( for d in <01..62>; do date -d "+$d days" +%m-%d ; done ; ) | grep -Po "(?<=$nextMonth-)\d+" | sort -rn | head -1)
grep -P is a GNU-ism but since this is for Linux. Otherwise you can replace by a more conventional grep + cut .
The for loop generates all MM-DD for the next 31 days (10-30,10-31, 11-01,11-02. ), grep keeps those of the current month (30, 31) (there is a "lookbehind" to keep only those after 10- ), and then they are reverse ordered and the first is kept. Same for next month but the sequence is longer and can have days from three months.
Gotta love the modern computer paradigm of brute force and ignorance to run a program 62 times, fumble blindly through the output and then strip out what you don't want. Disk costs are basically zero due to caches and such, but running date to get the current month number (and therefore next month number as well) and then doing a simple lookup has to be more computationally efficient and elegant? Sure this gets you the task done in four lines instead of a big case statement, but that can't be the metric for "best" when you have to spend so much time tying it all together in the first place.
@Mokubai Plain fun. On a more philosophical level, this also let me rely on the authors of date for actual leap years and such (the only "side knowledge" I use in the solution above is that there are 12 months in a year)(scars of Y2K battles:)), and if the code had to do it very many times it would do a lookup. but using a table generated with date 🙂
How can I get the 1st and last date of the previous month in a Bash script?
I have scheduled a Bash script to run on the 1st of the month but I need to create 2 variables in it with the 1st and last date of the previous month, whatever those may be. Is it possible to do this using just Bash?
Most (currently all) of the answers here require GNU date and thus basically assume Linux. There are near-duplicate questions which show how to do this with POSIX / BSD / Mac date .
See e.g. stackoverflow.com/questions/13168463/… which is tagged linux but has several portable solutions to a related problem.
7 Answers 7
Unlike some answers, this will work for the 31st and any other day of the month. I use it to output unix timestamps but the output format is easily adjusted.
first=$(date --date="$(date +'%Y-%m-01') - 1 month" +%s) last=$(date --date="$(date +'%Y-%m-01') - 1 second" +%s)
Example (today's date is Feb 14, 2019):
To output in other formats, change final +%s to a different format such as +%Y-%m-%d or omit for default format in your locale.
In case you need, you can also back up an arbitrary number of months like this:
# variable must be >= 1 monthsago=23 date --date="$(date +'%Y-%m-01') - $ month" date --date="$(date +'%Y-%m-01') - $(( $ - 1 )) month - 1 second"
Example output (today's date is Feb 15, 2019):
Wed Mar 1 00:00:00 UTC 2017
Fri Mar 31 23:59:59 UTC 2017
You can try following date commands regardless of the day you are executing them to get first and last day of previous month
Firstday=`date -d "-1 month -$(($(date +%d)-1)) days"` Lastday=`date -d "-$(date +%d) days"`
Due to the varying length of months, I think the most dependable way to do this is to base the calendar offsets from the first day of the month rather than any other arbitrary date and then subtract the number of days.
In the snippet below, you can set $TODAY to whatever date you need and $LAST_MONTH_START and $LAST_MONTH_END will end up containing the previous month's start and end dates:
TODAY=$(date '+%F') # or whatever YYYY-MM-DD you need THIS_MONTH_START=$(date -d "$TODAY" '+%Y-%m-01') LAST_MONTH_START=$(date -d "$THIS_MONTH_START -1 month" '+%F') LAST_MONTH_END=$(date -d "$LAST_MONTH_START +1 month -1 day" '+%F')