Converts measures, mostly between metric and US style. "convert names" gives you the defined units. Working, but unfinished weekend fun project by [Richard Suchenwirth]. Examples: convert 10 km/h 6.21 mph convert 6 ft 4 in ;# tries to group commensurable measures 193.04 cm convert 6 ft 4 oz ;# ... else does them one by one 182.88 cm 113.40 g convert 37.4 C 99.32 F convert 193 cm ;# this, of course, is bad. Needs some work... 6.33 ft See also [Euro converter] - [Unit math]. See also the Units Conversion Library at http://units.sourceforge.net/ for a package that converts and reduces units by dimensional analysis. (i.e., it can convert ''milliwebers/femtocoulomb'' into ''ohms'') ---- proc convert {args} { set res {} array set tbl { in {2.54 cm} ft {30.48 cm} yd {0.9144 m} mi {1.6093 km} mph {1.6093 km/h} nmile {1.852 km} PS {0.7355 kW} sq.ft {.0929 m2} sq.mi {2.59 km2} EUR {1.95583 DM} C - F - gal {4.5436 l} oz {28.3495 g} lb {453.59 g} ton {1016.05 kg} } foreach i [array names tbl] { if {$tbl($i)=="-"} continue foreach {fac unit2} $tbl($i) break ;# inverted measures if {![llength [array names tbl $unit2]]} { set tbl($unit2) [list [expr 1./$fac] $i]} } if {$args==""} { error "usage: convert {? value unit... }?, or: convert names.\n\ Units:\n[convert names]" } if {$args=="names"} {return [lsort [array names tbl]]} if {[llength $args]==1} {set args [lindex $args 0]} foreach {n unit} $args { if {$unit=="F"} { set res [list [expr ($n-32)*5/9] C] } elseif {$unit=="C"} { set res [list [expr $n*9/5+32] F] } elseif [llength [array names tbl $unit]] { foreach {fac unit2} $tbl($unit) break lappend res [format %.2f [expr $n*$fac]] $unit2 } else { lappend res $n $unit } } foreach {x y} $res {lappend xx $x; ladd yy $y} if [llength $yy]==1 {set res [list [expr [join $xx +]] $yy] } return $res } proc ladd {_list what} { upvar $_list list if {![info exists list] || [lsearch $list $what] == -1} { lappend list $what } } ;# RS, updated 2003-08-04 ---- Here's a somewhat divergent implementation which I hope is more general and extensible. It doesn't convert multiple measurements as the original does. On the other hand, it's more forgiving of "3m" or "3meters" or "3 meter", or even "3metre" etc. as input. Feedback welcome. Updated 23 Feb 2000 to handle percentage conversions, include more temperature and distance initializations. Converting points to metric units seems to have an inherent binary representation problem. Updated 1 March 2000 to deal with significant digits better. -- [mailto:nelson@pinebush.com] # units.tcl -- # # A units conversion package. # # # Copyright 2000 Pinebush Technologies Inc. # # # # Thanks to Jonathan Guyer (jguyer@his.com) for help coping with # leading and trailing 0s (in parse) and significant digits (in # convert). # #----------------------------------------------------------------------- package require msgcat package provide units 1.0 namespace eval units { namespace export \ convert \ parse \ addAbbrev \ addConvert \ normalize # Get any localized unit strings msgcat::mcload [file join [file dirname [info script]] msgs] # "global" array(s) variable Abbrev variable Convert variable Defaults } #======================================================================= # Public procs #======================================================================= # units::parse -- # # Normalize a string that may express a measurement # # Actually, it's much more simple minded than that. All it does # it separate a leading number from the following text. # # Arguments: # string - A string including a number and units like "3m", # "4 in", "5meters", etc. # # Results: # Returns a list in the form {number units} # # proc units::parse { str } { # We could use a regexp but scan is smart about number formats, # let's leverage that. set count [scan $str "%e%s" number units] switch -- $count { 1 { return $str } 2 { # Re-parse to preserve trailing 0's regexp "(.*)$units" $str whole number # Get rid of leading and trailing whitespace set number [string trim $number] # Normalize units (meter -> m, " -> in, etc.) if {[string length $units]} { set units [normalize $units] } # Return parsed measurement with normalized units return [list $number $units] } default { error "Invalid measurement string, '$str', \ should be in the form 'number units'" } } # NOT REACHED } # units::parse # #----------------------------------------------------------------------- # units::convert -- # # Convert a normalized measurement string (as from parse) to # another unit. # # Arguments: # mea - The input measurement to convert # to - The new units for mea # # Results: # mea converted to different units. # proc units::convert { mea {to DEFAULT} } { variable Convert variable Defaults #---------------------------------------- # Parse the input set mea [string trim $mea] set mea [parse $mea] set numberIn [lindex $mea 0] set from [lindex $mea 1] # WUZ - it might be nice if [convert 12 in] assumed that mea was # in inches, that is, a no-op. #---------------------------------------- # Handle some special cases if {[string equal $to DEFAULT]} { set to $Defaults($from) } if {[string equal $from $to]} { return $mea } #---------------------------------------- # Do the conversion # WUZ - it would be nice to have some smarts here about converting # through intermediate units. For example, if we know in->cm, and # we know cm->m, we should be able to do in->m set conversion $Convert($from,$to) set factor [lindex $conversion 0] set offset [lindex $conversion 1] # Do the math in the highest precision possible. set holdPrecision $::tcl_precision set ::tcl_precision 17 set numberOut [expr {$numberIn * $factor + $offset}] set ::tcl_precision $holdPrecision #---------------------------------------- # Preserve significant digits # This code is based on some concepts at: # http://mathworld.wolfram.com/SignificantDigits.html # A regular expression to parse numbers set RE {(?ix) # Ignore case, extended syntax ([-+]?) # Optional leading sign ([0-9]*) # Integer part \.? # Optional decimal point ([0-9]*) # Fractional part (e?[0-9]*) # Optional exponent } # The significant digits in the input is all the digits before and # after the decimal point. regexp $RE $numberIn whole sign int frac exp set sig [string length "$int$frac"] # Parse the output regexp $RE $numberOut whole sign int frac exp if {$int == 0} { set int "" } # Remember the magnitude of numberOut set mag [string length $int] # Build an integer string from the parts. set str "$int$frac" # Pad with 0's in case numberOut doesn't have enoug digits. append str "00000000000000000" # Build a new float with all the significant digits before the decimal # so we can use round() to get the right number. set str [string range $str 0 [expr {$sig-1}]].[string range $str $sig end] # Round to the right number of significant digits. set str [expr {round($str)}] # Rebuild the output set numberOut $sign append numberOut [string range $str 0 [expr {$mag-1}]] if {[string length $str] != $mag} { append numberOut . append numberOut [string range $str $mag end] } append numberOut $exp #---------------------------------------- # Return the converted measurement return [list $numberOut $to] } # units::convert # #----------------------------------------------------------------------- # units::normalize -- # # Normalize a unit string to cannonical form. For example, " or # inches to in, meters or metre to m, etc. # # Arguments: # str - The unit string to process. # # Results: # Returns the normalized string or raises an error if str is # unrecongnized. # proc units::normalize { str } { variable Abbrev # WUZ - what about case? "Yards" vs. "yards" if { ! [info exists Abbrev($str)]} { error "Invalid unit string, '$str'." } return $Abbrev($str) } # units::normalize # #----------------------------------------------------------------------- # units::addAbbrev -- # # Add an abbreviation (or normal string) for a long unit string as # well as any translations for that long unit string. For # example, the abbreviation for "meter" is "m". If # UNIT_STRING_METER is in the message catalog with a translation # of "metre", then the abbreviation for "metre" is also "m" # # Arguments: # long - The long string such as "inches" or "point" # short - The normal string such as "in" or "pt" # # Results: # The abbreviation array is updated. # proc units::addAbbrev { long short } { variable Abbrev # Add the, presumably English, basic abbreviation. set Abbrev($long) $short # Add any translations we can find # # Build a key to look up this type of unit in the message catalog set key "UNIT_STRING_[string toupper $long]" # Try to look up the string set s [msgcat::mc $key] # If there's an entry in the catalog, store it with the # same abbreviation as the original English. if {! [string equal $key $s]} { set Abbrev($s) $short } } # units::addAbbrev # #----------------------------------------------------------------------- # units::addConvert -- # # Add a conversion from one unit to another and the reverse. # # All conversions are considered linear (e.g., 2.54cm/in) and # reversible (1/2.54 in/cm). The offset is provided for # temperature (F = (C-32) * 5/9, K = C - 273.15); maybe there are # other uses. # # Arguments: # from - The cannonical units to convert from (e.g., the result of # a call to [normalize] # to - The cannonical units to convert to. # factor - The slope of the from vs. to line # offset - The X-intercept of the from vs. to line # # Results: # The conversion (and it's inverse) are recorded. # If from hasn't been seen before, to is recorded as it's default # conversion. proc units::addConvert { from to factor {offset 0} } { variable Convert variable Defaults # We might have been called with a formula (e.g., 9/5), so make # sure it's a number for our calculations below. set factor [expr 1.0*$factor] set offset [expr 1.0*$offset] # Set up the conversion supplied by the caller set Convert($from,$to) [list $factor $offset] # And the reverse set Convert($to,$from) [list [expr 1/$factor] [expr {0.0-$offset/$factor}]] # The first conversion added becomes the default. if {! [info exists Defaults($from)]} { set Defaults($from) $to set Defaults($to) $from } } # units::addConvert # #======================================================================= # Private procs only below this line #======================================================================= # units::Init -- # # Initialize the abbreviation array used to normalize unit strings. # This is fairly complete for distance, has a bunch of temperatures. # # Arguments: # NONE # # Results: # The array is set with English and locale-specific entries. # (The locale-specific entries are a by-product of addAbbrev.) # proc units::Init { } { variable Abbrev # Convert between 53% and .53, etc. addAbbrev % % addConvert "" % 100 #---------------------------------------- # Temperatures addAbbrev F F addAbbrev C C addAbbrev K K addAbbrev R R addConvert C F 9/5 32 addConvert K F 9/5 -459.67 addConvert K R 9/5 0 addConvert C R 9/5 491.67 addConvert C K 1 273.15 addConvert F R 1 459.67 #---------------------------------------- # Distance # Missing units: mils, microns, angstroms, decimeters, others? # Yep: kilometers, nanometers addAbbrev in in addAbbrev inch in addAbbrev inches in addAbbrev \" in addAbbrev ft ft addAbbrev foot ft addAbbrev feet ft addAbbrev ' ft addAbbrev yd yd addAbbrev yard yd addAbbrev yards yd addAbbrev pt pt addAbbrev point pt addAbbrev points pt addAbbrev mm mm addAbbrev millimeter mm addAbbrev millimeters mm addAbbrev cm cm addAbbrev centimeter cm addAbbrev centimeters cm addAbbrev m m addAbbrev meters m addAbbrev meter m addConvert in cm 2.54 addConvert in pt 72 addConvert in ft 1/12 addConvert in yd 1/36 addConvert in mm 25.4 addConvert in m .0254 addConvert ft pt 12*72 addConvert ft yd 1/3 addConvert ft mm 304.8 addConvert ft cm 30.48 addConvert ft m .3048 addConvert yd pt 36*72 addConvert yd mm 914.4 addConvert yd cm 91.44 addConvert yd m .9144 addConvert mm pt 72/25.4 addConvert mm cm 1/10 addConvert mm m 1/1000 addConvert cm pt 72/2.54 addConvert cm m 1/100 } # units::Init # Initialize internal data structures units::Init ---- [Category Package] - [Category Mathematics]