Creating function to perform display-time arithmetic - bash

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

Related

Foreach hour:min:sec.ms

I would like to know how can I parse an foreach specifying hours, minutes, seconds and milliseconds. Something like this:
DURATION=00:14:17.82 // (00 hours, 14 minutes, 17 seconds and 82ms)
for ((i=1;i<=DURATION;i++)); do
// output the hour, minute, second and ms here
done
I want to iterate every milisecond, second, minute, hour! Like this:
00:14:17.82
00:14:17.83
00:14:17.84
00:14:17.85
...
00:14:18.01
00:14:18.02
...
01:00:10.15
01:00:10.16
01:00:10.17
...
I just know creating this for seconds and just seconds! But how can I make for hours, minutes, seconds and ms? Thank you so much!
NOTE: The following ideas assume a max DURATION of 23:59:59:99 or 24:00:00.00; upper ranges can certainly be increased but keep in mind the extra overhead (cpu and/or memory) for brace expansions, especially if generating huge ranges that dwarf DURATION (ie, generating large volumes of timestamps that you'll never process).
If you're going to manually enter the value for DURATION then you could manually enter the following brace expansion series:
for i in {00..00}:{00..14}:{00..17}.{00..82}
do
echo $i
done
NOTE: This relatively small set of of brace expansions was almost instantaneous and took up ~6 MB (RAM) on my system.
This generates:
00:00:00.00
00:00:00.01
00:00:00.02
00:00:00.03
00:00:00.04
... snip ...
00:14:17.78
00:14:17.79
00:14:17.80
00:14:17.81
00:14:17.82
Alternatively:
DURATION=00:14:17.82
for i in {00..23}:{00..59}:{00..59}.{00..99}
do
[[ "${i}" > "${DURATION}" ]] && break
# do other stuff
done
NOTE:
on my system this took about about 10 seconds and 1.6 GB (RAM) to expand the series of brace expansions before the loop was processed
this particular set of brace expansions creates 8.64 million timestamps but ...
we break out of the loop after processing ~ 86K timestamps (or ~1% of the timestamps generated by the brace expansions) so ...
we wasted a lot of cpu cycles and RAM generating timestamps that we never got around to using
Another idea is to write a 4-deep set of for loops and break out once we hit 00:14:17.82, eg:
DURATION='00:14:17.82'
for a in {00..23}; do
for b in {00..59}; do
for c in {00..59}; do
for d in {00..99}; do
[[ "${a}:${b}:${c}.${d}" > "${DURATION}" ]] && break 4
# do other stuff here
done
done
done
done
NOTE: While each brace expansion is fast (and uses a miniscule amount of RAM), the nested/repeated expansions is going to eat up a lot of time the longer it takes to get to ${DURATION}; however, unlike the previous ideas this one will at least let you start processing your timestamp values immediately, with intermittent (brief) delays as a new brace expansion is performed.
Of course we could just skip the brace expansion overhead and use 'normal' for loops, eg:
DURATION='00:14:17.82'
for ((a=0; a<=23; a++)); do
for ((b=0; b<=59; b++)); do
for ((c=0; c<=59; c++)); do
for ((d=0; d<=99; d++)); do
printf -v ts "%02d:%02d:%02d.%02d" ${a} ${b} ${c} ${d}
[[ "${ts}" > "${DURATION}" ]] && break 4
# do other stuff here
done
done
done
done
NOTES:
this eliminates the cpu/time/RAM overhead of the brace expansions
assumes you have a newer version of bash that supports printf -v
And for the masochists:
DURATION='00:14:17.82'
for ((i=0; i<8640000; i++)); do
hr=$(( ( i / 100 / 60 / 60 ) % 24 ))
min=$(( ( i / 100 / 60 ) % 60 ))
sec=$(( ( i / 100 ) % 60 ))
msec=$(( i % 100 ))
printf -v ts "%02d:%02d:%02d.%02d" ${hr} ${min} ${sec} ${msec}
[[ "${ts}" > "${DURATION}" ]] && break
# do other stuff
done
Just iterate over milliseconds and convert to the representation you want upon use. First, we write conversion functions:
millis_to_human() {
local h m s ms
# calculate all those hours, minutes, seconds...
((
ms = $1,
s = ms / 100,
ms %= 100,
m = s / 60,
s %= 60,
h = m / 60,
h %= 60,
1
))
printf "%02d:%02d:%02d.%02d\n" "$h" "$m" "$s" "$ms"
}
human_to_millis() {
# Match with regex
[[ $1 =~ ([0-9]*):([0-9]*):([0-9]*).([0-9]*) ]]
# Using 10# so that the number is always in base 10, even with leading 0.
echo $(( ( (
10#${BASH_REMATCH[1]} * 60 +
10#${BASH_REMATCH[2]} ) * 60 +
10#${BASH_REMATCH[3]} ) * 100 +
10#${BASH_REMATCH[4]} ))
}
Then just:
duration='00:14:17.82'
duration_ms=$(human_to_millis "$duration")
for ((i = 1; i <= duration_ms; ++i)); do
millis_to_human "$i"
done

Bash: Display Nano Seconds With Date

With date it is possible to generate formatted time strings like
date +"%Y-%m-%d-%H:%M:%S.%N"
With date it is also possible to create unix timestamps in nano seconds. Using
NANO_TIMESTAMP=$(date +%s%N)
Is it possible to use date to read in a nano second timestamp to create a formatted date string?
How can I pass a nano second timestamp to date?
I tried:
date -d$NANO_TIMESTAMP +%H:%M:%S.%N
date: invalid date ‘1550736813798767689’
date -d#$NANO_TIMESTAMP +"%Y-%m-%d-%H:%M:%S.%N"
date: time 1550736813798767689 is out of range
You can do that by first by converting the nanosecond value in EPOCH to seconds value in EPOCH and then use that for conversion to human readable strings
nano=1550736813798767689
date -d#"$(( $nano / 1000000000 ))" +"%Y-%m-%d-%H:%M:%S.%N"
For even more accurate representation back, take the modulo of the nano second value
withNano="$(( $nano % 1000000000 ))"
withoutNano="$(date -d#"$(( $nano / 1000000000 ))" +"%Y-%m-%d-%H:%M:%S")"
echo "$withoutNano.$withNano"
So putting this together in a wrapper function
from_nano_to_readable() {
(( $# )) || { printf '%s\n' 'provide atleast one argument' >&2 ; }
input="$1"
withNano="$(( $input % 1000000000 ))"
withoutNano="$(date -d#"$(( $input / 1000000000 ))" +"%Y-%m-%d-%H:%M:%S")"
printf '%s\n' "$withoutNano.$withNano"
}
and call it as
from_nano_to_readable 1550736813798767690
2019-02-21-03:13:33.798767690
Yes, but you have to do the math (division and modulo) yourself.
> set -x
> NANO_TIMESTAMP=$(date +%s%N)
++ date +%s%N
+ NANO_TIMESTAMP=1550740150623261543
> date -d#$((NANO_TIMESTAMP/(1000*1000*1000))).$((NANO_TIMESTAMP%(1000*1000*1000))) +%Y-%m-%d-%H:%M:%S.%N
+ date -d#1550740150.623261543 +%Y-%m-%d-%H:%M:%S.%N
2019-02-21-10:09:10.623261543
Like this but without math.
NANO_TIMESTAMP=$(date +%s%N)
secs=$(printf "%1d\n" ${NANO_TIMESTAMP: 0 : -9})
nanos=${NANO_TIMESTAMP: -9 : 9 }
printf '\r%s' $(TZ=UTC date -d#$secs.$nanos +"%Y-%m-%d-%H:%M:%S.%N")
With printf "%1d" I want to make sure, that there is at least one zero in the secs variable.

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

shell scripting time difference in variables

How can i get the time difference in 2 variables in shell
say i have 4 variables-
t1=07:50:19:612
t2=07:52:14:697
t3=10:20:54:201
t4=11:02:09:716
and i want to find difference in times
result=(t2-t1)+(t4-t3)
If milliseconds can't be ignored, I suggest you to define own shell functions :
function getMillis()
{
val=($(echo $1|grep -Eo "(00|[1-9][0-9]*)"))
mil=$(( ${val[0]} * 3600000 ))
mil=$(($mil + ${val[1]}*60000))
mil=$(($mil + ${val[2]}*1000))
mil=$(($mil + ${val[3]}))
echo $mil
}
function format()
{
hr=$(( $1 / 3600000 ))
mn=$(( $1 % 3600000 / 60000 ))
sc=$(( $1 % 60000 / 1000 ))
ms=$(( $1 % 1000 ))
echo "$hr hours, $mn mins, $sc secs, $ms millisecs"
}
Then you can obtain the desired result as :
res=$(( $(getMillis $t2) - $(getMillis $t1) + $(getMillis $t4) - $(getMillis $t3) ))
format $res
The code above is just to show how this can be done. There may be other elegant solutions present.
As far as I know, shell do not support date format for millisecond. The command date can handle time format, of which the precision is rounded up to second.
The following is an example for time format with second as mininum time unit:
t1=07:50:19
t2=07:52:14
t3=10:20:54
t4=11:02:09
t10=$(date -d $t1 +%s)
t20=$(date -d $t2 +%s)
t30=$(date -d $t3 +%s)
t40=$(date -d $t4 +%s)
result=$(expr $t20 - $t10 + $t40 - $t30)
echo $result
hour=$(expr $result / 3600)
min=$(expr $result % 3600 / 60)
sec=$(expr $result % 60)
echo $hour:$min:$sec

Resources