<!-- canonical: https://efficientnewlanguage.org/eml-p/projects/P002-unit-converter/ | updated: 2026-09-27 -->

# P002 Unit converter

Convert lengths, weights, temperatures and volumes from any unit to any other of the same kind, and back again, with every typed unit and value checked first - from a text menu.

EML-P project `projects/unit-converter` in the EML language repo: 3 module(s), entry `main.eml`, terminal UI. There, `eml project run projects/unit-converter` runs it and `eml project verify projects/unit-converter` replays every session under CPython (two hash seeds) and in the interpreter; the site build replays every session in the interpreter again and publishes a session only if its screen matches.

Built on verified corpus cases: unit-temperature-converter (https://efficientnewlanguage.org/cases/009-unit-temperature-converter/), dimensional-unit-guard (https://efficientnewlanguage.org/cases/202-dimensional-unit-guard/).

## Sessions

### bad-input - interpreter: byte-equal to the golden

Input:

```text
5
9

 1
parsec
1

1
kg
1
  km
mi
abc
1
km
m

1
km
m
-3
3
F
C
-500
3
K
C
-0.5
2
g
oz
nan
2
g
oz
inf
4
l
l
-0
5
6
```

Screen:

```text

== Unit converter ==
1) length  2) weight  3) temperature  4) volume  5) reverse last  6) quit
choice> 5
Nothing to reverse yet.

== Unit converter ==
1) length  2) weight  3) temperature  4) volume  5) reverse last  6) quit
choice> 9
Pick a number from 1 to 6.

== Unit converter ==
1) length  2) weight  3) temperature  4) volume  5) reverse last  6) quit
choice> 
Pick a number from 1 to 6.

== Unit converter ==
1) length  2) weight  3) temperature  4) volume  5) reverse last  6) quit
choice>  1
length units: mm cm m km in ft yd mi
from> parsec
Unknown unit: parsec. length units: mm cm m km in ft yd mi

== Unit converter ==
1) length  2) weight  3) temperature  4) volume  5) reverse last  6) quit
choice> 1
length units: mm cm m km in ft yd mi
from> 
Type a unit.

== Unit converter ==
1) length  2) weight  3) temperature  4) volume  5) reverse last  6) quit
choice> 1
length units: mm cm m km in ft yd mi
from> kg
kg is a weight unit, not a length unit.

== Unit converter ==
1) length  2) weight  3) temperature  4) volume  5) reverse last  6) quit
choice> 1
length units: mm cm m km in ft yd mi
from>   km
to> mi
value in km> abc
Not a number: abc

== Unit converter ==
1) length  2) weight  3) temperature  4) volume  5) reverse last  6) quit
choice> 1
length units: mm cm m km in ft yd mi
from> km
to> m
value in km> 
Type a number.

== Unit converter ==
1) length  2) weight  3) temperature  4) volume  5) reverse last  6) quit
choice> 1
length units: mm cm m km in ft yd mi
from> km
to> m
value in km> -3
A length cannot be negative.

== Unit converter ==
1) length  2) weight  3) temperature  4) volume  5) reverse last  6) quit
choice> 3
temperature units: C F K
from> F
to> C
value in F> -500
-500 F is below absolute zero (-459.67 F).

== Unit converter ==
1) length  2) weight  3) temperature  4) volume  5) reverse last  6) quit
choice> 3
temperature units: C F K
from> K
to> C
value in K> -0.5
-0.5 K is below absolute zero (0 K).

== Unit converter ==
1) length  2) weight  3) temperature  4) volume  5) reverse last  6) quit
choice> 2
weight units: mg g kg t oz lb
from> g
to> oz
value in g> nan
Not a finite number: nan

== Unit converter ==
1) length  2) weight  3) temperature  4) volume  5) reverse last  6) quit
choice> 2
weight units: mg g kg t oz lb
from> g
to> oz
value in g> inf
Not a finite number: inf

== Unit converter ==
1) length  2) weight  3) temperature  4) volume  5) reverse last  6) quit
choice> 4
volume units: ml l m3 tsp tbsp floz cup pt qt gal
from> l
to> l
value in l> -0
0 l = 0 l

== Unit converter ==
1) length  2) weight  3) temperature  4) volume  5) reverse last  6) quit
choice> 5
0 l = 0 l

== Unit converter ==
1) length  2) weight  3) temperature  4) volume  5) reverse last  6) quit
choice> 6
Bye.
```

### basic - interpreter: byte-equal to the golden

Input:

```text
1
km
mi
10
5
2
lb
kg
150
3
F
C
98.6
3
C
F
100
3
K
C
0
4
gal
l
2
4
cup
ml
1
5
1
ft
cm
6
6
```

Screen:

```text

== Unit converter ==
1) length  2) weight  3) temperature  4) volume  5) reverse last  6) quit
choice> 1
length units: mm cm m km in ft yd mi
from> km
to> mi
value in km> 10
10 km = 6.21371 mi

== Unit converter ==
1) length  2) weight  3) temperature  4) volume  5) reverse last  6) quit
choice> 5
6.21371 mi = 10 km

== Unit converter ==
1) length  2) weight  3) temperature  4) volume  5) reverse last  6) quit
choice> 2
weight units: mg g kg t oz lb
from> lb
to> kg
value in lb> 150
150 lb = 68.0389 kg

== Unit converter ==
1) length  2) weight  3) temperature  4) volume  5) reverse last  6) quit
choice> 3
temperature units: C F K
from> F
to> C
value in F> 98.6
98.6 F = 37 C

== Unit converter ==
1) length  2) weight  3) temperature  4) volume  5) reverse last  6) quit
choice> 3
temperature units: C F K
from> C
to> F
value in C> 100
100 C = 212 F

== Unit converter ==
1) length  2) weight  3) temperature  4) volume  5) reverse last  6) quit
choice> 3
temperature units: C F K
from> K
to> C
value in K> 0
0 K = -273.15 C

== Unit converter ==
1) length  2) weight  3) temperature  4) volume  5) reverse last  6) quit
choice> 4
volume units: ml l m3 tsp tbsp floz cup pt qt gal
from> gal
to> l
value in gal> 2
2 gal = 7.57082 l

== Unit converter ==
1) length  2) weight  3) temperature  4) volume  5) reverse last  6) quit
choice> 4
volume units: ml l m3 tsp tbsp floz cup pt qt gal
from> cup
to> ml
value in cup> 1
1 cup = 236.588 ml

== Unit converter ==
1) length  2) weight  3) temperature  4) volume  5) reverse last  6) quit
choice> 5
236.588 ml = 1 cup

== Unit converter ==
1) length  2) weight  3) temperature  4) volume  5) reverse last  6) quit
choice> 1
length units: mm cm m km in ft yd mi
from> ft
to> cm
value in ft> 6
6 ft = 182.88 cm

== Unit converter ==
1) length  2) weight  3) temperature  4) volume  5) reverse last  6) quit
choice> 6
Bye.
```

## Modules

### main.eml

```eml
# P002 unit converter: lengths, weights, temperatures and volumes, from any
# unit to any other unit of the same kind, and back again. What is typed is
# checked before anything is converted: the unit must exist and measure the
# kind chosen, and the value must be a finite number the quantity can have -
# no negative length, nothing below absolute zero.
import units
import text

def ask_unit(prompt, kind):
    # A unit of this kind, or None once the problem has been shown.
    text.trim(input(prompt)) => u
    units.kind_of(u) => k
    if k == None:
        if u == "":
            "Type a unit." ^0
        else:
            ("Unknown unit: " + u + ". " + kind + " units: " + text.join(units.symbols(kind))) ^0
        return None
    if k != kind:
        (u + " is a " + k + " unit, not a " + kind + " unit.") ^0
        return None
    return u

def ask_value(prompt, u, kind):
    # A value a quantity in unit u can have, or None once the problem has
    # been shown.
    text.trim(input(prompt)) => typed
    if typed == "":
        "Type a number." ^0
        return None
    try:
        float(typed) => x
    except ValueError:
        ("Not a number: " + typed) ^0
        return None
    if not (x - x == 0):
        ("Not a finite number: " + typed) ^0
        return None
    if x < units.lowest(u):
        if kind == "temperature":
            (text.num(x) + " " + u + " is below absolute zero (" + text.num(units.lowest(u)) + " " + u + ").") ^0
        else:
            ("A " + kind + " cannot be negative.") ^0
        return None
    return x

def convert_once(kind, last):
    # One conversion of this kind. Returns the conversion "reverse last"
    # works from next: this one, or the previous one if nothing was converted.
    (kind + " units: " + text.join(units.symbols(kind))) ^0
    ask_unit("from> ", kind) => frm
    if frm == None:
        return last
    ask_unit("to> ", kind) => to
    if to == None:
        return last
    ask_value("value in " + frm + "> ", frm, kind) => x
    if x == None:
        return last
    units.convert(x, frm, to) => y
    (text.num(x) + " " + frm + " = " + text.num(y) + " " + to) ^0
    return [x, frm, to, y]

None => last
True => running
while running:
    "" ^0
    "== Unit converter ==" ^0
    "1) length  2) weight  3) temperature  4) volume  5) reverse last  6) quit" ^0
    text.trim(input("choice> ")) => choice
    if choice == "1" or choice == "2" or choice == "3" or choice == "4":
        convert_once(units.kinds[int(choice) - 1], last) => last
    elif choice == "5":
        if last == None:
            "Nothing to reverse yet." ^0
        else:
            # From the exact result, not the six digits shown for it.
            units.convert(last[3], last[2], last[1]) => back
            (text.num(last[3]) + " " + last[2] + " = " + text.num(back) + " " + last[1]) ^0
            [last[3], last[2], last[1], back] => last
    elif choice == "6":
        False => running
    else:
        "Pick a number from 1 to 6." ^0
"Bye." ^0
```

Python projection of main.eml:

```python
import units
import text

def ask_unit(prompt, kind):
    u = text.trim(input(prompt))
    k = units.kind_of(u)
    if k == None:
        if u == "":
            print("Type a unit.")
        else:
            print("Unknown unit: " + u + ". " + kind + " units: " + text.join(units.symbols(kind)))
        return None
    if k != kind:
        print(u + " is a " + k + " unit, not a " + kind + " unit.")
        return None
    return u

def ask_value(prompt, u, kind):
    typed = text.trim(input(prompt))
    if typed == "":
        print("Type a number.")
        return None
    try:
        x = float(typed)
    except ValueError:
        print("Not a number: " + typed)
        return None
    if not x - x == 0:
        print("Not a finite number: " + typed)
        return None
    if x < units.lowest(u):
        if kind == "temperature":
            print(text.num(x) + " " + u + " is below absolute zero (" + text.num(units.lowest(u)) + " " + u + ").")
        else:
            print("A " + kind + " cannot be negative.")
        return None
    return x

def convert_once(kind, last):
    print(kind + " units: " + text.join(units.symbols(kind)))
    frm = ask_unit("from> ", kind)
    if frm == None:
        return last
    to = ask_unit("to> ", kind)
    if to == None:
        return last
    x = ask_value("value in " + frm + "> ", frm, kind)
    if x == None:
        return last
    y = units.convert(x, frm, to)
    print(text.num(x) + " " + frm + " = " + text.num(y) + " " + to)
    return [x, frm, to, y]

last = None
running = True
while running:
    print("")
    print("== Unit converter ==")
    print("1) length  2) weight  3) temperature  4) volume  5) reverse last  6) quit")
    choice = text.trim(input("choice> "))
    if choice == "1" or choice == "2" or choice == "3" or choice == "4":
        last = convert_once(units.kinds[int(choice) - 1], last)
    elif choice == "5":
        if last == None:
            print("Nothing to reverse yet.")
        else:
            back = units.convert(last[3], last[2], last[1])
            print(text.num(last[3]) + " " + last[2] + " = " + text.num(back) + " " + last[1])
            last = [last[3], last[2], last[1], back]
    elif choice == "6":
        running = False
    else:
        print("Pick a number from 1 to 6.")
print("Bye.")
```

### units.eml

```eml
# P002 unit converter - the units. Length, weight and volume convert through
# a base unit by a factor: the metre, the gram and the millilitre.
# Temperature cannot: its scales start from different zeros, so it converts
# through Celsius with an offset as well as a scale. The factors are the
# exact definitions (1 in = 2.54 cm, 1 lb = 453.59237 g, 1 US gallon =
# 3785.411784 ml), not rounded values.

["length", "weight", "temperature", "volume"] => kinds

{
    "length": ["mm", "cm", "m", "km", "in", "ft", "yd", "mi"],
    "weight": ["mg", "g", "kg", "t", "oz", "lb"],
    "temperature": ["C", "F", "K"],
    "volume": ["ml", "l", "m3", "tsp", "tbsp", "floz", "cup", "pt", "qt", "gal"],
} => unit_lists

# How many base units one unit is. The volumes from tsp on are US customary.
{
    "mm": 0.001,
    "cm": 0.01,
    "m": 1.0,
    "km": 1000.0,
    "in": 0.0254,
    "ft": 0.3048,
    "yd": 0.9144,
    "mi": 1609.344,
    "mg": 0.001,
    "g": 1.0,
    "kg": 1000.0,
    "t": 1000000.0,
    "oz": 28.349523125,
    "lb": 453.59237,
    "ml": 1.0,
    "l": 1000.0,
    "m3": 1000000.0,
    "tsp": 4.92892159375,
    "tbsp": 14.78676478125,
    "floz": 29.5735295625,
    "cup": 236.5882365,
    "pt": 473.176473,
    "qt": 946.352946,
    "gal": 3785.411784,
} => factors

def kind_of(u):
    # The kind a unit measures, or None for a unit this converter does not know.
    for k in kinds:
        if u in unit_lists[k]:
            return k
    return None

def symbols(kind):
    return unit_lists[kind]

def to_celsius(x, u):
    if u == "F":
        return (x - 32) * 5 / 9
    if u == "K":
        return x - 273.15
    return x

def from_celsius(c, u):
    if u == "F":
        return c * 9 / 5 + 32
    if u == "K":
        return c + 273.15
    return c

def convert(x, frm, to):
    # x in unit frm, expressed in unit to. Both units measure the same kind:
    # the menu checks that before it asks for a value.
    if kind_of(frm) == "temperature":
        return from_celsius(to_celsius(x, frm), to)
    return x * factors[frm] / factors[to]

def lowest(u):
    # The smallest value a quantity in unit u can have: absolute zero for a
    # temperature, nothing below zero for everything else.
    if u == "C":
        return -273.15
    if u == "F":
        return -459.67
    return 0.0
```

Python projection of units.eml:

```python
kinds = ["length", "weight", "temperature", "volume"]
unit_lists = {"length": ["mm", "cm", "m", "km", "in", "ft", "yd", "mi"], "weight": ["mg", "g", "kg", "t", "oz", "lb"], "temperature": ["C", "F", "K"], "volume": ["ml", "l", "m3", "tsp", "tbsp", "floz", "cup", "pt", "qt", "gal"]}
factors = {"mm": 0.001, "cm": 0.01, "m": 1.0, "km": 1000.0, "in": 0.0254, "ft": 0.3048, "yd": 0.9144, "mi": 1609.344, "mg": 0.001, "g": 1.0, "kg": 1000.0, "t": 1000000.0, "oz": 28.349523125, "lb": 453.59237, "ml": 1.0, "l": 1000.0, "m3": 1000000.0, "tsp": 4.92892159375, "tbsp": 14.78676478125, "floz": 29.5735295625, "cup": 236.5882365, "pt": 473.176473, "qt": 946.352946, "gal": 3785.411784}

def kind_of(u):
    for k in kinds:
        if u in unit_lists[k]:
            return k
    return None

def symbols(kind):
    return unit_lists[kind]

def to_celsius(x, u):
    if u == "F":
        return (x - 32) * 5 / 9
    if u == "K":
        return x - 273.15
    return x

def from_celsius(c, u):
    if u == "F":
        return c * 9 / 5 + 32
    if u == "K":
        return c + 273.15
    return c

def convert(x, frm, to):
    if kind_of(frm) == "temperature":
        return from_celsius(to_celsius(x, frm), to)
    return x * factors[frm] / factors[to]

def lowest(u):
    if u == "C":
        return -273.15
    if u == "F":
        return -459.67
    return 0.0
```

### text.eml

```eml
# P002 unit converter - text helpers. The interpreter that checks every
# session does not run string methods yet, so trimming and joining are
# written out here.

def trim(s):
    # s without the spaces at either end.
    0 => i
    len(s) => j
    while i < j and s[i] == " ":
        i + 1 => i
    while j > i and s[j - 1] == " ":
        j - 1 => j
    return s[i:j]

def join(words):
    # The words with one space between each two.
    "" => out
    for w in words:
        if out != "":
            out + " " => out
        out + w => out
    return out

def num(x):
    # Six significant digits, as a calculator shows them. Adding 0.0 turns
    # -0.0 into 0.0, so a typed "-0" never comes back as "-0".
    return "%g" % (x + 0.0)
```

Python projection of text.eml:

```python
def trim(s):
    i = 0
    j = len(s)
    while i < j and s[i] == " ":
        i = i + 1
    while j > i and s[j - 1] == " ":
        j = j - 1
    return s[i:j]

def join(words):
    out = ""
    for w in words:
        if out != "":
            out = out + " "
        out = out + w
    return out

def num(x):
    return "%g" % (x + 0.0)
```

## README

# P002 - Unit converter

Converts lengths, weights, temperatures and volumes from any unit to any other
unit of the same kind, and reverses the last conversion. A text menu; units are
typed by their symbol, and each kind lists its units before asking.

- `main.eml` - the menu loop and the checks on what is typed
- `units.eml` - the units: which kind each one measures, the factors that
  convert length, weight and volume through the metre, the gram and the
  millilitre, and the scale and offset that convert temperature through Celsius
- `text.eml` - trimming, joining words, and showing a number to six
  significant digits

What is checked before anything is converted: a unit must exist and measure the
kind chosen - typing `kg` while converting a length is refused, not converted;
a value must be a finite number; a length, weight or volume cannot be negative,
and no temperature can be below absolute zero. "Reverse last" works from the
exact result, not from the six digits shown for it.

Sessions: `sessions/basic.in` converts in all four kinds and reverses two
conversions; `sessions/bad-input.in` asks to reverse before anything was
converted, types an unknown and an empty choice, units that do not exist, are
empty or measure another kind, values that are empty, not numbers or not
finite, a negative length and temperatures below absolute zero.

Built on the verified corpus cases `unit-temperature-converter` (Celsius and
Fahrenheit, both ways) and `dimensional-unit-guard` (a unit carries the
dimension it measures, and a conversion across dimensions is refused rather
than computed).
