Converts measures, mostly between metric and US style. "convert names" gives you the defined units. Working, but unfinished weekend fun project by [Richard Suchenwirth] on 1999-07-22. 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] ---- [AM] (18 june 2004) Some remarks: * I would not use F and C for degrees Fahrenheit and centigrade - this leads to confusion with units like F (farad) and C (coulomb). Better: oF and oC. * The default list of units should be more extensive, of course :) * The problem of Yards versus yards as raised in the comments can not be solved merely by making the search case-insensitive: case does matter! A unit like Nm (newton-meter) would become equal to nm (nanometers) if you did. ---- # 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 ---- [fredderic]: The simplest way to do unit-to-unit conversions, is to take your starting unit, and do every conversion on it you know about, then see if you've found the unit you want. If not, you try again, and again, so long as you haven't exhausted all your conversion options (ie. as long as at least one conversion is performed per pass). You end up being able to convert between any two units in your table, so long as there is a chain between them. (I'm currently trying to upgrade it to handle multiple units, but that's a definite work in progress) set convert_table {} # temperature lappend convert_table oC +273.15 oK lappend convert_table oF +459.67 oR lappend convert_table oK *1.8 oR # distance lappend convert_table cm *10 mm lappend convert_table m *100 cm lappend convert_table km *1000 m lappend convert_table in *2.54 cm lappend convert_table ft *12 in lappend convert_table yd *3 ft lappend convert_table in *72 pt proc convert {src dest {keep_table ""} } { foreach {value unit} $src {break} if { $keep_table != {} } { upvar 1 $keep_table tbl catch {unset tbl} } set tbl($unit) $value set match 1 while { $match } { set match 0 foreach unit [array names tbl] { foreach indx [lsearch -all $::convert_table $unit] { switch [expr {$indx % 3}] { 1 {continue} 0 { set mult [lindex $::convert_table [incr indx]] set new_unit [lindex $::convert_table [incr indx]] } 2 { set mult [lindex $::convert_table [incr indx -1]] set new_unit [lindex $::convert_table [incr indx -1]] set mult [string map {* / + - - + / *} $mult] } } if { [info exists tbl($new_unit)] } continue set value [expr $tbl($unit) $mult] if { $new_unit eq $dest } {return $value} set tbl($new_unit) $value set match 1 } } } return } This procedure assumes you've already parsed the source unit string. Abreviations can fairly easily be added by 1-to-1 conversions between a units abreviation and its full name(s); it will slow down the conversion slightly as all the names for a unit will get added to the transition table, but I doubt it would matter. A better idea would probably be to do pre-conversions on the source and destination units. The trick with ''keep_table'' is handy when you want to give a list of possible conversions after the requested conversion fails; basically it just unsets the variable if it exists, and uses it as the transient conversion table instead of creating one locally. If you're doing that, then I'd recommend inventing a convention for imaginary units, like preceeding them with an '''!''' so you can filter them out easily. I was doing that in temperature conversions before I added oK and oR. The best part of this procedure is that when adding extra units to the table, feel free to flip a conversion around if it makes the numbers nicer. :) ---- [AMG]: Here's a simple [Tk] program that converts between various units. ====== #!/bin/sh # The next line restarts with tclsh.\ exec tclsh "$0" ${1+"$@"} package require Tcl 8.5 package require Tk set units { Length { foot 0.3048 "statute mile" 1609.347219 "nautical mile" 1852 ångström 1e-10 millimeter 1e-3 meter 1 kilometer 1e3 } Area { "square inch" 6.4516e-4 "square foot" 9.290304e-2 "US acre" 4046.873 "square meter" 1 } Volume { "US teaspoon" 4.928921595e-6 "US tablespoon" 14.7867647825e-6 "US fl ounce" 29.5735295625e-6 "US cup" 236.5882365e-6 "US pint" 473.176473e-6 "US quart" 946.352946e-6 "US gallon" 3.785411784e-3 milliliter 1e-6 liter 1e-3 "cubic meter" 1 } Mass { ounce 0.028349523125 pound 0.45359237 milligram 1e-6 gram 1e-3 kilogram 1 } Force { ounce 0.2780144081875 pound 4.448230531 newton 1 } } menu .menubar menu .category .menubar add cascade -label Category -menu .category . configure -menu .menubar ttk::frame .units pack .units -fill both -expand 1 wm resizable . 0 0 foreach {category _} $units { .category add command -label $category -command [list category $category] } proc convert {unit factor value} { global units category values foreach {unit2 factor2} [dict get $units $category] { if {$unit ne $unit2} { set value2 "" catch { set value2 [format %g\ [expr {double($value) * $factor / $factor2}]] } set values([list $category $unit2]) $value2 } } return 1 } proc category {newcategory} { global units category set category $newcategory destroy {*}[winfo children .units] foreach {unit factor} [dict get $units $category] { set name [string tolower [string map {. _} $unit]] ttk::entry .units.$name-entry -validate key\ -textvariable values([list $category $unit])\ -validatecommand [list convert $unit $factor %P] ttk::label .units.$name-label -text $unit -anchor w grid .units.$name-entry .units.$name-label -sticky nsew } grid columnconfigure .units 0 -weight 1 } category [lindex $units 0] # vim: set sts=4 sw=4 tw=80 et ft=tcl: ====== TODO: add more units, add support for units with a different zero point (temperature), add support for logarithmic units? ---- [IDG] Where do those absurdly precise conversion factors for pounds and ounces come from? Wikipedia states: ''In 1958 the United States and countries of the Commonwealth of Nations agreed to define the international avoirdupois pound to be exactly 0.45359237 kilograms. Consequently, since 1958, the international avoirdupois ounce is exactly 0.45359237⁄16 kg (28.349523125 g) by definition.'' Is this wrong? [AMG]: I derived the absurdly precise conversion factors based on gravitational acceleration being 9.8 m/s². But I went ahead and put in your numbers instead, since if there's a standard I'd prefer to follow it. Similarly, I also simplified the foot and statute mile conversions to match what's given in Wikipedia. I had started with the conversion I had memorized (3.28084 feet per meter), which is almost the reciprocal of the defined conversion from feet to meters (0.3048). I guess from now on I will have to remember 0.3048! Ditto 1852 meters per nautical mile, the exact value, whereas I had memorized 6076.12 feet per nautical mile, an approximation. ---- See also [units conversion library (Nelson)], [unit math] <> Package | Mathematics