units.tcl

Go to the documentation of this file.
00001 /* ----------------------------------------------*-TCL-*------------*/
00002 /* */
00003 /*   units.tcl*/
00004 /* */
00005 /*   The units package provides a conversion facility from a variety of*/
00006 /*   scientific and engineering shorthand notations into floating point*/
00007 /*   numbers.*/
00008 /* */
00009 /*   Robert W. Techentin*/
00010 /*   November 1, 2000*/
00011 /*   Copyright (C) Mayo Foundation.  All Rights Reserved.*/
00012 /* */
00013 /* -----------------------------------------------------------------*/
00014 package provide units 2.1
00015 
00016 package require Tcl 8.1
00017 
00018 namespace ::units {
00019 
00020     namespace export new
00021     namespace export convert
00022     namespace export reduce
00023 
00024     variable UnitTable
00025     variable PrefixTable
00026 }
00027 
00028 
00029 /* -----------------------------------------------------------------*/
00030 /* */
00031 /*  ::units::new --*/
00032 /* */
00033 /*   Add a new unit to the units table.  The new unit is defined*/
00034 /*   in terms of its baseUnits.  If baseUnits is "-primitive",*/
00035 /*   then it is assumed to be some magical new kind of quantity.*/
00036 /*   Otherwise, it must reduce to units already defined.*/
00037 /* */
00038 /* -----------------------------------------------------------------*/
00039 ret  ::units::new ( type args ) {
00040 
00041     variable UnitTable
00042     variable UnitList
00043 
00044     #  Check number of arguments
00045     switch [llength $args] {
00046     2 {
00047         set name [lindex $args 0]
00048         set baseUnits [lindex $args 1]
00049     }
00050     default {
00051         #  issue same error as C extension
00052         error "Wrong # args. units::new name baseUnits "
00053     }
00054     }
00055 
00056     # check for duplicates
00057     if { [info exists UnitTable($name)] } {
00058     error "unit '$name' is already defined"
00059     }
00060 
00061     # check for valid characters
00062     if { [regexp {[^a-zA-Z]} $name] } {
00063     error "non-alphabetic characters in unit name '$name'"
00064     }
00065 
00066     # Compute reduced units
00067     if { [catch {::units::reduce $baseUnits} reducedUnits] } {
00068     error "'$baseUnits' cannot be reduced to primitive units"
00069     }
00070 
00071     # add the unit, but don't return a value
00072     set UnitTable($name) $reducedUnits
00073     lappend UnitList $name $reducedUnits
00074     return
00075 }
00076 
00077 /* -----------------------------------------------------------------*/
00078 /* */
00079 /*  ::units::convert --*/
00080 /* */
00081 /*   Convert a value to the target units.*/
00082 /* */
00083 /*   If units are specified for the value, then they must*/
00084 /*   be compatible with the target units.  (i.e., you can */
00085 /*   convert "newtons" to "kg-m/s^2", but not to "sieverts".*/
00086 /* */
00087 /*  Arguments:*/
00088 /*   value  A value can be a floating point number, either with or*/
00089 /*          without units.  */
00090 /*   targetUnits  A units string which  may also include a scale factor.  */
00091 /* */
00092 /*  Results:*/
00093 /*   The return value is a scaled floating point number.*/
00094 /* */
00095 /* -----------------------------------------------------------------*/
00096 
00097 ret  ::units::convert ( type args ) {
00098 
00099     #  Check number of arguments
00100     switch [llength $args] {
00101     2 {
00102         set value [lindex $args 0]
00103         # make sure it isn't octal (bug 758702)
00104         set value [string trimleft $value "0"]
00105         set targetUnits [lindex $args 1]
00106     }
00107     default {
00108         #  issue same error as C extension
00109         error "Wrong # args. units::convert value targetUnits "
00110     }
00111     }
00112 
00113     #  Reduce each of value and target
00114     #  to primitive units
00115     set reducedValue [::units::reduce $value]
00116     set reducedTarget [::units::reduce $targetUnits]
00117 
00118     #  If the value has units, it must be compatible with
00119     #  the target.  (If it is unitless, then compatibility
00120     #  is not required.)
00121     if { [llength $reducedValue] > 1} {
00122     if {[lrange $reducedValue 1 end]!=[lrange $reducedTarget 1 end]} {
00123         error "'$value' and '$targetUnits' have incompatible units"
00124     }
00125     }
00126 
00127     #  Compute and return scaled value
00128     expr {[lindex $reducedValue 0] / [lindex $reducedTarget 0]}
00129 }
00130 
00131 
00132 /* -----------------------------------------------------------------*/
00133 /* */
00134 /*  ::units::reduce --*/
00135 /* */
00136 /*   Reduce a string of numbers, prefixes, units, exponents into a*/
00137 /*   single multiplicitive factor and sorted list of primitive units.*/
00138 /*   For example, the unit string for "newton", which is "m-kg/s^2"*/
00139 /*   would reduce to the list {1000.0 gram meter / second second}*/
00140 /* */
00141 /*   Unit String Syntax*/
00142 /* */
00143 /*   This procedure defines a valid unit string that may*/
00144 /*   be reduced to primitive units, so it is reasonable to*/
00145 /*   document valid unit string syntax here.*/
00146 /* */
00147 /*   A unit string consists of an optional scale factor followed*/
00148 /*   by zero or more subunit strings.  The scale factor must be*/
00149 /*   a valid floating point number.  */
00150 /* */
00151 /*   Subunits are separated by unit separator characters, which are */
00152 /*   " ", "-", "*", and "/".  It is not necessary to separate*/
00153 /*   the leading scale factor from the rest of the subunits.*/
00154 /* */
00155 /*   The forward slash seperator "/" indicates that following*/
00156 /*   subunits are in the denominator.  There can be at most*/
00157 /*   one "/" separator.*/
00158 /* */
00159 /*   Subunits can be floating point scale factors, but they*/
00160 /*   must be surrounded by valid separators.*/
00161 /* */
00162 /*   Subunits can be valid units or abbreviations from the*/
00163 /*   UnitsTable.  They may include a prefix from the PrefixTable.*/
00164 /*   They may include a plural suffix "s" or "es".  They may*/
00165 /*   also include a power string "^", followed by an integer,*/
00166 /*   after the unit name (or plural suffix, if there is one.)*/
00167 /* */
00168 /*   Examples of valid unit strings:  "meter", "/s", "kg-m/s^2",*/
00169 /*   "30second" "30 second", "30 seconds" "200*meter/20.5*second"*/
00170 /* */
00171 /*  Arguments:*/
00172 /*   unitString  string of units characters*/
00173 /* */
00174 /*  Results:*/
00175 /*   The return value is a list, the first element of which */
00176 /*   is the multiplicitive factor, and the remaining elements are*/
00177 /*   sorted reduced primitive units, possibly including the "/"*/
00178 /*   operator, which separates the numerator from the denominator.*/
00179 /* -----------------------------------------------------------------*/
00180 /* */
00181 
00182 ret  ::units::reduce ( type args ) {
00183 
00184     #  Check number of arguments
00185     switch [llength $args] {
00186     1 {
00187         set unitString [lindex $args 0]
00188     }
00189     default {
00190         #  issue same error as C extension
00191         error "Wrong # args. units::reduce unitString "
00192     }
00193     }
00194 
00195     # check for primitive unit - may already be reduced
00196     #  This gets excercised by new units
00197     if { "$unitString" == "-primitive" } {
00198     return $unitString
00199     }
00200 
00201     # trim leading and trailing white space
00202     set unitString [string trim $unitString]
00203 
00204     # Check cache of unitStrings
00205    if { [info exists ::units::cache($unitString)] } {
00206     return $::units::cache($unitString)
00207     }
00208 
00209     # Verify syntax of unit string
00210     #  It may contain, at most, one "/"
00211     if { [regexp {/.*/} $unitString] } {
00212     error "invalid unit string '$unitString':  only one '/' allowed"
00213     }
00214     #  It may contain only letters, digits, the powerstring ("^"),
00215     #  decimal points, and separators 
00216     if { [regexp {[^a-zA-Z0-9. \t*^/+-]} $unitString] } {
00217     error "invalid characters in unit string '$unitString'"
00218     }
00219 
00220     #  Check for leading scale factor
00221     #  If the leading characters are in floating point
00222     #  format, then extract and save them (including any
00223     #  minus signs) before handling subunit separators.
00224     #  This is based on a regexp from Roland B. Roberts which
00225     #  allows leading +/-, digits, decimals, and exponents.
00226     regexp {(^[-+]?(?:[0-9]+\.?[0-9]*|\.[0-9]+)(?:[eE][-+]?[0-9]+)?)?(.*)} \
00227         $unitString matchvar scaleFactor subunits
00228     #  Ensure that scale factor is a nice floating point number
00229     if { "$scaleFactor" == "" } {
00230     set scaleFactor 1.0
00231     } else {
00232     set scaleFactor [expr {double($scaleFactor)}]
00233     }
00234 
00235     #  replace all separators with spaces.
00236     regsub -all {[\t\-\*]} $subunits " " subunits
00237     #  add spaces around "/" character.
00238     regsub {/} $subunits " / " subunits
00239 
00240     #  The unitString is now essentially a well structured list
00241     #  of subunits, which may be processed as a list, and it
00242     #  may be necessary to process it recursively, without
00243     #  performing the string syntax checks again.  But check
00244     #  for errors.
00245     if { [catch {ReduceList $scaleFactor $subunits} result] } {
00246     error "$result in '$unitString'"
00247     }
00248 
00249     #  Store the reduced unit in a cache, so future lookups
00250     #  are much quicker.
00251     set ::units::cache($unitString) $result
00252 }
00253 
00254 
00255 /* -----------------------------------------------------------------*/
00256 /* */
00257 /*  ::units::ReduceList --*/
00258 /* */
00259 /*   Reduce a list of subunits to primitive units and a single*/
00260 /*   scale factor.*/
00261 /* */
00262 /*  Arguments:*/
00263 /*   factor      A scale factor, which is multiplied and divided*/
00264 /*               by subunit prefix values and constants.*/
00265 /*   unitString  A unit string which is syntactically correct*/
00266 /*               and includes only space separators.  This*/
00267 /*               string can be treated as a Tcl list.*/
00268 /* */
00269 /*  Results:*/
00270 /*   A valid unit string list, consisting of a single floating*/
00271 /*   point factor, followed by sorted primitive units.  If the */
00272 /*   forward slash separator "/" is included, then each of the*/
00273 /*   numerator and denominator is sorted, and common units have*/
00274 /*   been cancelled.*/
00275 /* */
00276 /* -----------------------------------------------------------------*/
00277 /* */
00278 ret  ::units::ReduceList ( type factor , type unitString ) {
00279 
00280     variable UnitList
00281     variable UnitTable
00282     variable PrefixTable
00283 
00284     # process each subunit in turn, starting in the numerator
00285     #
00286     #  Note that we're going to use a boolean flag to switch
00287     #  between numerator and denominator if we encounter a "/".
00288     #  This same style is used for processing recursively
00289     #  reduced subunits
00290     set numerflag 1
00291     set numerator [list]
00292     set denominator [list]
00293     foreach subunit $unitString {
00294 
00295     #  Check for "/"
00296     if { "$subunit" == "/" } {
00297         set numerflag [expr {$numerflag?0:1}]
00298         continue
00299     }
00300 
00301     #  Constant factor
00302     if { [string is double -strict $subunit] } {
00303         if { $subunit == 0.0 } {
00304         error "illegal zero factor"
00305         } else {
00306         if { $numerflag } {
00307             set factor [expr {$factor * $subunit}]
00308         } else {
00309             set factor [expr {$factor / $subunit}]
00310         }
00311         continue
00312         }
00313     }
00314 
00315     #  Check for power string (e.g. "s^2")
00316     #  We could use regexp to match and split in one operation,
00317     #  like {([^\^]*)\^(.*)} but that seems to be pretty durn
00318     #  slow, so we'll just using [string] operations.
00319     if { [set index [string first "^" $subunit]] >= 0 } {
00320         set subunitname [string range $subunit 0 [expr {$index-1}]]
00321         set exponent [string range $subunit [expr {$index+1}] end]
00322         if { ! [string is integer -strict $exponent] } {
00323         error "invalid integer exponent"
00324         }
00325         #  This is a good test and error message, but it won't
00326         #  happen, because the negative sign (hypen) has already
00327         #  been interpreted as a unit separator.  Negative
00328         #  exponents will trigger the 'invalid integer' message,
00329         #  because there is no exponent. :-)
00330         if { $exponent < 1 } {
00331         error "invalid non-positive exponent"
00332         }
00333     } else {
00334         set subunitname $subunit
00335         set exponent 1
00336     }
00337 
00338     # Check subunit name syntax
00339     if { ! [string is alpha -strict $subunitname] } {
00340         error "invalid non-alphabetic unit name"
00341     }
00342 
00343     #  Try looking up the subunitname.  
00344     #
00345     #  Start with the unit name.  But if the unit ends in "s"
00346     #  or "es", then we want to try shortened (singular)
00347     #  versions of the subunit as well.
00348     set unitValue ""
00349 
00350     set subunitmatchlist [list $subunitname]
00351     if { [string range $subunitname end end] == "s" } {
00352         lappend subunitmatchlist [string range $subunitname 0 end-1]
00353     }
00354     if { [string range $subunitname end-1 end] == "es" } {
00355         lappend subunitmatchlist [string range $subunitname 0 end-2]
00356     }
00357 
00358     foreach singularunit $subunitmatchlist {
00359 
00360         set len [string length $singularunit]
00361 
00362         #  Search the unit list in order, because we 
00363         #  wouldn't want to accidentally match the "m" 
00364         #  at the end of "gram" and conclude that we 
00365         #  have "meter".  
00366         foreach {name value} $UnitList {
00367 
00368         #  Try to match the string starting at the
00369         #  at the end, just in case there is a prefix.
00370         #  We only have a match if both the prefix and
00371         #  unit name are exact matches.
00372         set pos [expr {$len - [string length $name]}]
00373         #set pos [expr {$len-1}]
00374         if { [string range $singularunit $pos end] == $name } {
00375 
00376             set prefix [string range $singularunit 0 [expr {$pos-1}]]
00377             set matchsubunit $name
00378 
00379             #  If we have no prefix or a valid prefix, 
00380             #  then we've got an actual match.
00381             if { ("$prefix" == "") || \
00382                 [info exists PrefixTable($prefix)] } {
00383             #  Set the unit value string
00384             set unitValue $value
00385             # done searching UnitList
00386             break
00387             }
00388         }
00389         # check for done 
00390         if { $unitValue != "" } {
00391             break
00392         }
00393         }
00394     }
00395 
00396     # Check for not-found
00397     if { "$unitValue" == "" } {
00398         error "invalid unit name '$subunitname'"
00399     }
00400 
00401     #  Multiply the factor by the prefix value
00402     if { "$prefix" != "" } { 
00403         #  Look up prefix value recursively, so abbreviations
00404         #  like "k" for "kilo" will work.  Note that we
00405         #  don't need error checking here (as we do for
00406         #  unit lookup) because we have total control over
00407         #  the prefix table.
00408         while { ! [string is double -strict $prefix] } {
00409         set prefix $PrefixTable($prefix)
00410         }
00411         # Save prefix multiple in factor
00412         set multiple [expr {pow($prefix,$exponent)}]
00413         if { $numerflag } {
00414         set factor [expr {$factor * $multiple}]
00415         } else {
00416         set factor [expr {$factor / $multiple}]
00417         }
00418     }
00419 
00420 
00421     # Is this a primitive subunit?
00422     if { "$unitValue" == "-primitive" } {
00423         # just append the matching subunit to the result
00424         # (this doesn't have prefix or trailing "s")
00425         for {set i 0} {$i<$exponent} {incr i} {
00426         if { $numerflag } {
00427             lappend numerator $matchsubunit
00428         } else {
00429             lappend denominator $matchsubunit
00430         }
00431         }
00432     } else {
00433         #  Recursively reduce, unless it is in the cache
00434         if { [info exists ::units::cache($unitValue)] } {
00435         set reducedUnit $::units::cache($unitValue)
00436         } else {
00437         set reducedUnit [::units::reduce $unitValue]
00438         set ::units::cache($unitValue) $reducedUnit
00439         }
00440 
00441         #  Include multiple factor from reduced unit
00442         set multiple [expr {pow([lindex $reducedUnit 0],$exponent)}]
00443         if { $numerflag } {
00444         set factor [expr {$factor * $multiple}]
00445         } else {
00446         set factor [expr {$factor / $multiple}]
00447         }
00448 
00449         #  Add primitive subunits to numerator/denominator
00450         #
00451         #  Note that we're use a nested boolean flag to switch
00452         #  between numerator and denominator.  Subunits in
00453         #  the numerator of the unitString are processed
00454         #  normally, but subunits in the denominator of
00455         #  unitString must be inverted.
00456         set numerflag2 $numerflag
00457         foreach u [lrange $reducedUnit 1 end] {
00458         if { "$u" == "/" } {
00459             set numerflag2 [expr {$numerflag2?0:1}]
00460             continue
00461         }
00462         #  Append the reduced units "exponent" times
00463         for {set i 0} {$i<$exponent} {incr i} {
00464             if { $numerflag2 } {
00465             lappend numerator $u
00466             } else {
00467             lappend denominator $u
00468             }
00469         }
00470         }
00471     }
00472     }
00473 
00474     #  Sort both numerator and denominator
00475     set numerator [lsort $numerator]
00476     set denominator [lsort $denominator]
00477 
00478     #  Cancel any duplicate units.
00479     #  Foreach and for loops don't work well for this.
00480     #  (We keep changing list length).
00481     set i 0
00482     while {$i < [llength $numerator]} {
00483     set u [lindex $numerator $i]
00484     set index [lsearch $denominator $u]
00485     if { $index >= 0 } {
00486         set numerator [lreplace $numerator $i $i]
00487         set denominator [lreplace $denominator $index $index]
00488     } else {
00489         incr i
00490     }
00491     }
00492 
00493     #  Now we've got numerator, denominator, and factors.
00494     #  Assemble the result into a single list.
00495     if { [llength $denominator] > 0 } {
00496     set result [eval list $factor $numerator "/" $denominator]
00497     } else {
00498     set result [eval list $factor $numerator]
00499     }
00500 
00501     #  Now return the result
00502     return $result
00503 }
00504 
00505 
00506 /* -----------------------------------------------------------------*/
00507 /* */
00508 /*   Initialize namespace variables*/
00509 /* */
00510 /* -----------------------------------------------------------------*/
00511 namespace ::units {
00512 
00513      PrefixList =  {
00514     yotta        1e24
00515     zetta        1e21
00516     exa          1e18
00517     peta         1e15
00518     tera         1e12
00519     giga         1e9
00520     mega         1e6
00521     kilo         1e3
00522     hecto        1e2
00523     deka         1e1
00524     deca         1e1
00525     deci         1e-1
00526     centi        1e-2
00527     milli        1e-3
00528     micro        1e-6
00529     nano         1e-9
00530     pico         1e-12
00531     femto        1e-15
00532     atto         1e-18
00533     zepto        1e-21
00534     yocto        1e-24
00535     Y            yotta
00536     Z            zetta
00537     E            exa
00538     P            peta
00539     T            tera
00540     G            giga
00541     M            mega
00542     k            kilo
00543     h            hecto
00544     da           deka
00545     d            deci
00546     c            centi
00547     m            milli
00548     u            micro
00549     n            nano
00550     p            pico
00551     f            femto
00552     a            atto
00553     z            zepto
00554     y            yocto
00555     }
00556 
00557     array  PrefixTable =  $PrefixList
00558 
00559 
00560      SIunits =  {
00561     meter        -primitive
00562     gram         -primitive
00563     second       -primitive
00564     ampere       -primitive
00565     kelvin       -primitive
00566     mole         -primitive
00567     candela      -primitive
00568     radian       meter/meter
00569     steradian    meter^2/meter^2
00570     hertz        /second
00571     newton       meter-kilogram/second^2
00572     pascal       kilogram/meter-second^2
00573     joule        meter^2-kilogram/second^2
00574     watt         meter^2-kilogram/second^3
00575     coulomb      second-ampere
00576     volt         meter^2-kilogram/second^3-ampere
00577     farad        second^4-ampere^2/meter^2-kilogram
00578     ohm      meter^2-kilogram/second^3-ampere^2
00579     siemens      second^3-ampere^2/meter^2-kilogram
00580     weber        meter^2-kilogram/second^2-ampere
00581     tesla        kilogram/second^2-ampere
00582     henry        meter^2-kilogram/second^2-ampere^2
00583     lumen        candela-steradian
00584     lux          candela-steradian/meter^2
00585     becquerel    /second
00586     gray         meter^2/second^2
00587     sievert      meter^2/second^2
00588     }
00589      SIabbrevs =  {
00590     m            meter
00591     g            gram
00592     s            second
00593     A            ampere
00594     K            kelvin
00595     mol          mole
00596     cd           candela
00597     rad          radian
00598     sr           steradian
00599     Hz           hertz
00600     N            newton
00601     Pa           pascal
00602     J            joule
00603     W            watt
00604     C            coulomb
00605     V            volt
00606     F            farad
00607     S            siemens
00608     Wb           weber
00609     T            tesla
00610     H            henry
00611     lm           lumen
00612     lx           lux
00613     Bq           becquerel
00614     Gy           gray
00615     Sv           sievert
00616     }
00617 
00618     /*   Selected non-SI units from Appendix B of the Guide for*/
00619     /*   the use of the International System of Units*/
00620      nonSIunits =  {
00621     angstrom              1.0E-10meter
00622     astronomicalUnit      1.495979E11meter
00623     atmosphere            1.01325E5pascal
00624     bar                   1.0E5pascal
00625     calorie               4.1868joule
00626     curie                 3.7E10becquerel
00627     day                   8.64E4second
00628     degree                1.745329E-2radian
00629     erg                   1.0E-7joule
00630     faraday               9.648531coulomb
00631     fermi                 1.0E-15meter
00632         foot                  3.048E-1meter
00633     gauss                 1.0E-4tesla
00634     gilbert               7.957747E-1ampere
00635     grain                 6.479891E-5kilogram
00636     hectare               1.0E4meter^2
00637     hour                  3.6E3second
00638     inch                  2.54E-2meter
00639     lightYear             9.46073E15meter
00640     liter                 1.0E-3meter^3
00641     maxwell               1.0E-8weber
00642     mho                   1.0siemens
00643     micron                1.0E-6meter
00644     mil                   2.54E-5meter
00645     mile                  1.609344E3meter
00646     minute                6.0E1second
00647     parsec                3.085E16meter
00648     pica                  4.233333E-3meter
00649     pound                 4.535924E-1kilogram
00650     revolution            6.283185radian
00651     revolutionPerMinute   1.047198E-1radian/second
00652     yard                  9.144E-1meter
00653     year                  3.1536E7second
00654     }
00655      nonSIabbrevs =  {
00656     AU           astronomicalUnit
00657     ft           foot
00658     gr           grain
00659     ha           hectare
00660     h            hour
00661     in           inch
00662     L            liter
00663     Mx           maxwell
00664     mi           mile
00665     min          minute
00666     pc           parsec
00667     lb           pound
00668     r            revolution
00669     rpm          revolutionPerMinute
00670     yd           yard
00671     }
00672 
00673     foreach {name value} $SIunits {
00674     lappend UnitList $name $value
00675      UnitTable = ($name) $value
00676     }
00677     foreach {name value} $nonSIunits {
00678     lappend UnitList $name $value
00679      UnitTable = ($name) $value
00680     }
00681     foreach {name value} $SIabbrevs {
00682     lappend UnitList $name $value
00683      UnitTable = ($name) $value
00684     }
00685     foreach {name value} $nonSIabbrevs {
00686     lappend UnitList $name $value
00687      UnitTable = ($name) $value
00688     }
00689 
00690 }
00691 

Generated on 21 Sep 2010 for Gui by  doxygen 1.6.1