Engineering Notation

AMG: The following code has numerous performance, security, correctness, and stylistic shortcomings and should not be used except as a learning exercise.

A way to convert any arbitrary number to engineering notation [L1 ]

 # eng - return engineering notation of any given number in a 2 element list as {num eng_unit}
 # pico nano micro milli 1 kilo Mega Giga Tera
 # use: eng <num> <unit>   eg. eng 123.456E6 Hz
 # First - scan to check if n is a number, if not then just return with input given {n u}
 proc eng {n u} {
        if ![scan $n %g res] {return [list $n {} $u]}
        if [expr $n>=1E-12 && $n<1E-9] {list [expr $n*1E12] p$u} \
        elseif [expr $n>=1E-9 && $n<1E-6] {list [expr $n*1E9] n$u} \
        elseif [expr $n>=1E-6 && $n<1E-3] {list [expr $n*1E6] u$u} \
        elseif [expr $n>=1E-3 && $n<1] {list [expr $n*1E3] m$u} \
        elseif [expr $n>=1 && $n<1E3] {list [format %g $n] $u} \
        elseif [expr $n>=1E3 && $n<1E6] {list [expr $n/1E3] k$u} \
        elseif [expr $n>=1E6 && $n<1E9] {list [expr $n/1E6] M$u} \
        elseif [expr $n>=1E9 && $n<1E12] {list [expr $n/1E9] G$u} \
        elseif [expr $n>=1E12 && $n<1E15] {list [expr $n/1E12] T$u} \
        else {list [format %g $n] $u}
 ### Example Output
 %eng 1.23456E-2 V
 12.3456 mV
 %eng 209357.209857E5 Hz
 20.9357209857 GHz
 %eng 0.012e-7 H
 1.2 nH

CvK (Dec 2006) - is there any way this could be simplified ?

Bryan Oakley notes: that's a lot of unbraced expressions! MJ - even more so if you consider that if and elseif pass the first following argument to expr as well. Always brace your expr-essions.

For that matter, "if [expr ..." is a bit redundant.

AMG: The rampant indenting was hurting my eyes, so I removed it. This version should be a bit easier to read.

MJ - Try the following (with the additional benefit that it is easier to add more powers of 1000):

 proc eng {num u} {
        array set orders {
         -8 y -7 z -6 a -5 f -4 p -3 n -2 u -1 m 0 {} 1 k 2 M 3 G 4 T 5 P 6 E 7 Z 8 Y
        set numInfo  [split [format %e $num] e]
        set order [expr {[scan [lindex $numInfo 1] %d] / 3}]
        if {[catch {set orders($order)} prefix]} {return [list $num $u]}
        set num [expr {$num/pow(10,3*$order)}]
        return [list $num $prefix$u]

CvK - MJ, Thanks for your solution. It looks better and perhaps more readable. I agree that the nested "if [expr .." is redundant. Syntax highlighting helped with the aesthetics of indentation which is lost with the pre-formatting in this wiki. I tried your example but it gave a syntax error ...

 syntax error in expression "$num/(10.**(3*$order))": unexpected operator *

MJ - That's what I get for testing under 8.5 only. In 8.5 ** is a new operator. Fixed above. BTW not only is "if [expr .." redundant, it is also slower and invokes triple substitution which can lead to very unexpected or even unsecure behaviour. Example:

 % set a 1
 % set b {$a}
 % set c {$b}
 % if [expr $c] {puts "3 times is a charm :-)"} {puts "... or not"}
 3 times a charm :-)
 % set a 0
 % if [expr $c] {puts "3 times is a charm :-)"} {puts "... or not"}
 ... or not

CvK - The speed difference as measured by [clock clicks] is about 5 fold. I measured an average of about 430 [clock clicks] for the "if..elseif" version, while MJ's "pow()..array-lookup" version measured about 80 [clock clicks], approximately 5 times faster. Not sure what units of time [clock clicks] returns, but the measure is a relative difference anyway.

NEM - clock clicks returns a system-dependent value. You can use the time command for benchmarking.

 () 3 % time {eng 209357.209857E5 Hz } 1000 ; # ifelse version
 222.984 microseconds per iteration
 () 5 % time {eng 209357.209857E5 Hz } 1000 ; # array lookup version
 88.785 microseconds per iteration

aa - After turning all the

 elseif [expr ...] {list ...} \


 elseif {...} {list ...} \

I found the two routines to be roughly equivalent in time taken.

CvK - Oh, Just had a wiki edit conflict! probably aa - How did you remove or change the elseif [expr... ??

While aa was saying "I found the two routines to be roughly equivalent in time taken." I was going to say - time is cool! using this measurement I see that the array lookup version is about 8 times faster!

 () 1 % info tclversion
 () 2 % source eng.tcl; # elseif version
 () 3 % source eng2.tcl; # array lookup version
 () 4 % time {eng 209357.209857E5 Hz } 1000
 171.405 microseconds per iteration
 () 5 % time {eng2 209357.209857E5 Hz } 1000
 20.767 microseconds per iteration
 () 6 %

AMG: [time] is a marvel. Use it.

Very often I see code that contains supposed optimizations that only succeed in introducing bugs and (get this) slowing down execution. Example: At work I came across some code written by a highly competent programmer which contained a single, solitary global variable. The global was scoped to a function (i.e. "static"); it was going incognito. It cached the directional cosine matrix, following the assumption that a given latitude/longitude would be used several times in a row. When I saw this, my spider sense screamed "thread unsafe!" (which, as some will point out, is redundant), and I changed the function to recalculate the DCM every time. I thought to myself that it's always a good thing to sacrifice performance for stability, especially in my line of work. Someone else came along and profiled the code with and without my change. Guess what. Even though my version had a longer minimum execution time, it had a shorter average execution time! The original programmer's assumption about multiple calls in a row was incorrect. (One more thing: When I asked the programmer about it, he told me that he thought "static" made the variable thread-safe. Sheesh, if a man can write a correct, efficient, well-documented geodetic-geocentric coordinate conversion and utility library but can't remember the basic rules of thread safety, then I conclude that threads are beyond mortal comprehension.)

Want to make an optimization but don't know if the speedup is sufficient to justify the increased code complexity? [time] it! You might discover that your optimization actually decreases performance. Boy, wouldn't that be embarrassing? Or you might find that it cuts the time down by 90%, giving you major bragging rights. [time]! Impress your friends! Strike fear into the hearts of your foes! And best of all, it's fast. :^)

 % proc nop args {}
 % time nop 1000000
 0.256976 microseconds per iteration
 % time {time nop} 1000000
 5.92878 microseconds per iteration
 % expr {5.92878 - 0.256976}

CvK - removing the elseif [expr.. as suggested by aa actually slightly improves the execution time by about 35% (20uS down to 13uS) This is interesting and helps to see how Tcl works.

 () 1 % source eng.tcl ; # elseif [expr ... version
 () 2 % source eng2.tcl ; # array pow() lookup version
 () 3 % source eng3.tcl ; # elseif {..} {.. version
 () 4 % time {eng 209357.209857E5 Hz } 1000
 178.058 microseconds per iteration
 () 5 % time {eng2 209357.209857E5 Hz } 1000
 21.676 microseconds per iteration
 () 6 % time {eng3 209357.209857E5 Hz } 1000
 13.13 microseconds per iteration
 () 7 %

MJ - Of course all this trouble should really only be taken for performance bottlenecks, so first use a profiler. If the performance is good enough, easily maintainable and secure code is way more important than performance. For example, if bracing your expressions would have been 10x slower, I would still brace my expressions in most cases because of the double substitution if you don't.

CvK - Just for completeness, here is a simple way to reverse this operation and normalise the engineering notation back into floating point.

 # Reverse Operation - normalise engineering notation to float
 # num = number to convert, eng = engineering unit prefix or order
 proc eng2norm {num eng} {
   array set orders {
   a 1e-18 f 1e-15 p 1e-12 n 1e-9 u 1e-6 m 1e-3 {} 1 k 1e3 M 1e6 G 1e9 T 1e12 P 1e15
   if {[catch {set orders($eng)} res]} {return "Bad eng order!"}
   return [format %g [expr {$res * $num}]]

BTW - what improvements does Tcl8.5 offer? See Changes in Tcl/Tk 8.5

FMW - An example of a procedure to convert an arbitrary number to the same value but in engineering notation of the format -000.0000E-000 where the exponent is always a multiple of 3.

Is there a better way to do this?

#Conv2Eng: Convert a number in to Engineering Notation where the
#            exponent is provided as a multiple of 3.
#Usage: set Result [Conv2Eng $Number $Precision]
#  Result    = String in 000.0000E+000 Engineering Notation format
#  Number    = Any string that TCL recognizes as a number
#  Precision = The quantity of characters to the right of the decimal point
proc Conv2Eng {Number Precision} {

  #$Number must be a number
  if {![scan $Number %e foo]} {
    return 0

  #$Precision must be an integer
  if {![scan $Precision %d foo]} {
    return 0

  #Convert Number to floating point with specified precision
  set InPrecision [expr $Precision + 4]
  set InNumber [format "%.${InPrecision}E" $Number]

  #Split Number in to Mantissa and ExponentNumber 
  set InNumberSplit [split $InNumber E]
  set InMantissa [lindex $InNumberSplit 0]
  set InExponent [lindex $InNumberSplit 1]

  #Store the exponent sign
  set ExponentSign [string index $InExponent 0]

  #Capture the exponent value, remove leading zeros
  set InExponentValue [string trim [string range $InExponent 1 3] "0"]

  #If no digits remain then assume zero exponent
  if {[string length $InExponentValue] < 1} {
    set InExponentValue 0

  #Convert Exponent to a multiple of 3
  #But only if exponent is not 0
  if {$InExponent != 0} {

    #Calculate Exponent in terms of multiples of 3
    set Exponent [expr [expr $InExponentValue / 3] * 3]

    #Use remainder of above calculation to move mantissa around decimal point
    set Remainder [expr $InExponentValue % 3]
    #Positive exponents reduce value when decimal moves right
    #Negative exponents increase value when decimal moves right
    if {$ExponentSign == "-"} {
      if {$Remainder > 0} {
        set Exponent [expr $Exponent + 3]
        set Remainder [expr 3 - $Remainder]

    switch $Remainder {
      0 {set Mantissa [expr $InMantissa]}
      1 {set Mantissa [expr $InMantissa * 10]}
      2 {set Mantissa [expr $InMantissa * 100]}
      default {set Mantissa [expr $InMantissa]}

    } else {
      set Exponent 0
      set Mantissa $InMantissa

  #Return string formatted in the engineering style
  set Mantissa [format "%0.${Precision}f" $Mantissa]
  set Exponent [format "%#03u" ${Exponent}]
  return "${Mantissa}E${ExponentSign}${Exponent}"
}; #Conv2Eng

PN - This should provide you with eng units in a tighter package

proc ::eng {num {p 3}} {
        lassign [split [format %e $num] e] mant exp
        scan $exp %d exp ; # scan to stop octal issues and incr with sign issues
        while {[expr $exp % 3] != 0} {
                set mant [expr $mant*10] ; incr exp -1
        return [format %.${p}f $mant]E$exp

and its fast

(bin) 100 % time {eng 209357.209857} 10000
12.6776 microseconds per iteration
(bin) 101 % time {eng 123456789.24689} 10000
14.8718 microseconds per iteration
(bin) 102 % time {eng -123456789.24689} 10000
15.3979 microseconds per iteration
(bin) 103 % time {eng -0.0000024689} 10000
6.3128 microseconds per iteration
(bin) 104 % time {eng 209357.209857} 10000
12.5725 microseconds per iteration