docs: 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)