## Unit converter

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 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
}
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:[email protected]]`

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 ([email protected]) 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 \
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
([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.
set sig [string length "\$int\$frac"]

# Parse the output

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
#
#-----------------------------------------------------------------------
#
#     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
}
}
#
#-----------------------------------------------------------------------
#
#     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
}
}
#
#=======================================================================
# 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.

#----------------------------------------
# Temperatures

#----------------------------------------
# Distance
# Missing units: mils, microns, angstroms, decimeters, others?
# Yep: kilometers, nanometers

}
# 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 {
inch                0.0254
foot                0.3048
"statute mile"   1609.347219
"nautical mile"  1852
"Planck length"     1.616199e-35
å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
slug                0.068521765561961046335582743012613
pound               0.45359237
milligram           1e-6
gram                1e-3
kilogram            1
} Force {
ounce               0.2780138509537812
pound               4.4482216152605
newton              1
} Speed {
"speed of light"    299792458
knot                0.514444
mile/hour           0.44704
kilometer/hour      2.777778e-1
meter/second        1
} "Plane Angle" {
degree              0.0027777777777777777777777777777778
arcminute           4.6296296296296296296296296296296e-5
arcsecond           7.716049382716049382716049382716e-7
revolution          1
} Pressure {
"pound/sq in"       6894.75729
"pound/sq ft"       47.880259
"inch of mercury"   3386.389
atmosphere          101325
bar                 1e5
kilopascal          1e3
millibar            1e2
pascal              1
} Time {
century             3155695200
year                31556952
week                604800
day                 86400
hour                3600
minute              60
second              1
millisecond         1e-3
"atomic time unit"  2.418884254e-17
"Planck time"       1.351211868e-43
}
}

ttk::frame .units
pack .units -fill both -expand 1
wm resizable . 0 0
wm title . Convert

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.

### Screenshots

gold added pix, for AMG's unit conversion script above

AMG: The screenshot would be more interesting if it showed numbers. Updated. As you can see, one nautical mile is (exactly) 1852 meters or (very nearly) 6072.12 feet; also, nautical miles are 15.078% longer than statute miles. Hmm, I wonder if it would be helpful to indicate which numbers are exact and which are approximations.

 Category Package Category Mathematics