Parse a string containing a sexagesimal number.

    This can handle several types of delimiters and will process
    reasonably valid strings. See examples.

    Parameters
    ----------
    hmsdms : str
        String containing a sexagesimal number.

    Returns
    -------
    d : dict

        parts : a 3 element list of floats
            The three parts of the sexagesimal number that were
            identified.
        vals : 3 element list of floats
            The numerical values of the three parts of the sexagesimal
            number.
        sign : int
            Sign of the sexagesimal number; 1 for positive and -1 for
            negative.
        units : {"degrees", "hours"}
            The units of the sexagesimal number. This is infered from
            the characters present in the string. If it a pure number
            then units is "degrees".

    Examples
    --------
    >>> phmsdms("12") == {
    ... 'parts': [12.0, None, None],
    ... 'sign': 1,
    ... 'units': 'degrees',
    ... 'vals': [12.0, 0.0, 0.0]
    ... }
    True

    >>> phmsdms("12h") == {
    ... 'parts': [12.0, None, None],
    ... 'sign': 1,
    ... 'units': 'hours',
    ... 'vals': [12.0, 0.0, 0.0]
    ... }
    True

    >>> phmsdms("12d13m14.56") == {
    ... 'parts': [12.0, 13.0, 14.56],
    ... 'sign': 1,
    ... 'units': 'degrees',
    ... 'vals': [12.0, 13.0, 14.56]
    ... }
    True

    >>> phmsdms("12d13m14.56") == {
    ... 'parts': [12.0, 13.0, 14.56],
    ... 'sign': 1,
    ... 'units': 'degrees',
    ... 'vals': [12.0, 13.0, 14.56]
    ... }
    True

    >>> phmsdms("12d14.56ss") == {
    ... 'parts': [12.0, None, 14.56],
    ... 'sign': 1,
    ... 'units': 'degrees',
    ... 'vals': [12.0, 0.0, 14.56]
    ... }
    True

    >>> phmsdms("14.56ss") == {
    ... 'parts': [None, None, 14.56],
    ... 'sign': 1,
    ... 'units': 'degrees',
    ... 'vals': [0.0, 0.0, 14.56]
    ... }
    True

    >>> phmsdms("12h13m12.4s") == {
    ... 'parts': [12.0, 13.0, 12.4],
    ... 'sign': 1,
    ... 'units': 'hours',
    ... 'vals': [12.0, 13.0, 12.4]
    ... }
    True

    >>> phmsdms("12:13:12.4s") == {
    ... 'parts': [12.0, 13.0, 12.4],
    ... 'sign': 1,
    ... 'units': 'degrees',
    ...  'vals': [12.0, 13.0, 12.4]
    ... }
    True

    But `phmsdms("12:13mm:12.4s")` will not work.

def phmsdms(hmsdms):
    """Parse a string containing a sexagesimal number.

    This can handle several types of delimiters and will process
    reasonably valid strings. See examples.

    Parameters
    ----------
    hmsdms : str
        String containing a sexagesimal number.

    Returns
    -------
    d : dict

        parts : a 3 element list of floats
            The three parts of the sexagesimal number that were
            identified.
        vals : 3 element list of floats
            The numerical values of the three parts of the sexagesimal
            number.
        sign : int
            Sign of the sexagesimal number; 1 for positive and -1 for
            negative.
        units : {"degrees", "hours"}
            The units of the sexagesimal number. This is infered from
            the characters present in the string. If it a pure number
            then units is "degrees".

    Examples
    --------
    >>> phmsdms("12") == {
    ... 'parts': [12.0, None, None],
    ... 'sign': 1,
    ... 'units': 'degrees',
    ... 'vals': [12.0, 0.0, 0.0]
    ... }
    True

    >>> phmsdms("12h") == {
    ... 'parts': [12.0, None, None],
    ... 'sign': 1,
    ... 'units': 'hours',
    ... 'vals': [12.0, 0.0, 0.0]
    ... }
    True

    >>> phmsdms("12d13m14.56") == {
    ... 'parts': [12.0, 13.0, 14.56],
    ... 'sign': 1,
    ... 'units': 'degrees',
    ... 'vals': [12.0, 13.0, 14.56]
    ... }
    True

    >>> phmsdms("12d13m14.56") == {
    ... 'parts': [12.0, 13.0, 14.56],
    ... 'sign': 1,
    ... 'units': 'degrees',
    ... 'vals': [12.0, 13.0, 14.56]
    ... }
    True

    >>> phmsdms("12d14.56ss") == {
    ... 'parts': [12.0, None, 14.56],
    ... 'sign': 1,
    ... 'units': 'degrees',
    ... 'vals': [12.0, 0.0, 14.56]
    ... }
    True

    >>> phmsdms("14.56ss") == {
    ... 'parts': [None, None, 14.56],
    ... 'sign': 1,
    ... 'units': 'degrees',
    ... 'vals': [0.0, 0.0, 14.56]
    ... }
    True

    >>> phmsdms("12h13m12.4s") == {
    ... 'parts': [12.0, 13.0, 12.4],
    ... 'sign': 1,
    ... 'units': 'hours',
    ... 'vals': [12.0, 13.0, 12.4]
    ... }
    True

    >>> phmsdms("12:13:12.4s") == {
    ... 'parts': [12.0, 13.0, 12.4],
    ... 'sign': 1,
    ... 'units': 'degrees',
    ...  'vals': [12.0, 13.0, 12.4]
    ... }
    True

    But `phmsdms("12:13mm:12.4s")` will not work.

    """
    units = None
    sign = None
    # Floating point regex:
    # http://www.regular-expressions.info/floatingpoint.html
    #
    # pattern1: find a decimal number (int or float) and any
    # characters following it upto the next decimal number.  [^0-9\-+]*
    # => keep gathering elements until we get to a digit, a - or a
    # +. These three indicates the possible start of the next number.
    pattern1 = re.compile(r"([-+]?[0-9]*\.?[0-9]+[^0-9\-+]*)")

    # pattern2: find decimal number (int or float) in string.
    pattern2 = re.compile(r"([-+]?[0-9]*\.?[0-9]+)")

    hmsdms = hmsdms.lower()
    hdlist = pattern1.findall(hmsdms)

    parts = [None, None, None]

    def _fill_right_not_none():
        # Find the pos. where parts is not None. Next value must
        # be inserted to the right of this. If this is 2 then we have
        # already filled seconds part, raise exception. If this is 1
        # then fill 2. If this is 0 fill 1. If none of these then fill
        # 0.
        rp = reversed(parts)
        for i, j in enumerate(rp):
            if j is not None:
                break
        if i == 0:
            # Seconds part already filled.
            raise ValueError("Invalid string.")
        elif i == 1:
            parts[2] = v
        elif i == 2:
            # Either parts[0] is None so fill it, or it is filled
            # and hence fill parts[1].
            if parts[0] is None:
                parts[0] = v
            else:
                parts[1] = v

    for valun in hdlist:
        try:
            # See if this is pure number.
            v = float(valun)
            # Sexagesimal part cannot be determined. So guess it by
            # seeing which all parts have already been identified.
            _fill_right_not_none()
        except ValueError:
            # Not a pure number. Infer sexagesimal part from the
            # suffix.
            if "hh" in valun or "h" in valun:
                m = pattern2.search(valun)
                parts[0] = float(valun[m.start():m.end()])
                units = "hours"
            if "dd" in valun or "d" in valun:
                m = pattern2.search(valun)
                parts[0] = float(valun[m.start():m.end()])
                units = "degrees"
            if "mm" in valun or "m" in valun:
                m = pattern2.search(valun)
                parts[1] = float(valun[m.start():m.end()])
            if "ss" in valun or "s" in valun:
                m = pattern2.search(valun)
                parts[2] = float(valun[m.start():m.end()])
            if "'" in valun:
                m = pattern2.search(valun)
                parts[1] = float(valun[m.start():m.end()])
            if '"' in valun:
                m = pattern2.search(valun)
                parts[2] = float(valun[m.start():m.end()])
            if ":" in valun:
                # Sexagesimal part cannot be determined. So guess it by
                # seeing which all parts have already been identified.
                v = valun.replace(":", "")
                v = float(v)
                _fill_right_not_none()
        if not units:
            units = "degrees"

    # Find sign. Only the first identified part can have a -ve sign.
    for i in parts:
        if i and i < 0.0:
            if sign is None:
                sign = -1
            else:
                raise ValueError("Only one number can be negative.")

    if sign is None:  # None of these are negative.
        sign = 1

    vals = [abs(i) if i is not None else 0.0 for i in parts]

    return dict(sign=sign, units=units, vals=vals, parts=parts)