Determining durration between dates - bash

I'm still pretty new to programming in BASH and i'm racking my brain on a problem. I'm trying to code a script for my work that would make it easy to calculate a refund on a given package cancelled before it's regular end date. Now the simple stuff like inputs and other such things I'm sure I can figure out with googling and experimentation (though should you choose to help with that I would be most appreciative).
What i'm not sure I could find how to do on my own is a script or set of commands that would be able to calculate the number of days / months between two dates and use that variable to calculate the total ammount of refund. I'll try and explain what I am trying to do here:
Input two dates (MM/DD/YYYY format) and get the difference in months and day, IE 3 months 13 days.
Input Base monthly rate
(Base * # of months) + ((base/30) * # of days) = Ammount customer used
Input how much customer payed - Ammount used = Ammount to be refunded
Then preferably it prints it in a manner that one could use to "show their work"
I know this is a bit much to ask for help with but I really could use a hand with the date calculator.
Thanks for any help in advance!
PS I did do a bit of searching beforehand and I found questions SIMILAR to this but nothing that really fit exactly what I needed.

Using my dateutils you could go for
ddiff '2010-12-05' '2012-04-10' -f '%mmo %dd'
=>
16mo 5d

In python, you can use datetime.strptime() method from datetime module:
$ from=01/25/2011 to=01/30/2012
$ dur=`python -c "from datetime import datetime; print((datetime.strptime('$to', '%m/%d/%Y')-datetime.strptime('$from', '%m/%d/%Y')).days)"`
$ echo $dur
370

Assuming you do not want days to be greater than 30, this would do the work in bash:
date1="12/20/2011"
date2="02/18/2012"
y1=${date1/*\/*\//}
y2=${date2/*\/*\//}
m1=${date1/\/*\/*/}
m2=${date2/\/*\/*/}
d1=${date1/\/$y1/}
d1=${d1/$m1\//}
d2=${date2/\/$y2/}
d2=${d2/$m2\//}
m=$(( $y2 * 12 - $y1 * 12 + $m2 - $m1 ))
d=$(( $d2 - $d1 ))
test $d -lt 0 && { d=$(( $d + 30 )); m=$(( $m - 1 )); }
echo "$m month(s) and $d day(s)."
The date parsing can be simplified with the following if you always have 2 characters for DD and MM:
y1=${date1:6:4}
y2=${date2:6:4}
m1=${date1:0:2}
m2=${date2:0:2}
d1=${date1:3:2}
d2=${date2:3:2}

Using awk, you could:
date1="12/20/2011"
date2="02/19/2012"
echo -e "$date1\n$date2" | awk -F "/" -v base=15 '
NR == 1 {m1=$1; d1=$2; y1=$3}
NR == 2 {m2=$1; d2=$2; y2=$3}
END{m=12*(y2-y1) + m2-m1; d=d2-d1; m=d<0 ? m-1 : m; d=d<0 ? d+30 : d
print m " month(s) and " d " day(s)"
print "refund: " (base*m + (base/30.0)*d) "€"}'

Related

How do I create 100 files with 1 random number in each of them and give them permissions based on the number

I need to create a bash script that creates 100 files with random numbers in them.
I tried to do it using:
for i in {1..100}; do $RANDOM > RANDOM.txt
I don't know if that's the correct way to do it.
And then I need to give the files reading writing and execution permissions based on the number inside the file. No idea how to do that.
I tried using:
if [ $i%2==0 ]
then
echo chmod +rw $RANDOM.txt
but that doesn't seem to work
Just got some feedback, it turns out I was doing everything wrong.
I had to create 100 files 1.txt to 100.txt I used touch {1..100}.txt and then paste 1 random number in each of those files. Should I use echo or shuf to do this?
I think it'd be simplest to use chmod with octal permissions, like 0777 for rwxrwxrwx etc.
Example:
#!/bin/bash
for((i=0; i<100; ++i)) {
rnd=$RANDOM # pick a random number
(( perm = rnd % 512 )) # make it in the range [0, 512) (0000-0777 oct)
printf -v octperm "%04o" $perm # convert to octal
file=$rnd.txt # not sure if you meant to name it after the number
echo $rnd > $file
chmod $octperm $file # chmod with octal number
}
Excerpt of files:
-r-xrw--wx 1 ted users 5 15 dec 17.53 6515.txt
---x-wxrwx 1 ted users 5 15 dec 17.53 6751.txt
-rwx-w--w- 1 ted users 5 15 dec 17.53 8146.txt
-rw-r----- 1 ted users 5 15 dec 17.53 8608.txt
--w--w---x 1 ted users 5 15 dec 17.53 8849.txt
--wx----wx 1 ted users 5 15 dec 17.53 8899.txt
--wxrwx-wx 1 ted users 5 15 dec 17.53 8955.txt
-rw-r-xrw- 1 ted users 5 15 dec 17.53 9134.txt
...
If you want to take your current umask into consideration, you could do that too, by masking away the bits in the permission indicated by the umask.
#!/bin/bash
(( um = ~$(umask) )) # bitwise negated umask
for((i=0; i<100; ++i)) {
rnd=$RANDOM
(( perm = (rnd % 01000) & um )) # [0000,0777] bitwise AND umask
printf -v octperm "%04o" $perm
file=$i.$rnd.txt # using $i. to make name unique
echo $rnd > $file
chmod $octperm $file
}
If your umask is currently 0022 the above example would not create any files writeable for group and/or others while the other (normal) permissions would be random.
First, you need to echo the random number, not use it as a command.
Second, if you want to use the same random number as the filename and content, you need to save it to a variable. Otherwise you'll get a different number each time you write $RANDOM.
Third, that's not how you do arithmetic and conditions inside [], any shell scripting tutorial should show the correct way. You can also use a bash arithmetic expression with (( expression )).
#!/bin/bash
for i in {1..100}
do
r=$RANDOM
echo "$r" > "$r.txt"
if (( i % 2 == 0 ))
then
chmod +rw "$r.txt"
fi
done
From Ted Lyngmo's answer
With some bashisms, like using integer variables properties and avoiding forks...
declare -i um=" ~$(umask) " i rnd perm
for((i=100;i--;)){
rnd=RANDOM
perm=' ( rnd % 01000 ) & um '
printf -v file file-%03d-%04X.txt $i $rnd
printf -v octperm "%04o" "$perm"
echo "$rnd" > "$file"
chmod "$octperm" "$file"
}
(Filename is built with file number as decimal AND random number in hexadecimal)
About performances
Maybe a little quicker, because of avoiding forks and using integers.
( The for((;;)){ ;} syntax used here is not quicker, just different (shorter)...
In fact, for ((i=100;i--;)) ;do ...;done is (insensibly) slower than for i in {1..100};do ...;done! I just wanted to use this unusual syntax for extreme bashism... ;)
Some comparison:
export TIMEFORMAT=$'(%U + %S) / \e[1m%R\e[0m : %P'
About forks, trying 1'000 variable assignment for formatting, using printf:
time for i in {1..1000};do var=$(printf "foo");done
(0.773 + 0.347) / 1.058 : 105.93
time for i in {1..1000};do printf -v var "foo";done
(0.006 + 0.000) / 0.006 : 99.80
From 1.5 seconds to 6 milliseconds on my host!!! There are no discussion: forks (syntax $(printf...)) is to be avoided!!
About integer properties (using 100'000 binary operations):
declare -i intvar
time for i in {1..100000};do var=$(( ( 431214 % 01000 ) & -19 ));done
(0.272 + 0.005) / 0.278 : 99.46
time for i in {1..100000};do intvar=' ( 431214 % 01000 ) & -19 ';done
(0.209 + 0.000) / 0.209 : 99.87
From 0,28 seconds to 0.21 seconds, this is less significant, but.
About for i in { vs for ((i= (now using 1'000'000 loops):
time for i in {1..1000000};do :;done
(1.600 + 0.000) / 1.602 : 99.86
time for ((i=1000000;i--;));do :;done
(1.880 + 0.001) / 1.882 : 99.95
But this is clearly less significant (care about memory consumtion, using braces).
With awk, you could try following once. This program also take care of closing the open files in backend(in case we get error "too many files opened" once). Written and tested in GNU awk.
awk -v numberOfFiles="100" '
BEGIN{
srand()
for(i=1;i<=numberOfFiles;i++){
out=(int(1 + rand() * 10000))
print out > (out".txt")
system("chmod +rw " out".txt")
close(out)
}
}'
I have created an awk variable named numberOfFiles where I have given 100(as per need to create 100 files), you can keep it as per your need too, in future if you need to change it and we need not to change anything in rest of program here.

Converting a number into the format minutes:seconds.hundreths in bash script?

I am implementing the top command. I need to calculate the Time+ field just like the top command.
As of right now, I am getting the utime and stime then I put the system to sleep and then getting it these values again. I add the 4 quantities and divide the total by 100 which gives me a number. Here is the code for reference:
oldutime=$(awk '{print $14}' /proc/$word/stat )
oldstime=$(awk '{print $15}' /proc/$word/stat )
newutime=$(awk '{print $14}' /proc/$word/stat )
newstime=$(awk '{print $15}' /proc/$word/stat )
total_time=`expr $oldutime + $oldstime + $newutime + $newstime`
timee=$((total_time / 100))
After this I need to format this number so that it looks like the output of TIME+ field (minutes: seconds.hunderehts) in the top program and I need help doing that. I was looking at the date command but couldn't figure it out.
EDIT:
Desired for format: 0:00.24 (minutes:seconds.hunderdths)
Output: 360
The following scriptlet can be used to format time in milliseconds:
Code assume total_time is calculated - either per OP question, or modified for comments (using $((...))) instead of expr.
# Calculate total time in tick
total_time=...
ticks_per_sec=$(getconf CLK_TCK)
# Seconds times 100
s100=$((total_time*100/ticks_per_sec))
printf '%d:%02d.%02d\n' $((s100/100/60)) $((s100/100%60)) $((s100%100))

calculating days and displaying them as year, months, days between two dates shell script

I am trying to write a shell script which is going to determine the difference in years, months and days between the present date and Easter from a user input year. For example the user inputs 1995 and the script should calculate how many years have passed since then and to convert these days into years, months and days and display the results.
I'm pasting all of my code
#!/bin/bash
echo "This script will show you on which day is Easter for the chosen year of the Gregorian calendar!"
x=0
read x
A=$((x % 19))
B=$((x / 100))
C=$((x % 100))
D=$((B / 4))
E=$((B % 4))
G=$(((8 * B + 13) / (25)))
H=$(((19 * A + B - D - G + 15) % (30)))
M=$(((A + 11 * H) / (319)))
J=$((C / 4))
K=$((C % 4))
L=$(((2 * E + 2 * J - K - H + M + 32) % (7)))
N=$(((H - M + L + 90) / (25)))
P=$(((H - M + L + N + 19) % (32)))
Z=$(date --date="$x-$N-$P" +%A)
echo
echo "Easter is `date --date="$x-$N-$P"`"
([ "`cal 2 $x | grep 29`" != "" ] && echo -e "\n$x is a leap year\n")
([ "`cal 2 $x | grep 29`" = "" ] && echo -e "\n$x is not a leap year\n")
yearnow=$(date +%Y)
year=$(($x - $yearnow))
year1=$(($yearnow - $x))
if (($x > $yearnow))
then
echo "There are $year years until Easter in $x."
else
echo "$year1 years have passed since Easter in $x."
fi
pmonths=0
if (($x > $yearnow))
then
pmonths=$(($year * 12))
echo "There are $pmonths months until Easter."
else
pmonths=$(($year1 * 12))
echo "$pmonths months have passed since Easter in $x."
fi
#checking and counting how many leap and normal years are there between the present year and the chosen one
counter=1
leapycounter=0
nycounter=0
if (($x > $yearnow))
then
while (($counter < $year))
do
leapy=$(($x + $counter))
if (($leapy == (($leapy / 4) - ($leapy / 100) + ($leapy / 400))))
then leapycounter=$(($leapycounter + 1))
else nycounter=$(($nycounter + 1))
fi
counter=$(($counter + 1))
done
fi
#checking if the present year is leap so the days left from this year can be calculated
if (($x > $yearnow))
then
datenow=$(date +%j)
datenow=`echo $datenow|sed 's/^0*//'`
if (($yearnow == (($yearnow / 4) - ($yearnow / 100) + ($yearnow / 400))))
then
datenow=$((366 - $datenow))
else
datenow=$((365 - $datenow))
fi
datethen=$(date --date="$x-$N-$P" +%j)
datethen=`echo $datethen|sed 's/^0*//'`
days=$(($datethen + $datenow))
lyc=$((($leapycounter * 366) + ($nycounter * 365)))
dayspassed=$(($lyc + $days))
echo "There are $dayspassed days until Easter."
else
datethen=$(date --date="$x-$N-$P" +%j)
datethen=`echo $datethen|sed 's/^0*//'`
if (($yearnow == (($yearnow / 4) - ($yearnow / 100) + ($yearnow / 400))))
then
datethen=$((366 - $datethen))
else
datethen=$((365 - $datethen))
fi
datenow=$(date +%j)
datenow=`echo $datenow|sed 's/^0*//'`
days=$(($datethen + $datenow))
lyc=$((($leapycounter * 366) + ($nycounter * 365)))
dayspassed=$(($lyc + $days))
echo "$dayspassed days have passed since Easter in $x."
fi
#this should be converting the days into years, months and days
dtomconst=$(((((365/12)*3)+(366/12))/4))
months=$(($dayspassed / $dtomconst))
monthsleft=$(($months % 12))
years=$(($months / 12))
daysleft=$((($dayspassed - ($monthsleft * $dtomconst)) - (365*$years)))
echo "months are $months"
echo "daysleft are $daysleft"
echo $years
months=$(($months + $monthsleft))
echo $monthsleft
echo "months after calculations: $months"
So the problem is that it doesn't calculate the days properly especially for past years. Also if the user inputs a year like 1888 the script displays a mistake and I don't know why.
If somebody can say a word or two about my problem I would be really grateful. Thank you in advance.
As pointed out in the comments, the challenge with the script will be determining the day on which Easter occurred for a given year as the date varies from year to year given the order of weeks within each year. Further complicating the difference calculate is the concept of month as leap-year varies the length of February. There is also the occasional leap-second thrown in for good measure.
However, as indicated in the comment, once you have arrived at Easter for a given year, you can let the date function do most of the remaining work for you. Given any date, you can pass that value to the date function and covert the value into seconds since epoch. Note, this doesn't directly help for years prior to 1970, but you can extend further back manipulating the number of seconds per year as a close approximation.
Once you have Easter, getting the current time, expressed in terms of seconds since epoch, is trivial with date. You then have a difference to work with, converting the number of seconds that represent the time from the chosen Easter and now that can then be expressed in terms of years, days, hours, minutes, seconds. Granted these will have to be augmented to account for leap effects depending on the level of exactness required.
The following is a simple example of approaching the time difference problem. The function included, provides the difference given the time in seconds and then declares (as needed) the years, days, hours, minutes and seconds represented by the time given as an argument. This doesn't solve all of your issues, but hopefully it will help as a framework for handling that information in an easier manner. Let me know if you have any questions about the content:
#!/bin/bash
## time difference function takes an argument in seconds, and then calculates,
# declares and fills variables 'years' 'days' 'hours' 'minutes' and 'seconds'
# representing the time in seconds given as the argument. The values are only
# declared as necessary allowing a test for their presence.
function sec2ydhms {
[ -n $1 ] || { printf "%s() error: insufficient arguments\n" "$FUNCNAME"; return 1; }
local secperday=$((24 * 3600))
local secperyr=$((365 * secperday))
local remain=$1
# printf "\nremain: %s\n\n" "$remain"
if [ "$remain" -ge "$secperyr" ]; then
declare -g years=$((remain/secperyr))
remain=$((remain - (years * secperyr)))
fi
if [ "$remain" -ge "$secperday" ]; then
declare -g days=$((remain/secperday))
remain=$((remain - (days * secperday)))
fi
if [ "$remain" -ge 3600 ]; then
declare -g hours=$((remain/3600))
remain=$((remain - (hours * 3600)))
fi
if [ "$remain" -ge 60 ]; then
declare -g minutes=$((remain/60))
fi
declare -g seconds=$((remain - (minutes * 60)))
}
oifs=$IFS # save old IFS, and set to only break on newline
IFS=$'\n' # allowing date formats containing whitespace
printf "\n Enter the date for Easter (in past): "
read edate # read date entered
eepoch=$(date -d "$edate" +%s) # convert Easter date to seconds since epoch
now=$(date +%s) # get current time since epoch
sec2ydhms $((now-eepoch)) # compute time from Easter in Y,D,H,M,S
## print time since Easter
printf "\n Time since %s:\n\n" "$(date -d #"${eepoch}")"
[ -n "$years" ] && printf " %4s years\n" "$years"
[ -n "$days" ] && printf " %4s days\n" "$days"
[ -n "$hours" ] && printf " %4s hours\n" "$hours"
[ -n "$minutes" ] && printf " %4s minutes\n" "$minutes"
[ -n "$seconds" ] && printf " %4s seconds\n\n" "$seconds"
exit 0
output:
$ bash easter.sh
Enter the date for Easter (in past): 03/21/1985
Time since Thu Mar 21 00:00:00 CST 1985:
29 years
254 days
21 hours
12 minutes
16 seconds

Comparing Two Timestamps Within a Second

I am writing a shell script that parses a CSV file and performs some calculations.
The timestamps are in the form: HH:MM:SSS.sss and stored in variables: $t2 and $t1.
I would like to know the difference between the two stamps (it will always be less than one second) and report this as $t3 in seconds (ie: 0.020)
t3=$t2-$t1
But the above code is just printing the two variable with a minus sign between - how do I compare the two timestamps?
Here's a funky way to do it! Strip off all the whole seconds to get the milliseconds. Do the subtraction. If result has gone negative it's because the seconds overflowed, so add back in 1000ms. Slot a decimal point on the front to make seconds from milliseconds.
#!/bin/bash -xv
t1="00:00:02.001"
t2="00:00:03.081"
ms1=${t1/*\./}
ms2=${t2/*\./}
t3=$((10#$ms2-10#$ms1))
[[ $t3 < 0 ]] && t3=$((t3+1000))
t3=$(echo "scale=3; $t3/1000"|bc)
echo $t3
You can use awk maths to compute this difference in 2 timestamps after converting both timestamps to their milli-second value:
t1=04:13:32.234
t2=04:13:32.258
awk -F '[ :.]+' '{
t1=($1*60*60 + $2*60 + $3)*1000 + $4
t2=($5*60*60 + $6*60 + $7)*1000 + $8
print (t2-t1)/60}' <<< "$t1 $t2"
0.4
Formula used for conversion:
timestamp value (ms) = (hour * 60 * 60 + minute * 60 + second ) * 1000 + milli-second

Changing variable on set of conditions

This question is related to the thread here:
Today's date, minus X days in shell script
But because I'm now manipulating the variable, I started another thread.
As described above, I need to get today's date minus 200 days, with the Year, Month, and Day in separate variables (in this question I'll use 200, though in the other it's 222). However, I need to represent January as 0, February as 1 (or 01), March as 2 (or 02), etc... I tried this:
MONTHS200=$(date -j -v-200d -v-1m +"%m")
if ${MONTHS200}=01; then
${MONTH200}=0
else ${MONTHS200}=${MONTH200}
fi
But I get the error ./update_newdateformat.sh: line 20: 12=01: command not found ./update_newdateformat.sh: line 23: 12=: command not found The -v-1m works for all months except January, because it goes to 12, instead of 0
Here's how to shift all the month number down by 1 n your script:
MONTHS200=$(date -j -v-320d +"%m")
# Remove leading zero if there is one, so it doesn't cause problems later
MONTHS200=${MONTHS200#0}
MONTHS200=$((MONTHS200-1))
Here is how to use if and = (assignment) syntax in shell:
if [[ "${MONTHS200}" == "01" ]]; then
MONTHS200="0"
else
MONTHS200=${AnotherVariable}
fi
Note that for numerical comparisons, you need to use:
-eq instead of ==
-ne instead of !=
-lt instead of <
-le instead of <=
-gt instead of >
-ge instead of >=
For example:
if [[ "${MONTHS200}" -eq 1 ]]; then
I would take advantage of bash features (I assume OSX bash is recent enough -- I might be wrong). You only need to call date once with
read year month day < <(date -j -v-200d +"%Y %m %d")
month=$(( 10#$month - 1 ))
You avoid the octal issue by forcing bash to use base-10

Resources