Returns the sexagesimal representation of a decimal number.

    Parameters
    ----------
    deci : float
        Decimal number to be converted into sexagesimal. If `lower` and
        `upper` are given then the number is normalized to the given
        range before converting to sexagesimal.
    pre : int
        The last part of the sexagesimal number is rounded to these
        many decimal places. This can be negative. Default is 3.
    trunc : bool
        If True then the last part of the sexagesimal number is
        truncated and not rounded to `pre` decimal places. Default is
        False.
    lower : int
        Lower bound of range to which number is to be normalized.
    upper : int
        Upper bound of range to which number is to be normalized.
    b : bool
        Affects type of normalization. See docstring for `normalize`.
    upper_trim : bool
        If `lower` and `upper` are given and this is True, then if the
        first part of the sexagesimal number is equal to `upper`, it is
        replaced with `lower` (value used is int(lower)). This converts numbers
        such as "24 00 00.000" to "00 00 00.000". Default value is False.

    Returns
    -------
    s : 4 element tuple; (int, int, int, float)
        A tuple of sign and the three parts of the sexagesimal
        number. Sign is 1 for positive and -1 for negative values. The
        sign applies to the whole angle and not to any single part,
        i.e., all parts are positive and the sign multiplies the
        angle. The first and second parts of the sexagesimal number are
        integers and the last part is a float.

    Notes
    -----
    The given decimal number `deci` is converted into a sexagesimal
    number. The last part of the sexagesimal number is rounded to `pre`
    number of decimal points. If `trunc == True` then instead of
    rounding, the last part is truncated.

    If `lower` and `upper` are given then the number is normalized to
    the given range before converting into sexagesimal format. The `b`
    argument determines the type of normalization. See docstring of the
    `normalize` function for details.

    If `upper_trim` is True then, if after convertion to sexagesimal
    the first part is equal to `upper`, it is replaced with `lower` (value used
    is int(lower)). This is useful in cases where numbers such as "24 00 00.00"
    needs to be converted into "00 00 00.00"

    The returned sign, first element of tuple, applies to the whole
    number and not just to a single part.

    Examples
    --------
    >>> deci2sexa(-11.2345678)
    (-1, 11, 14, 4.444)
    >>> deci2sexa(-11.2345678, pre=5)
    (-1, 11, 14, 4.44408)
    >>> deci2sexa(-11.2345678, pre=4)
    (-1, 11, 14, 4.4441)
    >>> deci2sexa(-11.2345678, pre=4, trunc=True)
    (-1, 11, 14, 4.444)

    >>> deci2sexa(-11.2345678, pre=1)
    (-1, 11, 14, 4.4)
    >>> deci2sexa(-11.2345678, pre=0)
    (-1, 11, 14, 4.0)
    >>> deci2sexa(-11.2345678, pre=-1)
    (-1, 11, 14, 0.0)

    >>> x = 23+59/60.0+59.99999/3600.0

    To 3 decimal places, this number is 24 or 0 hours.

    >>> deci2sexa(x, pre=3, lower=0, upper=24, upper_trim=True)
    (1, 0, 0, 0.0)
    >>> deci2sexa(x, pre=3, lower=0, upper=24, upper_trim=False)
    (1, 24, 0, 0.0)

    To 5 decimal places, we get back the full value.

    >>> deci2sexa(x, pre=5, lower=0, upper=24, upper_trim=True)
    (1, 23, 59, 59.99999)

def deci2sexa(deci, pre=3, trunc=False, lower=None, upper=None,
              b=False, upper_trim=False):
    """Returns the sexagesimal representation of a decimal number.

    Parameters
    ----------
    deci : float
        Decimal number to be converted into sexagesimal. If `lower` and
        `upper` are given then the number is normalized to the given
        range before converting to sexagesimal.
    pre : int
        The last part of the sexagesimal number is rounded to these
        many decimal places. This can be negative. Default is 3.
    trunc : bool
        If True then the last part of the sexagesimal number is
        truncated and not rounded to `pre` decimal places. Default is
        False.
    lower : int
        Lower bound of range to which number is to be normalized.
    upper : int
        Upper bound of range to which number is to be normalized.
    b : bool
        Affects type of normalization. See docstring for `normalize`.
    upper_trim : bool
        If `lower` and `upper` are given and this is True, then if the
        first part of the sexagesimal number is equal to `upper`, it is
        replaced with `lower` (value used is int(lower)). This converts numbers
        such as "24 00 00.000" to "00 00 00.000". Default value is False.

    Returns
    -------
    s : 4 element tuple; (int, int, int, float)
        A tuple of sign and the three parts of the sexagesimal
        number. Sign is 1 for positive and -1 for negative values. The
        sign applies to the whole angle and not to any single part,
        i.e., all parts are positive and the sign multiplies the
        angle. The first and second parts of the sexagesimal number are
        integers and the last part is a float.

    Notes
    -----
    The given decimal number `deci` is converted into a sexagesimal
    number. The last part of the sexagesimal number is rounded to `pre`
    number of decimal points. If `trunc == True` then instead of
    rounding, the last part is truncated.

    If `lower` and `upper` are given then the number is normalized to
    the given range before converting into sexagesimal format. The `b`
    argument determines the type of normalization. See docstring of the
    `normalize` function for details.

    If `upper_trim` is True then, if after convertion to sexagesimal
    the first part is equal to `upper`, it is replaced with `lower` (value used
    is int(lower)). This is useful in cases where numbers such as "24 00 00.00"
    needs to be converted into "00 00 00.00"

    The returned sign, first element of tuple, applies to the whole
    number and not just to a single part.

    Examples
    --------
    >>> deci2sexa(-11.2345678)
    (-1, 11, 14, 4.444)
    >>> deci2sexa(-11.2345678, pre=5)
    (-1, 11, 14, 4.44408)
    >>> deci2sexa(-11.2345678, pre=4)
    (-1, 11, 14, 4.4441)
    >>> deci2sexa(-11.2345678, pre=4, trunc=True)
    (-1, 11, 14, 4.444)

    >>> deci2sexa(-11.2345678, pre=1)
    (-1, 11, 14, 4.4)
    >>> deci2sexa(-11.2345678, pre=0)
    (-1, 11, 14, 4.0)
    >>> deci2sexa(-11.2345678, pre=-1)
    (-1, 11, 14, 0.0)

    >>> x = 23+59/60.0+59.99999/3600.0

    To 3 decimal places, this number is 24 or 0 hours.

    >>> deci2sexa(x, pre=3, lower=0, upper=24, upper_trim=True)
    (1, 0, 0, 0.0)
    >>> deci2sexa(x, pre=3, lower=0, upper=24, upper_trim=False)
    (1, 24, 0, 0.0)

    To 5 decimal places, we get back the full value.

    >>> deci2sexa(x, pre=5, lower=0, upper=24, upper_trim=True)
    (1, 23, 59, 59.99999)

    """
    if lower is not None and upper is not None:
        deci = normalize(deci, lower=lower, upper=upper, b=b)

    sign = 1
    if deci < 0:
        deci = abs(deci)
        sign = -1

    hd, f1 = divmod(deci, 1)
    mm, f2 = divmod(f1 * 60.0, 1)
    sf = f2 * 60.0

    # Find the seconds part to required precision.
    fp = 10 ** pre
    if trunc:
        ss, _ = divmod(sf * fp, 1)
    else:
        ss = round(sf * fp, 0)

    ss = int(ss)

    # If ss is 60 to given precision then update mm, and if necessary
    # hd.
    if ss == 60 * fp:
        mm += 1
        ss = 0
    if mm == 60:
        hd += 1
        mm = 0

    hd = int(hd)
    mm = int(mm)
    if lower is not None and upper is not None and upper_trim:
        # For example 24h0m0s => 0h0m0s.
        if hd == upper:
            hd = int(lower)

    if hd == 0 and mm == 0 and ss == 0:
        sign = 1

    ss /= float(fp)
    # hd and mm parts are integer values but of type float
    return (sign, hd, mm, ss)