Version 5 of Unit converter

Updated 2002-09-09 08:30:30

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.


 proc convert {args} {
    set res {}
    global tbl
    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
        lspread $tbl($i) -> fac unit2 ;# 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]] {
            lspread $tbl($unit) -> fac unit2
            lappend res [format %.2f [expr $n*$fac]]
            lappend res $unit2
        } else {
            lappend res $n; lappend res $unit
        }
    }
    set yy ""; # current implementation of ladd fails on nonexistent variable.
    foreach {x y} $res {lappend xx $x; ladd yy $y}
    if [llength $yy]==1 {set res [list [expr [join $xx +]] $yy] }
    return $res
 }

Note: This function makes use of [lspread] from Bag of algorithms, and [ladd] and [lel] from Additional list functions.


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]]

 # 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 \
         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