Unit Converter

 


import os

import json

from datetime import datetime

from pathlib import Path

from collections import defaultdict


# ============================================================

# CONVERSION DATA

# ============================================================


CATEGORIES = {


    # ── LENGTH ──────────────────────────────────────────────

    "length": {

        "base":  "meter",

        "units": {

            "meter":      1,

            "kilometer":  1000,

            "centimeter": 0.01,

            "millimeter": 0.001,

            "micrometer": 1e-6,

            "nanometer":  1e-9,

            "mile":       1609.344,

            "yard":       0.9144,

            "foot":       0.3048,

            "inch":       0.0254,

            "nautical mile": 1852,

            "light year": 9.461e15,

        }

    },


    # ── WEIGHT / MASS ────────────────────────────────────────

    "weight": {

        "base":  "kilogram",

        "units": {

            "kilogram":     1,

            "gram":         0.001,

            "milligram":    1e-6,

            "microgram":    1e-9,

            "metric ton":   1000,

            "pound":        0.453592,

            "ounce":        0.0283495,

            "stone":        6.35029,

            "quintal":      100,

            "carat":        0.0002,

        }

    },


    # ── TEMPERATURE ──────────────────────────────────────────

    "temperature": {

        "base":  "celsius",

        "units": {

            "celsius":    None,   # special handling

            "fahrenheit": None,

            "kelvin":     None,

            "rankine":    None,

        }

    },


    # ── VOLUME ───────────────────────────────────────────────

    "volume": {

        "base":  "liter",

        "units": {

            "liter":          1,

            "milliliter":     0.001,

            "cubic meter":    1000,

            "cubic centimeter": 0.001,

            "cubic inch":     0.0163871,

            "cubic foot":     28.3168,

            "gallon (US)":    3.78541,

            "gallon (UK)":    4.54609,

            "quart (US)":     0.946353,

            "pint (US)":      0.473176,

            "cup (US)":       0.236588,

            "fluid ounce":    0.0295735,

            "tablespoon":     0.0147868,

            "teaspoon":       0.00492892,

        }

    },


    # ── SPEED ────────────────────────────────────────────────

    "speed": {

        "base":  "meter/second",

        "units": {

            "meter/second":    1,

            "kilometer/hour":  0.277778,

            "mile/hour":       0.44704,

            "foot/second":     0.3048,

            "knot":            0.514444,

            "mach":            340.29,

            "light speed":     299792458,

        }

    },


    # ── AREA ─────────────────────────────────────────────────

    "area": {

        "base":  "square meter",

        "units": {

            "square meter":      1,

            "square kilometer":  1e6,

            "square centimeter": 1e-4,

            "square millimeter": 1e-6,

            "square mile":       2589988.11,

            "square yard":       0.836127,

            "square foot":       0.092903,

            "square inch":       6.4516e-4,

            "hectare":           10000,

            "acre":              4046.86,

            "cent":              40.4686,    # Indian unit

            "bigha":             2508.38,    # common Indian unit

        }

    },


    # ── TIME ─────────────────────────────────────────────────

    "time": {

        "base":  "second",

        "units": {

            "second":       1,

            "millisecond":  0.001,

            "microsecond":  1e-6,

            "nanosecond":   1e-9,

            "minute":       60,

            "hour":         3600,

            "day":          86400,

            "week":         604800,

            "month":        2628000,   # avg 30.44 days

            "year":         31536000,

            "decade":       315360000,

            "century":      3153600000,

        }

    },


    # ── DATA / STORAGE ───────────────────────────────────────

    "data": {

        "base":  "byte",

        "units": {

            "bit":       0.125,

            "byte":      1,

            "kilobyte":  1024,

            "megabyte":  1024**2,

            "gigabyte":  1024**3,

            "terabyte":  1024**4,

            "petabyte":  1024**5,

            "kibibyte":  1024,

            "mebibyte":  1024**2,

            "gibibyte":  1024**3,

        }

    },


    # ── PRESSURE ─────────────────────────────────────────────

    "pressure": {

        "base":  "pascal",

        "units": {

            "pascal":      1,

            "kilopascal":  1000,

            "megapascal":  1e6,

            "bar":         100000,

            "millibar":    100,

            "atmosphere":  101325,

            "psi":         6894.76,

            "mmHg":        133.322,

            "torr":        133.322,

            "inHg":        3386.39,

        }

    },


    # ── ENERGY ───────────────────────────────────────────────

    "energy": {

        "base":  "joule",

        "units": {

            "joule":          1,

            "kilojoule":      1000,

            "megajoule":      1e6,

            "calorie":        4.184,

            "kilocalorie":    4184,

            "watt-hour":      3600,

            "kilowatt-hour":  3.6e6,

            "electronvolt":   1.602e-19,

            "BTU":            1055.06,

            "therm":          105480400,

            "foot-pound":     1.35582,

        }

    },


    # ── FUEL EFFICIENCY ──────────────────────────────────────

    "fuel": {

        "base":  "km/l",

        "units": {

            "km/l":    1,

            "l/100km": None,   # inverse — special handling

            "mpg (US)": 0.425144,

            "mpg (UK)": 0.354006,

            "miles/l":  1.60934,

        }

    },


}


# ============================================================

# HISTORY FILE

# ============================================================


HISTORY_FILE = "unit_converter_history.json"


def load_history():

    if Path(HISTORY_FILE).exists():

        try:

            with open(HISTORY_FILE, "r") as f:

                return json.load(f)

        except:

            pass

    return []



def save_history(entry):

    history = load_history()

    history.append(entry)

    history = history[-50:]

    with open(HISTORY_FILE, "w") as f:

        json.dump(history, f, indent=2)



# ============================================================

# CONVERSION LOGIC

# ============================================================


def convert_temperature(value, from_unit, to_unit):

    """Special handling for temperature conversions."""

    # Convert to Celsius first

    if from_unit == "celsius":

        celsius = value

    elif from_unit == "fahrenheit":

        celsius = (value - 32) * 5 / 9

    elif from_unit == "kelvin":

        celsius = value - 273.15

    elif from_unit == "rankine":

        celsius = (value - 491.67) * 5 / 9

    else:

        return None


    # Convert from Celsius to target

    if to_unit == "celsius":

        return celsius

    elif to_unit == "fahrenheit":

        return celsius * 9 / 5 + 32

    elif to_unit == "kelvin":

        return celsius + 273.15

    elif to_unit == "rankine":

        return (celsius + 273.15) * 9 / 5

    return None



def convert_fuel(value, from_unit, to_unit):

    """Special handling for fuel efficiency (l/100km is inverse)."""

    # Convert to km/l first

    if from_unit == "l/100km":

        kml = 100 / value if value != 0 else 0

    else:

        factor = CATEGORIES["fuel"]["units"].get(from_unit, 1)

        kml    = value * factor


    # Convert from km/l to target

    if to_unit == "l/100km":

        return 100 / kml if kml != 0 else 0

    else:

        factor = CATEGORIES["fuel"]["units"].get(to_unit, 1)

        return kml / factor



def convert(value, from_unit, to_unit, category):

    """

    Universal conversion function.

    Returns converted value or None.

    """

    from_unit = from_unit.lower().strip()

    to_unit   = to_unit.lower().strip()


    if from_unit == to_unit:

        return value


    cat_data = CATEGORIES.get(category)

    if not cat_data:

        return None


    # Special categories

    if category == "temperature":

        return convert_temperature(value, from_unit, to_unit)

    if category == "fuel":

        return convert_fuel(value, from_unit, to_unit)


    units = cat_data["units"]


    if from_unit not in units or to_unit not in units:

        return None


    # Convert via base unit

    value_in_base = value * units[from_unit]

    result        = value_in_base / units[to_unit]

    return result



# ============================================================

# FORMAT RESULT

# ============================================================


def fmt_result(value):

    """Smart formatting: remove unnecessary decimals."""

    if value is None:

        return "N/A"

    if abs(value) >= 1e12 or (abs(value) < 1e-6 and value != 0):

        return f"{value:.6e}"

    if value == int(value):

        return f"{int(value):,}"

    if abs(value) >= 100:

        return f"{value:,.4f}"

    if abs(value) >= 1:

        return f"{value:.6f}".rstrip("0").rstrip(".")

    return f"{value:.10f}".rstrip("0").rstrip(".")



# ============================================================

# DISPLAY UNIT LIST

# ============================================================


def list_units(category):

    cat_data = CATEGORIES.get(category)

    if not cat_data:

        print(f"  Unknown category: {category}")

        return


    units = list(cat_data["units"].keys())

    print(f"\n  Units in '{category}':")

    print("  " + "-"*40)

    for i, unit in enumerate(units, 1):

        print(f"  {i:>3}. {unit}")

    print("  " + "-"*40)



# ============================================================

# QUICK CONVERT (ALL TO ONE)

# ============================================================


def convert_one_to_all(value, from_unit, category):

    """Show value converted to ALL units in the category."""

    cat_data = CATEGORIES.get(category)

    if not cat_data:

        return


    print(f"\n  {fmt_result(value)} {from_unit}  =")

    print("  " + "-"*45)


    for unit in cat_data["units"].keys():

        if unit == from_unit.lower():

            continue

        result = convert(value, from_unit, unit, category)

        if result is not None:

            print(f"  {fmt_result(result):<20}  {unit}")


    print("  " + "-"*45)



# ============================================================

# INTERACTIVE CONVERSION

# ============================================================


def do_convert(category):

    list_units(category)


    from_unit = input(f"\n  From unit: ").strip().lower()

    to_unit   = input(f"  To unit  : ").strip().lower()


    cat_data = CATEGORIES.get(category, {})

    units    = cat_data.get("units", {})


    # Fuzzy match

    def fuzzy_match(u, unit_list):

        if u in unit_list:

            return u

        matches = [k for k in unit_list if u in k or k in u]

        return matches[0] if matches else None


    from_unit = fuzzy_match(from_unit, units) or from_unit

    to_unit   = fuzzy_match(to_unit, units) or to_unit


    if from_unit not in units:

        print(f"  Unknown unit: {from_unit}")

        return

    if to_unit not in units:

        print(f"  Unknown unit: {to_unit}")

        return


    try:

        value = float(input(f"  Value     : ").strip())

    except ValueError:

        print("  Invalid value.")

        return


    result = convert(value, from_unit, to_unit, category)


    if result is None:

        print("  Conversion failed.")

        return


    print(f"\n  {'='*45}")

    print(f"  {fmt_result(value)} {from_unit}")

    print(f"  =  {fmt_result(result)} {to_unit}")

    print(f"  {'='*45}")


    # Show all option

    show_all = input("\n  Show conversion to ALL units? (y/n): ").strip().lower()

    if show_all == "y":

        convert_one_to_all(value, from_unit, category)


    # Save to history

    save_history({

        "time":     datetime.now().strftime("%d-%m-%Y %H:%M"),

        "category": category,

        "value":    value,

        "from":     from_unit,

        "result":   result,

        "to":       to_unit,

    })



# ============================================================

# QUICK CONVERTER (COMMON PAIRS)

# ============================================================


def quick_convert():

    print("\n  QUICK CONVERSIONS")

    print("  " + "-"*40)


    quick_pairs = [

        ("1",  "km",       "mile",       "length"),

        ("2",  "kg",       "pound",      "weight"),

        ("3",  "celsius",  "fahrenheit", "temperature"),

        ("4",  "liter",    "gallon (US)","volume"),

        ("5",  "meter/second", "km/hour","speed"),

        ("6",  "hectare",  "acre",       "area"),

        ("7",  "gigabyte", "megabyte",   "data"),

        ("8",  "bar",      "psi",        "pressure"),

        ("9",  "kilocalorie","joule",    "energy"),

        ("10", "km/l",     "mpg (US)",   "fuel"),

    ]


    for num, f, t, cat in quick_pairs:

        result = convert(1, f, t, cat)

        print(f"  {num:>3}. 1 {f:<18} = {fmt_result(result)} {t}")


    print("  " + "-"*40)

    print("\n  Enter a number to use that conversion,")

    choice = input("  or press Enter to go back: ").strip()


    if not choice:

        return


    # Find the pair

    selected = None

    for num, f, t, cat in quick_pairs:

        if choice == num:

            selected = (f, t, cat)

            break


    if not selected:

        return


    from_unit, to_unit, category = selected


    try:

        value = float(input(f"\n  Enter value in {from_unit}: ").strip())

    except ValueError:

        print("  Invalid value.")

        return


    result = convert(value, from_unit, to_unit, category)

    print(f"\n  {fmt_result(value)} {from_unit}  =  {fmt_result(result)} {to_unit}")


    save_history({

        "time":     datetime.now().strftime("%d-%m-%Y %H:%M"),

        "category": category,

        "value":    value,

        "from":     from_unit,

        "result":   result,

        "to":       to_unit,

    })



# ============================================================

# SHOW HISTORY

# ============================================================


def show_history():

    history = load_history()

    if not history:

        print("\n  No conversion history yet.")

        return


    print("\n" + "="*65)

    print(f"  CONVERSION HISTORY  (last {len(history)})")

    print("="*65)

    print(f"  {'TIME':<18} {'CAT':<14} {'FROM':<25} TO")

    print("  " + "-"*60)


    for h in reversed(history[-20:]):

        from_str = f"{fmt_result(h['value'])} {h['from']}"

        to_str   = f"{fmt_result(h['result'])} {h['to']}"

        print(f"  {h['time']:<18} {h['category']:<14} "

              f"{from_str:<25} {to_str}")

    print("="*65)



# ============================================================

# MAIN MENU

# ============================================================


def print_menu():

    print("\n" + "-"*50)

    print("  UNIT CONVERTER")

    print("-"*50)

    print("  1.  Length")

    print("  2.  Weight / Mass")

    print("  3.  Temperature")

    print("  4.  Volume")

    print("  5.  Speed")

    print("  6.  Area")

    print("  7.  Time")

    print("  8.  Data / Storage")

    print("  9.  Pressure")

    print("  10. Energy")

    print("  11. Fuel Efficiency")

    print("  12. Quick conversions (common pairs)")

    print("  13. Conversion history")

    print("  0.  Exit")

    print("-"*50)



MENU_MAP = {

    "1":  "length",

    "2":  "weight",

    "3":  "temperature",

    "4":  "volume",

    "5":  "speed",

    "6":  "area",

    "7":  "time",

    "8":  "data",

    "9":  "pressure",

    "10": "energy",

    "11": "fuel",

}



def main():

    print("\n" + "="*55)

    print("     UNIT CONVERTER")

    print("="*55)

    print(f"\n  {len(CATEGORIES)} categories  |  "

          f"{sum(len(v['units']) for v in CATEGORIES.values())}+ units")

    print("  Length, Weight, Temperature, Volume, Speed,")

    print("  Area, Time, Data, Pressure, Energy, Fuel")


    while True:

        print_menu()

        choice = input("  > ").strip()


        if choice in MENU_MAP:

            do_convert(MENU_MAP[choice])


        elif choice == "12":

            quick_convert()


        elif choice == "13":

            show_history()


        elif choice == "0":

            print("\n  Goodbye!\n")

            break


        else:

            print("  Invalid choice.")



# ============================================================

# RUN

# ============================================================


if __name__ == "__main__":

    main()

No comments: