What is the most elegant way to calculate the previous business day in shell ksh script ?
What I got until now is :
#!/bin/ksh
set -x
DAY_DIFF=1
case `date '+%a'` in
"Sun")
DAY_DIFF=2
;;
"Mon")
DAY_DIFF=3
;;
esac
PREV_DT=`perl -e '($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst)=localtime(time()-${DAY_DIFF}*24*60*60);printf "%4d%02d%02d",$year+1900,$mon+1,$mday;'`
echo $PREV_DT
How do I make the ${DAY_DIFF} variable to be transmitted as value and not as string ?
#!/bin/ksh
# GNU date is a veritable Swiss Army Knife...
((D=$(date +%w)+2))
if [ $D -gt 3 ]; then D=1; fi
PREV_DT=$(date -d "-$D days" +%F)
Here is a solution that doesn't use Perl. It works both with ksh and sh.
#!/bin/ksh
diff=-1
[ `date +%u` == 1 ] && diff=-3
seconds=$((`date +%s` + $diff * 24 * 3600))
format=+%Y-%m-%d
if date --help 2>/dev/null | grep -q -- -d ; then
# GNU date (e.g., Linux)
date -d "1970-01-01 00:00 UTC + $seconds seconds" $format
else
# For BSD date (e.g., Mac OS X)
date -r $seconds $format
fi
Well, if running Perl counts as part of the script, then develop the answer in Perl. The next question is - what defines a business day? Are you a shop/store that is open on Sunday? Saturday? Or a 9-5 Monday to Friday business? What about holidays?
Assuming you're thinking Monday to Friday and holidays are temporarily immaterial, then you can use an algorithm in Perl that notes that wday will be 0 on Sunday through 6 on Saturday, and therefore if wday is 1, you need to subtract 3 * 86400 from time(); if wday is 0, you need to subtract 2 * 86400; and if wday is 6, you need to subtract 1 * 86400. That's what you've got in the Korn shell stuff - just do it in the Perl instead:
#!/bin/perl -w
use strict;
use POSIX;
use constant SECS_PER_DAY => 24 * 60 * 60;
my(#days) = (2, 3, 1, 1, 1, 1, 1);
my($now) = time;
my($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst)=localtime($now);
print strftime("%Y-%m-%d\n", localtime($now - $days[$wday] * SECS_PER_DAY));
This does assume you have the POSIX module; if not, then you'll need to do roughly the same printf() as you used. I also use ISO 8601 format for dates by preference (also used by XSD and SQL) - hence the illustrated format.
This should work for Solaris and Linux. It's realy complicating on Unix that you can not use the same commandline arguments on all Unix derivates.
On Linux you can use date -d '-d24 hour ago' to get the last day
on Solaris its TZ=CET+24 date. I guess other UNIX'es works the same way as Solaris does.
#!/usr/bin/ksh
lbd=5 # last business day (1=Mon, 2=Thu ... 6=Sat, 7=Sun)
lbd_date="" # last business day date
function lbdSunOS
{
typeset back=$1
typeset tz=`date '+%Z'` # timezone
lbd_date=`TZ=${tz}+$back date '+%Y%m%d'`
}
function lbdLinux
{
typeset back=$1
lbd_date=`date -d "-d$back hour ago"`
}
function calcHoursBack
{
typeset lbd=$1
typeset dow=`date '+%u'` # day of the week
if [ $dow -ge $lbd ]
then
return $(((dow-lbd)*24))
else
return $(((dow-lbd+7)*24))
fi
}
# Main
calcHoursBack $lbd
lbd`uname -s` $?
echo $lbd_date
Related
I'm creating a condition that checks the actual date in epoch format and compares another string in epoch format; if the strings are more than 10 days old, do something...
i tried something like this:
#!/bin/bash
timeago='10 days ago'
actual_date=$(date --date "now" +'%s')
last_seen_filter=$(date --date "$timeago" +'%s')
echo "INFO: actual_date=$actual_date, last_seen_filter=$last_seen_filter" >&2
if [ "$actual_date" -lt "$last_seen_filter" ]; then
echo "something"
else
echo "do something"
fi
or
#!/bin/bash
cutoff=$(date -d '10 days ago' +%s)
key="1624684050 1624688000"
while read -r "$key"
do
age=$(date -d "now" +%s)
if (($age < $cutoff))
then
printf "Warning! key %s is older than 10 days\n" "$key" >&2
fi
done < input
That's not enough for what I need, I have epoch dates in a file called converted_data, i need to include this strings on if comparision.
1624684050
1634015250
1614661650
1622005650
It's not clear what you're trying to do but maybe this with GNU awk will get you started:
$ awk '
BEGIN { today=strftime("%F") }
{
secs = $0
days = 0
date = strftime("%F",secs)
while ( strftime("%F",secs+=(24*60*60)) < today ) {
++days
}
print $0":", date, "->", today, "=", days
}
' file
1624684050: 2021-06-26 -> 2021-06-13 = 0
1634015250: 2021-10-12 -> 2021-06-13 = 0
1614661650: 2021-03-01 -> 2021-06-13 = 102
1622005650: 2021-05-26 -> 2021-06-13 = 17
The 0s for future dates are because you only asked about 10-days past dates so I don't care to adapt for both past and future deltas. I also didn't put much thought into it so check the logic and the math!
I need to sort data on a weekly base and all i have are dates in a logfile.
Therefore to sort out data per week i would like to create a list with the dates of all mondays for a given year. I have tried to work something out and the only idea i currently have is to use ncal with year and month as argument looping over all months and extracting all mondays. Isn't there a more efficient way?
To get all mondays, by getting all dates and filtering by Mondays:
for i in `seq 0 365`
do date -d "+$i day"
done | grep Mon
Of course, you could also take a monday and keep incrementing by 7 days.
hope that's what you mean. Below can be changed to vary the output formats of the dates.
date command can be used for that, dunno if ncal is any more/less efficient.
I know you went for "binning" now, but here is a more readable v.
$ cat /tmp/1.sh
#!/bin/bash
test -z "$year" && {
echo "I expect you to set \$year environment variable"
echo "In return I will display you the Mondays of this year"
exit 1
}
# change me if you would like the date format to be different
# man date would tell you all the combinations you can use here
DATE_FORMAT="+%Y-%m-%d"
# change me if you change the date format above. I need to be
# able to extract the year from the date I'm shoing you
GET_YEAR="s/-.*//"
# this value is a week, in milliseconds. Changing it would change
# what I'm doing.
WEEK_INC=604800
# Use another 3-digit week day name here, to see dates for other week days
DAY_OF_WEEK=Mon
# stage 1, let's find us the first day of the week in this year
d=1
# is it DAY_OF_WEEK yet?
while test "$(date -d ${year}-1-${d} +%a)" != "$DAY_OF_WEEK"; do
# no, so let's look at the next day
d=$((d+1));
done;
# let's ask for the milliseconds for that DAY_OF_WEEK that I found above
umon=$(date -d ${year}-1-${d} +%s)
# let's loop until we break from inside
while true; do
# ndate is the date that we testing right now
ndate=$(date -d #$umon "$DATE_FORMAT");
# let's extract year
ny=$(echo $ndate|sed "$GET_YEAR");
# did we go over this year? If yes, then break out
test $ny -ne $year && { break; }
# move on to next week
umon=$((umon+WEEK_INC))
# display the date so far
echo "$ndate"
done
No need to iterate over all 365 or 366 days in the year. The following executes date at most 71 times.
#!/bin/bash
y=2011
for d in {0..6}
do
if (( $(date -d "$y-1-1 + $d day" '+%u') == 1)) # +%w: Mon == 1 also
then
break
fi
done
for ((w = d; w <= $(date -d "$y-12-31" '+%j') - 1; w += 7))
do
date -d "$y-1-1 + $w day" '+%Y-%m-%d'
done
Output:
2011-01-03
2011-01-10
2011-01-17
2011-01-24
2011-01-31
2011-02-07
2011-02-14
2011-02-21
2011-02-28
2011-03-07
. . .
2011-11-28
2011-12-05
2011-12-12
2011-12-19
2011-12-26
Another option that I've come up based on the above answers. The start and end date can now be specified.
#!/bin/bash
datestart=20110101
dateend=20111231
for tmpd in {0..6}
do
date -d "$datestart $tmpd day" | grep -q Mon
if [ $? = 0 ];
then
break
fi
done
for ((tmpw = $tmpd; $(date -d "$datestart $tmpw day" +%s) <= $(date -d "$dateend" +%s); tmpw += 7))
do
echo `date -d "$datestart $tmpw day" +%d-%b-%Y`
done
You can get the current week number using date. Maybe you can sort on that:
$ date +%W -d '2011-02-18'
07
#####DATE1=201609
#### DATE2=201508
How to calculate the difference between these two date and get output as count of no of month
ie
201609-201508=13month
The calculation of the time difference is generally a complicated task, even for a single calendar type (and there are many). Many programming languages have a built-in support for date and time manipulation operations, including calculation of the time difference. But the most useful feature available in the popular shells is the date command which lacks this feature, unfortunately.
Therefore, we should whether write a script in another language, or make some assumptions such as the number of days in the year.
For example, in Perl the task is done with just four lines of code:
perl -e $(cat <<'PerlScript'
use Time::Piece;
my $t1 = Time::Piece->strptime($ARGV[0], '%Y%m');
my $t2 = Time::Piece->strptime($ARGV[1], '%Y%m');
printf "%d months\n", ($t1 - $t2)->months;
PerlScript
) 201609 201508
However, the difference of Time::Piece objects is an instance of Time::Seconds which actually assumes that
there are 24 hours in a day, 7 days in a week, 365.24225 days in a
year and 12 months in a year.
which indirectly confirms my words regarding the complexity of the task.
Then let's make the same assumption, and write a simple shell script:
DATE1=201609
DATE2=201508
printf '(%d - %d) / 2629744.2\n' \
$(date -d ${DATE1}01 +%s) \
$(date -d ${DATE2}01 +%s) | bc
where 2629744.2 is the number of seconds in month, i.e. 3600 * 24 * (365.24225 / 12).
Note, most of the shells do not support floating point arithmetic. That's why we need to invoke external tools such as bc.
The script outputs 13. This is a portable version. You may run it in the standard shell, Bash, Korn shell, or Zsh, for instance. If you want to put the result into a variable, just wrap the printf command in $( ... ):
months=$(printf '(%d - %d) / 2629744.2\n' \
$(date -d ${DATE1}01 +%s) \
$(date -d ${DATE2}01 +%s) | bc)
printf '%d - %d = %d months\n' $DATE1 $DATE2 $months
You can try below solution -
[vipin#hadoop ~]$ cat time.awk
{
diff1=((substr($1,1,4)) - (substr($2,1,4)))
diff2=((substr($1,5,2)) - (substr($2,5,2)))
if(diff1 > 0)
{
if(diff2 > 0)
{
print (diff1+diff2)
}
else if (diff2 = 0)
{
print (diff1+diff2)
}
else
{
diff2=(12+((substr($1,5,2)-(substr($2,5,2)))))
diff1=(diff1-1)
print (diff1+diff2)
}
}
else if(diff1 == 0)
{
if(diff2 > 0)
{
print (diff1+diff2)
}
else if (diff2 == 0)
{
print (diff1+diff2)
}
else
{
print "Argument 2 is greater than 1"
}
}
else
{
print "Argument 2 is greater than 1"
}
}
Test Cases -
[vipin#hadoop ~]$ cat time1.txt && awk -f time.awk time1.txt
201611 201601
10
[vipin#hadoop ~]$ cat time2.txt && awk -f time.awk time2.txt
201601 201611
Argument 2 is greater than 1
[vipin#hadoop ~]$ cat time3.txt && awk -f time.awk time3.txt
201511 201601
Argument 2 is greater than 1
[vipin#hadoop ~]$ cat time4.txt && awk -f time.awk time4.txt
201611 201611
0
Let's try something quick and dirty that won't work for all months in history, but it's probably good enough: convert YYYYMM to the number of months since year 0:
$ ym2m() {
if [[ $1 =~ ^([0-9]{4})([0-9]{2})$ ]]; then
echo $(( 10#${BASH_REMATCH[1]} * 12 + 10#${BASH_REMATCH[2]} ))
else
return 1
fi
}
$ ym2m 201609
24201
$ ym2m 201508
24188
$ echo $(( $(ym2m 201609) - $(ym2m 201508) ))
13
Notes:
requires bash version 4.3 (I think)
ym2m => "year-month to months"
uses 10#number in the arithmetic expression to ensure "08" and "09" are not treated as invalid octal numbers.
I'm trying to convert strings, describing a time interval, to the corresponding number of seconds.
After some experimenting I figured out that I can use date like this:
soon=$(date -d '5 minutes 10 seconds' +%s); now=$(date +%s)
echo $(( $soon-$now ))
but I think there should be an easier way to convert strings like "5 minutes 10 seconds" to the corresponding number of seconds, in this example 310. Is there a way to do this in one command?
Note: although portability would be useful, it isn't my top priority.
You could start at epoch
date -d"1970-01-01 00:00:00 UTC 5 minutes 10 seconds" "+%s"
310
You could also easily sub in times
Time="1 day"
date -d"1970-01-01 00:00:00 UTC $Time" "+%s"
86400
There is one way to do it, without using date command in pure bash (for portability)
Assuming you just have an input string to convert "5 minutes 10 seconds" in a bash variable with a : de-limiter as below.
$ convertString="00:05:10"
$ IFS=: read -r hour minute second <<< "$convertString"
$ secondsValue=$(((hour * 60 + minute) * 60 + second))
$ printf "%s\n" "$secondsValue"
310
You can run the above commands directly on the command-line without the $ mark.
This will do (add the epoch 19700101):
$ date -ud '19700101 5 minutes 10 seconds' +%s
310
It is important to add a -u to avoid local time (and DST) effects.
$ TZ=America/Los_Angeles date -d '19700101 5 minutes 10 seconds' +%s
29110
Note that date could do some math:
$ date -ud '19700101 +5 minutes 10 seconds -47 seconds -1 min' +%s
203
The previous suggestions didn't work properly on alpine linux, so here's a small helper function that is POSIX compliant, is easy to use and also supports calculations (just as a side effect of the implementation).
The function always returns an integer based on the provided parameters.
$ durationToSeconds '<value>' '<fallback>'
$ durationToSeconds "1h 30m"
5400
$ durationToSeconds "$someemptyvar" 1h
3600
$ durationToSeconds "$someemptyvar" "1h 30m"
5400
# Calculations also work
$ durationToSeconds "1h * 3"
10800
$ durationToSeconds "1h - 1h"
0
# And also supports long forms for year, day, hour, minute, second
$ durationToSeconds "3 days 1 hour"
262800
# It's also case insensitive
$ durationToSeconds "3 Days"
259200
function durationToSeconds () {
set -f
normalize () { echo $1 | tr '[:upper:]' '[:lower:]' | tr -d "\"\\\'" | sed 's/years\{0,1\}/y/g; s/months\{0,1\}/m/g; s/days\{0,1\}/d/g; s/hours\{0,1\}/h/g; s/minutes\{0,1\}/m/g; s/min/m/g; s/seconds\{0,1\}/s/g; s/sec/s/g; s/ //g;'; }
local value=$(normalize "$1")
local fallback=$(normalize "$2")
echo $value | grep -v '^[-+*/0-9ydhms]\{0,30\}$' > /dev/null 2>&1
if [ $? -eq 0 ]
then
>&2 echo Invalid duration pattern \"$value\"
else
if [ "$value" = "" ]; then
[ "$fallback" != "" ] && durationToSeconds "$fallback"
else
sedtmpl () { echo "s/\([0-9]\+\)$1/(0\1 * $2)/g;"; }
local template="$(sedtmpl '\( \|$\)' 1) $(sedtmpl y '365 * 86400') $(sedtmpl d 86400) $(sedtmpl h 3600) $(sedtmpl m 60) $(sedtmpl s 1) s/) *(/) + (/g;"
echo $value | sed "$template" | bc
fi
fi
set +f
}
Edit : Yes. I developed for OP after comment and checked on Mac OS X, CentOS and Ubuntu. One liner, POSIX compliant command for converting "X minutes Y seconds" format to seconds. That was the question.
echo $(($(echo "5 minutes 10 seconds" | cut -c1-2)*60 + $(echo "5 minutes 10 seconds" | cut -c1-12 | awk '{print substr($0,11)}')))
OP told me via comment that he wants for "X minutes Y seconds" format not for HH:MM:SS format. The command with date and "+%s" is throwing error on (my) Mac. OP wanted to grab the numerical values from "X minutes Y seconds" format and convert it to seconds. First I extracted the minute in digit (take it as equation A) :
echo "5 minutes 10 seconds" | cut -c1-2)
then I extracted the seconds part (take it as equation B) :
echo "5 minutes 10 seconds" | cut -c1-12 | awk '{print substr($0,11)}'
Now multiply minute by 60 then add with the other :
echo $((equation A)*60) + (equation B))
OP should ask the others to check my developmental version (but working) of command before using it for automatic repeated usage like we do with cron on a production server.
If we want to run this on a log file with values in "X minutes Y seconds" format, we have to change echo "5 minutes 10 seconds" to cat file | ... like command. I kept a gist of it too if I or others ever need we can use it with cat to run on server log files with x minutes y seconds like log format.
Although off-topic (what I understood, question has not much to do with current time), this is not working for POSIX-compliant OS to get current time in seconds :
date -d "1970-01-01 00:00:00 UTC 5 minutes 10 seconds" "+%s"
It will throw error on MacOS X but work on most GNU/Linux distro. That +%s part will throw error on POSIX-compliant OS upon complicated usage. These commands are mostly suitable to get current time in seconds on POSIX compliant to any kind of unix like OS :
awk 'BEGIN{srand(); print srand()}'
perl -le 'print time'
If OP needs can extend it by generating current time in seconds and subtract. I hope it will help.
---- OLD Answer before EDIT ----
You can get the current time without that date -- echo | awk '{print systime();}' or wget -qO- http://www.timeapi.org/utc/now?\\s. Other way to convert time to second is echo "00:20:40.25" | awk -F: '{ print ($1 * 3600) + ($2 * 60) + $3 }'.
The example with printf shown in another answer is near perfect.
That thing you want is always needed by the basic utilities of GNU/Linux - gnu.org/../../../../../Setting-an-Alarm.html
Way to approach really depends how much foolproof way you need.
I need to know the first monday of the current month using Cygwin bash.
One Liner:
d=$(date -d "today 1300" '+%Y%m01'); w=$(date -d $d '+%w'); i=$(( (8 - $w) % 7)); answer=$(( $d + $i ));
The result is stored in $answer. It uses working variables $d, $w, and $i.
Proof (assuming you just ran the one liner above):
echo $answer; echo $(date -d $answer '+%w')
Expected Result: Monday of current month in YYYYMMDD. On the next line, a 1 for the day of the week.
Expanded Proof (checks the next 100 month's Mondays):
for x in {1..100}; do d=$(date -d "+$x months 1300" '+%Y%m01'); w=$(date -d $d '+%w'); i=$(( (8 - $w) % 7)); answer=$(( $d + $i )); echo $answer; echo $(date -d $answer '+%w'); done | egrep -B1 '^[^1]$'
Expected Result: NOTHING
(If there are results, something is broken)
Breaking it down
The first statement gets the first day of the current month, and stores that in $d, formatted as YYYYMMDD.
The second statement gets the day of the week number of the date $d, and stores that in $w.
The third statement computes the increment of days to add and stores it in $i. Zero is perfectly valid here, because...
The last statement computes the sum of the date $d (as an integer) and the increment $i (as an integer). This works because the domain of the $i is 0 to 6, and we will always start at the first day of the month. This can quickly be converted back to a date variable (see Proof for example of this).
This has been tested on BASH v4.1 (CentOS 6), v4.4 (Ubuntu), and v5 (Archlinux)
A one-liner--I hope it's correct
d=$(date -d date +%Y%m"01" +%u);date -d date +%Y%m"0"$(((9-$d)%7))
the variable d contains the day of week (1..7) where 1 is Monday
then I print the current year and month changing the day with $((9-$d))
This should do it, but I have no Cygwin here to test:
#!/bin/bash
# get current year and month:
year=$( date +"%Y" )
month=$( date +"%m" )
# for the first 7 days in current month :
for i in {1..7}
do
# get day of week (dow) for that date:
dow=$( date -d "${year}-${month}-${i}" +"%u" )" "
# if dow is 1 (Monday):
if [ "$dow" -eq 1 ]
then
# print date of that Monday in default formatting:
date -d "${year}-${month}-${i}"
break
fi
done
See manpage date(1) for more information.