units.tcl
Go to the documentation of this file.00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
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
00032
00033
00034
00035
00036
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
00080
00081
00082
00083
00084
00085
00086
00087
00088
00089
00090
00091
00092
00093
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
00135
00136
00137
00138
00139
00140
00141
00142
00143
00144
00145
00146
00147
00148
00149
00150
00151
00152
00153
00154
00155
00156
00157
00158
00159
00160
00161
00162
00163
00164
00165
00166
00167
00168
00169
00170
00171
00172
00173
00174
00175
00176
00177
00178
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
00258
00259
00260
00261
00262
00263
00264
00265
00266
00267
00268
00269
00270
00271
00272
00273
00274
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
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
00619
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