units.tcl 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687
  1. #----------------------------------------------*-TCL-*------------
  2. #
  3. # units.tcl
  4. #
  5. # The units package provides a conversion facility from a variety of
  6. # scientific and engineering shorthand notations into floating point
  7. # numbers.
  8. #
  9. # Robert W. Techentin
  10. # November 1, 2000
  11. #
  12. # Copyright 2000-2004 Mayo Foundation. All Rights Reserved.
  13. # $Id: units.tcl,v 1.2 2004/02/05 22:24:23 techenti Exp $
  14. #
  15. #-----------------------------------------------------------------
  16. package provide units 1.0
  17. package require Tcl 8.1
  18. namespace eval ::units {
  19. namespace export new
  20. namespace export convert
  21. namespace export reduce
  22. variable UnitTable
  23. variable PrefixTable
  24. }
  25. #-----------------------------------------------------------------
  26. #
  27. # ::units::new --
  28. #
  29. # Add a new unit to the units table. The new unit is defined
  30. # in terms of its baseUnits. If baseUnits is "-primitive",
  31. # then it is assumed to be some magical new kind of quantity.
  32. # Otherwise, it must reduce to units already defined.
  33. #
  34. #-----------------------------------------------------------------
  35. proc ::units::new { args } {
  36. variable UnitTable
  37. variable UnitList
  38. # Check number of arguments
  39. switch [llength $args] {
  40. 2 {
  41. set name [lindex $args 0]
  42. set baseUnits [lindex $args 1]
  43. }
  44. default {
  45. # issue same error as C extension
  46. error "Wrong # args. units::new name baseUnits "
  47. }
  48. }
  49. # check for duplicates
  50. if { [info exists UnitTable($name)] } {
  51. error "unit '$name' is already defined"
  52. }
  53. # check for valid characters
  54. if { [regexp {[^a-zA-Z]} $name] } {
  55. error "non-alphabetic characters in unit name '$name'"
  56. }
  57. # Compute reduced units
  58. if { [catch {::units::reduce $baseUnits} reducedUnits] } {
  59. error "'$baseUnits' cannot be reduced to primitive units"
  60. }
  61. # add the unit, but don't return a value
  62. set UnitTable($name) $reducedUnits
  63. lappend UnitList $name $reducedUnits
  64. return
  65. }
  66. #-----------------------------------------------------------------
  67. #
  68. # ::units::convert --
  69. #
  70. # Convert a value to the target units.
  71. #
  72. # If units are specified for the value, then they must
  73. # be compatible with the target units. (i.e., you can
  74. # convert "newtons" to "kg-m/s^2", but not to "sieverts".
  75. #
  76. # Arguments:
  77. # value A value can be a floating point number, either with or
  78. # without units.
  79. # targetUnits A units string which may also include a scale factor.
  80. #
  81. # Results:
  82. # The return value is a scaled floating point number.
  83. #
  84. #-----------------------------------------------------------------
  85. proc ::units::convert { args } {
  86. # Check number of arguments
  87. switch [llength $args] {
  88. 2 {
  89. set value [lindex $args 0]
  90. set targetUnits [lindex $args 1]
  91. }
  92. default {
  93. # issue same error as C extension
  94. error "Wrong # args. units::convert value targetUnits "
  95. }
  96. }
  97. # Reduce each of value and target
  98. # to primitive units
  99. set reducedValue [::units::reduce $value]
  100. set reducedTarget [::units::reduce $targetUnits]
  101. # If the value has units, it must be compatible with
  102. # the target. (If it is unitless, then compatibility
  103. # is not required.)
  104. if { [llength $reducedValue] > 1} {
  105. if {[lrange $reducedValue 1 end]!=[lrange $reducedTarget 1 end]} {
  106. error "'$value' and '$targetUnits' have incompatible units"
  107. }
  108. }
  109. # Compute and return scaled value
  110. expr {[lindex $reducedValue 0] / [lindex $reducedTarget 0]}
  111. }
  112. #-----------------------------------------------------------------
  113. #
  114. # ::units::reduce --
  115. #
  116. # Reduce a string of numbers, prefixes, units, exponents into a
  117. # single multiplicitive factor and sorted list of primitive units.
  118. # For example, the unit string for "newton", which is "m-kg/s^2"
  119. # would reduce to the list {1000.0 gram meter / second second}
  120. #
  121. # Unit String Syntax
  122. #
  123. # This procedure defines a valid unit string that may
  124. # be reduced to primitive units, so it is reasonable to
  125. # document valid unit string syntax here.
  126. #
  127. # A unit string consists of an optional scale factor followed
  128. # by zero or more subunit strings. The scale factor must be
  129. # a valid floating point number.
  130. #
  131. # Subunits are separated by unit separator characters, which are
  132. # " ", "-", "*", and "/". It is not necessary to separate
  133. # the leading scale factor from the rest of the subunits.
  134. #
  135. # The forward slash seperator "/" indicates that following
  136. # subunits are in the denominator. There can be at most
  137. # one "/" separator.
  138. #
  139. # Subunits can be floating point scale factors, but they
  140. # must be surrounded by valid separators.
  141. #
  142. # Subunits can be valid units or abbreviations from the
  143. # UnitsTable. They may include a prefix from the PrefixTable.
  144. # They may include a plural suffix "s" or "es". They may
  145. # also include a power string "^", followed by an integer,
  146. # after the unit name (or plural suffix, if there is one.)
  147. #
  148. # Examples of valid unit strings: "meter", "/s", "kg-m/s^2",
  149. # "30second" "30 second", "30 seconds" "200*meter/20.5*second"
  150. #
  151. # Arguments:
  152. # unitString string of units characters
  153. #
  154. # Results:
  155. # The return value is a list, the first element of which
  156. # is the multiplicitive factor, and the remaining elements are
  157. # sorted reduced primitive units, possibly including the "/"
  158. # operator, which separates the numerator from the denominator.
  159. #-----------------------------------------------------------------
  160. #
  161. proc ::units::reduce { args } {
  162. # Check number of arguments
  163. switch [llength $args] {
  164. 1 {
  165. set unitString [lindex $args 0]
  166. }
  167. default {
  168. # issue same error as C extension
  169. error "Wrong # args. units::reduce unitString "
  170. }
  171. }
  172. # check for primitive unit - may already be reduced
  173. # This gets excercised by new units
  174. if { "$unitString" == "-primitive" } {
  175. return $unitString
  176. }
  177. # Check cache of unitStrings
  178. if { [info exists ::units::cache($unitString)] } {
  179. return $::units::cache($unitString)
  180. }
  181. # Verify syntax of unit string
  182. # It may contain, at most, one "/"
  183. if { [regexp {/.*/} $unitString] } {
  184. error "invalid unit string '$unitString': only one '/' allowed"
  185. }
  186. # It may contain only letters, digits, the powerstring ("^"),
  187. # decimal points, and separators
  188. if { [regexp {[^a-zA-Z0-9. \t*^/+-]} $unitString] } {
  189. error "invalid characters in unit string '$unitString'"
  190. }
  191. # Check for leading scale factor
  192. # If the leading characters are in floating point
  193. # format, then extract and save them (including any
  194. # minus signs) before handling subunit separators.
  195. # This is based on a regexp from Roland B. Roberts which
  196. # allows leading +/-, digits, decimals, and exponents.
  197. regexp {(^[-+]?(?:[0-9]+\.?[0-9]*|\.[0-9]+)(?:[eE][-+]?[0-9]+)?)?(.*)} \
  198. $unitString matchvar scaleFactor subunits
  199. # Ensure that scale factor is a nice floating point number
  200. if { "$scaleFactor" == "" } {
  201. set scaleFactor 1.0
  202. } else {
  203. set scaleFactor [expr {double($scaleFactor)}]
  204. }
  205. # replace all separators with spaces.
  206. regsub -all {[\t\-\*]} $subunits " " subunits
  207. # add spaces around "/" character.
  208. regsub {/} $subunits " / " subunits
  209. # The unitString is now essentially a well structured list
  210. # of subunits, which may be processed as a list, and it
  211. # may be necessary to process it recursively, without
  212. # performing the string syntax checks again. But check
  213. # for errors.
  214. if { [catch {ReduceList $scaleFactor $subunits} result] } {
  215. error "$result in '$unitString'"
  216. }
  217. # Store the reduced unit in a cache, so future lookups
  218. # are much quicker.
  219. set ::units::cache($unitString) $result
  220. }
  221. #-----------------------------------------------------------------
  222. #
  223. # ::units::ReduceList --
  224. #
  225. # Reduce a list of subunits to primitive units and a single
  226. # scale factor.
  227. #
  228. # Arguments:
  229. # factor A scale factor, which is multiplied and divided
  230. # by subunit prefix values and constants.
  231. # unitString A unit string which is syntactically correct
  232. # and includes only space separators. This
  233. # string can be treated as a Tcl list.
  234. #
  235. # Results:
  236. # A valid unit string list, consisting of a single floating
  237. # point factor, followed by sorted primitive units. If the
  238. # forward slash separator "/" is included, then each of the
  239. # numerator and denominator is sorted, and common units have
  240. # been cancelled.
  241. #
  242. #-----------------------------------------------------------------
  243. #
  244. proc ::units::ReduceList { factor unitString } {
  245. variable UnitList
  246. variable UnitTable
  247. variable PrefixTable
  248. # process each subunit in turn, starting in the numerator
  249. #
  250. # Note that we're going to use a boolean flag to switch
  251. # between numerator and denominator if we encounter a "/".
  252. # This same style is used for processing recursively
  253. # reduced subunits
  254. set numerflag 1
  255. set numerator [list]
  256. set denominator [list]
  257. foreach subunit $unitString {
  258. # Check for "/"
  259. if { "$subunit" == "/" } {
  260. set numerflag [expr {$numerflag?0:1}]
  261. continue
  262. }
  263. # Constant factor
  264. if { [string is double -strict $subunit] } {
  265. if { $subunit == 0.0 } {
  266. error "illegal zero factor"
  267. } else {
  268. if { $numerflag } {
  269. set factor [expr {$factor * $subunit}]
  270. } else {
  271. set factor [expr {$factor / $subunit}]
  272. }
  273. continue
  274. }
  275. }
  276. # Check for power string (e.g. "s^2")
  277. # We could use regexp to match and split in one operation,
  278. # like {([^\^]*)\^(.*)} but that seems to be pretty durn
  279. # slow, so we'll just using [string] operations.
  280. if { [set index [string first "^" $subunit]] >= 0 } {
  281. set subunitname [string range $subunit 0 [expr {$index-1}]]
  282. set exponent [string range $subunit [expr {$index+1}] end]
  283. if { ! [string is integer -strict $exponent] } {
  284. error "invalid integer exponent"
  285. }
  286. # This is a good test and error message, but it won't
  287. # happen, because the negative sign (hypen) has already
  288. # been interpreted as a unit separator. Negative
  289. # exponents will trigger the 'invalid integer' message,
  290. # because there is no exponent. :-)
  291. if { $exponent < 1 } {
  292. error "invalid non-positive exponent"
  293. }
  294. } else {
  295. set subunitname $subunit
  296. set exponent 1
  297. }
  298. # Check subunit name syntax
  299. if { ! [string is alpha -strict $subunitname] } {
  300. error "invalid non-alphabetic unit name"
  301. }
  302. # Try looking up the subunitname.
  303. #
  304. # Start with the unit name. But if the unit ends in "s"
  305. # or "es", then we want to try shortened (singular)
  306. # versions of the subunit as well.
  307. set unitValue ""
  308. set subunitmatchlist [list $subunitname]
  309. if { [string range $subunitname end end] == "s" } {
  310. lappend subunitmatchlist [string range $subunitname 0 end-1]
  311. }
  312. if { [string range $subunitname end-1 end] == "es" } {
  313. lappend subunitmatchlist [string range $subunitname 0 end-2]
  314. }
  315. foreach singularunit $subunitmatchlist {
  316. set len [string length $singularunit]
  317. # Search the unit list in order, because we
  318. # wouldn't want to accidentally match the "m"
  319. # at the end of "gram" and conclude that we
  320. # have "meter".
  321. foreach {name value} $UnitList {
  322. # Try to match the string starting at the
  323. # at the end, just in case there is a prefix.
  324. # We only have a match if both the prefix and
  325. # unit name are exact matches.
  326. set pos [expr {$len - [string length $name]}]
  327. #set pos [expr {$len-1}]
  328. if { [string range $singularunit $pos end] == $name } {
  329. set prefix [string range $singularunit 0 [expr {$pos-1}]]
  330. set matchsubunit $name
  331. # If we have no prefix or a valid prefix,
  332. # then we've got an actual match.
  333. if { ("$prefix" == "") || \
  334. [info exists PrefixTable($prefix)] } {
  335. # Set the unit value string
  336. set unitValue $value
  337. # done searching UnitList
  338. break
  339. }
  340. }
  341. # check for done
  342. if { $unitValue != "" } {
  343. break
  344. }
  345. }
  346. }
  347. # Check for not-found
  348. if { "$unitValue" == "" } {
  349. error "invalid unit name '$subunitname'"
  350. }
  351. # Multiply the factor by the prefix value
  352. if { "$prefix" != "" } {
  353. # Look up prefix value recursively, so abbreviations
  354. # like "k" for "kilo" will work. Note that we
  355. # don't need error checking here (as we do for
  356. # unit lookup) because we have total control over
  357. # the prefix table.
  358. while { ! [string is double -strict $prefix] } {
  359. set prefix $PrefixTable($prefix)
  360. }
  361. # Save prefix multiple in factor
  362. set multiple [expr {pow($prefix,$exponent)}]
  363. if { $numerflag } {
  364. set factor [expr {$factor * $multiple}]
  365. } else {
  366. set factor [expr {$factor / $multiple}]
  367. }
  368. }
  369. # Is this a primitive subunit?
  370. if { "$unitValue" == "-primitive" } {
  371. # just append the matching subunit to the result
  372. # (this doesn't have prefix or trailing "s")
  373. for {set i 0} {$i<$exponent} {incr i} {
  374. if { $numerflag } {
  375. lappend numerator $matchsubunit
  376. } else {
  377. lappend denominator $matchsubunit
  378. }
  379. }
  380. } else {
  381. # Recursively reduce, unless it is in the cache
  382. if { [info exists ::units::cache($unitValue)] } {
  383. set reducedUnit $::units::cache($unitValue)
  384. } else {
  385. set reducedUnit [::units::reduce $unitValue]
  386. set ::units::cache($unitValue) $reducedUnit
  387. }
  388. # Include multiple factor from reduced unit
  389. set multiple [expr {pow([lindex $reducedUnit 0],$exponent)}]
  390. if { $numerflag } {
  391. set factor [expr {$factor * $multiple}]
  392. } else {
  393. set factor [expr {$factor / $multiple}]
  394. }
  395. # Add primitive subunits to numerator/denominator
  396. #
  397. # Note that we're use a nested boolean flag to switch
  398. # between numerator and denominator. Subunits in
  399. # the numerator of the unitString are processed
  400. # normally, but subunits in the denominator of
  401. # unitString must be inverted.
  402. set numerflag2 $numerflag
  403. foreach u [lrange $reducedUnit 1 end] {
  404. if { "$u" == "/" } {
  405. set numerflag2 [expr {$numerflag2?0:1}]
  406. continue
  407. }
  408. # Append the reduced units "exponent" times
  409. for {set i 0} {$i<$exponent} {incr i} {
  410. if { $numerflag2 } {
  411. lappend numerator $u
  412. } else {
  413. lappend denominator $u
  414. }
  415. }
  416. }
  417. }
  418. }
  419. # Sort both numerator and denominator
  420. set numerator [lsort $numerator]
  421. set denominator [lsort $denominator]
  422. # Cancel any duplicate units.
  423. # Foreach and for loops don't work well for this.
  424. # (We keep changing list length).
  425. set i 0
  426. while {$i < [llength $numerator]} {
  427. set u [lindex $numerator $i]
  428. set index [lsearch $denominator $u]
  429. if { $index >= 0 } {
  430. set numerator [lreplace $numerator $i $i]
  431. set denominator [lreplace $denominator $index $index]
  432. } else {
  433. incr i
  434. }
  435. }
  436. # Now we've got numerator, denominator, and factors.
  437. # Assemble the result into a single list.
  438. if { [llength $denominator] > 0 } {
  439. set result [eval list $factor $numerator "/" $denominator]
  440. } else {
  441. set result [eval list $factor $numerator]
  442. }
  443. # Now return the result
  444. return $result
  445. }
  446. #-----------------------------------------------------------------
  447. #
  448. # Initialize namespace variables
  449. #
  450. #-----------------------------------------------------------------
  451. namespace eval ::units {
  452. set PrefixList {
  453. yotta 1e24
  454. zetta 1e21
  455. exa 1e18
  456. peta 1e15
  457. tera 1e12
  458. giga 1e9
  459. mega 1e6
  460. kilo 1e3
  461. hecto 1e2
  462. deka 1e1
  463. deca 1e1
  464. deci 1e-1
  465. centi 1e-2
  466. milli 1e-3
  467. micro 1e-6
  468. nano 1e-9
  469. pico 1e-12
  470. femto 1e-15
  471. atto 1e-18
  472. zepto 1e-21
  473. yocto 1e-24
  474. Y yotta
  475. Z zetta
  476. E exa
  477. P peta
  478. T tera
  479. G giga
  480. M mega
  481. k kilo
  482. h hecto
  483. da deka
  484. d deci
  485. c centi
  486. m milli
  487. u micro
  488. n nano
  489. p pico
  490. f femto
  491. a atto
  492. z zepto
  493. y yocto
  494. }
  495. array set PrefixTable $PrefixList
  496. set SIunits {
  497. meter -primitive
  498. gram -primitive
  499. second -primitive
  500. ampere -primitive
  501. kelvin -primitive
  502. mole -primitive
  503. candela -primitive
  504. radian meter/meter
  505. steradian meter^2/meter^2
  506. hertz /second
  507. newton meter-kilogram/second^2
  508. pascal kilogram/meter-second^2
  509. joule meter^2-kilogram/second^2
  510. watt meter^2-kilogram/second^3
  511. coulomb second-ampere
  512. volt meter^2-kilogram/second^3-ampere
  513. farad second^4-ampere^2/meter^2-kilogram
  514. ohm meter^2-kilogram/second^3-ampere^2
  515. siemens second^3-ampere^2/meter^2-kilogram
  516. weber meter^2-kilogram/second^2-ampere
  517. tesla kilogram/second^2-ampere
  518. henry meter^2-kilogram/second^2-ampere^2
  519. lumen candela-steradian
  520. lux candela-steradian/meter^2
  521. becquerel /second
  522. gray meter^2/second^2
  523. sievert meter^2/second^2
  524. }
  525. set SIabbrevs {
  526. m meter
  527. g gram
  528. s second
  529. A ampere
  530. K kelvin
  531. mol mole
  532. cd candela
  533. rad radian
  534. sr steradian
  535. Hz hertz
  536. N newton
  537. Pa pascal
  538. J joule
  539. W watt
  540. C coulomb
  541. V volt
  542. F farad
  543. S siemens
  544. Wb weber
  545. T tesla
  546. H henry
  547. lm lumen
  548. lx lux
  549. Bq becquerel
  550. Gy gray
  551. Sv sievert
  552. }
  553. # Selected non-SI units from Appendix B of the Guide for
  554. # the use of the International System of Units
  555. set nonSIunits {
  556. angstrom 1.0E-10meter
  557. astronomicalUnit 1.495979E11meter
  558. atmosphere 1.01325E5pascal
  559. bar 1.0E5pascal
  560. calorie 4.1868joule
  561. curie 3.7E10becquerel
  562. day 8.64E4second
  563. degree 1.745329E-2radian
  564. erg 1.0E-7joule
  565. faraday 9.648531coulomb
  566. fermi 1.0E-15meter
  567. foot 3.048E-1meter
  568. gauss 1.0E-4tesla
  569. gilbert 7.957747E-1ampere
  570. grain 6.479891E-5kilogram
  571. hectare 1.0E4meter^2
  572. hour 3.6E3second
  573. inch 2.54E-2meter
  574. lightYear 9.46073E15meter
  575. liter 1.0E-3meter^3
  576. maxwell 1.0E-8weber
  577. mho 1.0siemens
  578. micron 1.0E-6meter
  579. mil 2.54E-5meter
  580. mile 1.609344E3meter
  581. minute 6.0E1second
  582. parsec 3.085E16meter
  583. pica 4.233333E-3meter
  584. pound 4.535924E-1kilogram
  585. revolution 6.283185radian
  586. revolutionPerMinute 1.047198E-1radian/second
  587. yard 9.144E-1meter
  588. year 3.1536E7second
  589. }
  590. set nonSIabbrevs {
  591. AU astronomicalUnit
  592. ft foot
  593. gr grain
  594. ha hectare
  595. h hour
  596. in inch
  597. L liter
  598. Mx maxwell
  599. mi mile
  600. min minute
  601. pc parsec
  602. lb pound
  603. r revolution
  604. rpm revolutionPerMinute
  605. yd yard
  606. }
  607. foreach {name value} $SIunits {
  608. lappend UnitList $name $value
  609. set UnitTable($name) $value
  610. }
  611. foreach {name value} $nonSIunits {
  612. lappend UnitList $name $value
  613. set UnitTable($name) $value
  614. }
  615. foreach {name value} $SIabbrevs {
  616. lappend UnitList $name $value
  617. set UnitTable($name) $value
  618. }
  619. foreach {name value} $nonSIabbrevs {
  620. lappend UnitList $name $value
  621. set UnitTable($name) $value
  622. }
  623. }