List months ending on the same day of the week - bash

This is an assignment. I have the following code:
#! /bin/bash
y=$1
if [ -z $1 ] # if year is not specified use the current year
then y=(`date +%Y`)
fi
for m in {1..12}; do
if [ $m -eq 12 ] # december exception
then echo $(date -d $m/1/$y +%b) - $(date -d "$(($m%12+1))/1/$y" +%A)
break
fi
echo $(date -d $m/1/$y +%b) - $(date -d "$(($m%12+1))/1/$y - 1 days" +%A) # print the last day of the week for the month
done
It lists the last day of the week for every month:
Jan - Monday
Feb - Monday
Mar - Thursday
Apr - Saturday
May - Tuesday
Jun - Thursday
Jul - Sunday
Aug - Wednesday
Sep - Friday
Oct - Monday
Nov - Wednesday
Dec - Saturday
Now I need to reverse it, so that it lists months ending on every day of the week like so:
Sunday - Jul
Monday - Jan Feb Oct
Tuesday - May
Wednesday - Aug Nov
Thursday - Mar Jun
Friday - Sep
Saturday - Apr Dec
I'm thinking of a nested loop,
for d in {1..7};
And storing months in an array?

#! /usr/bin/env bash
# if year is not specified use the current year
declare -r year="${1:-$(date +%Y)}"
# associative array (aka hash table)
declare -A months_per_day=()
for m in {01..12}; do
day_month=$(LANG=C date -d "${year}-${m}-01 +1 month -1 day" +"%A %b")
months_per_day[${day_month% *}]+=" ${day_month#* }"
done
for day in Sunday Monday Tuesday Wednesday Thursday Friday Saturday; do
echo "${day} -${months_per_day[${day}]:-}"
done
Output:
Sunday - Jul
Monday - Jan Feb Oct
Tuesday - May
Wednesday - Aug Nov
Thursday - Mar Jun
Friday - Sep
Saturday - Apr Dec

Using GNU awk for time functions, with lots of intermediate and descriptively named variables to make it easy to understand:
$ cat tst.sh
#!/usr/bin/env bash
awk -v year="$1" '
BEGIN {
OFS = " - "
year = (year == "" ? strftime("%Y") : year)
secsInDay = 24*60*60
for ( mthNr=1; mthNr<=12; mthNr++ ) {
lastDayEpochSecs = mktime(year " " (mthNr+1) " 1 12 0 0") - secsInDay
mthAbbrDayName = strftime("%b %A", lastDayEpochSecs)
split(mthAbbrDayName,m)
mthAbbr = m[1]
dayName = m[2]
mthNr2mthAbbr[mthNr] = mthAbbr
mthAbbr2dayName[mthAbbr] = dayName
dayName2mthAbbrs[dayName] = \
(dayName in dayName2mthAbbrs ? dayName2mthAbbrs[dayName] " " : "" ) mthAbbr
}
for ( mthNr=1; mthNr<=12; mthNr++ ) {
mthAbbr = mthNr2mthAbbr[mthNr]
dayName = mthAbbr2dayName[mthAbbr]
print mthAbbr, dayName
}
print "\n--------\n"
for ( dayName in dayName2mthAbbrs ) {
mthAbbrs = dayName2mthAbbrs[dayName]
print dayName, mthAbbrs
}
}
'
$ ./tst.sh
Jan - Monday
Feb - Monday
Mar - Thursday
Apr - Saturday
May - Tuesday
Jun - Thursday
Jul - Sunday
Aug - Wednesday
Sep - Friday
Oct - Monday
Nov - Wednesday
Dec - Saturday
--------
Tuesday - May
Friday - Sep
Sunday - Jul
Thursday - Mar Jun
Saturday - Apr Dec
Monday - Jan Feb Oct
Wednesday - Aug Nov
The above will be much faster than calling date multiple times in a shell loop and is trivial to modify to do anything else you need.

This answer refactors your implementation as a reusable getlastday function. Then, we go through the loop 7 times matching the getlastday to the matching months, and print it out:
#!/bin/bash
getlastday() {
if [ $m -eq 12 ]; then
echo $(date -d "$(($m%12+1))/1/$y" +%A)
return
fi
echo $(date -d "$(($m%12+1))/1/$y - 1 days" +%A) # print the last day of the week for the month
}
y=$1
if [ -z $1 ] # if year is not specified use the current year
then y=(`date +%Y`)
fi
for d in Monday Tuesday Wednesday Thursday Friday Saturday Sunday; do
months=()
for m in {1..12}; do
lastday=$(getlastday)
if [ $lastday != $d ]; then continue; fi
months+=($(date -d $m/1/$y +%b))
done
echo $d - ${months[#]}
done
[EDIT: version 2]
As per #markp-fuso's comment below the above has an inefficiency in that there are 84 iterations to build the list.
The following is an improvement where we construct an res result array with 7 entries in it. One for each day in the week. Then we use +%u to get the day number instead of the day string. This will give us the index to the res result append where to append the month to:
#!/bin/bash
y=$1
if [ -z $1 ] # if year is not specified use the current year
then y=(`date +%Y`)
fi
res=("Sunday -" "Monday -" "Tuesday -" "Wednesday -" "Thursday -" "Friday -" "Saturday -")
for m in {1..12}; do
mstr=$(date -d $m/1/$y +%b)
if [ $m -eq 12 ]; then
d=$(date -d "$(($m%12+1))/1/$y" +%u)
else
d=$(date -d "$(($m%12+1))/1/$y - 1 days" +%u)
fi
res[$d]="${res[$d]} $mstr"
done
for d in {0..6}; do
echo "${res[$d]}"
done

Using jq your task can be solved:
INPUT='
Jan - Monday
Feb - Monday
Mar - Thursday
Apr - Saturday
May - Tuesday
Jun - Thursday
Jul - Sunday
Aug - Wednesday
Sep - Friday
Oct - Monday
Nov - Wednesday
Dec - Saturday
'
jq -Rrs '
(split("\n") | map(split(" - "))) as $input | # split lines and split each line by "-"
reduce ("Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday") as $day # iterate over all weekdays
([]; . + [ ($input | map(select(.[1] == $day)) | # select entries for $day
.[0][1] + " - " + (map(.[0]) | join(" "))) # generate output for $day
]
) | .[]
' <<< "$INPUT"
Output
Sunday - Jul
Monday - Jan Feb Oct
Tuesday - May
Wednesday - Aug Nov
Thursday - Mar Jun
Friday - Sep
Saturday - Apr Dec

Try this Shellcheck-clean code:
#! /bin/bash -p
year=${1-$(date +%Y)}
months=( Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec )
day_months=( 'Sunday -' 'Monday -' 'Tuesday -' 'Wednesday -'
'Thursday -' 'Friday -' 'Saturday -' )
for m in {1..12}; do
last_day=$(date -d "$year-$m-1 +1 month -1 day" +%w)
day_months[last_day]+=" ${months[m-1]}"
done
printf '%s\n' "${day_months[#]}"

declare -A a
for i in '1/1 + 1 year' {2..12}/1; do
d=($(LC_ALL=C date '+%A %B' -d "$i - 1 second"))
a["$d"]+=${a["$d"]:+ }${d[1]}
done
for i in {Sun,Mon,Tues,Wednes,Thurs,Fri,Satur}day; do
echo "$i - ${a["$i"]}"
done
This prints the list for the current year:
Sunday - July
Monday - January February October
Tuesday - May
Wednesday - August November
Thursday - March June
Friday - September
Saturday - December April

technically the only info you'll need is what Jan ends on, and either (a) whether year is leap or not, or (b) whether Feb ends on same day of week as Jan.
The rest are easily deduced without running date 12 times

Using ksh93:
#!/bin/ksh93
# if year is not specified use the current year
year="${1:-$(date +%Y)}"
typeset -A days
for month in Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec
do
lastday=$(printf "%(%A)T" "final day $month $year")
days[$lastday]+=" $month"
done
for day in Sunday Monday Tuesday Wednesday Thursday Friday Saturday
do
printf "%-9s -%s\n" "$day" "${days[$day]:-}"
done
For 2022 produces the following output:
Sunday - Jul
Monday - Jan Feb Oct
Tuesday - May
Wednesday - Aug Nov
Thursday - Mar Jun
Friday - Sep
Saturday - Apr Dec
For 2023 produces the following output:
Sunday - Apr Dec
Monday - Jul
Tuesday - Jan Feb Oct
Wednesday - May
Thursday - Aug Nov
Friday - Mar Jun
Saturday - Sep

Related

How to cut the first Sunday to Saturday of each month in a year?

We have green zone logic where the job has to run only between first Sunday to Saturday, i.e. 7 days starting from first Sunday of every month. I'm using the below awk command to get that, but somewhere it is breaking. I'm just trying for first 3 months i.e Jan to March
seq 75 | awk ' BEGIN {ti=" 0 0 0"}
function dtf(fmt,dy) { return strftime(fmt,mktime("2020 1 " dy ti)) }
{ day=dtf("%A %F",$0);mm=dtf("%m",$0);if(day~/Sunday/ || a[mm]) a[mm]++ ; if(a[mm]<8) print day } '
My output is below, which is incorrect:
Wednesday 2020-01-01
Thursday 2020-01-02
Friday 2020-01-03
Saturday 2020-01-04
Sunday 2020-01-05
Monday 2020-01-06
Tuesday 2020-01-07
Wednesday 2020-01-08
Thursday 2020-01-09
Friday 2020-01-10
Saturday 2020-01-11
Saturday 2020-02-01
Sunday 2020-02-02
Monday 2020-02-03
Tuesday 2020-02-04
Wednesday 2020-02-05
Thursday 2020-02-06
Friday 2020-02-07
Saturday 2020-02-08
Sunday 2020-03-01
Monday 2020-03-02
Tuesday 2020-03-03
Wednesday 2020-03-04
Thursday 2020-03-05
Friday 2020-03-06
Saturday 2020-03-07
Expected output:
Sunday 2020-01-05
Monday 2020-01-06
Tuesday 2020-01-07
Wednesday 2020-01-08
Thursday 2020-01-09
Friday 2020-01-10
Saturday 2020-01-11
Sunday 2020-02-02
Monday 2020-02-03
Tuesday 2020-02-04
Wednesday 2020-02-05
Thursday 2020-02-06
Friday 2020-02-07
Saturday 2020-02-08
Sunday 2020-03-01
Monday 2020-03-02
Tuesday 2020-03-03
Wednesday 2020-03-04
Thursday 2020-03-05
Friday 2020-03-06
Saturday 2020-03-07
How can I adjust the awk command to get the expected output?
Any other solutions using other bash tools are also welcome.
I suggest the following alternative to awk:
#! /usr/bin/env bash
for month in {01..03}; do
for day in {01..13}; do
date -d "2020-$month-$day" '+%A %F'
done |
grep -A6 -m1 -F Sunday
done
The script is not very efficient, but does the job. For each month, we simply print the dates of the 13 first days in that month. We know that the green zone has to be in that area, therefore we do not need the remaining days of the month.
The date format is Weekday YYYY-MM-DD. We use grep to find and print the first Sunday, print the 6 days behind that Sunday (-A6) and exit because we limited the search to one match (-m1).
The procedure described above is done for each of the months 1 to 3.
Here's a simple way to get GNU awk to create a list of dates and day names for any given year:
$ cat tst.awk
BEGIN {
year = (year == "" ? 2020 : year)
beg = mktime(year " 1 1 12 0 0")
for (i=0; i<=400; i++) {
dateday = strftime("%F %A", beg+24*60*60*i)
split(dateday,d,/[ -]/)
if ( d[1] != year ) {
break
}
print d[1], d[2], d[3], d[4]
}
}
.
$ awk -f tst.awk | head -20
2020 01 01 Wednesday
2020 01 02 Thursday
2020 01 03 Friday
2020 01 04 Saturday
2020 01 05 Sunday
2020 01 06 Monday
2020 01 07 Tuesday
2020 01 08 Wednesday
2020 01 09 Thursday
2020 01 10 Friday
2020 01 11 Saturday
2020 01 12 Sunday
2020 01 13 Monday
2020 01 14 Tuesday
2020 01 15 Wednesday
2020 01 16 Thursday
2020 01 17 Friday
2020 01 18 Saturday
2020 01 19 Sunday
2020 01 20 Monday
I'm starting at noon and looping from 0 to 400 days and breaking when the year changes just so I don't have to try to accommodate DST or leap years or leap seconds in the determination of days in the year in a more accurate calculation.
Just add some code to test for the current month being different from the previous and the current day name being a Sunday and print 7 days starting there, e.g.:
$ cat tst.awk
BEGIN {
year = (year == "" ? 2020 : year)
beg = mktime(year " 1 1 12 0 0")
for (i=0; i<=400; i++) {
dateday = strftime("%F %A", beg+24*60*60*i)
split(dateday,d,/[ -]/)
if ( d[1] != year ) {
break
}
dayName[d[2]+0][d[3]+0] = d[4]
}
for (monthNr=1; monthNr<=3; monthNr++) {
for (dayNr=1; dayNr in dayName[monthNr]; dayNr++) {
if (dayName[monthNr][dayNr] == "Sunday") {
for (i=0; i<7; i++) {
printf "%s %04d-%02d-%02d\n", dayName[monthNr][dayNr+i], year, monthNr, dayNr+i
}
break
}
}
}
}
.
$ awk -f tst.awk
Sunday 2020-01-05
Monday 2020-01-06
Tuesday 2020-01-07
Wednesday 2020-01-08
Thursday 2020-01-09
Friday 2020-01-10
Saturday 2020-01-11
Sunday 2020-02-02
Monday 2020-02-03
Tuesday 2020-02-04
Wednesday 2020-02-05
Thursday 2020-02-06
Friday 2020-02-07
Saturday 2020-02-08
Sunday 2020-03-01
Monday 2020-03-02
Tuesday 2020-03-03
Wednesday 2020-03-04
Thursday 2020-03-05
Friday 2020-03-06
Saturday 2020-03-07
There are slightly more efficient ways to do it but the above is clear and simple and will run in the blink of an eye.
A (rather wordy - I don't have time to make it shorter:-) ) Perl solution:
#!/usr/bin/perl
use strict;
use warnings;
use feature 'say';
use Time::Piece;
use Time::Seconds;
my $year = shift || localtime->year;
first_week($year, $_) for 1 ..12;
sub first_week {
my ($yr, $mn) = #_;
$mn = sprintf '%02d', $mn;
# Use midday to avoid DST issues
my $start = Time::Piece->strptime(
"$year-$mn-01 12:00:00",
'%Y-%m-%d %H:%M:%S'
);
$start += ONE_DAY while $start->day ne 'Sun';
for (1 .. 7) {
say $start->strftime('%A %Y-%m-%d');
$start += ONE_DAY;
}
}
Try this
for i in $(seq 12); do cal ${i} 2020 | awk -v month=${i} 'NF==7 && !/^Su/{ for (j=0;j<=6;j++){print "2020-"month"-"$1+j;}exit}'
EDIT : Updated code for printing day
for i in $(seq 2); do cal ${i} 2020 | awk -v month=${i} 'NF==7 && !/^Su/{for (j=0;j<=6;j++){print strftime("%A %F", mktime("2020 " month " " $1+j " 0 0 0"))}exit}'; done;
Demo for Jan and Feb
$for i in $(seq 2); do cal ${i} 2020 | awk -v month=${i} 'NF==7 && !/^Su/{a[0]="Sunday";a[1]="Monday";a[2]="Tuesday";a[3]="Wednesday";a[4]="Thursday";a[5]="Friday";a[6]="Saturday";for (j=0;j<=6;j++){print a[j]" " "2020-"month"-"$1+j}exit}'; done;
Sunday 2020-1-5
Monday 2020-1-6
Tuesday 2020-1-7
Wednesday 2020-1-8
Thursday 2020-1-9
Friday 2020-1-10
Saturday 2020-1-11
Sunday 2020-2-2
Monday 2020-2-3
Tuesday 2020-2-4
Wednesday 2020-2-5
Thursday 2020-2-6
Friday 2020-2-7
Saturday 2020-2-8
$
With Perl, using DateTime
use warnings;
use strict;
use feature 'say';
use DateTime;
my $dt = DateTime->new(year => 2020, month => 1, day => 1);
my $first_sunday = 7 - $dt->day_of_week + 1; # day of month for first Sun
while (1) {
my $day = $dt->day;
if ($day >= $first_sunday and $day < $first_sunday + 7) {
say $dt->ymd, " (", $dt->day_abbr, ")";
}
}
continue {
$dt->add(days => 1);
if ($dt->day == 1) { # new month
last if $dt->month > 3;
$first_sunday = 7 - $dt->day_of_week + 1;
}
}
This keeps a state (on the first in a month in finds out what day the first Sunday is), what is quite suitable if the program is meant to generate and go through all dates from the span of interest.
On the other hand, the program may need to check for a given day; perhaps it runs daily and needs to check for that day. Then it is simpler to see whether the day is between the first and second Sunday in the month
my $dt = DateTime->today;
while ( $dt->add(days => 1)->month <= 3) {
if ($dt->day_of_week == 7) { # it's a Sunday
if ($dt->weekday_of_month == 1) { # first Sunday in the month
say $dt->ymd, " (", $dt->day_abbr, ")";
}
}
else {
my $sdt = $dt->clone; # preserve $dt
$sdt->subtract( $dt->day_of_week ); # drop to previous Sunday
if ($sdt->weekday_of_month == 1) { # was first Sunday in the month
say $dt->ymd, " (", $dt->day_abbr, ")";
}
}
}
The while loop around the code is there to facilitate a check.
For days other than Sunday we drop to the past Sunday, to check whether that was the first Sunday in the month. If so, then our day is within the required interval. If the day is a Sunday we only need to check whether it is the first one in the month.
The code can be made a bit more efficient and concise if that matters
if ( (my $dow = $dt->day_of_week) == 7) {
if ($dt->weekday_of_month == 1) {
say $dt->ymd, " (", $dt->day_abbr, ")";
}
}
elsif ( $dt->clone->subtract(days => $dow)->weekday_of_month == 1 ) {
say $dt->ymd, " (", $dt->day_abbr, ")";
}
... on the account of readability.
$ printf "%s\n" 2020-{01..03}-01 \
| xargs -I{} date -d "{}" "+{} %u" \
| join -j3 - <(seq 0 6) \
| xargs -n3 sh -c 'date -d "$1 + 7 days - $2 days + $3 days" "+%A %F"' --
There is some nasty stuff in here, but I'll try to explain. The idea is to compute the day of the week of the first day of the month (assume u). If you know that, you know directly which day is the first Sunday (7-u days later). So from that point forward you only need to compute the next 6 days.
Use brace expansion to generate the months you are interested in
Use xargs to compute the day of the week and output it as YYYY-MM-DD u
Per day, we want to create a list of 7 strings YYYY-MM-DD u d where d runs from 0 to 6. For this we use a nasty join hack. By telling join to join to files on a non-existing field, we create an outer product.
Use xargs in combination with sh to create a command that accepts 3 arguments and do the computation.
This method is now easily expanded to other months and years:
$ printf "%s\n" 20{20..30}-{01..12}-01 | xargs ...
The above looks a bit messy, and you might be more interested in the loop version:
for yyyymm in {2020..2030}-{01..03}; do
u=$(date -d "$yyyymm-01" "+%u");
for ((dd=7-u;dd<14-u;++dd)); do
date -d "$yyyymm-01 + $dd days" "+%A %F"
done
done
Previous solution:
This is for the first 3 months of 2020:
$ printf "%s\n" 2020-{01..03}-{01..13} \
| xargs -n1 -I{} date -d '{}' '+%A %F' \
| awk -F"[- ]" '/Sun/{a[$3]++} a[$3]==1'
This is for the first years 2020 till 2030
$ printf "%s\n" 20{20..30}-{01..12}-{01..13} \
| xargs -n1 -I{} date -d '{}' '+%A %F' \
| awk -F"[- ]" '/Sun/{a[$2,$3]++} a[$2,$3]==1'
This is understood in 3 steps:
Use brace-expansion to create a list of the first 13 days of months and years you are interested in. This works nicely because the bash starts expanding left to right. This means that the day is the fast-running index. We ask for the first 13 days, because we know that the first Sunday must be within the first 7 days.
Convert the days to the expected format using xargs and date
Use awk to do the filtering.
By adding one more condition, I'm able to make it work. a[mm]<8 && a[mm]>0
seq 75 | awk '
BEGIN { ti=" 0 0 0" }
function dtf(fmt,dy) {
return strftime(fmt,mktime("2020 1 " dy ti))
}
{ day=dtf("%A %F",$0);
mm=dtf("%m",$0);
if(day~/Sunday/ || a[mm]) a[mm]++ ;
if(a[mm]<8 && a[mm]>0 ) print day
}'
Output:
Sunday 2020-01-05
Monday 2020-01-06
Tuesday 2020-01-07
Wednesday 2020-01-08
Thursday 2020-01-09
Friday 2020-01-10
Saturday 2020-01-11
Sunday 2020-02-02
Monday 2020-02-03
Tuesday 2020-02-04
Wednesday 2020-02-05
Thursday 2020-02-06
Friday 2020-02-07
Saturday 2020-02-08
Sunday 2020-03-01
Monday 2020-03-02
Tuesday 2020-03-03
Wednesday 2020-03-04
Thursday 2020-03-05
Friday 2020-03-06
Saturday 2020-03-07
As a additional note, though I hardcoded 1 for the month, when the day parameter is >31 mktime() just moves to the next month. So in a way you can pass julian day to mktime with month set to 1.
echo -e "1\n31\n32\n60\n61\n366" | awk '
BEGIN { ti=" 0 0 0" }
function dtf(fmt,dy) {
return strftime(fmt,mktime("2020 1 " dy ti))
}
{
day=dtf("%A %F",$0);
j=dtf("%j",$0);
print j,day
}'
Output:
001 Wednesday 2020-01-01
031 Friday 2020-01-31
032 Saturday 2020-02-01
060 Saturday 2020-02-29
061 Sunday 2020-03-01
366 Thursday 2020-12-31

How to calculate date in the past from given date (also in past) using "date -d 'xx/xx/xxxx - xx' "

I have a input date:
01-01-2019 08:30:00
And I would like to back it in time by:
2hours 10minutes
So the new date should be like:
01-01-2019 06:20
I have tried commands:
(1)date -d "2019-01-01 08:30:00 + 1hour"
which return: Tue Jan 1 09:30:00 STD 2019 (ok, but seems to be lucky shot)
(2)date -d "2019-01-01 08:30:00 - 1hour"
which return: Tue Jan 1 11:30:00 STD 2019 that means +3hours (wrong)
(3)date -d "2019-01-01 08:30:00 + 3hours"
which return: Tue Jan 1 07:30:00 STD 2019 that means -1hours (wrong)
(4)date -d "2019-01-01 08:30:00 + 2minutes"
which return: Tue Jan 1 07:31:00 STD 2019 that means -59minutes (wrong)
You probably need to explicitly mention your timezone:
$ TZ=STD date -d '2019-01-01 08:30:00 STD + 1 hour'
Tue Jan 1 09:30:00 STD 2019
$ TZ=STD date -d '2019-01-01 08:30:00 STD - 3 hour'
Tue Jan 1 05:30:00 STD 2019
$ TZ=STD date -d '2019-01-01 08:30:00 STD + 2 minutes'
Tue Jan 1 08:32:00 STD 2019
This seems to alleviate dates confusion.
The is only one most reliable way of doing that:
date -d "#$(( $(date -d '2019-01-01 08:30:00' +%s) - ( 2 * 60 + 10 ) * 60 ))"
First convert to seconds since epoch.
Then subtract the time you want.
Then use date to format the output.

Bash script to iterate through a specific time range, one second at a time

I am trying to create some sort of loop in bash that will iterate through a specific time range, one second at a time.
At each interval, it will perform a duty with that timestamp I.e. "Wed Mar 2 12:00:03 CDT 2018"
I am having a hard time wrapping my head around how to make a loop that will iterate every second of time, and when it hits 60 seconds, update the minute, etc.
Thoughts? This seems obvious but the right syntax escapes me.
this is one way of doing it, delegate time computations to date
$ for i in {1..10}; do date -d "+$i seconds"; done
Sun Mar 11 20:40:57 UTC 2018
Sun Mar 11 20:40:58 UTC 2018
Sun Mar 11 20:40:59 UTC 2018
Sun Mar 11 20:41:00 UTC 2018
Sun Mar 11 20:41:01 UTC 2018
Sun Mar 11 20:41:02 UTC 2018
Sun Mar 11 20:41:03 UTC 2018
Sun Mar 11 20:41:04 UTC 2018
Sun Mar 11 20:41:05 UTC 2018
Sun Mar 11 20:41:06 UTC 2018
Sun Mar 11 20:41:07 UTC 2018
if you want to iterate up to a certain time, change the for loop to a while loop and a counter.
To make a loop with time you need to format date/time in such a way that can be comparable. The most easy trick is to transform date to something that will look like an integer and you then use lt,le,eq,gt,ge operators of bash.
Consider this :
$ date
Mon Mar 12 00:16:29 EET 2018 #this format/data type is not comparable by bash
$ date +%Y%m%d%H%M%S
20180312001629 #this is an integer representing the current time
This is a sample loop from now up to a specific time point:
stop=$(date +%Y%m%d%H%M%S --date "2018-03-12 00:20:55")
while [[ $(date +%Y%m%d%H%M%S) -le $stop ]];do #comparing current date & time with stop date & time
echo "date now is $(date)";
sleep 1; #sleep 1 second
done
date now is Mon Mar 12 00:20:51 EET 2018
date now is Mon Mar 12 00:20:52 EET 2018
date now is Mon Mar 12 00:20:53 EET 2018
date now is Mon Mar 12 00:20:54 EET 2018
date now is Mon Mar 12 00:20:55 EET 2018
The trick here is that the command
stop=$(date +%Y%m%d%H%M%S --date "2018-03-12 00:20:55")
converts given date (using the --date flag) to a kind of integer format.
Then the while loop keeps comparing current date in the same integer format with the stop date.
This is a similar script to start/stop at a specific time, but it is a bit "resources hungry" since it keeps comparing current time to start time:
date; #just print the current date & time
compl=false;
start=$(date +%Y%m%d%H%M%S --date "2018-03-12 01:45:50");
stop=$(date +%Y%m%d%H%M%S --date "2018-03-12 01:45:55");
while true;do
while [[ $(date +%Y%m%d%H%M%S) -ge $start ]] && [[ $(date +%Y%m%d%H%M%S) -le $stop ]];do
echo "date now is $(date)";
sleep 1;
compl=true;
done;
($compl) && break;
done
Mon Mar 12 01:45:37 EET 2018
date now is Mon Mar 12 01:45:50 EET 2018
date now is Mon Mar 12 01:45:51 EET 2018
date now is Mon Mar 12 01:45:52 EET 2018
date now is Mon Mar 12 01:45:53 EET 2018
date now is Mon Mar 12 01:45:54 EET 2018
date now is Mon Mar 12 01:45:55 EET 2018
An alternative to start/stop a script at specific time would be to use epoch date / epoch seconds.
Epoch date is measured in seconds since the epoch (1970-01-01 UTC).
Every date can be expressed as epoch seconds, using the format date +%s.
date #just print the current date & time
start="2018-03-12 02:17:52"
stop="2018-03-12 02:17:57"
timerequired=$(( $(date +%s --date "$start") - $(date +%s) ))
sleep $(($timerequired)) #sleep till the starting time
while [[ $(date +%s) -le $(date +%s --date "$stop") ]];do
echo "date now is $(date)";
sleep 1;
done
Mon Mar 12 02:17:39 EET 2018
date now is Mon Mar 12 02:17:52 EET 2018
date now is Mon Mar 12 02:17:53 EET 2018
date now is Mon Mar 12 02:17:54 EET 2018
date now is Mon Mar 12 02:17:55 EET 2018
date now is Mon Mar 12 02:17:56 EET 2018
date now is Mon Mar 12 02:17:57 EET 2018
Finally, you can use external tools to automate things based on date & time like cron, anacron , at, etc
Echo a date for every second in a range:
from=$(date -d "Wed Mar 2 12:59:58 CDT 2018" +%s)
to=$(date -d "Wed Mar 2 13:00:04 CDT 2018" +%s)
for s in $(seq $from $to); do echo $(date -d #$s); done
The date -d takes a well formed date as input. +%s formats it into seconds since epoch. So you get two big values like from=1520013598, to=...3604. The date -d #1520013598 transforms the timestamp back to a human readable date.
Your range might be in a variable like start and stop, and you might like to foobar ($time) instead of echoing it:
start="Wed Mar 2 12:59:58 CDT 2018"
stop="Wed Mar 2 13:00:04 CDT 2018"
from=$(date -d "$start" +%s)
to=$(date -d "$stop" +%s)
for s in $(seq $from $to)
do
timestamp=$(date -d #$s)
foobar $timestamp
done

Loop minutes and echo with bash

I want to iterate all the minutes in a month (the purpose will be to generate a CSV file).
But when I try this:
d="2016-09-01 00:00:00"
while [ "$d" != "2016-09-30 00:00:00" ]; do
echo $d
d=$(date --utc "+%Y-%m-%d %H:%M:00" -d "$d + 1 minute" )
done
Both the hour and minute are being incremented:
2016-09-01 00:00:00
2016-09-01 01:01:00
2016-09-01 02:02:00
2016-09-01 03:03:00
2016-09-01 04:04:00
2016-09-01 05:05:00
2016-09-01 06:06:00
2016-09-01 07:07:00
2016-09-01 08:08:00
What am I doing wrong and how to correctly loop minutes?
I would work with Unix timestamps instead.
d=$(date --utc +%s -d "2016-09-01 00:00:00")
end=$(date --utc +%s -d "2016-09-30 00:00:00")
while [ "$d" != "$end" ]; do
date --utc "+%Y-%m-%d %H:%M:00" -d "#$d"
d=$(( d + 60 ))
done
You can add timezone UTC after your date string variable $d to get the right output:
d="2016-09-01 00:00:00"
for ((i=0; i<=60; i++)); do
date --utc -d "$d UTC + $i minute"
done
Thu Sep 1 00:00:00 UTC 2016
Thu Sep 1 00:01:00 UTC 2016
Thu Sep 1 00:02:00 UTC 2016
Thu Sep 1 00:03:00 UTC 2016
Thu Sep 1 00:04:00 UTC 2016
Thu Sep 1 00:05:00 UTC 2016
...
...
Thu Sep 1 00:55:00 UTC 2016
Thu Sep 1 00:56:00 UTC 2016
Thu Sep 1 00:57:00 UTC 2016
Thu Sep 1 00:58:00 UTC 2016
Thu Sep 1 00:59:00 UTC 2016
Thu Sep 1 01:00:00 UTC 2016
Note use of UTC after $d.
Using + after a time component in the date string is used for ' time zone correction' not for doing what you want to do. Interestingly, inverting date and time works:
$ date "+%Y-%m-%d %H:%M:00" -d "21:31:00 2016-09-03 + 1 minute "
2016-09-03 21:32:00
while the other way around messes with timezones and offsets so the result might depend on your local configuration:
$ TZ=Europe/London date "+%Y-%m-%d %H:%M:00" -d "2016-09-03 21:32:00 + 1 minute "
2016-09-03 21:33:00
$ TZ=Europe/Brussels date "+%Y-%m-%d %H:%M:00" -d "2016-09-03 21:32:00 + 1 minute "
2016-09-03 22:33:00
$ TZ=Asia/Singapore date "+%Y-%m-%d %H:%M:00" -d "2016-09-03 21:32:00 + 1 minute "
2016-09-04 04:33:00

How to find date n days in the future from past date?

I want to find a date that is 57 –working– days after an arbitrary date using date or bash. For instance,
today is august 21st,
reference date is july 15th,
what days will be 57 working days after july 15th?
This should work to just get all days
date -d '7/15/14 +57 days'
To get number of work days (M..F) you can do something lazy like this
#!/bin/bash
days=0
for ((i=1;i>0;i++)) do
future=$(date -d "7/15/14 +$i days" '+%w')
((future!=0 && future!=6)) && ((days++)) # ignore sunday (0) and saturday (6)
((days==57)) && date -d "7/15/14 +$i days" && break
done
e.g.
> ./abovescript
> Thu Oct 2 00:00:00 CDT 2014
Weird solution:
day=21
mon=8
year=2014
days=4
curl -s "http://www.timeanddate.com/scripts/dateserver.php?mode=addweekdays&d1=$day&m1=$mon&y1=$year&type=add&ad=$days&atyp=0&ach=3" | sed -n 's|.*<h2>Result: \(.*\)</h2>.*|\1|p'
prints
Wednesday, August 27, 2014
the date after 4 working days from 2014.08.21
for the
day=15
mon=7
year=2014
days=57
prints
Friday, October 3, 2014

Resources