Comparing Two Timestamps Within a Second - shell

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

Related

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))

Creating function to perform display-time arithmetic

Trying to create a function to get a number for hours(0-24) and a number for minute(0-60) and add 15 minutes to it. Issue is when I try to put a number 45 and up, I get a number over 60.
Created if then statement that takes my input and if the minutes are over 60, it will subtract 60 from minutes variable and add 1 to hour variable.
Expected results are that it will convert the time if over 60 minutes into 1 hour and remainder into minutes. The actual results are that sometimes the if then statement doesn't run correctly and it does both if AND then statements or sometimes I get a negative number.
#!/bin/bash
read -p "Hours: " hr1
read -p "Minutes: " min1
min1=$((min1+15))
Time() {
echo $min1
if $min1 > 60
then
min1=$((min1-60))
if $min1 < 0
then $((min1*-1))
fi
hr1=$((hr1+1))
else
$min1 + 15
fi
}
Time
echo $min1
echo $hr1
You can greatly simplify your Time() function by using shell arithmetic expressions delimited by (( and )). Here stand-alone arithmetic expressions are only valid with Bash shell but not posix shells where it is undefined
(( hr1 += min1 / 60, min1 %= 60 ))
Splits into two expressions separated by a comma ,:
hr1 += min1 / 60 ⇔ hr1 = hr1 + min1 / 60
min1 / 60 is evaluated in priority to the addition.
Shell arithmetic is integer only
min1 %= 60 ⇔ min1 = min1 % 60
The modulo reminder of min1 / 60
#!/usr/bin/env bash
read -r -p 'Hours: ' hr1
read -r -p 'Minutes: ' min1
# Bash stand-alone arithmetic expression add 15 to min1
(( min1+=15 ))
Time() {
echo "${min1}"
# Two Bash stand-alone arithmetic expressions separated by ,
# Add integer division of min1 by 60 to hr1
# Truncate min1 to the integer division reminder of itself by 60
(( hr1 += min1 / 60, min1 %= 60 ))
}
Time
echo "${min1}"
echo "${hr1}"
There are many, many ways to add minutes to a given time and then obtain the resulting hours and minutes. A systematic way is using the date function as Oliver Gaida shows. A manual conversion is fine for learning purposes, but you will want to ensure you deal with the addition of time that causes the hours to roll into the next day.
In order to handle all aspects of the conversion, it is useful to convert the total time to time into seconds. At that point, you can perform all necessary tests to determine if the total time has rolled the hours into the next day, etc..
Since you seem to only want to capture the hours and minutes of the resulting time and are not concerned with the number of days, you can simply test the number of seconds against the seconds-per-day and if the number of seconds exceeds seconds-per-day, simply reduce the number of seconds modulo by seconds-per-day.
A short function getHM() (your Time() function) to update the values in hr1 and min1 could be similar to the following:
## function converting number of seconds to hours, minutes (discarding days)
getHM() {
test -z "$1" && { ## validate input given
printf "error: insufficient arguments getHM()\n" >&2
return 1
}
local secs="$1" ## local variables seconds
local secsperday=$((3600 * 24)) ## seconds-per-day
local days=$((secs / secsperday)) ## days
(( days > 0 )) && { ## seconds exceed seconds-per-day
printf "error: time exceeds 24 hours, days discarded.\n" >&2
secs=$((secs % secsperday)) ## reduce secs modulo by secsperday
}
hr1=$((secs / 3600)) ## update hr1 & min1 values
min1=$(((secs - hr1 * 3600) / 60))
}
As noted in the function, it simply discards any additional days in order to return the resulting hours (0-23) and minutes (0-59).
Adding that to a short example and you could do:
#!/bin/bash
declare -i hr1=-1 min1=-1 addminutes=15 ## initialize variables
## function converting number of seconds to hours, minutes (discarding days)
getHM() {
test -z "$1" && { ## validate input given
printf "error: insufficient arguments getHM()\n" >&2
return 1
}
local secs="$1" ## local variables seconds
local secsperday=$((3600 * 24)) ## seconds-per-day
local days=$((secs / secsperday)) ## days
(( days > 0 )) && { ## seconds exceed seconds-per-day
printf "error: time exceeds 24 hours, days discarded.\n" >&2
secs=$((secs % secsperday)) ## reduce secs modulo by secsperday
}
hr1=$((secs / 3600)) ## update hr1 & min1 values
min1=$(((secs - hr1 * 3600) / 60))
}
## loop until valid input received
while ((hr1 < 0)) || ((hr1 > 23)) || ((min1 < 0)) || ((min1 > 59)); do
read -p "Hours: " hr1
read -p "Minutes: " min1
done
## convert all to seconds adding desired 15 minutes
secs=$((hr1 * 3600 + (min1 + addminutes) * 60))
getHM "$secs" ## call function to update hr1 & min1
printf "\nHours : %d\nMinutes : %d\n" "$hr1" "$min1"
(note: when using the arithmetic operator for comparison, e.g. ((...)), any non-integer values are evaluated as 0, so if you want to validate the use provides only integer input, you need to do that in the while loop after the read is complete -- and reset either variable to -1 if a non-integer value is detected)
Example Use/Output
No adjustment in hour required:
$ bash gethm.sh
Hours: 23
Minutes: 44
Hours : 23
Minutes : 59
Addition causing total time to land precisely at the start of a day:
$ bash gethm.sh
Hours: 23
Minutes: 45
error: time exceeds 24 hours, days discarded.
Hours : 0
Minutes : 0
(note: the error message provided if the total time causes the hours to roll into the next day. It is informational only and you can remove it to suit your needs)
Example showing the roll to 1 minute past the new day:
$ bash gethm.sh
Hours: 23
Minutes: 46
error: time exceeds 24 hours, days discarded.
Hours : 0
Minutes : 1
Look things over and let me know if you have further questions
With date it is easy. Convert the date to epoche-seconds, add 900 seconds and convert it back.
date --date="#$(echo $(($(date --date="22:53" +"%s")+900)))" +"%H:%M"
23:08

How do you add to time value returned by date?

I'm using the date function and trying to add minutes to the returned time (if it exceeds 60 it doesn't matter)
but every time I add to the time it removes the leading 0 and returns an odd value
time=$(date +%R)
time=$(sed -e 's/://g' <<< $time)
start=$(($time + 0051))
echo $start
output should be 4 digit ie 0445
edit: it's being treated as octal because of the leading 0.
To add 51 minutes to the current time with GNU date:
date "+%R" -d "+51 min"
Output (e.g.):
08:18

Sum durations in bash

I am getting execution time of various processes in a file from their respective log files. The file with execution time looks similar to following (it may have hundreds of entries)
1:00:01.11
2:2.20
1.02
The first line is hours:minutes:seconds, the second line is minutes:seconds and, the third is just seconds.
I want to sum all entries to come to a total execution time. How can I achieve this in bash? If not bash, then can you provide me some examples from other scripting language to sum timestamps?
To complement Matt Jacob's elegant perl solution with a (POSIX-compliant) awk solution:
awk -F: '{ n=0; for(i=NF; i>=1; --i) secs += $i * 60 ^ n++ } END { print secs }' file
With the sample input, this outputs (the sum of all time spans in seconds):
3724.33
See the section below for how to format this value as a time span, similar to the input (01:02:04.33).
Explanation:
-F: splits the input lines into fields by :, so that the resulting fields ($1, $2, ...) represent the hour, minute, and seconds components individually.
n=0; for(i=NF; i>=1; --i) secs += $i * 60 ^ n++ enumerates the fields in reverse order (first seconds, then minutes, then hours, if defined; NF is the number of fields) and multiplies each field with the appropriate multiple of 60 to yield an overall value in seconds, stored in variable secs, cumulatively across lines.
END { print secs } is executed after all lines have been processed and simply prints the cumulative value in seconds.
Formatting the output as a time span:
Custom output formatting must be used:
awk -F: '
{ n=0; for(i=NF; i>=1; --i) secs += $i * 60 ^ n++ }
END {
hours = int(secs / 3600)
minutes = int((secs - hours * 3600) / 60)
secs = secs % 60
printf "%02d:%02d:%05.2f\n", hours, minutes, secs
}
' file
The above yields (the equivalent of 3724.33 seconds):
01:02:04.33
The END { ... } block splits the total number of seconds accumulated in secs back into hours, minutes, and seconds, and outputs the result with appropriate formatting of the components using printf.
The reason that utilities such as date and GNU awk's (nonstandard) date-formatting functions cannot be used to format the output is twofold:
The standard time format specifier %H wraps around at 24 hours, so if the cumulative time span exceeds 24 hours, the output would be incorrect.
Fractional seconds would be lost (the granularity of Unix time stamps is whole seconds).
The mostly-readable full perl script:
use strict;
use warnings;
my $seconds = 0;
while (<DATA>) {
my #fields = reverse(split(/:/));
for my $i (0 .. $#fields) {
$seconds += $fields[$i] * 60 ** $i;
}
}
print "$seconds\n";
__DATA__
1:00:01.11
2:2.20
1.02
Or, the barely-readable one-liner version:
$ perl -F: -wane '#F = reverse(#F); $seconds += $F[$_] * 60 ** $_ for 0 .. $#F; END { print "$seconds\n" }' times.log
Output:
3724.33
In both cases, we're splitting each line on the H:M:S separator : and then reversing the array so that we can process from right-to-left. To get the total time in seconds, we can rely on a neat trick where we multiply each field by powers of 60.
If you want the result in H:M:S format instead of raw seconds, strftime() from the POSIX core module makes it easy:
use POSIX qw(strftime);
print strftime('%H:%M:%S', gmtime($seconds)), "\n";
Output:
01:02:04

Determining durration between dates

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) "€"}'

Resources