Title: Make a unit converter in Python
This example shows how to make a universal unit converter. Well, not completely universal. It only converts between reasonably "normal" units like time, distance, and temperature. It can't convert really weird units like converting from seconds to distance fallen under the influence of gravity on Earth.
Anyway, the example uses two main techniques. First, it stores conversion information to convert each known unit into a standard base unit. Second, it uses a linear equation to convert between units.
Base Units
You could make a big table holding values to convert between any pair of similar units. For example, it would hold values to convert from seconds to minutes, seconds to hours, seconds to days, minutes to seconds, days to seconds, etc. That would work, but would require N2 values for N units, which would be a lot of values.
Instead this example stores values to convert from each unit to one common base unit for that type of quantity. For example, it stores values to convert from units of distance into meters. In other words, feet to meters, miles to meters, centimeters to meters, etc.
Now to convert from unit_1 to unit_2, the code first converts unit_1 to meters and then converts that result to unit_2. I'll show you exactly that how works in a while.
Linear Equations
You can convert many units with just multiplication. For example, to convert kilograms to pounds, you just multiply by 2.205.
That works for weight (pounds, kilograms, stone), volume (liters, cups, pints), distance (meters, inches, ells), time (seconds, hours, fortnights), area (acres, hectares, square inches), and probably some others. Unfortunately, it doesn't work for temperature because different temperature scales have different zero positions. For example, the formula to convert from degrees Fahrenheit to degrees Celsius is °C = (°F - 32) * 5 / 9. (Quick quiz: What's special about the temperature -40°C?)
More generally, we can convert from unit_1 to unit_2 by using a formula of the form unit_2 = (unit_1 + a) * b + c.
The program stores the values a, b, and c for each of the units it can convert.
UnitConverter
The example uses a UnitConverter class to perform conversions. Here's the class declaration and its constructor, which stores the conversion values.
class UnitConverter:
'''Provide unit conversion services.'''
def __init__(self):
'''Load conversion values.'''
# The conversion 'x': [a, b, c] means to convert 'x' to the base unit
# you use base_unit = (x + a) * b + c.
self.conversions = {
# Distances in meters.
'Distance': {
'm': [0, 1, 0],
'in': [0, 0.0254, 0],
'ft': [0, 0.3048, 0],
'yd': [0, 0.9144, 0],
'mi': [0, 1609.34, 0],
'km': [0, 1000, 0],
'cm': [0, 0.01, 0],
'mm': [0, 0.001, 0],
'ell': [0, 1.143, 0],
'furlong': [0, 201.168, 0],
'league': [0, 5556, 0],
},
# Weight in grams.
'Weight': {
'g': [0, 1, 0],
'lb': [0, 453.592, 0],
'oz': [0, 28.3495, 0],
'ton': [0, 907185, 0],
'kg': [0, 1000, 0],
'tonne': [0, 1000000, 0],
'stone': [0, 6350.29, 0],
},
# Volume in liters.
'Volume': {
'l': [0, 1, 0],
'cup': [0, 0.236588, 0],
'fl oz': [0, 0.0295735, 0],
'tsp': [0, 0.00492892, 0],
'tbl': [0, 0.01479, 0],
'pint': [0, 0.473176, 0],
'qt': [0, 0.946353, 0],
'gal': [0, 3.78541, 0],
'cl': [0, 0.01, 0],
'ml': [0, 0.001, 0],
},
# Time in seconds.
'Time': {
'sec': [0, 1, 0],
'min': [0, 60, 0],
'hour': [0, 3600, 0],
'day': [0, 1440, 0],
'week': [0, 10080, 0],
'year': [0, 525600, 0],
'fortnight': [0, 20160, 0],
},
# Temperature in Kelvins.
'Temperature': {
# base_unit = (x + a) * b + c.
'K': [0, 1, 0],
'C': [0, 1, 273.15], # C = (K + 0) * 1 + 273.15
'F': [-32, 0.5555, 273.15], # K = (F - 32) * 5/9 + 273.15
},
}
# Invert the conversions so, for example, we can see
# that mm is a Distance and hour is a Time.
self.quantities = {}
for quantity in self.conversions:
for unit in self.conversions[quantity]:
# E.g. quantities['mm'].append('Distance')
self.quantities[unit] = quantity
The constructor first stores the unit conversion factors. It creates a conversion dictionary that groups conversion values by their quantity. For example, the distances are in an entry with key Distance. That entry's content is another dictionary with keys equal to unit names like ft and mm. The values for each unit are the conversion values a, b, and c.
Notice that each category converts all of its values into a base unit. For example, distances are stored with conversions into meters. You can use any base unit you like (inches, feet, angstroms Å) as long as all units use the same base unit.
Most of these entries set a and b to 0. Only the temperature entries need them for this example. (Fun fact: Did you know there are other temperature scales than F, C, and K? Rankine is like Fahrenheit but with 0 set at absolute zero. There are even others including Réaumur, Newton, Delisle, Rømer, Leiden, and Wedgwood.)
After it defines the conversion values, the constructor creates a new dictionary to map unit names (like ft or day) back to their categories (like Distance or Time).
The next section explains how the example uses the values to make the conversions.
Making Conversions
To make the conversion from_unit ⇒ to_unit, the code first converts from_unit ⇒ base_unit and then converts base_unit ⇒ to_unit.
The conversion values make the first conversion easy using this formula:
The second conversion is a bit trickier. We have conversion values that would let us make the following conversion.
Don't despair! If you solve that equation for to_unit, you get this:
Now you can convert from base_unit to to_unit.
The following Convert method uses those equations to convert from one unit to another.
def Convert(self, from_amount, from_unit, to_unit):
'''Convert from to .'''
# Get the unit quantities.
if from_unit not in self.quantities:
raise ValueError(f'Unknown unit {from_unit}')
if to_unit not in self.quantities:
raise ValueError(f'Unknown unit {to_unit}')
from_quantity = self.quantities[from_unit]
to_quantity = self.quantities[to_unit]
# Make sure they have the same quantity. E.g. Volume or Time.
if from_quantity != to_quantity:
raise ValueError(
f'Cannot convert {from_unit} (a {from_quantity}) to '
f'{to_unit} (a {to_quantity})')
# Get the unit data.
from_values = self.conversions[from_quantity][from_unit]
to_values = self.conversions[to_quantity][to_unit]
# Make the conversion.
base = (from_amount + from_values[0]) * from_values[1] + from_values[2]
to_amount = (base - to_values[2]) / to_values[1] - to_values[0]
return to_amount
First, the method uses self.quantities to get the type of the from and to units. If it cannot find either of the units in the quantities dictionary, it raises an exception.
Next, if the two units don't have the same quantity type, for example if you try to convert feet to liters, the method raises an exception.
The code then gets the conversion factors for each of the units, uses the previous equations to convert from_unit ⇒ base_unit ⇒ to_unit, and returns the result.
Main Program
Here's the example's main program.
converter = UnitConverter()
# These work.
print(f'1 ft = {converter.Convert(1, "ft", "in")} in')
print(f'1 furlong = {converter.Convert(1, "furlong", "yd")} yd')
print(f'1 kg = {converter.Convert(1, "kg", "g")} g')
print(f'1 F = {converter.Convert(1, "F", "C")} C')
print(f'1 C = {converter.Convert(1, "C", "F")} F')
# This is a unit mismatch.
print(f'1 ft = {converter.Convert(1, "ft", "g")} g')
The code creates a UnitConverter and then uses it to convert feet into inches, furlongs into yards, kilograms into grams, degrees Fahrenheit into degrees Celsius, and degrees Celsius into degrees Fahrenheit. It then tries and fails to convert feet into grams. If you look closely at the picture at the top of this post, you'll see the error message.
Conclusion
The converter works and is easy to use. If you like, you can add more units or more abbreviations. For example, you could allow "feet" in addition to "ft." Either add more entries to the conversions dictionary or implement a lookup table to convert extra names to known names.
Download the example to experiment with it and to see additional details.
|