#!/usr/bin/python3
# unicon.py - Chris X Edwards - 2020-10-05
# This started out as just a study for functionality to put in xed.ch/p/gg
# but it seemed like it might be useful as a stand-alone program too.
# It is a system for replicating most of the units functionality of the HP48.
# This includes many symbolic units features. It can convert between pretty much
# everything the HP48 could. This includes the following.
# * "UVAL" - Returns number part of a unit on L1, i.e. strips the unit part.
# * "UBASE" - Converts to SI base units only.
# * "CONVERT" - Converts to any units. L2 to L1's units. (Ignores number part of level 1)
# The exceptions that I know of (and am still working on).
# * "UFACT" - L2 in L1's units and any needed SI units. (Ignores number part of level 1)
# * "->UNIT" - Takes a number from L2 and combines it with a unit object on L1. (Ignores number part of level 1)
# * addition and subtraction (only for dimensionally consistent units) (results are in units from level 1)
# * multiplication and division
# * inversion
# * raising to power
# * percentage calculations (only for dimensionally consistent units)
# * Comparisons of values (only for dimensionally consistent units)
# * Trignometric operations (only for planar angle units)
# * Overly messy unorganized symbolic forms more complex than: 1_u^N*v^M*w/(a^I*b^J*c)

import argparse

UnitCatalog= {
"a":("Are","100_m^2",{'m':2}),
"A":("Ampere","1_A",{'A':1}),
"acre":("Acre","4046.87260987_m^2",{'m':2}),
"arcmin":("Minute of arc","2.90888208666e-4_r",{'r':1}),
"arcs":("Second of arc","4.8481368111e-8_r",{'r':1}),
"atm":("Atmosphere","101325_kg/(m*s^2)",{'kg':1,'m':-1,'s':-2}),
"au":("Astronomical unit","1.495979e11_m",{'m':1}),
"Å":("Angstrom","1e-10_m",{'m':1}),
"b":("Barn","1e-28_m^2",{'m':2}),
"bara":("Bar","100000_kg/(m*s^2)",{'kg':1,'m':-1,'s':-2}),
"bbl":("Barrel",".158987294298_m^3",{'m':3}),
"Bq":("Becquerel","1_1/s",{'s':-1}),
"Btu":("International Table Btu","1055.05585262_kg*m^2/s^2",{'kg':1,'m':2,'s':-2}),
"bu":("Bushel",".03523907_m^3",{'m':3}),
"c":("Speed of light","299792458_m/s",{'m':1,'s':-1}),
"C":("Coulomb","1_A*s",{'A':1,'s':1}),
"cal":("Calorie","4.1868_kg*m^2/s^2",{'kg':1,'m':2,'s':-2}),
"cd":("Candela","1_cd",{'cd':1}),
"chain":("Chain","20.1168402337_m",{'m':1}),
"Ci":("Curie","2.7e10_1/s",{'s':-1}),
"ct":("Carat",".0002_kg",{'kg':1}),
"cu":("US cup","2.365882365e-4_m^3",{'m':3}),
"°":("Degree","1.74532925199e-2_r",{'r':1}),
"d":("Day","86400_s",{'s':1}),
"dyn":("Dyne","1e-5_kg*m/s^2",{'kg':1,'m':1,'s':-2}),
"erg":("Erg","1e-7_kg*m^2/s^2",{'kg':1,'m':2,'s':-2}),
#"eV":("Electron volt","1.60217733e-19_kg*m^2/s^2",{'kg':1,'m':2,'s':-2}), #OLD
"eV":("Electron volt","1.602176634e-19_kg*m^2/s^2",{'kg':1,'m':2,'s':-2}),  #2019+ *exact*
"F":("Farad","1_A^2*s^4/(kg*m^2)",{'A':2,'s':4,'kg':-1,'m':-2}),
"fath":("Fathom","1.82880365761_m",{'m':1}),
"fbm":("Board foot",".002359737216_m^3",{'m':3}),
"fc":("Footcandle","10.7639104167_cd*sr/m^2",{'cd':1,'sr':1,'m':-2}),
"Fdy":("Faraday","96487_A*s",{'A':1,'s':1}),
"fermi":("Fermi","1e-15_m",{'m':1}),
"flam":("Footlambert","3.42625909964_cd/m^2",{'cd':1,'m':-2}),
"ft":("International foot",".3048_m",{'m':1}),
"ftUS":("Survey foot",".304800609601_m",{'m':1}),
"g":("Gram","1e-3_kg",{'kg':1}),
"ga":("Standard freefall","9.80665_m/s^2",{'m':1,'s':-2}),
"gal":("US gallon",".003785411784_m^3",{'m':3}),
"galC":("Canadian gallon",".00454609_m^3",{'m':3}),
"galUK":("UK gallon",".004546092_m^3",{'m':3}),
"gf":("Gram-force",".00980665_kg*m/s^2",{'kg':1,'m':1,'s':-2}),
"gmol":("Gram-mole","1_mol",{'mol':1}),
"grad":("Grade","1.57079632679e-2_r",{'r':1}),
"grain":("Grain",".00006479891_kg",{'kg':1}),
"Gy":("Gray","1_m^2/s^2",{'m':2,'s':-2}),
"H":("Henry","1_kg*m^2/(A^2*s^2)",{'kg':1,'m':2,'A':-2,'s':-2}),
"ha":("Hectare","1e4_m^2",{'m':2}),
"h":("Hour","3600_s",{'s':1}),
"hp":("Horsepower","745.699871582_kg*m^2/s^3",{'kg':1,'m':2,'s':-3}),
"Hz":("Hertz","1_1/s",{'s':-1}),
"in":("Inch",".0254_m",{'m':1}),
"inHg":("Inches of mercury, 0°C","3386.38815789_kg/(m*s^2)",{'kg':1,'m':-1,'s':-2}),
"inH2O":("Inces of water, 60°F","248.84_kg/(m*s^2)",{'kg':1,'m':-1,'s':-2}),
"J":("Joule","1_kg*m^2/s^2",{'kg':1,'m':2,'s':-2}),
"K":("Kelvins","1_K",{'K':1}),
"kg":("Kilogram","1_kg",{'kg':1}),
"kip":("Kilopound-force","4448.22161526_kg*m/s^2",{'kg':1,'m':1,'s':-2}),
"knot":("Nautical miles per hour",".5144444444444_m/s",{'m':1,'s':-1}),
"kph":("Kilometers per hour",".277777777778_m/s",{'m':1,'s':-1}),
"l":("Liter",".001_m^3",{'m':3}),
"lam":("Lambert","3183.09886184_cd/m^2",{'cd':1,'m':-2}),
"lb":("Avoirdupois pound",".45359237_kg",{'kg':1}),
"lbf":("Pound-force","4.44822161526_kg*m/s^2",{'kg':1,'m':1,'s':-2}),
"lmol":("Pound-mole","453.59237_mol",{'mol':1}),
"lbt":("Troy_pound",".3732417216_kg",{'kg':1}),
"lm":("Lumen","1_cd*sr",{'cd':1,'sr':1}),
"lx":("Lux","1_cd*sr/m^2",{'cd':1,'sr':1,'m':-2}),
"lyr":("Light year","9.46052840488e15_m",{'m':1}),
"m":("Meter","1_m",{'m':1}),
"μ":("Micron","1e-6_m",{'m':1}),
"mho":("Mho","1_A^2*s^3/(kg*m^2)",{'A':2,'s':3,'kg':-1,'m':-2}),
"mi":("International mile","1609.344_m",{'m':1}),
"mil":("Mil",".0000254_m",{'m':1}),
"min":("Minute","60_s",{'s':1}),
"miUS":("US statute mile","1609.34721869_m",{'m':1}),
"mmHg":("Millimeter of mercury (torr), 0°C","133.322368421_kg/(m*s^2)",{'kg':1,'m':-1,'s':-2}),
"mol":("Mole","1_mol",{'mol':1}),
"mph":("Miles per hour",".44704_m/s",{'m':1,'s':-1}),
"N":("Newton","1_kg*m/s^2",{'kg':1,'m':1,'s':-2}),
"nmi":("Nautical mile","1852_m",{'m':1}),
"Ω":("Ohm","1_kg*m^2/(A^2*s^3)",{'kg':1,'m':2,'A':-2,'s':-3}),
"oz":("Ounce",".028349523125_kg",{'kg':1}),
"ozfl":("US fluid ounce","2.95735295625e-5_m^3",{'m':3}),
"ozt":("Troy ounce",".0311034768_kg",{'kg':1}),
"ozUK":("UK fluid ounce","2.8413075e-5_m^3",{'m':3}),
"P":("Poise",".1_kg/(m*s)",{'kg':1,'m':-1,'s':-1}),
"Pa":("Pascal","1_kg/(m*s^2)",{'kg':1,'m':-1,'s':-2}),
"pc":("Parsec","3.08567818585e16_m",{'m':1}),
"pdl":("Poundal",".138254954376_kg*m/s^2",{'kg':1,'m':1,'s':-2}),
"ph":("Phot","10000_cd*sr/m^2",{'cd':1,'sr':1,'m':-2}),
"pk":("Peck",".0088097675_m^3",{'m':3}),
"psi":("Pounds per square inch","6894.75728317_kg/(m*s^2)",{'kg':1,'m':-1,'s':-2}),
"pt":("Pint",".000473176473_m^3",{'m':3}),
"qt":("Quart",".000946352946_m^3",{'m':3}),
"r":("Radian","1_r",{'r':1}),
"R":("Roentgen",".000258_A*s/kg",{'A':1,'s':1,'kg':-1}),
"°R":("Degrees Rankine","0.555555555556_K",{'K':1}),
"rad":("Rad",".01_m^2/s^2",{'m':2,'s':-2}),
"rd":("Rod","5.02921005842_m",{'m':1}),
"rem":("Rem",".01_m^2/s^2",{'m':2,'s':-2}),
"rpm":("Revolutions per minute",".0166666666667_1/s",{'s':-1}),
"s":("Second","1_s",{'s':1}),
"S":("Siemens","1_A^2*s^3/(kg*m^2)",{'A':2,'s':3,'kg':-1,'m':-2}),
"sb":("Stilb","10000_cd/m^2",{'cd':1,'m':-2}),
"slug":("Slug","14.5939029372_kg",{'kg':1}),
"sr":("Steradian","1_sr",{'sr':1}),
"st":("Stere","1_m^3",{'m':3}),
"St":("Stokes",".0001_m^2/s",{'m':2,'s':-1}),
"Sv":("Sievert","1_m^2/s^2",{'m':2,'s':-2}),
"t":("Metric ton","1000_kg",{'kg':1}),
"T":("Tesla","1_kg/(A*s^2)",{'kg':1,'A':-1,'s':-2}),
"tbsp":("Tablespoon","1.47867647813e-5_m^3",{'m':3}),
"therm":("EEC therm","105506000_kg*m^2/s^2",{'kg':1,'m':2,'s':-2}),
"ton":("Short ton","907.18474_kg",{'kg':1}),
"tonUK":("Long (UK)","1016.0469088_kg",{'kg':1}),
"torr":("Torr (mmHg)","133.322368421_kg/(m*s^2)",{'kg':1,'m':-1,'s':-2}),
"tsp":("Teaspoon","4.92892159375e-6_m^3",{'m':3}),
"u":("Unified atomic mass","1.6605402e-27_kg",{'kg':1}),
"V":("Volt","1_kg*m^2/(A*s^3)",{'kg':1,'m':2,'A':-1,'s':-3}),
"W":("Watt","1_kg*m^2/s^3",{'kg':1,'m':2,'s':-3}),
"Wb":("Weber","1_kg*m^2/(A*s^2)",{'kg':1,'m':2,'A':-1,'s':-2}),
"yd":("International yard",".9144_m",{'m':1}),
"yr":("Year","31556925.9747_s",{'s':1}),
"dB":("Decibel","1_1",{}),
}

# WTF about these?
#"°C":("Degree Celsius","1_K or 274.15_K"),
#"°F":("Degrees Fahrenheit","0.555555555556_K or 255.927777778_K"),
# Or these not in the HP48?
# "G":("Gauss,.0001_kg/(A*s^2)"),
# "Mx":("Maxwell",".00000001_kg*m^2/(A*s^2)"),

# SI Prefixes
PrefixCatalog= {
'y':1e-24,
'z':1e-21,
'a':1e-18,
'f':1e-15,
'p':1e-12,
'n':1e-9,
'μ':1e-6,
'm':1e-3,
'c':1e-2,
'd':1e-1,
'h':1e2,
'k':1e3,
'M':1e6,
'G':1e9,
'T':1e12,
'P':1e15,
'E':1e18,
'Z':1e21,
'Y':1e24,
'yocto':1e-24,
'zepto':1e-21,
'atto':1e-18,
'femto':1e-15,
'pico':1e-12,
'nano':1e-9,
'micro':1e-6,
'milli':1e-3,
'centi':1e-2,
'deci':1e-1,
'hecto':1e2,
'kilo':1e3,
'mega':1e6,
'giga':1e9,
'tera':1e12,
'peta':1e15,
'exa':1e18,
'zetta':1e21, 
'yotta':1e24,
'Kibi':1024,
'Mebi':1024^2,
'Gibi':1024^3,
'Tebi':1024^4,
'Pebi':1024^5,
}

# And some non-HP48
# Kibi(byte) KiB = 1024^1
# Mebi(byte) MiB = 1024^2
# Gibi(byte) GiB = 1024^3
# Tebi(byte) TiB = 1024^4
# Pebi(byte) PiB = 1024^5

# == Base
# m|meter
# kg|kilogram
# s|second
# A|ampere
# K|kelvins
# cd|candela
# mol|mole
# r|radian
# sr|seradian

class UnitVal:
    base_units= ['m','kg','s','A','K','cd','mol','r','sr']
    def __init__(self,unitstring=''):
        self.num,self.den,self.val= '','',0.0
        self.raw_unit_terms= [] # A list of units with exponents. Dups and derived here. E.g. mph= ['mi^1','h^-1']
        self.TD= dict() # Term dictionary holds all unique parts of base units. Key is unit, val is exponent.
        self.conv2basefactor= 1 # Multiply by this to get base units.
        if unitstring:
            self.rawstring= unitstring
            self.parse_unit_text(unitstring)
            self.simplify_units()
        else:
            self.rawstring= ''
    def simplify_units(self):
        '''Looks at a parsed UnitVal object and tries to differetiate between
        core SI base units and derived units. It tries to convert the derived
        units to base units. Then it tries to factor out anything extraneous.'''
        new_raw_terms= [] # A new simplified updated list of raw terms with only base units.
        for dt in self.raw_unit_terms:
            if '^' in dt: # Explicit exponent found.
                N,E= dt.split('^') # Name of unit and Exponent value.
                E= int(E)
                if N in UnitVal.base_units: # Base unit.
                    debug(f'{N} is a base unit!')
                    new_raw_terms.append(dt)
                    self.conv2basefactor*= float(UnitCatalog[N][1].split('_')[0]) # Pull base unit's factor.
                elif N in UnitCatalog: # Proper known derived unit.
                    debug("Processing a known proper derived unit: "+N)
                    UnitInBaseUnits= UnitVal(UnitCatalog[N][1]) # Conversion factor's object.
                    self.conv2basefactor*= (UnitInBaseUnits.val ** E)
                    for nu in UnitInBaseUnits.TD: # Go through replacement unit's dict to update exponents.
                        debug(f"Master Unit Name: {N}")
                        debug(f"Master Unit Exponent: {E}")
                        debug(f"New Unit Exponent: {UnitInBaseUnits.TD[nu]}")
                        expon= E * UnitInBaseUnits.TD[nu] # Update replaced unit for this exponent context.
                        new_raw_terms.append(f'{nu}^{expon}')
                else: # Some kind of unknown garbage.                
                    debug(f"Unknown unit: {N}")
                    raise ValueError
        debug("Processing new_raw_terms: "+','.join(new_raw_terms))
        # All units are base units at this point. Now to cancel/simplify units where possible.
        for dt in new_raw_terms:
            if '^' in dt: # Explicit exponent found.
                N,E= dt.split('^') # Name of unit and Exponent value.
                if not N in self.TD: # A unit that has thus far not been seen. I.e not in term dictionary.
                    if E: # Omit from the term dictionary if exponent 0. Which should be strange.
                        self.TD[N]= int(E) # Make a record in the dictionary.
                else: # This unit is redundant.
                    self.TD[N]+= int(E) # Add up the exponents to factor into one term.
                    if self.TD[N] == 0: # If the exponent has now canceled to zero...
                        self.TD.pop(N)  # ...remove it from the dictionary.
        debug(f"Terms in simplify_units: {self.TD}")
    def prefix_reduction(self,pre_l): # Accepts a raw_unit_terms list that may contain prefixes.
        l= [] # Output list free of SI prefixes.
        for t in pre_l: # Iterate through terms in the list.
            t_unit,t_expon= t.split('^')
            if t_unit not in UnitCatalog: # Seek prefix explanations only when necessary. kg not k+g.
                for p in PrefixCatalog: # Go through all the prefixes looking for this.
                    if t.find(p) == 0: # Is this prefix at start (pos 0) of unit term?
                        psu= t.replace(p,'',1) # Prefix stripped unit (e.g. kilomho to mho).
                        if not psu.split('^')[0] in UnitCatalog: # Check again to make sure it's not a wrong guess.
                            print(f'{psu} not in catalog, trying other interpretations')
                            continue # Something dumb like kilowatt to ilowatt. Keep looking for kilo prefix.
                        l.append(psu) # Replace with non-prefix unit.
                        print(f"Prefix '{p}' suspected in '{t}' - converting to: '{l[-1]}'")
                        self.val*= PrefixCatalog[p] ** int(t_expon)
                        break # If a valid good unit is found, finding a second one is bad.
            else:
                l.append(t) # Put this untouched right into the output list.
        return l # A list with the prefixes multiplied out. Perhaps dups at this stage.
    def parse_unit_text(self,s):
        '''Takes a string and if it is in proper units format, creates a list of the terms.'''
        self.unitstring= s
        dtermL,ntermL= [],[]
        self.num= s # Just in case a numerator is all there is.
        if '/' in s: # Some indication of a fraction.
            debug(f"Contains some kind of fraction: {s}")
            if s.count('/') != 1: # Make sure it's just numerator over denominator, no other funny stuff.
                print("Error: multiple fractions. Must have one numerator and one denominator.")
                raise SystemExit
            else: # There should only be a single numerator and a single denominator.
                self.num,self.den= s.split('/')[0:2] # Extract them.
                dtermL= [self.den] # List of each denominator term.
            if '*' in self.den:
                if self.den[0] != '(' and self.den[-1] != ')':
                    print("ERROR: Unit denominators with multiple terms must be enclosed in parentheses.")
                    raise SystemExit
                else: # There are multiple terms and they are properly in parens.
                    dtermL= self.den.strip('()').split('*') # List of each denimenator term.
            elif self.den == '': # "x/" Blank denominator.
                print("ERROR: Found / but no denominator.")
                raise SystemExit
            else: # Just one denominator term.
                self.den= [ self.den.strip('()') ] # A list of one item.
                debug(f"Single denominator: {self.den}")
        if '_' in self.rawstring: # Formatted like a unit? E.g. ".0254_m"
            if self.num.count('_') != 1: # Make sure it doesn't have too much unit hinting.
                print("ERROR: Does not look like a unit. No '_' or too many.")
                raise SystemExit
            else:
                val_str,self.num= self.num.split('_')[0:2] # Extract value from numerator.
                self.val= float(val_str)
            if '*' in self.num:
                ntermL= self.num.split('*') # List of each denominator term.
            elif self.num == '':
                print("ERROR: Found _ but no unit content.")
                raise SystemExit
            else:
                ntermL= [ self.num ]
            templist= []
            for nt in ntermL:
                if not '^' in nt: # Add a '^1' to exponentless values for parsing consistency.
                    nt+= '^1'
                elif nt == 1: # The 1s are from things like Hz=_1^1/s^1 and should be ignored.
                    break
                templist.append(nt)
            ntermL= templist
            for dt in dtermL:
                dt= dt.strip('()')
                if '^' in dt: # Explicit exponent found.
                    dt_parts= dt.split('^')[0:2]
                    ntermL.append( dt_parts[0]+'^'+str(int(dt_parts[1])*-1) )
                else:
                    ntermL.append( dt+'^-1' ) # Denomenator with no exponents are now numerator with negative exponents.
        else:
            print(self.num)
            print("ERROR: Vaguely not looking like a unit.")
            raise SystemExit
        self.raw_unit_terms= self.prefix_reduction(ntermL) # A list of terms from input. E.g. ['kg^1', 'm^2', 'A^-1', 's^-2']
        debug("In parse_unit_text...")
        debug(self)
    def ubase(self):
        nlist,dlist= [],[] # Numerator and denominator lists.
        for t in self.TD: # Go through all terms in base terms dictionary.
            e= self.TD[t]
            if e<0:
                if e<-1:
                    dlist.append(f"{t}^{-1*e}")
                else:
                    dlist.append(f"{t}")
            else:
                if e>1:
                    nlist.append(f"{t}^{e}")
                else:
                    nlist.append(f"{t}")
        if len(nlist)>1:
            n= '*'.join(nlist)
        elif len(nlist)>0:
            n= nlist[0]
        else:
            n= "1"
        if len(dlist)>1:
            d= '/('+'*'.join(dlist)+')'
        elif len(dlist)>0:
            d= '/'+dlist[0]
        else:
            d= ''
        return n+d
    def convert(self,T): # Takes another unit object, T, which will be the target.
        debug("TERMS:")
        debug(f'self.raw_unit_terms: {self.raw_unit_terms}')
        debug(f'T.raw_unit_terms: {T.raw_unit_terms}')
        debug(f'self.ubase(): {self.ubase()}')
        debug(f'T.ubase(): {T.ubase()}')
        debug(f'self.conv2basefactor: {self.conv2basefactor}')
        debug(f'T.conv2basefactor: {T.conv2basefactor}')
        if self.TD != T.TD: # Python3 dictionaries actually compare properly!
            print("CONVERT Error: Inconsistent Units")
            print(f'{self.ubase()}    !=    {T.ubase()}')
            raise SystemExit
        return (self.val/T.val) * (self.conv2basefactor / T.conv2basefactor) # New value.
    def __repr__(self):
        return f"{self.val} {self.num} {self.den} ["+ ",".join(self.raw_unit_terms)+"]"

def test_unit_base(u,answer):
    V= UnitVal(u)
    if answer == V.TD:
        status= "OK"
    else:
        status= "BAD"
    print(f'val:{V.val} conv2basefactor:{V.conv2basefactor} = {V.val*V.conv2basefactor} ')
    debug('Raw Terms: '+', '.join(V.raw_unit_terms))
    debug('Value: '+str(V))
    print(f"===== {u} =================================== "+status)

def test_conversion(f,t,answer): # From, To
    import math
    print(f'===== Conversion from: {f}  to: {t}')
    F,T= UnitVal(f),UnitVal(t)
    newval= F.convert(T) # Numerical value in the target units, e.g. 1_in -> 1_cm is float 2.54.
    e= 1-(newval/answer)
    if abs(e)>1e-20:
        error_free_digits= f'{abs(math.log10(abs(e))):.2f}'
    else:
        error_free_digits= "OK"
    print(f'computed:{newval} truth:{answer} clean digits:{error_free_digits} diff:{newval-answer:0.12f}')
    if (abs(newval - answer)<1e-7): # Check if calculated result is close enough to proper answer.
        print("OK")
    else:
        print("BAD")

def list_available_units():
    for uci in UnitCatalog.keys():
        print(uci,UnitCatalog[uci][2])

def unit_unit_test():
    # Testing all the defined derived units from the UnitCatalog.
    for uci in UnitCatalog.keys():
        test_unit_base('1_'+uci,UnitCatalog[uci][2])
        #input()

    test_unit_base("60_m",{'m':1})
    test_unit_base("61_m/s",{'m':1,'s':-1})
    test_unit_base("62_Wb",{'kg':1,'m':2,'s':-2,'A':-1})
    test_unit_base("62_Wb",{'kg':1,'m':2,'s':-2,'A':-1})
    test_unit_base("63_J",{'kg':1,'m':2,'s':-2})
    test_unit_base("64_mi/h",{'m':1,'s':-1})
    test_unit_base("65_Wb^18/Wb^18",{}) # Full cancel - empty units dict.
    test_unit_base("66_Wb^9/Wb^18",{'kg':-9,'m':-18,'A':9,'s':18}) # Full cancel - empty units dict.
    test_unit_base("67_bu^2/(T^2*kg)",{'kg':-3,'m':6,'A':2,'s':4}) # kg/(A*s^2)
    test_unit_base("2.2_lb",{'kg':1})
    test_unit_base("39.37_in",{'m':1})
    test_unit_base("0.0002470966147763_acre",{'m':2})
    test_unit_base("2.77777e-4_h",{'s':1})
    test_unit_base("3600_m/h",{'m':1,'s':-1})
    test_unit_base("9.47817120313e-4_Btu",{'kg':1,'m':2,'s':-2})

    test_conversion("453.59237_g","1_lb",1)
    test_conversion("1_slug*W^2/N^3","1_m",14.5939029372)
    test_conversion("1_min","1_s",60)
    test_conversion("2_knot","1_m/s",1.02888888889)
    test_conversion("2_knot","1_ft/h",12152.2309711)
    test_conversion("2_yr","1_s",63113851.9494)
    test_conversion("25.4_mm","1_in",1)
    test_conversion("66_Glyr","1_lyr",66e9)
    test_conversion("6_cu", "1_qt", 1.5)
    test_conversion("6_cu", "1_l", 1.419529419)
    test_conversion("1_Btu/h", "1_W", .293071070172)
    test_conversion("1_Btu", "1_J", 1055.05585262)
    test_conversion("1_Btu/(lb*h)", "1_W/kg", .6461111111111)
    test_conversion("1_kW/kg","1_Btu/(lb*h)",1547.72141015)
    test_conversion("1000_W/kg","1_Btu/(lb*h)",1547.72141015)
    test_conversion("1_km","1_m",1000)
    test_conversion("1_m","1_km",.001000)
    test_conversion("5280_ft","1_km",1.609344)
    test_conversion(".01_m^2","1_cm^2",100)
    test_conversion("1_kilom","1_m",1000)
    test_conversion("10000_m","1_km",10)
    test_conversion("10000_g","1_kg",10)
    test_conversion("254_cm","1_in",100)
    test_conversion("1_oz","1_kg",.028349523125)
    test_conversion("252_oz*in","1_kg*cm",18.1459628)
    test_conversion("1_Btu/(lb*h)","1_kW/kg",6.46111111111e-4)
    test_conversion("1_lbf*ft", "1_N*m", 1.355818)
    test_conversion("1_cSt","1_m^2/s",1e-6)
    test_conversion("1_cSt","1_m^2/h",.0036)
    test_conversion("1_1/(m^2*s^3)","1_1/(in^2*h^3)",30100584.96)
    test_conversion("1_kg*m","1_lb*ft",7.23301385121)
    test_conversion("1_kg*m/yr^2","1_lb*ft/yr^2",7.23301385121)
    test_conversion("5_1/(s^2)","1_1/min^2",18000)
    test_conversion("1_m/s^2","1_m/min^2",3600)
    test_conversion("1_kg*m/s^2","1_lb*ft/s^2",7.23301385121)
    test_conversion("1_kg*m/(s*s)","1_lb*ft/s^2",7.23301385121)
    test_conversion("1_m/s^2)","1_in/s^2",39.3700787402)
    test_conversion("1_m/s","1_ft/s",3.28083989501)
    test_conversion("1_eV","1_kcal",3.8267348094e-23) # Old SI standards - on HP48
    test_conversion("1_eV","1_kcal",3.8292940583e-23) # New SI 2019 standard.
    test_conversion("1_m","1_cm",100)
    test_conversion("100_cm","1_m",1)
    #test_conversion("8_Wb","1_T/Å^2",8e-20)
    #test_conversion("8_mWb","1_TT/Å^2",8e-20)
    #test_conversion("1_W","1_Mx",10e8) # WIP since Mx seems to not be in in the HP48.

#== Usage ==
usage= """A unit conversion utility.

When only a single unit is supplied, converts it to an SI base form.
For two specified units the first is converted into the second.
A unit has the following form:

   VAL_N1^E1*N2^E2*Nn^En/(D1^E1*D2^E2*Dn^En)

Where VAL is the numeric value, i.e. how much of this unit.
The N terms are the numerator units with optional exponents.
The D terms are the (optional) denominator units with optional exponents.
If there are no numerator terms, use "1".
Although there can only be one numerator and one denominator separated by one
/, each can have as many terms as necessary.
All SI prefixes and abbreviations are valid where unambiguous.

Examples of valid units:

    1_1/(in^2*h^3)
    1_lbf*ft
    1_Btu/(lb*h)
    1_m^2/s
    2.54_cm
    1_megaW/ton

Use quotes to prevent shell interpretations of parentheses, etc. Note that you
can skip the value and underscore part of the target unit in a conversion."""
#== Parser ==
parser= argparse.ArgumentParser(description=usage,formatter_class=argparse.RawTextHelpFormatter)
parser.add_argument('-t', '--test',action="store_true",help="run a unit unit test")
parser.add_argument('-l', '--list',action="store_true",help="show all known units")
parser.add_argument('-d', '--debug',action="store_true",help=argparse.SUPPRESS)
parser.add_argument('unitA',nargs="?") # Convert from this.
parser.add_argument('unitB',nargs="?") # Convert to this.
p= parser.parse_args()

debug= lambda x:None # Do nothing by default.
if p.debug:
    debug= lambda m:print(m)

if p.test:
    unit_unit_test()
elif p.list:
    list_available_units()
elif p.unitA and p.unitB:
    if '_' not in p.unitB:
        tunits= p.unitB
    else:
        tunits= p.unitB.split('_')[1]
    p.unitB= '1_'+tunits # Override any dumb user input supplied as target value. That which shall be calculated!
    F,T= UnitVal(p.unitA),UnitVal(p.unitB)
    T.val= F.convert(T) # Get the real converted value.
    print(f'{F.rawstring} = {T.val}_{tunits}')
elif p.unitA: # Second arg unitB is missing so just breakdown unitA to SI units.
    unit_known= UnitVal(p.unitA)
    SIval= unit_known.convert(UnitVal("1_"+unit_known.ubase()))
    print(f"Unit in SI base: {SIval}_{unit_known.ubase()}")
else:
    parser.print_help()

