"""siunit - A module to support dimensioned arithmetic using the SI system.

Dimensioned numbers are instances of siunit.Dn().  Arithmetic between Dn's
with incompatible units raises TypeError.  Arithmetic between Dn's with
compatible units produces a result with appropriate units.  For example, ::

  >>> m=Dn('3kg')
  >>> a=Dn('2m/s^2')
  >>> f=m*a
  >>> print(f)
  6.0N
"""

# MIT License

# Copyright (c) 2019 <NAME>

# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:

# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.

# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

from numbers import Number
from math import sqrt as math_sqrt
import re
try:
    from numpy import array
except ImportError:
    pass


class Unit:
    """Base class for base unit and derived unit definitions.  Not directly
    instantiated.

    :param name: Full name of the unit.
    :param abbreviation: The official unit abbreviation.  Used for display
        and parsing.
    :param quantifies: A string (no white space allowed) that describes the
        quantity measured by the unit.
    """

    of = {}
    """Dictionary of all defined units.  Each Unit instance will be inserted
    under three keys: name, abbreviation, and quantifies."""

    _all = set()
    """Set of all defined units."""

    _measureable = dict((quantifies, unit_index)
        for (unit_index, quantifies) in enumerate(
            ['length', 'mass', 'time', 'electric_current', 'luminousity',
                'temperature', 'amount_of_substance', 'angle']))
    """Defines the entities quantified by BaseUnits. Also defines the order
    in which they appear in Dimension() exponent vectors."""

    def __init__(self, name, abbreviation, quantifies, display_order):
        self.name = name
        self.abbreviation = abbreviation
        self.quantifies = quantifies
        self.display_order = display_order
        self.of[self._irredundant(name)] = self
        self.of[self._irredundant(abbreviation)] = self
        self.of[self._irredundant(quantifies)] = self
        self._all.add(self)

    def _irredundant(self, ident):
        """Validates identifier as previously unused."""
        if ident in self.of:
            raise ValueError(' '.join([ident, 'already defined.']))
        else:
            return ident

    def __repr__(self):
        return ''.join(
            [self.__class__.__name__, '(', self.reprvals(), ')'])


class BaseUnit(Unit):
    """Defines an SI base unit.

    :param name: Full name of the unit.
    :param abbreviation: The official unit abbreviation.  Used for display
        and parsing.
    :param quantifies: A string (no white space allowed) that describes the
        quantity measured by the unit.
    :param display_order: A integer that controls the print-out order
        for this unit for the __str__ method..  Lower display_order units
        print first.
    """
    named = {}
    index_order = [None] * len(Unit._measureable)

    def __init__(self, name, abbreviation, quantifies, display_order):
        unit_index = self._measureable[quantifies]
        super().__init__(name, abbreviation, quantifies, display_order)
        self.unit_index = unit_index
        self.named[name] = self
        self.index_order[Unit._measureable[self.quantifies]] = self
        self._dimension = Dimension(**{self.name:1})

    def reprvals(self):
        return ', '.join([repr(x) for x in [self.name, self.abbreviation,
            self.quantifies, self.display_order]])

    @property
    def dimension(self):
        """Returns an instance of Dimension() representing this unit."""
        return self._dimension

    def gloss(self):
        """Glossary text for this unit."""
        return [self.name, self.abbreviation, self.quantifies, 'SI base unit']


class DerivedUnit(Unit):
    """Defines an SI derived unit.

    :param name: Full name of the unit.
    :param abbreviation: The official unit abbreviation.  Used for display
        and parsing.
    :param quantifies: A string (no white space allowed) that describes the
        quantity measured by the unit.
    :param display_order: A integer that controls the print-out order
        for this unit.  Lower display_order units print first.
    :param dimension: An instance of Dimension() that defines the derivation
        of this unit.
    """

    with_basis = {}
    """Dictionary of all DerivedUnit instances, indexed by a Dimension
    vector in tuple() form."""

    _factor_order = []
    """List of DerivedUnit instances in the prefered order for factoring
    out from complex dimension exponent vectors."""

    def __init__(self, name, abbreviation, quantifies, display_order,
            dimension):
        super().__init__(name, abbreviation, quantifies, display_order)
        self.dimension = Dimension(dimension)
        self.with_basis[tuple(self.dimension._exponents)] = self

    def reprvals(self):
        return ', '.join([repr(x) for x in [self.name, self.abbreviation,
            self.quantifies, self.display_order, self.dimension]])

    @classmethod
    def factor_order(cls):
        return cls._factor_order

    @classmethod
    def set_factor_order(cls, derived_units):
        """Sets the order for factoring out instances of DerivedUnit
        from a complex Dimension exponent vector.

        :param derived_units: A list of DerivedUnit instances, in order
            of decreasing preference.
        """
        l = []
        for du in derived_units:
            if du in l:
                raise ValueError(' '.join([du.name,
                    'duplicated in factor_order.']))
            if not isinstance(du, cls):
                raise ValueError(' '.join([repr(du),
                    'is not an instance of',
                    cls.__name__]))
            l.append(du)
        cls._factor_order = l

    def occurs_in(self, other_exponents):
        """Count the numer of times this dimension occurs in some unit
        exponent vector.

        :param other_exponents: An exponent vector.
        :returns: A count of the number of occurances.
        """
        # This is not smart enough to tease appart units where
        # one derived unit contributes a positive exponent and
        # another contributes a negative exponent.
        occurances = []
        for self_exp, other_exp in zip(
                self.dimension._exponents, other_exponents):
            if self_exp == 0:
                continue  # Not relevant.
            elif self_exp * other_exp < 0:
                return 0  # Different signs.
            elif abs(self_exp) <= abs(other_exp):
                occurances.append(other_exp // self_exp)
            else:
                return 0  # Too many.
        return min(occurances)

    def extract_from(self, exponents, count=None):
        """Extracts *count* occurances of *self's* exponent vector from
        the vector *exponents*.

        :param exponents: An exponent vector to be reduced.
        :param count: Optional.  Number of occurances to extract. Default=1.
        :returns: An updated (reduced) exponent vector.
        """
        count = 1 if count is None else int(count)
        return [y - (count * x)
            for x, y in zip(self.dimension._exponents, exponents)]

    @classmethod
    def factor(cls, exponents):
        """Factors a list of exponents into list of (dimension, count) tuples.
        This prepares an exponent list for display in maximally-factored
        form.

        :param exponents: An exponent vector.
        :returns: List of tuples of the form: (Dimension, exponent)
        """
        factored = []
        residue = exponents
        for derived_unit in cls.factor_order():
            occurances = derived_unit.occurs_in(residue)
            if occurances:
                #print ('du:', derived_unit.name)
                factored.append((derived_unit, occurances))
                #print('r before:', residue)
                residue = derived_unit.extract_from(residue, occurances)
                #print('r after:', residue)
        dims = [(d, residue[d.unit_index]) for d in BaseUnit.index_order
            if residue[d.unit_index] != 0]
        factored.extend(dims)
        return factored

    def gloss(self):
        """Glossary text for this unit."""
        return [self.name, self.abbreviation, self.quantifies,
            self.dimension.basis()]


class Dimension:
    """The computed dimensions of a number.  The dimensions are carried
    internally as a vector of exponents in a canonical order.  The vector
    contains one integer per measureable, representing the exponent on a
    unit, in the order defined by Unit._measureable.
    For example: ::

      m*kg is  [1, 1,  0, 0, 0, 0, 0, 0]
      m^2  is  [2, 0,  0, 0, 0, 0, 0, 0]
      m/s^2 is [1, 0, -2, 0, 0, 0, 0, 0]

    :param unit_spec: Can be one of:

        - Dimension() instance, which constructs a copy.
        - A string, which will be parsed, potentially raising an error.
        - An interable of integer-ish things, which will be interpreted as an
          exponent vector.
        - Omitted.

    :param kwargs: Any BaseUnit.name can be a kwarg parameter.  If unit_spec
        is omitted, a Dimension is constructed from kwargs.  If neither
        unit_spec nor any kwargs are supplied, a "dimensionless" Dimension()
        is constructed.
    """
    # Note: _parser_re is created on first attempt to parse a unit string.
    # If any new units are added after the regular expression is cached,
    # the cached pattern will be invalid. YAGN invalidation?
    _parser_re = None
    "Cached re match pattern use to parse unit strings."

    _preferred = dict()
    """Dictionary of display strings keyed by exponent vector.
    Used to trap out preferred, ie: "natural" display strings for
    the __str__ function.
    """

    def __init__(self, unit_spec=None, **kwargs):
        # If unit_spec is an instance of Dimension, construct copy.
        if isinstance(unit_spec, Dimension):
            self._exponents = unit_spec._exponents
            return
        # If unit_spec is a string, then try to parse it.
        if isinstance(unit_spec, str):
            self._exponents = self.parse(unit_spec)._exponents
            return
        # If unit_spec is not none, expect  a list of integers.
        if unit_spec is not None:
            if len(unit_spec) != len(Unit._measureable):
                raise ValueError(' '.join(['Length of exponent list is',
                    str(len(unit_spec)),
                        'but', str(len(Unit._measureable)), 'required.']))
            else:
                self._exponents = [int(x) for x in unit_spec]
            return
        # Create an empty exponent vector to fill with keywords params, or if
        # there are none, it will default to being a dimensionless quantity.
        self._exponents = [0] * len(Unit._measureable)
        for unit in kwargs:
            try:
                u = BaseUnit.named[unit]
            except IndexError:
                raise ValueError(' '.join([unit, 'is not a BaseUnit name.']))
            else:
                self._exponents[u.unit_index] = int(kwargs[unit])

    def __repr__(self):
        params = ', '.join(['='.join((nm,str(val)))
            for nm, val in [(u.name, self._exponents[u.unit_index])
                for u in BaseUnit.index_order] if val != 0])
        return ''.join([self.__class__.__name__, '(', params, ')'])

    @classmethod
    def prefer(cls, s, delete=None):
        """Add/update a preferred display string.

        :param s: A unit string.  The exponent vector that it represents
            will always be display as *s*.
        :param delete: Optional.  If truthy, *s* is deleted from the
            preferences dictionary.
        """
        dim = cls.parse(s)  # May raise ValueError.  Let caller handle.
        v = tuple(dim._exponents)
        if bool(delete):
            del cls._preferred[v]
        else:
            cls._preferred[v] = s

    def __str__(self):
        # See if there is a preferred display override.
        u = self._preferred.get(tuple(self._exponents), None)
        if u:
            return u
        # Look for an exact match to a derived unit for an easy win.
        u = DerivedUnit.with_basis.get(tuple(self._exponents), None)
        if u:
            return u.abbreviation
        # Convert to list of (Dimension, count) tuples, factoring out derived
        # units.
        dims = DerivedUnit.factor(self._exponents)
        # Prep for display.
        numerator = sorted([(d, v) for d, v in dims if v > 0],
            key=lambda tup: tup[0].display_order)
        numerator = ''.join([
            '^'.join([d.abbreviation, str(v)]) if v > 1 else d.abbreviation
                for d, v in numerator])
        denominator = sorted([(d, v) for d, v in dims if v < 0],
            key=lambda tup: tup[0].display_order)
        denominator = ''.join([
            '^'.join([d.abbreviation, str(abs(v))]) if v < -1 else d.abbreviation
                for d, v in denominator])
        if numerator and denominator:
            return '/'.join([numerator, denominator])
        elif denominator:
            return '/'.join(['1', denominator])
        else:
            return numerator

    def basis(self):
        """Return a human-readabble description of the basis for
        this dimension.
        """
        bases = [(d, self._exponents[d.unit_index])
            for d in BaseUnit.index_order
                if self._exponents[d.unit_index] != 0]
        return ' '.join(
            [''.join([d.name, '^' + str(exp) if exp != 1 else ''])
                for d, exp in bases if exp != 0])

    @classmethod
    def parse(cls, s):
        """Parse *s* as unit abbreviations and exponents, potentially in
        simple fraction notation.

        :param s: Unit specification as a string. No white space.
        :returns: Dimension, or raises ValueError.
        """
        # This parser is too ugly to live long.  But it seems to work.  The
        # better strategy is probably to replace this kludgery with a proper
        # recursive-descent parser.  But, for now.... meh.
        patt = cls._parser_pattern()
        # See if this is in fraction notation.
        f = s.split('/')
        if len(f) > 2:
            raise ValueError('Only simple fractions of units accepted.')
        numerator_str = f.pop(0)
        denominator_str = f.pop(0) if f else None
        numl = []
        while numerator_str:
            m = patt.match(numerator_str)
            if not m:
                raise ValueError(' '.join(['Syntax error in:', numerator_str]))
            numl.append(m.group(0))
            numerator_str = numerator_str[m.end(0):]
        numl2 = []
        while numl:
            d = numl.pop(0)
            if numl and numl[0][0] == '^':
                n = numl.pop(0)
                n = int(n[1:])
            else:
                n = 1
            numl2.append((d, n))
        # Start construction with a dimensionless Dimension()
        rslt = Dimension()
        for abbr, exp in numl2:
            d = Unit.of[abbr].dimension
            if exp < 0:
                d = Dimension()/d
                exp = -exp
            d = pow(d, exp)
            rslt *= d
        if denominator_str:
            denom = cls.parse(denominator_str)
            rslt = rslt / denom
        return rslt

    @classmethod
    def _parser_pattern(cls):
        "Construct the re pattern from abbreviations for defined units."
        if cls._parser_re is None:
            # re needs to have longest match strings first, so sort
            # abbreviations by decreasing length.
            tokens = sorted(set([u.abbreviation for u in Unit.of.values()]),
                key=lambda s:len(s), reverse=True)
            # In addition to unit abbreviations, we need to look for unit
            # exponents of the form:
            # up-caret, optional minus-sign, numbers,
            tokens.append(r'\^-?\d+')  # *** NO CARRIER
            pattern = '|'.join(tokens)
            cls._parser_re = re.compile(''.join(['(', pattern, ')']))
        return cls._parser_re

    # adding/subtracting mismatched units is invalid.
    def __add__(self, other):
        if self == other:
            return self
        raise TypeError('Adding mismatched units.')

    def __sub__(self, other):
        if self == other:
            return self
        raise TypeError('Subtracting mismatched units.')

    # multiply units by adding exponents.
    def __mul__(self, other):
        return self.__class__(
            [x + y for x, y in zip(self._exponents, other._exponents)])

    def __matmul__(self, other):
        return self.__mul__(other)

    def _div(self, other):
        return self.__class__(
            [x - y for x, y in zip(self._exponents, other._exponents)])

    def __truediv__(self, other):
        return self._div(other)

    def __floordiv__(self, other):
        return self._div(other)

    # Comparision operations only make sense for values of same units.
    # The __eq__ and __ne__ operators are defined so that they can be
    # used to check for equivalence of units.  The other comparison
    # operators raise.
    def __eq__(self, other):
        return min([self_exp == other_exp for self_exp, other_exp
                in zip(self._exponents, other._exponents)])

    def __ne__(self, other):
        return not self == other

    _invalid_comparison_msg = 'Dimensions can only be compared for equality.'
    def __lt__(self, other):
        raise TypeError(self._invalid_comparison_msg)

    def __gt__(self, other):
        raise TypeError(self._invalid_comparison_msg)

    def __le__(self, other):
        raise TypeError(self._invalid_comparison_msg)

    def __ge__(self, other):
        raise TypeError(self._invalid_comparison_msg)

    # For pow(), other must be a positive iteger.  To raise a Dimension
    # to a power, multiply the current exponent by other.
    def __pow__(self, other):
        o = int(other)
        if o < 0 or o != other:
            raise TypeError(
                'pow() only supported for positive integer exponentiation.')
        exp = [e * o for e in self._exponents]
        return self.__class__(exp)

    def __imul__(self, other):
        self._exponents = [x + y
            for x, y in zip(self._exponents, other._exponents)]
        return self

    def root(self, n=None):
        """Implement n-th root for dimensions.  All units must be evenly
        divisible by n.

        :param n: N-th root to take.  Default==2.
        """
        n = 2 if n is None else int(n)
        if n < 0:
            raise ValueError('Can only take positive roots of dimensions.')
        fail = max([e % n for e in self._exponents])
        if fail:
            raise ValueError(' '.join(['root(n) requires'
               'all dimension exponents to be divisble by n.']))
        return self.__class__([e // n for e in self._exponents])


# Define the base units for the SI system.
base_units = [
    BaseUnit('meter',    'm',   'length',              20),
    BaseUnit('kilogram', 'kg',  'mass',                10),
    BaseUnit('second',   's',   'time',                30),
    BaseUnit('ampere',   'A',   'electric_current',    40),
    BaseUnit('kelvin',   'K',   'temperature',         50),
    BaseUnit('mole',     'mol', 'amount_of_substance', 60),
    BaseUnit('candela',  'cd',  'luminousity',         70),
    BaseUnit('radian',   'rad', 'angle',               80),
]
"""The canonical list of SI base units."""

# Import-time self-check: Make sure that all base units are accounted for.
assert(len([x for x in BaseUnit.index_order if x is None]) == 0)


# Define some derived units for the SI system.
derived_units = [
    DerivedUnit('coulomb', 'C', 'charge', 90, Dimension(second=1, ampere=1)),
    DerivedUnit('hertz', 'Hz', 'frequency', 100, Dimension(second=-1)),
    DerivedUnit('newton', 'N', 'force', 110,
        Dimension(kilogram=1, meter=1, second=-2)),
    DerivedUnit('pascal', 'Pa', 'pressure', 130,
        Dimension(kilogram=1, meter=-1, second=-2)),
    DerivedUnit('joule', 'J', 'energy', 130,
        Dimension(kilogram=1, meter=2, second=-2)),
    DerivedUnit('watt', 'W', 'power', 140,
        Dimension(kilogram=1, meter=2, second=-3)),
    DerivedUnit('volt', 'V', 'electromotive_force', 150,
        Dimension(kilogram=1, meter=2, second=-3, ampere=-1)),
    DerivedUnit('farad', 'F', 'capacitance', 160,
        Dimension(kilogram=-1, meter=-2, second=4, ampere=2)),
    DerivedUnit('ohm', 'Ohm', 'resistance', 170,
        Dimension(kilogram=1, meter=2, second=-3, ampere=-2)),
    DerivedUnit('siemens', 'S', 'conductance', 180,
        Dimension(kilogram=-1, meter=-2, second=3, ampere=2)),
    DerivedUnit('weber', 'Wb', 'magnetic_flux', 190,
        Dimension(kilogram=1, meter=2, second=-2, ampere=-1)),
    DerivedUnit('tesla', 'T', 'magnetic_flux_density', 200,
        Dimension(kilogram=1, second=-2, ampere=-1)),
    DerivedUnit('henry', 'H', 'inductance', 210,
        Dimension(kilogram=1, meter=2, second=-2, ampere=-2)),
    DerivedUnit('lux', 'lx', 'illuminance', 220,
        Dimension(meter=-2, candela=1)),
    DerivedUnit('katal', 'kat', 'catalytic_activity', 230,
        Dimension(mole=1, second=-1)),
]
"""SI derived units."""

# FIXME: Design/implement a decent default factoring order.
# Set up the order to factor out derived units.
DerivedUnit.set_factor_order([
    Unit.of['W'],
    Unit.of['N'],
])

# FIXME: Implement a default set of preferences, as necessary.
# Set up display traps for things that come out strangely using the canonical
# conversion in __str__().
#Dimension.prefer('Ohm/m^2')



class Dn:
    """Dimensioned number.  The numeric parameter accepts any type, and passes
    through arithmetic operations.  This allows smooth operation with complex
    numbers, numpy arrays, etc.

    :param n: A number-ish thing .  Can be a numeric value, a Numpy array, or
        if a string an attempt will be made to parse it as a number with
        units.  If the string is *only* units, it constructs a dimensioned
        value of 1.
    :param units: Omitted when *n* is a string, as the units are taken from
        parsing the string.  Otherwise, if *n* is not a string and *units*
        is omitted, a dimensionless instance of Dimension() is used. Otherwise
        *units* will be passed to the Dimension() constructor.
    """
    _mismatched_units_message = 'Comparing values with different units.'
    _unsupported_op_message = 'operation unsupported for dimensioned numbers.'

    def __init__(self, n, units=None):
        if isinstance(n, str):
            n, units = self.parse(n)
        self.n = n
        # TODO: So.... do I want to eliminate the redunant copy constructor here?
        self.units = Dimension() if units is None else Dimension(units)

    def __repr__(self):
        params = ', '.join([repr(x) for x in [self.n, self.units]])
        return ''.join([self.__class__.__name__, '(', params, ')'])

    def __str__(self):
        return ''.join([str(x) for x in [self.n, self.units]])

    @classmethod
    def parse(cls, s):
        """Parse a string into tuple of number and Dimension instance. The
        string must parse completely, or else this will raise.  Parse makes
        some assumptions about type: If the string starts with a '(', the
        expression is passed to the complex() constructor.  If the string
        starts with a '[', the expression is passed to the numpy.array()
        constructor.  Otherwise, it is passed to the float() constructor.

        :param s: A string to be parsed.
        :returns: A tuple of (Number, Dimension)
        """
        if s[0] in '([':
            "split point is balancing close bracket"
            p, n = 1, 1
            while n:
                if s[p] in '([':
                    n += 1
                elif s[p] in ')]':
                    n -= 1
                p += 1
        else:
            "split point is first char not part of number."
            p = 0
            while s[p] in '+-0123456789.eE':
                p += 1
        num, dim = s[0:p], s[p:]
        if not len(num):
            number = 1
        elif num[0] == '[':
            number = array(num)
        elif num[0] == '(':
            number = complex(num)
        else:
            number = float(num)
        dimension = Dimension.parse(dim)
        return number, dimension

    def _maybe_promote(self, n):
        """If n is dimensionless numeric, turn it into a dimensionless Dn."""
        return self.__class__(n, Dimension()) if isinstance(n, Number) else n

    def __add__(self, other):
        other = self._maybe_promote(other)
        return self.__class__(self.n + other.n, self.units + other.units)

    def __sub__(self, other):
        other = self._maybe_promote(other)
        return self.__class__(self.n - other.n, self.units - other.units)

    def __mul__(self, other):
        other = self._maybe_promote(other)
        return self.__class__(self.n * other.n, self.units * other.units)

    def __matmul__(self, other):
        other = self._maybe_promote(other)
        return self.__class__(self.n @ other.n, self.units @ other.units)

    def __truediv__(self, other):
        other = self._maybe_promote(other)
        return self.__class__(self.n / other.n, self.units / other.units)

    def __floordiv__(self, other):
        other = self._maybe_promote(other)
        return self.__class__(self.n // other.n, self.units // other.units)

    def __mod__(self, other):
        raise TypeError(' '.join(['mod', self._unsupported_op_message]))

    def __divmod__(self, other):
        raise TypeError(' '.join(['divmod', self._unsupported_op_message]))

    def __pow__(self, other, modulo=None):
        if modulo is not None:
            raise TypeError(' '.join(['pow() with modulo',
                self._unsupported_op_message]))
        o = int(other)
        return self.__class__(self.n ** o, self.units ** o)

    # Comparisions only make sense for Dn()'s with same units.
    def __eq__(self, other):
        other = self._maybe_promote(other)
        if self.units == other.units:
            return self.n == other.n
        raise TypeError(self._mismatched_units_message)

    def __ne__(self, other):
        other = self._maybe_promote(other)
        if self.units == other.units:
            return self.n != other.n
        raise TypeError(self._mismatched_units_message)

    def __lt__(self, other):
        other = self._maybe_promote(other)
        if self.units == other.units:
            return self.n < other.n
        raise TypeError(self._mismatched_units_message)

    def __gt__(self, other):
        other = self._maybe_promote(other)
        if self.units == other.units:
            return self.n > other.n
        raise TypeError(self._mismatched_units_message)

    def __le__(self, other):
        other = self._maybe_promote(other)
        if self.units == other.units:
            return self.n <= other.n
        raise TypeError(self._mismatched_units_message)

    def __ge__(self, other):
        other = self._maybe_promote(other)
        if self.units == other.units:
            return self.n >= other.n
        raise TypeError(self._mismatched_units_message)

    def __radd__(self, other):
        other = self._maybe_promote(other)
        return self.__class__(other.n + self.n, other.units + self.units)

    def __rsub__(self, other):
        other = self._maybe_promote(other)
        return self.__class__(other.n - self.n, other.units - self.units)

    def __rmul__(self, other):
        other = self._maybe_promote(other)
        return self.__class__(other.n * self.n, other.units * self.units)

    def __rmatmul__(self, other):
        other = self._maybe_promote(other)
        return self.__class__(other.n @ self.n, other.units @ self.units)

    def __rtruediv__(self, other):
        other = self._maybe_promote(other)
        return self.__class__(other.n / self.n, other.units / self.units)

    def __rfloordiv__(self, other):
        other = self._maybe_promote(other)
        return self.__class__(other.n // self.n, other.units // self.units)

    def __neg__(self):
        return self.__class__(-self.n, self.units)

    def __pos__(self):
        # FIXME: Is it really necessary to call a copy constructor here???
        # Verify and simplify if possible.
        return self.__class__(self.n, self.units)

    def __abs__(self):
        return self.__class__(abs(self.n), self.units)

    def __iadd__(self, other):
        other = self._maybe_promote(other)
        self.units += other.units
        self.n += other.n
        return self

    def __isub__(self, other):
        other = self._maybe_promote(other)
        self.units -= other.units
        self.n -= other.n
        return self

    def __imul__(self, other):
        other = self._maybe_promote(other)
        self.units *= other.units
        self.n *= other.n
        return self

    def __imatmul__(self, other):
        other = self._maybe_promote(other)
        self.units @= other.units
        self.n @= other.n
        return self

    def __itruediv__(self, other):
        other = self._maybe_promote(other)
        self.units /= other.units
        self.n /= other.n
        return self

    def __ifloordiv__(self, other):
        other = self._maybe_promote(other)
        self.units //= other.units
        self.n //= other.n
        return self

    def sqrt(self):
        """Implement sqrt() for dimensioned numbers."""
        return self.__class__(math_sqrt(self.n), self.units.root())

    def root(self, n):
        """Implement n-th root for dimensioned numbers."""
        n = int(n)
        return self.__class__(pow(self.n, float(1.0/n)), self.units.root(n))

# Exported functions
def prefer(dimension_str):
    """Set an override string to display a complex dimension as
    explicitly specified.

    :param dimension_str: The dimension as a string.
    """
    Dimension.prefer(dimension_str)

def sqrt(dimensioned_number):
    """Take the square root of a Dn() instance. All of the
    dimension exponents must be divisible by 2.  math.sqrt()
    is called on the numeric part of the Dn().

    :param dimensioned_number: A Dn() instance.
    :returns: A Dn() instance.
    """
    return dimensioned_number.sqrt()

def root(dimensioned_number, n):
    """Take the n-th root of a Dn() instance.  All of the
    dimension exponents must be divisible by n.  math.pow() is
    called with an exponent of 1.0/float(n) on the numeric
    part of the dimensioned number.

    :param dimensioned_number: A Dn() instance.
    :param n: A positive integer.
    :returns: A Dn() instance.
    """
    return dimensioned_number.root(n)

def unit_of(s):
    """Find a unit for *s*, or raise.

    :param s: A unit name, unit abbrevation, or unit description.
    :returns: An instance of BaseUnit() or DerivedUnit().
    """
    return Unit.of[s]

def set_factor_order(derived_unit_list):
    """Set the prefered factoring order for attempting to find
    derived units withing a complex dimension.

    :param derived_unit_list: A list of derived unit names and/or
        abbreviations.
    """
    du = []
    for s in derived_unit_list:
        u = Unit.of[s]
        if not isinstance(u, DerivedUnit):
            raise ValueError(' '.join([s, 'is not a DerivedUnit.']))
        if u in du:
            raise ValueError(' '.join(s, 'duplicated.'))
        du.append(u)
    DerivedUnit.set_factor_order(du)

def unit_definitions():
    """Convenience function to get the set of all unit definitions.

    :returns: The set of unit definitions.
    """
    return Unit._all

__all__ = ['Dn', 'sqrt', 'root', 'unit_of', 'set_factor_order',
    'unit_definitions']
"""siunit - A module to support dimensioned arithmetic using the SI system.

Dimensioned numbers are instances of siunit.Dn().  Arithmetic between Dn's
with incompatible units raises TypeError.  Arithmetic between Dn's with
compatible units produces a result with appropriate units.  For example, ::

  >>> m=Dn('3kg')
  >>> a=Dn('2m/s^2')
  >>> f=m*a
  >>> print(f)
  6.0N
"""

# MIT License

# Copyright (c) 2019 <NAME>

# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:

# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.

# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

from numbers import Number
from math import sqrt as math_sqrt
import re
try:
    from numpy import array
except ImportError:
    pass


class Unit:
    """Base class for base unit and derived unit definitions.  Not directly
    instantiated.

    :param name: Full name of the unit.
    :param abbreviation: The official unit abbreviation.  Used for display
        and parsing.
    :param quantifies: A string (no white space allowed) that describes the
        quantity measured by the unit.
    """

    of = {}
    """Dictionary of all defined units.  Each Unit instance will be inserted
    under three keys: name, abbreviation, and quantifies."""

    _all = set()
    """Set of all defined units."""

    _measureable = dict((quantifies, unit_index)
        for (unit_index, quantifies) in enumerate(
            ['length', 'mass', 'time', 'electric_current', 'luminousity',
                'temperature', 'amount_of_substance', 'angle']))
    """Defines the entities quantified by BaseUnits. Also defines the order
    in which they appear in Dimension() exponent vectors."""

    def __init__(self, name, abbreviation, quantifies, display_order):
        self.name = name
        self.abbreviation = abbreviation
        self.quantifies = quantifies
        self.display_order = display_order
        self.of[self._irredundant(name)] = self
        self.of[self._irredundant(abbreviation)] = self
        self.of[self._irredundant(quantifies)] = self
        self._all.add(self)

    def _irredundant(self, ident):
        """Validates identifier as previously unused."""
        if ident in self.of:
            raise ValueError(' '.join([ident, 'already defined.']))
        else:
            return ident

    def __repr__(self):
        return ''.join(
            [self.__class__.__name__, '(', self.reprvals(), ')'])


class BaseUnit(Unit):
    """Defines an SI base unit.

    :param name: Full name of the unit.
    :param abbreviation: The official unit abbreviation.  Used for display
        and parsing.
    :param quantifies: A string (no white space allowed) that describes the
        quantity measured by the unit.
    :param display_order: A integer that controls the print-out order
        for this unit for the __str__ method..  Lower display_order units
        print first.
    """
    named = {}
    index_order = [None] * len(Unit._measureable)

    def __init__(self, name, abbreviation, quantifies, display_order):
        unit_index = self._measureable[quantifies]
        super().__init__(name, abbreviation, quantifies, display_order)
        self.unit_index = unit_index
        self.named[name] = self
        self.index_order[Unit._measureable[self.quantifies]] = self
        self._dimension = Dimension(**{self.name:1})

    def reprvals(self):
        return ', '.join([repr(x) for x in [self.name, self.abbreviation,
            self.quantifies, self.display_order]])

    @property
    def dimension(self):
        """Returns an instance of Dimension() representing this unit."""
        return self._dimension

    def gloss(self):
        """Glossary text for this unit."""
        return [self.name, self.abbreviation, self.quantifies, 'SI base unit']


class DerivedUnit(Unit):
    """Defines an SI derived unit.

    :param name: Full name of the unit.
    :param abbreviation: The official unit abbreviation.  Used for display
        and parsing.
    :param quantifies: A string (no white space allowed) that describes the
        quantity measured by the unit.
    :param display_order: A integer that controls the print-out order
        for this unit.  Lower display_order units print first.
    :param dimension: An instance of Dimension() that defines the derivation
        of this unit.
    """

    with_basis = {}
    """Dictionary of all DerivedUnit instances, indexed by a Dimension
    vector in tuple() form."""

    _factor_order = []
    """List of DerivedUnit instances in the prefered order for factoring
    out from complex dimension exponent vectors."""

    def __init__(self, name, abbreviation, quantifies, display_order,
            dimension):
        super().__init__(name, abbreviation, quantifies, display_order)
        self.dimension = Dimension(dimension)
        self.with_basis[tuple(self.dimension._exponents)] = self

    def reprvals(self):
        return ', '.join([repr(x) for x in [self.name, self.abbreviation,
            self.quantifies, self.display_order, self.dimension]])

    @classmethod
    def factor_order(cls):
        return cls._factor_order

    @classmethod
    def set_factor_order(cls, derived_units):
        """Sets the order for factoring out instances of DerivedUnit
        from a complex Dimension exponent vector.

        :param derived_units: A list of DerivedUnit instances, in order
            of decreasing preference.
        """
        l = []
        for du in derived_units:
            if du in l:
                raise ValueError(' '.join([du.name,
                    'duplicated in factor_order.']))
            if not isinstance(du, cls):
                raise ValueError(' '.join([repr(du),
                    'is not an instance of',
                    cls.__name__]))
            l.append(du)
        cls._factor_order = l

    def occurs_in(self, other_exponents):
        """Count the numer of times this dimension occurs in some unit
        exponent vector.

        :param other_exponents: An exponent vector.
        :returns: A count of the number of occurances.
        """
        # This is not smart enough to tease appart units where
        # one derived unit contributes a positive exponent and
        # another contributes a negative exponent.
        occurances = []
        for self_exp, other_exp in zip(
                self.dimension._exponents, other_exponents):
            if self_exp == 0:
                continue  # Not relevant.
            elif self_exp * other_exp < 0:
                return 0  # Different signs.
            elif abs(self_exp) <= abs(other_exp):
                occurances.append(other_exp // self_exp)
            else:
                return 0  # Too many.
        return min(occurances)

    def extract_from(self, exponents, count=None):
        """Extracts *count* occurances of *self's* exponent vector from
        the vector *exponents*.

        :param exponents: An exponent vector to be reduced.
        :param count: Optional.  Number of occurances to extract. Default=1.
        :returns: An updated (reduced) exponent vector.
        """
        count = 1 if count is None else int(count)
        return [y - (count * x)
            for x, y in zip(self.dimension._exponents, exponents)]

    @classmethod
    def factor(cls, exponents):
        """Factors a list of exponents into list of (dimension, count) tuples.
        This prepares an exponent list for display in maximally-factored
        form.

        :param exponents: An exponent vector.
        :returns: List of tuples of the form: (Dimension, exponent)
        """
        factored = []
        residue = exponents
        for derived_unit in cls.factor_order():
            occurances = derived_unit.occurs_in(residue)
            if occurances:
                #print ('du:', derived_unit.name)
                factored.append((derived_unit, occurances))
                #print('r before:', residue)
                residue = derived_unit.extract_from(residue, occurances)
                #print('r after:', residue)
        dims = [(d, residue[d.unit_index]) for d in BaseUnit.index_order
            if residue[d.unit_index] != 0]
        factored.extend(dims)
        return factored

    def gloss(self):
        """Glossary text for this unit."""
        return [self.name, self.abbreviation, self.quantifies,
            self.dimension.basis()]


class Dimension:
    """The computed dimensions of a number.  The dimensions are carried
    internally as a vector of exponents in a canonical order.  The vector
    contains one integer per measureable, representing the exponent on a
    unit, in the order defined by Unit._measureable.
    For example: ::

      m*kg is  [1, 1,  0, 0, 0, 0, 0, 0]
      m^2  is  [2, 0,  0, 0, 0, 0, 0, 0]
      m/s^2 is [1, 0, -2, 0, 0, 0, 0, 0]

    :param unit_spec: Can be one of:

        - Dimension() instance, which constructs a copy.
        - A string, which will be parsed, potentially raising an error.
        - An interable of integer-ish things, which will be interpreted as an
          exponent vector.
        - Omitted.

    :param kwargs: Any BaseUnit.name can be a kwarg parameter.  If unit_spec
        is omitted, a Dimension is constructed from kwargs.  If neither
        unit_spec nor any kwargs are supplied, a "dimensionless" Dimension()
        is constructed.
    """
    # Note: _parser_re is created on first attempt to parse a unit string.
    # If any new units are added after the regular expression is cached,
    # the cached pattern will be invalid. YAGN invalidation?
    _parser_re = None
    "Cached re match pattern use to parse unit strings."

    _preferred = dict()
    """Dictionary of display strings keyed by exponent vector.
    Used to trap out preferred, ie: "natural" display strings for
    the __str__ function.
    """

    def __init__(self, unit_spec=None, **kwargs):
        # If unit_spec is an instance of Dimension, construct copy.
        if isinstance(unit_spec, Dimension):
            self._exponents = unit_spec._exponents
            return
        # If unit_spec is a string, then try to parse it.
        if isinstance(unit_spec, str):
            self._exponents = self.parse(unit_spec)._exponents
            return
        # If unit_spec is not none, expect  a list of integers.
        if unit_spec is not None:
            if len(unit_spec) != len(Unit._measureable):
                raise ValueError(' '.join(['Length of exponent list is',
                    str(len(unit_spec)),
                        'but', str(len(Unit._measureable)), 'required.']))
            else:
                self._exponents = [int(x) for x in unit_spec]
            return
        # Create an empty exponent vector to fill with keywords params, or if
        # there are none, it will default to being a dimensionless quantity.
        self._exponents = [0] * len(Unit._measureable)
        for unit in kwargs:
            try:
                u = BaseUnit.named[unit]
            except IndexError:
                raise ValueError(' '.join([unit, 'is not a BaseUnit name.']))
            else:
                self._exponents[u.unit_index] = int(kwargs[unit])

    def __repr__(self):
        params = ', '.join(['='.join((nm,str(val)))
            for nm, val in [(u.name, self._exponents[u.unit_index])
                for u in BaseUnit.index_order] if val != 0])
        return ''.join([self.__class__.__name__, '(', params, ')'])

    @classmethod
    def prefer(cls, s, delete=None):
        """Add/update a preferred display string.

        :param s: A unit string.  The exponent vector that it represents
            will always be display as *s*.
        :param delete: Optional.  If truthy, *s* is deleted from the
            preferences dictionary.
        """
        dim = cls.parse(s)  # May raise ValueError.  Let caller handle.
        v = tuple(dim._exponents)
        if bool(delete):
            del cls._preferred[v]
        else:
            cls._preferred[v] = s

    def __str__(self):
        # See if there is a preferred display override.
        u = self._preferred.get(tuple(self._exponents), None)
        if u:
            return u
        # Look for an exact match to a derived unit for an easy win.
        u = DerivedUnit.with_basis.get(tuple(self._exponents), None)
        if u:
            return u.abbreviation
        # Convert to list of (Dimension, count) tuples, factoring out derived
        # units.
        dims = DerivedUnit.factor(self._exponents)
        # Prep for display.
        numerator = sorted([(d, v) for d, v in dims if v > 0],
            key=lambda tup: tup[0].display_order)
        numerator = ''.join([
            '^'.join([d.abbreviation, str(v)]) if v > 1 else d.abbreviation
                for d, v in numerator])
        denominator = sorted([(d, v) for d, v in dims if v < 0],
            key=lambda tup: tup[0].display_order)
        denominator = ''.join([
            '^'.join([d.abbreviation, str(abs(v))]) if v < -1 else d.abbreviation
                for d, v in denominator])
        if numerator and denominator:
            return '/'.join([numerator, denominator])
        elif denominator:
            return '/'.join(['1', denominator])
        else:
            return numerator

    def basis(self):
        """Return a human-readabble description of the basis for
        this dimension.
        """
        bases = [(d, self._exponents[d.unit_index])
            for d in BaseUnit.index_order
                if self._exponents[d.unit_index] != 0]
        return ' '.join(
            [''.join([d.name, '^' + str(exp) if exp != 1 else ''])
                for d, exp in bases if exp != 0])

    @classmethod
    def parse(cls, s):
        """Parse *s* as unit abbreviations and exponents, potentially in
        simple fraction notation.

        :param s: Unit specification as a string. No white space.
        :returns: Dimension, or raises ValueError.
        """
        # This parser is too ugly to live long.  But it seems to work.  The
        # better strategy is probably to replace this kludgery with a proper
        # recursive-descent parser.  But, for now.... meh.
        patt = cls._parser_pattern()
        # See if this is in fraction notation.
        f = s.split('/')
        if len(f) > 2:
            raise ValueError('Only simple fractions of units accepted.')
        numerator_str = f.pop(0)
        denominator_str = f.pop(0) if f else None
        numl = []
        while numerator_str:
            m = patt.match(numerator_str)
            if not m:
                raise ValueError(' '.join(['Syntax error in:', numerator_str]))
            numl.append(m.group(0))
            numerator_str = numerator_str[m.end(0):]
        numl2 = []
        while numl:
            d = numl.pop(0)
            if numl and numl[0][0] == '^':
                n = numl.pop(0)
                n = int(n[1:])
            else:
                n = 1
            numl2.append((d, n))
        # Start construction with a dimensionless Dimension()
        rslt = Dimension()
        for abbr, exp in numl2:
            d = Unit.of[abbr].dimension
            if exp < 0:
                d = Dimension()/d
                exp = -exp
            d = pow(d, exp)
            rslt *= d
        if denominator_str:
            denom = cls.parse(denominator_str)
            rslt = rslt / denom
        return rslt

    @classmethod
    def _parser_pattern(cls):
        "Construct the re pattern from abbreviations for defined units."
        if cls._parser_re is None:
            # re needs to have longest match strings first, so sort
            # abbreviations by decreasing length.
            tokens = sorted(set([u.abbreviation for u in Unit.of.values()]),
                key=lambda s:len(s), reverse=True)
            # In addition to unit abbreviations, we need to look for unit
            # exponents of the form:
            # up-caret, optional minus-sign, numbers,
            tokens.append(r'\^-?\d+')  # *** NO CARRIER
            pattern = '|'.join(tokens)
            cls._parser_re = re.compile(''.join(['(', pattern, ')']))
        return cls._parser_re

    # adding/subtracting mismatched units is invalid.
    def __add__(self, other):
        if self == other:
            return self
        raise TypeError('Adding mismatched units.')

    def __sub__(self, other):
        if self == other:
            return self
        raise TypeError('Subtracting mismatched units.')

    # multiply units by adding exponents.
    def __mul__(self, other):
        return self.__class__(
            [x + y for x, y in zip(self._exponents, other._exponents)])

    def __matmul__(self, other):
        return self.__mul__(other)

    def _div(self, other):
        return self.__class__(
            [x - y for x, y in zip(self._exponents, other._exponents)])

    def __truediv__(self, other):
        return self._div(other)

    def __floordiv__(self, other):
        return self._div(other)

    # Comparision operations only make sense for values of same units.
    # The __eq__ and __ne__ operators are defined so that they can be
    # used to check for equivalence of units.  The other comparison
    # operators raise.
    def __eq__(self, other):
        return min([self_exp == other_exp for self_exp, other_exp
                in zip(self._exponents, other._exponents)])

    def __ne__(self, other):
        return not self == other

    _invalid_comparison_msg = 'Dimensions can only be compared for equality.'
    def __lt__(self, other):
        raise TypeError(self._invalid_comparison_msg)

    def __gt__(self, other):
        raise TypeError(self._invalid_comparison_msg)

    def __le__(self, other):
        raise TypeError(self._invalid_comparison_msg)

    def __ge__(self, other):
        raise TypeError(self._invalid_comparison_msg)

    # For pow(), other must be a positive iteger.  To raise a Dimension
    # to a power, multiply the current exponent by other.
    def __pow__(self, other):
        o = int(other)
        if o < 0 or o != other:
            raise TypeError(
                'pow() only supported for positive integer exponentiation.')
        exp = [e * o for e in self._exponents]
        return self.__class__(exp)

    def __imul__(self, other):
        self._exponents = [x + y
            for x, y in zip(self._exponents, other._exponents)]
        return self

    def root(self, n=None):
        """Implement n-th root for dimensions.  All units must be evenly
        divisible by n.

        :param n: N-th root to take.  Default==2.
        """
        n = 2 if n is None else int(n)
        if n < 0:
            raise ValueError('Can only take positive roots of dimensions.')
        fail = max([e % n for e in self._exponents])
        if fail:
            raise ValueError(' '.join(['root(n) requires'
               'all dimension exponents to be divisble by n.']))
        return self.__class__([e // n for e in self._exponents])


# Define the base units for the SI system.
base_units = [
    BaseUnit('meter',    'm',   'length',              20),
    BaseUnit('kilogram', 'kg',  'mass',                10),
    BaseUnit('second',   's',   'time',                30),
    BaseUnit('ampere',   'A',   'electric_current',    40),
    BaseUnit('kelvin',   'K',   'temperature',         50),
    BaseUnit('mole',     'mol', 'amount_of_substance', 60),
    BaseUnit('candela',  'cd',  'luminousity',         70),
    BaseUnit('radian',   'rad', 'angle',               80),
]
"""The canonical list of SI base units."""

# Import-time self-check: Make sure that all base units are accounted for.
assert(len([x for x in BaseUnit.index_order if x is None]) == 0)


# Define some derived units for the SI system.
derived_units = [
    DerivedUnit('coulomb', 'C', 'charge', 90, Dimension(second=1, ampere=1)),
    DerivedUnit('hertz', 'Hz', 'frequency', 100, Dimension(second=-1)),
    DerivedUnit('newton', 'N', 'force', 110,
        Dimension(kilogram=1, meter=1, second=-2)),
    DerivedUnit('pascal', 'Pa', 'pressure', 130,
        Dimension(kilogram=1, meter=-1, second=-2)),
    DerivedUnit('joule', 'J', 'energy', 130,
        Dimension(kilogram=1, meter=2, second=-2)),
    DerivedUnit('watt', 'W', 'power', 140,
        Dimension(kilogram=1, meter=2, second=-3)),
    DerivedUnit('volt', 'V', 'electromotive_force', 150,
        Dimension(kilogram=1, meter=2, second=-3, ampere=-1)),
    DerivedUnit('farad', 'F', 'capacitance', 160,
        Dimension(kilogram=-1, meter=-2, second=4, ampere=2)),
    DerivedUnit('ohm', 'Ohm', 'resistance', 170,
        Dimension(kilogram=1, meter=2, second=-3, ampere=-2)),
    DerivedUnit('siemens', 'S', 'conductance', 180,
        Dimension(kilogram=-1, meter=-2, second=3, ampere=2)),
    DerivedUnit('weber', 'Wb', 'magnetic_flux', 190,
        Dimension(kilogram=1, meter=2, second=-2, ampere=-1)),
    DerivedUnit('tesla', 'T', 'magnetic_flux_density', 200,
        Dimension(kilogram=1, second=-2, ampere=-1)),
    DerivedUnit('henry', 'H', 'inductance', 210,
        Dimension(kilogram=1, meter=2, second=-2, ampere=-2)),
    DerivedUnit('lux', 'lx', 'illuminance', 220,
        Dimension(meter=-2, candela=1)),
    DerivedUnit('katal', 'kat', 'catalytic_activity', 230,
        Dimension(mole=1, second=-1)),
]
"""SI derived units."""

# FIXME: Design/implement a decent default factoring order.
# Set up the order to factor out derived units.
DerivedUnit.set_factor_order([
    Unit.of['W'],
    Unit.of['N'],
])

# FIXME: Implement a default set of preferences, as necessary.
# Set up display traps for things that come out strangely using the canonical
# conversion in __str__().
#Dimension.prefer('Ohm/m^2')



class Dn:
    """Dimensioned number.  The numeric parameter accepts any type, and passes
    through arithmetic operations.  This allows smooth operation with complex
    numbers, numpy arrays, etc.

    :param n: A number-ish thing .  Can be a numeric value, a Numpy array, or
        if a string an attempt will be made to parse it as a number with
        units.  If the string is *only* units, it constructs a dimensioned
        value of 1.
    :param units: Omitted when *n* is a string, as the units are taken from
        parsing the string.  Otherwise, if *n* is not a string and *units*
        is omitted, a dimensionless instance of Dimension() is used. Otherwise
        *units* will be passed to the Dimension() constructor.
    """
    _mismatched_units_message = 'Comparing values with different units.'
    _unsupported_op_message = 'operation unsupported for dimensioned numbers.'

    def __init__(self, n, units=None):
        if isinstance(n, str):
            n, units = self.parse(n)
        self.n = n
        # TODO: So.... do I want to eliminate the redunant copy constructor here?
        self.units = Dimension() if units is None else Dimension(units)

    def __repr__(self):
        params = ', '.join([repr(x) for x in [self.n, self.units]])
        return ''.join([self.__class__.__name__, '(', params, ')'])

    def __str__(self):
        return ''.join([str(x) for x in [self.n, self.units]])

    @classmethod
    def parse(cls, s):
        """Parse a string into tuple of number and Dimension instance. The
        string must parse completely, or else this will raise.  Parse makes
        some assumptions about type: If the string starts with a '(', the
        expression is passed to the complex() constructor.  If the string
        starts with a '[', the expression is passed to the numpy.array()
        constructor.  Otherwise, it is passed to the float() constructor.

        :param s: A string to be parsed.
        :returns: A tuple of (Number, Dimension)
        """
        if s[0] in '([':
            "split point is balancing close bracket"
            p, n = 1, 1
            while n:
                if s[p] in '([':
                    n += 1
                elif s[p] in ')]':
                    n -= 1
                p += 1
        else:
            "split point is first char not part of number."
            p = 0
            while s[p] in '+-0123456789.eE':
                p += 1
        num, dim = s[0:p], s[p:]
        if not len(num):
            number = 1
        elif num[0] == '[':
            number = array(num)
        elif num[0] == '(':
            number = complex(num)
        else:
            number = float(num)
        dimension = Dimension.parse(dim)
        return number, dimension

    def _maybe_promote(self, n):
        """If n is dimensionless numeric, turn it into a dimensionless Dn."""
        return self.__class__(n, Dimension()) if isinstance(n, Number) else n

    def __add__(self, other):
        other = self._maybe_promote(other)
        return self.__class__(self.n + other.n, self.units + other.units)

    def __sub__(self, other):
        other = self._maybe_promote(other)
        return self.__class__(self.n - other.n, self.units - other.units)

    def __mul__(self, other):
        other = self._maybe_promote(other)
        return self.__class__(self.n * other.n, self.units * other.units)

    def __matmul__(self, other):
        other = self._maybe_promote(other)
        return self.__class__(self.n @ other.n, self.units @ other.units)

    def __truediv__(self, other):
        other = self._maybe_promote(other)
        return self.__class__(self.n / other.n, self.units / other.units)

    def __floordiv__(self, other):
        other = self._maybe_promote(other)
        return self.__class__(self.n // other.n, self.units // other.units)

    def __mod__(self, other):
        raise TypeError(' '.join(['mod', self._unsupported_op_message]))

    def __divmod__(self, other):
        raise TypeError(' '.join(['divmod', self._unsupported_op_message]))

    def __pow__(self, other, modulo=None):
        if modulo is not None:
            raise TypeError(' '.join(['pow() with modulo',
                self._unsupported_op_message]))
        o = int(other)
        return self.__class__(self.n ** o, self.units ** o)

    # Comparisions only make sense for Dn()'s with same units.
    def __eq__(self, other):
        other = self._maybe_promote(other)
        if self.units == other.units:
            return self.n == other.n
        raise TypeError(self._mismatched_units_message)

    def __ne__(self, other):
        other = self._maybe_promote(other)
        if self.units == other.units:
            return self.n != other.n
        raise TypeError(self._mismatched_units_message)

    def __lt__(self, other):
        other = self._maybe_promote(other)
        if self.units == other.units:
            return self.n < other.n
        raise TypeError(self._mismatched_units_message)

    def __gt__(self, other):
        other = self._maybe_promote(other)
        if self.units == other.units:
            return self.n > other.n
        raise TypeError(self._mismatched_units_message)

    def __le__(self, other):
        other = self._maybe_promote(other)
        if self.units == other.units:
            return self.n <= other.n
        raise TypeError(self._mismatched_units_message)

    def __ge__(self, other):
        other = self._maybe_promote(other)
        if self.units == other.units:
            return self.n >= other.n
        raise TypeError(self._mismatched_units_message)

    def __radd__(self, other):
        other = self._maybe_promote(other)
        return self.__class__(other.n + self.n, other.units + self.units)

    def __rsub__(self, other):
        other = self._maybe_promote(other)
        return self.__class__(other.n - self.n, other.units - self.units)

    def __rmul__(self, other):
        other = self._maybe_promote(other)
        return self.__class__(other.n * self.n, other.units * self.units)

    def __rmatmul__(self, other):
        other = self._maybe_promote(other)
        return self.__class__(other.n @ self.n, other.units @ self.units)

    def __rtruediv__(self, other):
        other = self._maybe_promote(other)
        return self.__class__(other.n / self.n, other.units / self.units)

    def __rfloordiv__(self, other):
        other = self._maybe_promote(other)
        return self.__class__(other.n // self.n, other.units // self.units)

    def __neg__(self):
        return self.__class__(-self.n, self.units)

    def __pos__(self):
        # FIXME: Is it really necessary to call a copy constructor here???
        # Verify and simplify if possible.
        return self.__class__(self.n, self.units)

    def __abs__(self):
        return self.__class__(abs(self.n), self.units)

    def __iadd__(self, other):
        other = self._maybe_promote(other)
        self.units += other.units
        self.n += other.n
        return self

    def __isub__(self, other):
        other = self._maybe_promote(other)
        self.units -= other.units
        self.n -= other.n
        return self

    def __imul__(self, other):
        other = self._maybe_promote(other)
        self.units *= other.units
        self.n *= other.n
        return self

    def __imatmul__(self, other):
        other = self._maybe_promote(other)
        self.units @= other.units
        self.n @= other.n
        return self

    def __itruediv__(self, other):
        other = self._maybe_promote(other)
        self.units /= other.units
        self.n /= other.n
        return self

    def __ifloordiv__(self, other):
        other = self._maybe_promote(other)
        self.units //= other.units
        self.n //= other.n
        return self

    def sqrt(self):
        """Implement sqrt() for dimensioned numbers."""
        return self.__class__(math_sqrt(self.n), self.units.root())

    def root(self, n):
        """Implement n-th root for dimensioned numbers."""
        n = int(n)
        return self.__class__(pow(self.n, float(1.0/n)), self.units.root(n))

# Exported functions
def prefer(dimension_str):
    """Set an override string to display a complex dimension as
    explicitly specified.

    :param dimension_str: The dimension as a string.
    """
    Dimension.prefer(dimension_str)

def sqrt(dimensioned_number):
    """Take the square root of a Dn() instance. All of the
    dimension exponents must be divisible by 2.  math.sqrt()
    is called on the numeric part of the Dn().

    :param dimensioned_number: A Dn() instance.
    :returns: A Dn() instance.
    """
    return dimensioned_number.sqrt()

def root(dimensioned_number, n):
    """Take the n-th root of a Dn() instance.  All of the
    dimension exponents must be divisible by n.  math.pow() is
    called with an exponent of 1.0/float(n) on the numeric
    part of the dimensioned number.

    :param dimensioned_number: A Dn() instance.
    :param n: A positive integer.
    :returns: A Dn() instance.
    """
    return dimensioned_number.root(n)

def unit_of(s):
    """Find a unit for *s*, or raise.

    :param s: A unit name, unit abbrevation, or unit description.
    :returns: An instance of BaseUnit() or DerivedUnit().
    """
    return Unit.of[s]

def set_factor_order(derived_unit_list):
    """Set the prefered factoring order for attempting to find
    derived units withing a complex dimension.

    :param derived_unit_list: A list of derived unit names and/or
        abbreviations.
    """
    du = []
    for s in derived_unit_list:
        u = Unit.of[s]
        if not isinstance(u, DerivedUnit):
            raise ValueError(' '.join([s, 'is not a DerivedUnit.']))
        if u in du:
            raise ValueError(' '.join(s, 'duplicated.'))
        du.append(u)
    DerivedUnit.set_factor_order(du)

def unit_definitions():
    """Convenience function to get the set of all unit definitions.

    :returns: The set of unit definitions.
    """
    return Unit._all

__all__ = ['Dn', 'sqrt', 'root', 'unit_of', 'set_factor_order',
    'unit_definitions']
siunit - A module to support dimensioned arithmetic using the SI system.  Dimensioned numbers are instances of siunit.Dn().  Arithmetic between Dn's with incompatible units raises TypeError.  Arithmetic between Dn's with compatible units produces a result with appropriate units.  For example, ::    >>> m=Dn('3kg')   >>> a=Dn('2m/s^2')   >>> f=m*a   >>> print(f)   6.0N # MIT License # Copyright (c) 2019 <NAME> # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. Base class for base unit and derived unit definitions.  Not directly     instantiated.      :param name: Full name of the unit.     :param abbreviation: The official unit abbreviation.  Used for display         and parsing.     :param quantifies: A string (no white space allowed) that describes the         quantity measured by the unit. Dictionary of all defined units.  Each Unit instance will be inserted     under three keys: name, abbreviation, and quantifies. Set of all defined units. Defines the entities quantified by BaseUnits. Also defines the order     in which they appear in Dimension() exponent vectors. Validates identifier as previously unused. Defines an SI base unit.      :param name: Full name of the unit.     :param abbreviation: The official unit abbreviation.  Used for display         and parsing.     :param quantifies: A string (no white space allowed) that describes the         quantity measured by the unit.     :param display_order: A integer that controls the print-out order         for this unit for the __str__ method..  Lower display_order units         print first. Returns an instance of Dimension() representing this unit. Glossary text for this unit. Defines an SI derived unit.      :param name: Full name of the unit.     :param abbreviation: The official unit abbreviation.  Used for display         and parsing.     :param quantifies: A string (no white space allowed) that describes the         quantity measured by the unit.     :param display_order: A integer that controls the print-out order         for this unit.  Lower display_order units print first.     :param dimension: An instance of Dimension() that defines the derivation         of this unit. Dictionary of all DerivedUnit instances, indexed by a Dimension     vector in tuple() form. List of DerivedUnit instances in the prefered order for factoring     out from complex dimension exponent vectors. Sets the order for factoring out instances of DerivedUnit         from a complex Dimension exponent vector.          :param derived_units: A list of DerivedUnit instances, in order             of decreasing preference. Count the numer of times this dimension occurs in some unit         exponent vector.          :param other_exponents: An exponent vector.         :returns: A count of the number of occurances. # This is not smart enough to tease appart units where # one derived unit contributes a positive exponent and # another contributes a negative exponent. # Not relevant. # Different signs. # Too many. Extracts *count* occurances of *self's* exponent vector from         the vector *exponents*.          :param exponents: An exponent vector to be reduced.         :param count: Optional.  Number of occurances to extract. Default=1.         :returns: An updated (reduced) exponent vector. Factors a list of exponents into list of (dimension, count) tuples.         This prepares an exponent list for display in maximally-factored         form.          :param exponents: An exponent vector.         :returns: List of tuples of the form: (Dimension, exponent) #print ('du:', derived_unit.name) #print('r before:', residue) #print('r after:', residue) Glossary text for this unit. The computed dimensions of a number.  The dimensions are carried     internally as a vector of exponents in a canonical order.  The vector     contains one integer per measureable, representing the exponent on a     unit, in the order defined by Unit._measureable.     For example: ::        m*kg is  [1, 1,  0, 0, 0, 0, 0, 0]       m^2  is  [2, 0,  0, 0, 0, 0, 0, 0]       m/s^2 is [1, 0, -2, 0, 0, 0, 0, 0]      :param unit_spec: Can be one of:          - Dimension() instance, which constructs a copy.         - A string, which will be parsed, potentially raising an error.         - An interable of integer-ish things, which will be interpreted as an           exponent vector.         - Omitted.      :param kwargs: Any BaseUnit.name can be a kwarg parameter.  If unit_spec         is omitted, a Dimension is constructed from kwargs.  If neither         unit_spec nor any kwargs are supplied, a "dimensionless" Dimension()         is constructed. # Note: _parser_re is created on first attempt to parse a unit string. # If any new units are added after the regular expression is cached, # the cached pattern will be invalid. YAGN invalidation? Dictionary of display strings keyed by exponent vector.     Used to trap out preferred, ie: "natural" display strings for     the __str__ function. # If unit_spec is an instance of Dimension, construct copy. # If unit_spec is a string, then try to parse it. # If unit_spec is not none, expect  a list of integers. # Create an empty exponent vector to fill with keywords params, or if # there are none, it will default to being a dimensionless quantity. Add/update a preferred display string.          :param s: A unit string.  The exponent vector that it represents             will always be display as *s*.         :param delete: Optional.  If truthy, *s* is deleted from the             preferences dictionary. # May raise ValueError.  Let caller handle. # See if there is a preferred display override. # Look for an exact match to a derived unit for an easy win. # Convert to list of (Dimension, count) tuples, factoring out derived # units. # Prep for display. Return a human-readabble description of the basis for         this dimension. Parse *s* as unit abbreviations and exponents, potentially in         simple fraction notation.          :param s: Unit specification as a string. No white space.         :returns: Dimension, or raises ValueError. # This parser is too ugly to live long.  But it seems to work.  The # better strategy is probably to replace this kludgery with a proper # recursive-descent parser.  But, for now.... meh. # See if this is in fraction notation. # Start construction with a dimensionless Dimension() # re needs to have longest match strings first, so sort # abbreviations by decreasing length. # In addition to unit abbreviations, we need to look for unit # exponents of the form: # up-caret, optional minus-sign, numbers, # *** NO CARRIER # adding/subtracting mismatched units is invalid. # multiply units by adding exponents. # Comparision operations only make sense for values of same units. # The __eq__ and __ne__ operators are defined so that they can be # used to check for equivalence of units.  The other comparison # operators raise. # For pow(), other must be a positive iteger.  To raise a Dimension # to a power, multiply the current exponent by other. Implement n-th root for dimensions.  All units must be evenly         divisible by n.          :param n: N-th root to take.  Default==2. # Define the base units for the SI system. The canonical list of SI base units. # Import-time self-check: Make sure that all base units are accounted for. # Define some derived units for the SI system. SI derived units. # FIXME: Design/implement a decent default factoring order. # Set up the order to factor out derived units. # FIXME: Implement a default set of preferences, as necessary. # Set up display traps for things that come out strangely using the canonical # conversion in __str__(). #Dimension.prefer('Ohm/m^2') Dimensioned number.  The numeric parameter accepts any type, and passes     through arithmetic operations.  This allows smooth operation with complex     numbers, numpy arrays, etc.      :param n: A number-ish thing .  Can be a numeric value, a Numpy array, or         if a string an attempt will be made to parse it as a number with         units.  If the string is *only* units, it constructs a dimensioned         value of 1.     :param units: Omitted when *n* is a string, as the units are taken from         parsing the string.  Otherwise, if *n* is not a string and *units*         is omitted, a dimensionless instance of Dimension() is used. Otherwise         *units* will be passed to the Dimension() constructor. # TODO: So.... do I want to eliminate the redunant copy constructor here? Parse a string into tuple of number and Dimension instance. The         string must parse completely, or else this will raise.  Parse makes         some assumptions about type: If the string starts with a '(', the         expression is passed to the complex() constructor.  If the string         starts with a '[', the expression is passed to the numpy.array()         constructor.  Otherwise, it is passed to the float() constructor.          :param s: A string to be parsed.         :returns: A tuple of (Number, Dimension) If n is dimensionless numeric, turn it into a dimensionless Dn. # Comparisions only make sense for Dn()'s with same units. # FIXME: Is it really necessary to call a copy constructor here??? # Verify and simplify if possible. Implement sqrt() for dimensioned numbers. Implement n-th root for dimensioned numbers. # Exported functions Set an override string to display a complex dimension as     explicitly specified.      :param dimension_str: The dimension as a string. Take the square root of a Dn() instance. All of the     dimension exponents must be divisible by 2.  math.sqrt()     is called on the numeric part of the Dn().      :param dimensioned_number: A Dn() instance.     :returns: A Dn() instance. Take the n-th root of a Dn() instance.  All of the     dimension exponents must be divisible by n.  math.pow() is     called with an exponent of 1.0/float(n) on the numeric     part of the dimensioned number.      :param dimensioned_number: A Dn() instance.     :param n: A positive integer.     :returns: A Dn() instance. Find a unit for *s*, or raise.      :param s: A unit name, unit abbrevation, or unit description.     :returns: An instance of BaseUnit() or DerivedUnit(). Set the prefered factoring order for attempting to find     derived units withing a complex dimension.      :param derived_unit_list: A list of derived unit names and/or         abbreviations. Convenience function to get the set of all unit definitions.      :returns: The set of unit definitions.