Split string in ksh - linux

I got a string as follow :
foo=0j0h0min0s
What would be the best way to convert it in seconds without using date ?
I tried something like this that sounded pretty nice but no luck :
#> IFS=: read -r j h min s <<<"$foo"
#> time_s=$((((j * 24 + h) * 60 + min) * 60 + s))
ksh: syntax error: `<' unexpected
Any idea is welcome, I just can't use date -d to make conversion as it is not present on the system I am working on.

<<<"$foo" is mainly a bash-ism. It is supported in some/newer ksh. (google 'ksh here string' ).
Your read is trying to split at :, wich is not present in your input
If you first get rid of characters, you can split at blank (as ususal)
and changing the here-string to a here-doc
#!/bin/ksh
foo=1j2h3min4s
read -r j h min s << END
"${foo//[a-z]/ }"
END
# or echo "${foo//[a-z]/ }" | read -r j h min s
time_s=$((((j * 24 + h) * 60 + min) * 60 + s))
echo ">$foo< = >${foo//[a-z]/ }< = $j|$h|$min|$s => >$time_s<"
>1j2h3min4s< = >1 2 3 4 < = "1|2|3|4 " => >93784<
# or using array, easy to assign, more typing where used
typeset -a t=( ${foo//[a-z]/ } )
time_s=$(( (( t[0] * 24 + t[1]) * 60 + t[2]) * 60 + t[3] ))
echo ">$foo< = >${foo//[a-z]/ }< = ${t[0]}|${t[1]}|${t[2]}|${t[3]} => >$time_s<"

Related

Time difference in shell (hour)

I'm trying to calculate time difference stored inside of two variables inside of a shell script, I'm observing the following pattern:
hhmm -> 0950
so:
time1=1333
time2=0950
Now I need to calculate the difference in time between time1 and time2, as for now I have tried:
deltaTime=$(($time1-$time2))
but I'm facing the following error message
1333-0950: value too great for base (error token is "0950")
I'm expecting as a result: $deltaTime=0343
Unfortunately, I am strictly bound to use this time pattern. I have already researched for a solution online, some of them propose to use date -d... but I couldn't get it to work :(
Your approach has two issues.
First issue: bash recognizes numbers with leading zeroes as octal. You can force base10 by adding 10# prefix.
Second issue: it is incorrect to consider strings in hhmm format as numbers and substract them. e.g. 1333-950=383 but difference between 09:50 and 13:33 is 3 hours and 43 minutes. You should convert string values to common units, e.g. to minutes, substract them and convert back to hhmm format.
time1=1333
time2=0950
str2min()
{
printf "%u" $((10#${1%??} * 60 + 10#${1#??}))
}
min2str()
{
printf "%02u%02u" $(($1 / 60)) $(($1 % 60))
}
time1m=$(str2min $time1)
time2m=$(str2min $time2)
timediff=$(($time1m - $time2m))
deltaTime=$(min2str $timediff)
You could use this implementation maybe?
#!/usr/bin/env bash
diff_hhmm() {
local -r from=$1
local -i from_hh=10#${from:0:2} # skip 0 chars, read 2 chars (`${from:0:2}`) using base 10 (`10#`)
local -ri from_mm=10#${from:2:2} # skip 2 chars, read 2 chars (`${from:0:2}`) using base 10 (`10#`)
local -r upto=$2
local -ri upto_hh=10#${upto:0:2}
local -ri upto_mm=10#${upto:2:2}
local -i diff_hh
local -i diff_mm
# Compute difference in minutes
(( diff_mm = from_mm - upto_mm ))
# If it's negative, we've "breached" into the previous hour, so adjust
# the `diff_mm` value to be modulo 60 and compensate the `from_hh` var
# to reflect that we've already subtracted some of the minutes there.
if (( diff_mm < 0 )); then
(( diff_mm += 60 ))
(( from_hh -= 1 ))
fi
# Compute difference in hours
(( diff_hh = from_hh - upto_hh ))
# Ensure the result is modulo 24, the number of hours in a day.
if (( diff_hh < 0 )); then
(( diff_hh += 24 ))
fi
# Print the values with 0-padding if necessary.
printf '%02d%02d\n' "$diff_hh" "$diff_mm"
}
$ diff_hhmm 1333 0950
0343
$ diff_hhmm 0733 0950
2143
$ diff_hhmm 0733 0930
2203
Or an even shorter implementation using a big arithmetic compound command ((( ... )) ) and inlining some variables:
diff_hhmm_terse() {
local -i diff_hh diff_mm
((
diff_mm = 10#${1:2:2} - 10#${2:2:2},
diff_hh = 10#${1:0:2} - 10#${2:0:2},
diff_hh -= diff_mm < 0 ? 1 : 0,
diff_mm += diff_mm < 0 ? 60 : 0,
diff_hh += diff_hh < 0 ? 24 : 0
))
printf '%02d%02d\n' "$diff_hh" "$diff_mm"
}
Do you have the possibility to drop the leading zero?
As you can see from my prompt:
Prompt> echo $((1333-0950))
-bash: 1333-0950: value too great for base (error token is "0950")
Prompt> echo $((1333-950))
383
Other proposal:
date '+%s'
Let me give you some examples:
date '+%s'
1662357975
... (after some time)
date '+%s'
1662458180
=>
echo $((1662458180-1662357975))
100205 (amount of seconds)
=>
echo $(((1662458180-1662357975)/3600))
27 (amount of hours)
This bash one-liner may be used if time difference is not negative (that is, time1 >= time2):
printf '%04d\n' $(( 10#$time1 - 10#$time2 - (10#${time1: -2} < 10#${time2: -2} ? 40 : 0) ))

Do math on CSH or SH only with string variables

I have string variables MIN and SEC (minute and seconds).
MIN = "1"
SEC = "34"
I want to do calculations on these.
TOTSEC = MIN*60 + SEC
I tried:
expr $SEC + $MIN * 60
Result:
expr: non-numeric argument
Let it be known I am running busybox on a custom microcomputer and so have no access to bash,bc, and that other solution provides.
In sh, by which I'll assume you mean a POSIX shell, your best option is to use Arithmetic Expansion:
$ MIN=1
$ SEC=34
$ TOTSEC=$(( MIN * 60 + SEC ))
$ printf '%d\n' "$TOTSEC"
94
In csh however, the built-in math works quite differently:
% set MIN = 1
% set SEC = 34
% # TOTSEC = ( $MIN * 60 + $SEC )
% printf '%d\n' "$TOTSEC"
94
According to the man page, the # command permits numeric calculations to be performed and the result assigned to a variable.
Note that the expr command is external to the shell, so it should be usable from either one.
In sh:
$ TOTSEC=$(expr "$MIN" \* 60 + "$SEC")
And in csh:
% set TOTSEC = `expr "$MIN" \* 60 + "$SEC"`
Note: your sh may not be POSIX compliant. Most likely, it's ash, which is the ancestor of dash and FreeBSD's /bin/sh. You'll need to test in your environment.

Shell for loop, stopping at declaration

I'm trying to write a for loop that goes from 1 to 10, then calculates ( 1 through 10 mod 5) + 2. After that I want to display it like this (1 to 10 mod 5) + 2 = answer. However i'm getting an error at the beginning of the loop which is a syntax error.
for (( i = 0; i <= 10; i++)); do
calculate=(i % 5) + 2
echo ("("i "% 5) + 2" calculate)
done
Try these changes:
calculate=$(( i % 5 + 2 ))
# $(( ... )) is the shell's way to do arithmetic
echo "($i % 5) + 2 = " $calculate
# $x is a way to refer to the value of variable x
# (also inside a double-quoted string)
The for loop header is actually OK.

Add and Multiply numbers using shell script

I'm learning Shell scripting, I'm trying to write a small script that adds and multi number as shown below But amt value not display
value=212
amt=`expr "( $value * 2 + ( $value * 2 * .075 ) ) " | bc`
echo $amt
Your code works fine, but I suggest some improvements:
Use $(...) instead of backticks.
Replace expr with echo.
Example:
value=212
amt=$(echo "( $value * 2 + ( $value * 2 * .075 ) ) " | bc)
echo $amt
Output:
455.800
First things first. This:
value * 2 + (value * 2 * .075)
is a hot mess, this is equivalent:
value * 43 / 20
Next, I prefer to use awk for this job:
#!/usr/bin/awk -f
BEGIN {
value = 212
print value * 43 / 20
}

bc truncate floating point number

How do I truncate a floating point number using bc
e.g if I do
echo '4.2-1.3' | bc
which outputs 2.9 how I get it to truncate/use floor to get 2
Use / operator.
echo '(4.2-1.3) / 1' | bc
Dividing by 1 works ok if scale is 0 (eg, if you start bc with bc and don't change scale) but fails if scale is positive (eg, if you start bc with bc -l or increase scale). (See transcript below.) For a general solution, use a trunc function like the following:
define trunc(x) { auto s; s=scale; scale=0; x=x/1; scale=s; return x }
Transcript that illustrates how divide by 1 by itself fails in the bc -l case, but how trunc function works ok at truncating toward zero:
> bc -l
bc 1.06.95
[etc...]
for (x=-4; x<4; x+=l(2)) { print x,"\t",x/1,"\n"}
-4 -4.00000000000000000000
-3.30685281944005469059 -3.30685281944005469059
-2.61370563888010938118 -2.61370563888010938118
-1.92055845832016407177 -1.92055845832016407177
-1.22741127776021876236 -1.22741127776021876236
-.53426409720027345295 -.53426409720027345295
.15888308335967185646 .15888308335967185646
.85203026391961716587 .85203026391961716587
1.54517744447956247528 1.54517744447956247528
2.23832462503950778469 2.23832462503950778469
2.93147180559945309410 2.93147180559945309410
3.62461898615939840351 3.62461898615939840351
define trunc(x) { auto s; s=scale; scale=0; x=x/1; scale=s; return x }
for (x=-4; x<4; x+=l(2)) { print x,"\t",trunc(x),"\n"}
-4 -4
-3.30685281944005469059 -3
-2.61370563888010938118 -2
-1.92055845832016407177 -1
-1.22741127776021876236 -1
-.53426409720027345295 0
.15888308335967185646 0
.85203026391961716587 0
1.54517744447956247528 1
2.23832462503950778469 2
2.93147180559945309410 2
3.62461898615939840351 3
Try the following solution. It will truncate anything after the decimal point without a problem:
echo 'x = 4.2 - 1.3; scale = 0; x / 1' | bc -l
echo 'x = l(101) / l(10); scale = 0; x / 1' | bc -l
You can make the code a tad shorter by performing calculations directly on the numbers:
echo 'scale = 0; (4.2 - 1.3) / 1' | bc -l
echo 'scale = 0; (l(101) / l(10)) / 1' | bc -l
In general, you can use this function to get only the integer part of a number:
define int(x) {
auto s;
s = scale;
scale = 0;
x /= 1; /* This will have the effect of truncating x to its integer value */
scale = s;
return (x);
}
Save that code into a file (let's call it int.bc) and run the following command:
echo 'int(4.2 - 1.3);' | bc -l int.bc
The variable governing the amount of decimals on division is scale.
So, if scale is 0 (the default), dividing by 1 would truncate to 0 decimals:
$ echo '(4.2-1.3) / 1 ' | bc
2
In other operations, the number of decimals is calculated from the scale (number of decimals) of each operand. In add, subtract and multiplication, for example, the resulting scale is the biggest of both:
$ echo ' 4.2 - 1.33333333 ' | bc
2.86666667
$ echo ' 4.2 - 1.333333333333333333 ' | bc
2.866666666666666667
$ echo ' 4.2000 * 1.33 ' | bc
5.5860
Instead, in division, the number of decimals is strictly equal to th evalue of the variable scale:
$ echo 'scale=0;4/3;scale=3;4/3;scale=10;4/3' | bc
1
1.333
1.3333333333
As the value of scale has to be restored, it is better to define a function (GNU syntax):
$ echo ' define int(x){ os=scale;scale=0;x=x/1;scale=os;return(x) }
int( 4.2-1.3 )' | bc
2
Or in older POSIX language:
$ echo ' define i(x){
o=scale;scale=0;x=x/1;scale=o;return(x)
}
i( 4.2-1.3 )' | bc
2
You say:
truncate/use floor
And those are not the same thing in all cases. The other answers so far only show you how to truncate (i.e. "truncate towards zero" i.e. "discard the part after the decimal").
For negative numbers, the behavior is different.
To wit:
truncate(-2.5) = -2
floor(-2.5) = -3
So, here is a floor function for bc:
# Note: trunc(x) is defined as noted elsewhere in the other answers
define floor(x) {
auto t
t=trunc(x)
if (t>x) {
return t-1
} else {
return t
}
}
Aside:
You can put this, and other helper functions, in a file. For instance, I have this alias in my shell:
alias bc='bc -l ~/.bcinit'
And so whenever I run bc, I get all my utility functions from ~/.bcinit available by default.
Also, there is a good list of bc functions here: http://phodd.net/gnu-bc/code/funcs.bc
You may do something like this:
$ printf "%.2f\n" $(echo "(4530 / 4116 - 1) * 100" | bc -l)
10.06
Here I am trying to find the % change. Not purely bc though.

Resources