2

I have a program that outputs a time in the format

2s576039936ns

I need to convert this to the following format:

2.576039936

How can I do this in shell script?

Update: Please note that the ns part is not padded to 9 digits, so output like 2s513ns can occur and should be transformed to 2.000000513.

3
  • 4
    The answer depends very much. As you know, shell variables are untyped. Do you simply want to transform the text representation, or do you intend to perform floating-point arithmetic with the result? If so, please note that the shell is not designed for that.
    – AdminBee
    Mar 5, 2021 at 8:20
  • 1
    Shell only understands integers. There's nothing like a float in a shell. You can change a string, but you can't make it a float.
    – choroba
    Mar 5, 2021 at 8:20
  • 1
    @choroba, bash only understands integers. But several other shells (ksh93, zsh, yash, fish) don't have that limitation. Mar 5, 2021 at 15:02

7 Answers 7

7

This solution will handle 2s576039936ns as well as 2s123ns:

$ echo 2s576039936ns | tr '[:alpha:]' ' ' | awk -- '{ printf "%d.%09d",$1,$2 }'
2.576039936

$ echo 2s123ns | tr '[:alpha:]' ' ' | awk -- '{ printf "%d.%09d",$1,$2 }'
2.000000123
6

You could use this feature of Bash (not sure if this is POSIX):

x=2s576039936ns
y=${x/s/.}
z=${y/ns/}
echo $z

Or sed:

echo 2s576039936ns | sed 's/s/./; s/ns//'

Both methods first replace the s with a dot, then remove the ns.

And for something completely different:

echo 2s576039936ns | awk -F '[ns]' '{print $1 "." $2}'

This is assuming you only want to change the format and not the numbers.

2
  • 1
    ${param/pattern/replacemen} is from ksh93 and has not been added to the POSIX sh specification (contrary to ${param#pattern} for instance which is from ksh88). Mar 5, 2021 at 14:54
  • 3
    Also assumes that the ns value has been written with zero padding (%09d).
    – Paul Price
    Mar 5, 2021 at 19:12
4

With perl:

$ echo 10000s12576039936ns 1s1ns 0001s1000000ns 12s0ns |
   perl -Mbignum -pe 's{(\d+)s(\d+)ns}{$1 + $2 / 1e9}ge'
10012.576039936 1.000000001 1.001 12

Note that arbitrary precision (here using bignum in perl) is generally needed when dealing with second+nanosecond values in arithmetic operations, like in the struct timespec manipulated by clock_gettime() for instance if you want to preserve precision up to the nanosecond as your processor/C compiler's double type generally used by tools that do arithmetics (awk, perl without bignum, shells...) don't have enough precision.

With zsh:

string='10000s12576039936ns 1s1ns 0001s1000000ns 12s0ns'

set -o extendedglob
newstring=${string//(#b)(<->)s(<->)ns/\
$((match[1] + match[2] / 10**9))${${:-.${(l[9][0])match[2]}}%%(.|)0#}}

Would give you 10012.576039936 1.000000001 1.001 12 in $newstring as well (here using integer arithmetic on the two numbers as zsh also uses double for its floats).

2

if using awk is also an option:

awk -F'n?s' '$0=$1"."$2'

The n?s defined as field seperator sets both ns or s strings as the FS; it's ERE that means optional n followed by an s character which every awk supports FS to be defined as Extended Regular Expression.

0
1

Since it seems you only want to transform the "text representation", you can pipe the output of the command through sed. The following command first removes ns and then replaces s by a .:

~$ echo "2s576039936ns" | sed -e 's/ns//' -e 's/s/./'
2.576039936

Update

In a comment you stated that the nanosecond part is not zero-padded to exactly 9 digits. In that case, the output will be incorrect. To remedy, you can use the following awk solution:

~$ echo "2s576039936ns" | awk -F'n?s' '{printf "%d.%09d\n",$1,$2}'

This will use either s or ns as field separator (multi-character field separators will be interpreted as regular expression, not merely as multi-character string), and print fields one and two (the latter now zero-padded to 9 decimals) with . as separator.

So in your script, you could use

t=$( your_command | awk -F'n?s' '{printf "%d.%09d\n",$1,$2}' )
1

Another copuple of ways in bash:

For this specific purpose this work:

echo "${value%%s*}.${value:2:${#value}-4}"

For similar case, with more precision for the real part of the integer:

[[ $value =~ ^([[:digit:]]+)s([[:digit:]]+)ns$ ]]
new_value="${BASH_REMATCH[1]}.${BASH_REMATCH[2]}"
0

With a POSIX shell - or bash for that matter - you cannot transform the number in one step. Here I've replaced the s with a decimal point and then stripped the ns

v='2s576039936ns'

t="${v%%s*}.${v#*s}"
t="${v%ns}"

echo "$t"
2.576039936

You've tagged with so you can simplify a little, but it still takes two steps

v='2s576039936ns'

t="${v%ns}"
t="${t/s/.}"

echo "$t"
2.576039936

Alternatively, jump out to an external command and do it all in one step

v='2s576039936ns'

t=$(echo "$v" | sed -E sed -E 's/([[:digit:]]+)s(([[:digit:]]+)ns)?/\1.\3/')

echo "$t"
2.576039936

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .