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 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 at 8:20
  • 1
    @choroba, bash only understands integers. But several other shells (ksh93, zsh, yash, fish) don't have that limitation. – Stéphane Chazelas Mar 5 at 15:02
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}' )
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). – Stéphane Chazelas Mar 5 at 14:54
  • 3
    Also assumes that the ns value has been written with zero padding (%09d). – Paul Price Mar 5 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

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

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.