1 | # -*- mode: tcl; indent-tabs-mode: nil -*- |
---|
2 | # ---------------------------------------------------------------------- |
---|
3 | # COMPONENT: units - mechanism for converting numbers with units |
---|
4 | # |
---|
5 | # These routines make it easy to define a system of units, to decode |
---|
6 | # numbers with units, and convert a number from one set of units to |
---|
7 | # another. |
---|
8 | # ====================================================================== |
---|
9 | # AUTHOR: Michael McLennan, Purdue University |
---|
10 | # Copyright (c) 2004-2012 HUBzero Foundation, LLC |
---|
11 | # |
---|
12 | # See the file "license.terms" for information on usage and |
---|
13 | # redistribution of this file, and for a DISCLAIMER OF ALL WARRANTIES. |
---|
14 | # ====================================================================== |
---|
15 | package require Itcl |
---|
16 | |
---|
17 | namespace eval Rappture { # forward declaration } |
---|
18 | namespace eval Rappture::Units { # forward declaration } |
---|
19 | |
---|
20 | # ---------------------------------------------------------------------- |
---|
21 | # USAGE: define units ?-type name? ?-metric boolean? |
---|
22 | # USAGE: define units1->units2 {expr} |
---|
23 | # |
---|
24 | # Used to define a new fundamental type of units, or to define another |
---|
25 | # system of units based on a fundamental type. Once units are defined |
---|
26 | # in this manner, the "convert" function can be used to convert a number |
---|
27 | # in one system of units to another system. |
---|
28 | # ---------------------------------------------------------------------- |
---|
29 | proc Rappture::Units::define {what args} { |
---|
30 | if {[regexp {(.+)->(.+)} $what match new fndm]} { |
---|
31 | if {[llength $args] != 2} { |
---|
32 | error "wrong # args: should be \"define units1->units2 exprTo exprFrom\"" |
---|
33 | } |
---|
34 | # |
---|
35 | # Convert the units variables embedded in the conversion |
---|
36 | # expressions to something that Tcl can handle. We'll |
---|
37 | # use ${number} to represent the variables. |
---|
38 | # |
---|
39 | foreach {exprTo exprFrom} $args { break } |
---|
40 | regsub -all $new $exprTo {${number}} exprTo |
---|
41 | regsub -all $fndm $exprFrom {${number}} exprFrom |
---|
42 | |
---|
43 | Rappture::Units::System #auto $new \ |
---|
44 | -basis [list $fndm $exprTo $exprFrom] |
---|
45 | |
---|
46 | } elseif {[regexp {^/?[a-zA-Z]+[0-9]*$} $what]} { |
---|
47 | array set opts { |
---|
48 | -type "" |
---|
49 | -metric 0 |
---|
50 | } |
---|
51 | foreach {key val} $args { |
---|
52 | if {![info exists opts($key)]} { |
---|
53 | error "bad option \"$key\": should be [join [lsort [array names opts]] {, }]" |
---|
54 | } |
---|
55 | set opts($key) $val |
---|
56 | } |
---|
57 | eval Rappture::Units::System #auto $what [array get opts] |
---|
58 | } else { |
---|
59 | error "bad units definition \"$what\": should be something like m or /cm3 or A->m" |
---|
60 | } |
---|
61 | } |
---|
62 | |
---|
63 | # ---------------------------------------------------------------------- |
---|
64 | # USAGE: convert value ?-context units? ?-to units? ?-units on/off? |
---|
65 | # |
---|
66 | # Used to convert one value with units to another value in a different |
---|
67 | # system of units. If the value has no units, then the units are taken |
---|
68 | # from the -context, if that is supplied. If the -to system is not |
---|
69 | # specified, then the value is converted to fundamental units for the |
---|
70 | # current system. |
---|
71 | # ---------------------------------------------------------------------- |
---|
72 | proc Rappture::Units::convert {value args} { |
---|
73 | array set opts { |
---|
74 | -context "" |
---|
75 | -to "" |
---|
76 | -units "on" |
---|
77 | } |
---|
78 | foreach {key val} $args { |
---|
79 | if {![info exists opts($key)]} { |
---|
80 | error "bad option \"$key\": should be [join [lsort [array names opts]] {, }]" |
---|
81 | } |
---|
82 | set opts($key) $val |
---|
83 | } |
---|
84 | |
---|
85 | # |
---|
86 | # Parse the value into the number part and the units part. |
---|
87 | # |
---|
88 | set value [string trim $value] |
---|
89 | if {![regexp {^([-+]?[0-9]+\.?([0-9]+)?([eEdD][-+]?[0-9]+)?) *(/?[a-zA-Z]+[0-9]*)?$} $value match number dummy1 dummy2 units]} { |
---|
90 | set mesg "bad value \"$value\": should be real number with units" |
---|
91 | if {$opts(-context) != ""} { |
---|
92 | append mesg " of [Rappture::Units::description $opts(-context)]" |
---|
93 | } |
---|
94 | error $mesg |
---|
95 | } |
---|
96 | if {$units == ""} { |
---|
97 | set units $opts(-context) |
---|
98 | } |
---|
99 | |
---|
100 | # |
---|
101 | # Try to find the object representing the current system of units. |
---|
102 | # |
---|
103 | set units [Rappture::Units::System::regularize $units] |
---|
104 | set oldsys [Rappture::Units::System::for $units] |
---|
105 | if {$oldsys == ""} { |
---|
106 | set mesg "value \"$value\" has unrecognized units" |
---|
107 | if {$opts(-context) != ""} { |
---|
108 | append mesg ".\nShould be units of [Rappture::Units::description $opts(-context)]" |
---|
109 | } |
---|
110 | error $mesg |
---|
111 | } |
---|
112 | |
---|
113 | # |
---|
114 | # Convert the number to the new system of units. |
---|
115 | # |
---|
116 | if {$opts(-to) == ""} { |
---|
117 | # no units -- return the number as is |
---|
118 | return "$number$units" |
---|
119 | } |
---|
120 | return [$oldsys convert "$number$units" $opts(-to) $opts(-units)] |
---|
121 | } |
---|
122 | |
---|
123 | # ---------------------------------------------------------------------- |
---|
124 | # USAGE: description <units> |
---|
125 | # |
---|
126 | # Returns a description for the specified system of units. The |
---|
127 | # description includes the abstract type (length, temperature, etc.) |
---|
128 | # along with a list of all compatible systems. |
---|
129 | # ---------------------------------------------------------------------- |
---|
130 | proc Rappture::Units::description {units} { |
---|
131 | set sys [Rappture::Units::System::for $units] |
---|
132 | if {$sys == ""} { |
---|
133 | return "" |
---|
134 | } |
---|
135 | set mesg [$sys cget -type] |
---|
136 | set ulist [Rappture::Units::System::all $units] |
---|
137 | if {"" != $ulist} { |
---|
138 | append mesg " ([join $ulist {, }])" |
---|
139 | } |
---|
140 | return $mesg |
---|
141 | } |
---|
142 | |
---|
143 | # ---------------------------------------------------------------------- |
---|
144 | itcl::class Rappture::Units::System { |
---|
145 | public variable basis "" |
---|
146 | public variable type "" |
---|
147 | public variable metric 0 |
---|
148 | |
---|
149 | constructor {name args} { # defined below } |
---|
150 | |
---|
151 | public method basic {} |
---|
152 | public method fundamental {} |
---|
153 | public method convert {value units showUnits} |
---|
154 | private variable _system "" ;# this system of units |
---|
155 | |
---|
156 | public proc for {units} |
---|
157 | public proc all {units} |
---|
158 | public proc regularize {units} |
---|
159 | |
---|
160 | private common _base ;# maps unit name => System obj |
---|
161 | |
---|
162 | # metric conversion prefixes |
---|
163 | private common _prefix2factor |
---|
164 | array set _prefix2factor { |
---|
165 | c 1e-2 |
---|
166 | m 1e-3 |
---|
167 | u 1e-6 |
---|
168 | n 1e-9 |
---|
169 | p 1e-12 |
---|
170 | f 1e-15 |
---|
171 | a 1e-18 |
---|
172 | k 1e+3 |
---|
173 | M 1e+6 |
---|
174 | G 1e+9 |
---|
175 | T 1e+12 |
---|
176 | P 1e+15 |
---|
177 | } |
---|
178 | } |
---|
179 | |
---|
180 | # ---------------------------------------------------------------------- |
---|
181 | # CONSTRUCTOR |
---|
182 | # ---------------------------------------------------------------------- |
---|
183 | itcl::body Rappture::Units::System::constructor {name args} { |
---|
184 | if {![regexp {^/?[a-zA-Z]+[0-9]*$} $name]} { |
---|
185 | error "bad units declaration \"$name\"" |
---|
186 | } |
---|
187 | eval configure $args |
---|
188 | |
---|
189 | # |
---|
190 | # The -basis is a list {units exprTo exprFrom}, indicating the |
---|
191 | # fundamental system of units that this new system is based on, |
---|
192 | # and the expressions that can be used to convert this new system |
---|
193 | # to and from the fundamental system. |
---|
194 | # |
---|
195 | if {$basis != ""} { |
---|
196 | foreach {base exprTo exprFrom} $basis { break } |
---|
197 | if {![info exists _base($base)]} { |
---|
198 | error "fundamental system of units \"$base\" not defined" |
---|
199 | } |
---|
200 | while {$type == "" && $base != ""} { |
---|
201 | set obj $_base($base) |
---|
202 | set type [$obj cget -type] |
---|
203 | set base [lindex [$obj cget -basis] 0] |
---|
204 | } |
---|
205 | } |
---|
206 | set _system $name |
---|
207 | set _base($name) $this |
---|
208 | } |
---|
209 | |
---|
210 | # ---------------------------------------------------------------------- |
---|
211 | # USAGE: basic |
---|
212 | # |
---|
213 | # Returns the basic system of units for the current system. The |
---|
214 | # basic units may be the only units in this system. But if this |
---|
215 | # system has "-metric 1", the basic system is the system without |
---|
216 | # any metric prefixes. |
---|
217 | # ---------------------------------------------------------------------- |
---|
218 | itcl::body Rappture::Units::System::basic {} { |
---|
219 | return $_system |
---|
220 | } |
---|
221 | |
---|
222 | # ---------------------------------------------------------------------- |
---|
223 | # USAGE: fundamental |
---|
224 | # |
---|
225 | # Returns the fundamental system of units for the current system. |
---|
226 | # For example, the current units might be degrees F, but the |
---|
227 | # fundamental system might be degrees C. The fundamental system |
---|
228 | # depends on how each system is defined. You can see it as the |
---|
229 | # right-hand side of the -> arrow, as in "F->C". |
---|
230 | # ---------------------------------------------------------------------- |
---|
231 | itcl::body Rappture::Units::System::fundamental {} { |
---|
232 | if {$basis != ""} { |
---|
233 | set sys [Rappture::Units::System::for [lindex $basis 0]] |
---|
234 | return [$sys fundamental] |
---|
235 | } |
---|
236 | return $_system |
---|
237 | } |
---|
238 | |
---|
239 | # ---------------------------------------------------------------------- |
---|
240 | # USAGE: convert value newUnits showUnits |
---|
241 | # |
---|
242 | # Converts a value with units to another value with the specified |
---|
243 | # units. The value must have units that are compatible with the |
---|
244 | # current system. Returns a string that represented the converted |
---|
245 | # number and its new units. |
---|
246 | # ---------------------------------------------------------------------- |
---|
247 | itcl::body Rappture::Units::System::convert {value newUnits showUnits} { |
---|
248 | if {![regexp {^([-+]?[0-9]+\.?([0-9]+)?([eEdD][-+]?[0-9]+)?) *(/?[a-zA-Z]+[0-9]*)?$} $value match number dummy1 dummy2 units]} { |
---|
249 | error "bad value \"$value\": should be real number with units" |
---|
250 | } |
---|
251 | |
---|
252 | # |
---|
253 | # Check the base units coming in. They should match the base units |
---|
254 | # for the current system, or the base units for the fundamental basis. |
---|
255 | # If not, something went wrong with the caller. |
---|
256 | # |
---|
257 | set slash "" |
---|
258 | set prefix "" |
---|
259 | set power "1" |
---|
260 | if {$metric && [regexp {^(/?)([cmunpfakMGTP])([a-zA-Z]+)([0-9]*)$} $units match slash prefix base power]} { |
---|
261 | set baseUnits "$slash$base$power" |
---|
262 | } else { |
---|
263 | set baseUnits $units |
---|
264 | } |
---|
265 | if {![string equal $baseUnits $_system] |
---|
266 | && ![string equal $baseUnits [lindex $basis 0]]} { |
---|
267 | error "can't convert value \"$value\": should have units \"$_system\"" |
---|
268 | } |
---|
269 | |
---|
270 | # |
---|
271 | # If the number coming in has a metric prefix, convert the number |
---|
272 | # to the base system. |
---|
273 | # |
---|
274 | if {$prefix != ""} { |
---|
275 | if {$power == ""} { |
---|
276 | set power 1 |
---|
277 | } |
---|
278 | if {$slash == "/"} { |
---|
279 | set number [expr {$number/pow($_prefix2factor($prefix),$power)}] |
---|
280 | } else { |
---|
281 | set number [expr {$number*pow($_prefix2factor($prefix),$power)}] |
---|
282 | } |
---|
283 | } |
---|
284 | |
---|
285 | # |
---|
286 | # If the incoming units are a fundamental basis, then convert |
---|
287 | # the number from the basis to the current system. |
---|
288 | # |
---|
289 | if {[string equal $baseUnits [lindex $basis 0]]} { |
---|
290 | foreach {base exprTo exprFrom} $basis { break } |
---|
291 | set number [expr $exprFrom] |
---|
292 | } |
---|
293 | |
---|
294 | # |
---|
295 | # Check the base units for the new system of units. If they match |
---|
296 | # the current system, then we're almost done. Just handle the |
---|
297 | # metric prefix, if there is one. |
---|
298 | # |
---|
299 | set slash "" |
---|
300 | set prefix "" |
---|
301 | set power "1" |
---|
302 | if {$metric && [regexp {^(/?)([cmunpfakMGTP])([a-zA-Z]+)([0-9]*)$} $newUnits match slash prefix base power]} { |
---|
303 | set baseUnits "$slash$base$power" |
---|
304 | } else { |
---|
305 | set baseUnits $newUnits |
---|
306 | } |
---|
307 | if {[string equal $baseUnits $_system]} { |
---|
308 | if {$prefix != ""} { |
---|
309 | if {$power == ""} { |
---|
310 | set power 1 |
---|
311 | } |
---|
312 | if {$slash == "/"} { |
---|
313 | set number [expr {$number*pow($_prefix2factor($prefix),$power)}] |
---|
314 | } else { |
---|
315 | set number [expr {$number/pow($_prefix2factor($prefix),$power)}] |
---|
316 | } |
---|
317 | } |
---|
318 | if {$showUnits} { |
---|
319 | return "$number$newUnits" |
---|
320 | } |
---|
321 | return $number |
---|
322 | } |
---|
323 | |
---|
324 | # |
---|
325 | # If we want a different system of units, then convert this number |
---|
326 | # to the fundamental basis. If there is no fundamental basis, we |
---|
327 | # must already be in the fundamental basis. |
---|
328 | # |
---|
329 | set base $_system |
---|
330 | if {"" != $basis} { |
---|
331 | foreach {base exprTo exprFrom} $basis { break } |
---|
332 | set number [expr $exprTo] |
---|
333 | } |
---|
334 | |
---|
335 | set newsys [Rappture::Units::System::for $newUnits] |
---|
336 | return [$newsys convert "$number$base" $newUnits $showUnits] |
---|
337 | } |
---|
338 | |
---|
339 | # ---------------------------------------------------------------------- |
---|
340 | # CONFIGURATION OPTION: -basis |
---|
341 | # ---------------------------------------------------------------------- |
---|
342 | itcl::configbody Rappture::Units::System::basis { |
---|
343 | if {[llength $basis] != 3} { |
---|
344 | error "bad basis \"$name\": should be {units exprTo exprFrom}" |
---|
345 | } |
---|
346 | } |
---|
347 | |
---|
348 | # ---------------------------------------------------------------------- |
---|
349 | # CONFIGURATION OPTION: -metric |
---|
350 | # ---------------------------------------------------------------------- |
---|
351 | itcl::configbody Rappture::Units::System::metric { |
---|
352 | if {![string is boolean -strict $metric]} { |
---|
353 | error "bad value \"$metric\": should be boolean" |
---|
354 | } |
---|
355 | } |
---|
356 | |
---|
357 | # ---------------------------------------------------------------------- |
---|
358 | # USAGE: for units |
---|
359 | # |
---|
360 | # Returns the System object for the given system of units, or "" |
---|
361 | # if there is no system that matches the units string. |
---|
362 | # ---------------------------------------------------------------------- |
---|
363 | itcl::body Rappture::Units::System::for {units} { |
---|
364 | # |
---|
365 | # See if the units are a recognized system. If not, then try to |
---|
366 | # extract any metric prefix and see if what's left is a recognized |
---|
367 | # system. If all else fails, see if we can find a system without |
---|
368 | # the exact capitalization. The user might say "25c" instead of |
---|
369 | # "25C". Try to allow that. |
---|
370 | # |
---|
371 | if {[info exists _base($units)]} { |
---|
372 | return $_base($units) |
---|
373 | } else { |
---|
374 | set orig $units |
---|
375 | if {[regexp {^(/?)[cCmMuUnNpPfFaAkKgGtT](.+)$} $units match slash tail]} { |
---|
376 | set base "$slash$tail" |
---|
377 | if {[info exists _base($base)]} { |
---|
378 | set sys $_base($base) |
---|
379 | if {[$sys cget -metric]} { |
---|
380 | return $sys |
---|
381 | } |
---|
382 | } |
---|
383 | |
---|
384 | # check the base part for improper capitalization below... |
---|
385 | set units $base |
---|
386 | } |
---|
387 | |
---|
388 | set matching "" |
---|
389 | foreach u [array names _base] { |
---|
390 | if {[string equal -nocase $u $units]} { |
---|
391 | lappend matching $_base($u) |
---|
392 | } |
---|
393 | } |
---|
394 | if {[llength $matching] == 1} { |
---|
395 | set sys [lindex $matching 0] |
---|
396 | # |
---|
397 | # If we got rid of a metric prefix above, make sure |
---|
398 | # that the system is metric. If not, then we don't |
---|
399 | # have a match. |
---|
400 | # |
---|
401 | if {[string equal $units $orig] || [$sys cget -metric]} { |
---|
402 | return $sys |
---|
403 | } |
---|
404 | } |
---|
405 | } |
---|
406 | return "" |
---|
407 | } |
---|
408 | |
---|
409 | # ---------------------------------------------------------------------- |
---|
410 | # USAGE: all units |
---|
411 | # |
---|
412 | # Returns a list of all units compatible with the given units string. |
---|
413 | # Compatible units are determined by following all conversion |
---|
414 | # relationships that lead to the same base system. |
---|
415 | # ---------------------------------------------------------------------- |
---|
416 | itcl::body Rappture::Units::System::all {units} { |
---|
417 | set sys [Rappture::Units::System::for $units] |
---|
418 | if {$sys == ""} { |
---|
419 | return "" |
---|
420 | } |
---|
421 | |
---|
422 | if {"" != [$sys cget -basis]} { |
---|
423 | set basis [lindex [$sys cget -basis] 0] |
---|
424 | } else { |
---|
425 | set basis $units |
---|
426 | } |
---|
427 | |
---|
428 | set ulist $basis |
---|
429 | foreach u [array names _base] { |
---|
430 | set obj $_base($u) |
---|
431 | set b [lindex [$obj cget -basis] 0] |
---|
432 | if {$b == $basis} { |
---|
433 | lappend ulist $u |
---|
434 | } |
---|
435 | } |
---|
436 | return $ulist |
---|
437 | } |
---|
438 | |
---|
439 | # ---------------------------------------------------------------------- |
---|
440 | # USAGE: regularize units |
---|
441 | # |
---|
442 | # Examines the given expression of units and tries to regularize |
---|
443 | # it so it has the proper capitalization. For example, units like |
---|
444 | # "/CM3" are converted to "/cm3". If the units are not recognized, |
---|
445 | # then they are returned as-is. |
---|
446 | # ---------------------------------------------------------------------- |
---|
447 | itcl::body Rappture::Units::System::regularize {units} { |
---|
448 | set sys [for $units] |
---|
449 | if {$sys == ""} { |
---|
450 | return $units |
---|
451 | } |
---|
452 | # note: case-insensitive matching for metric prefix |
---|
453 | if {[regexp {^(/?)([cCmMuUnNpPfFaAkKgGtT]?)([a-zA-Z]+[0-9]+|[a-zA-Z]+)$} $units match slash prefix tail]} { |
---|
454 | if {[regexp {^[CUNFAK]$} $prefix]} { |
---|
455 | # we know that these should be lower case |
---|
456 | set prefix [string tolower $prefix] |
---|
457 | } elseif {[regexp {^[GT]$} $prefix]} { |
---|
458 | # we know that these should be upper case |
---|
459 | set prefix [string toupper $prefix] |
---|
460 | } |
---|
461 | return "$slash$prefix[string trimleft [$sys basic] /]" |
---|
462 | } |
---|
463 | return [$sys basic] |
---|
464 | } |
---|
465 | |
---|
466 | # ---------------------------------------------------------------------- |
---|
467 | |
---|
468 | # ---------------------------------------------------------------------- |
---|
469 | # Define common units... |
---|
470 | # ---------------------------------------------------------------------- |
---|
471 | Rappture::Units::define m -type length -metric yes |
---|
472 | Rappture::Units::define A->m {A*1.0e-10} {m*1.0e10} |
---|
473 | |
---|
474 | Rappture::Units::define /m3 -type density -metric yes |
---|
475 | Rappture::Units::define /m2 -type misc -metric yes |
---|
476 | |
---|
477 | Rappture::Units::define C -type temperature -metric no |
---|
478 | Rappture::Units::define K->C {K-273.15} {C+273.15} |
---|
479 | Rappture::Units::define F->C {(F-32)/1.8} {(1.8*C)+32} |
---|
480 | |
---|
481 | Rappture::Units::define eV -type energy -metric yes |
---|
482 | Rappture::Units::define J->eV {J/1.602177e-19} {eV*1.602177e-19} |
---|
483 | |
---|
484 | Rappture::Units::define V -type voltage -metric yes |
---|
485 | |
---|
486 | Rappture::Units::define s -type seconds -metric yes |
---|
487 | # can't use min becase tcl thinks its milli-in's |
---|
488 | # Rappture::Units::define min->s {min*60.00} {s/60.00} |
---|
489 | Rappture::Units::define h->s {h*3600.00} {s/3600.00} |
---|
490 | Rappture::Units::define d->s {d*86400.00} {s/86400.00} |
---|
491 | |
---|
492 | # can't put mol's in because tcl thinks its milli-ol's |
---|
493 | # Rappture::Units::define mol -type misc -metric yes |
---|
494 | Rappture::Units::define Hz -type misc -metric yes |
---|
495 | Rappture::Units::define Bq -type misc -metric yes |
---|
496 | |
---|
497 | Rappture::Units::define deg -type angle -metric no |
---|
498 | Rappture::Units::define rad -type angle -metric no |
---|
499 | Rappture::Units::define deg->rad {deg*(3.1415926535897931/180.00)} {rad*(180.00/3.1415926535897931)} |
---|