Rounds a number *val* with a single symmetric uncertainty *unc* or asymmetric uncertainties
    *unc* (interpreted as *up*) and *unc_down*, and calculates the orders of their magnitudes. They
    both can be a float or a list of floats for simultaneous evaluation. When *val* is a
    :py:class:`Number` instance, its combined uncertainty is used instead. Returns a 3-tuple
    containing:

    - The string representation of the central value.
    - The string representations of the uncertainties in a list. For the symmetric case, this list
      contains only one element.
    - The decimal magnitude.

    Examples:

    .. code-block:: python

        round_value(1.23, 0.456)        # -> ("123", ["46"], -2)
        round_value(1.23, 0.456, 0.987) # -> ("123", ["46", "99"], -2)

        round_value(1.23, [0.456, 0.312]) # -> ("123", [["456", "312"]], -3)

        vals = np.array([1.23, 4.56])
        uncs = np.array([0.45678, 0.078])
        round_value(vals, uncs) # -> (["1230", "4560"], [["457", "78"]], -3)

def round_value(val, unc=None, unc_down=None, method="publication"):
    """
    Rounds a number *val* with a single symmetric uncertainty *unc* or asymmetric uncertainties
    *unc* (interpreted as *up*) and *unc_down*, and calculates the orders of their magnitudes. They
    both can be a float or a list of floats for simultaneous evaluation. When *val* is a
    :py:class:`Number` instance, its combined uncertainty is used instead. Returns a 3-tuple
    containing:

    - The string representation of the central value.
    - The string representations of the uncertainties in a list. For the symmetric case, this list
      contains only one element.
    - The decimal magnitude.

    Examples:

    .. code-block:: python

        round_value(1.23, 0.456)        # -> ("123", ["46"], -2)
        round_value(1.23, 0.456, 0.987) # -> ("123", ["46", "99"], -2)

        round_value(1.23, [0.456, 0.312]) # -> ("123", [["456", "312"]], -3)

        vals = np.array([1.23, 4.56])
        uncs = np.array([0.45678, 0.078])
        round_value(vals, uncs) # -> (["1230", "4560"], [["457", "78"]], -3)
    """
    if isinstance(val, Number):
        unc, unc_down = val.get_uncertainty()
        val = val.nominal
    elif unc is None:
        raise ValueError("unc must be set when val is not a Number instance")

    # prepare unc values
    asym = unc_down is not None
    unc_up = unc
    if not asym:
        unc_down = unc_up

    if not is_numpy(val):
        # treat as lists for simultaneous rounding when not numpy arrays
        passed_list = isinstance(unc_up, (list, tuple)) or isinstance(unc_down, (list, tuple))
        unc_up = make_list(unc_up)
        unc_down = make_list(unc_down)

        # sanity checks
        if len(unc_up) != len(unc_down):
            raise ValueError("uncertainties should have same length when passed as lists")
        elif any(unc < 0 for unc in unc_up):
            raise ValueError("up uncertainties must be positive: {}".format(unc_up))
        elif any(unc < 0 for unc in unc_down):
            raise ValueError("down uncertainties must be positive: {}".format(unc_down))

        # to determine the precision, use the uncertainty with the smallest magnitude
        ref_mag = min(round_uncertainty(u, method=method)[1] for u in unc_up + unc_down)

        # convert the uncertainty and central value to match the reference magnitude
        scale = 1. / 10.**ref_mag
        val_str = match_precision(scale * val, "1")
        up_strs = [match_precision(scale * u, "1") for u in unc_up]
        down_strs = [match_precision(scale * u, "1") for u in unc_down]

        if passed_list:
            return (val_str, [up_strs, down_strs] if asym else [up_strs], ref_mag)
        else:
            return (val_str, [up_strs[0], down_strs[0]] if asym else [up_strs[0]], ref_mag)

    else:
        # sanity checks
        if (unc_up < 0).any():
            raise ValueError("up uncertainties must be positive: {}".format(unc_up))
        elif (unc_down < 0).any():
            raise ValueError("down uncertainties must be positive: {}".format(unc_down))

        # to determine the precision, use the uncertainty with the smallest magnitude
        ref_mag_up = round_uncertainty(unc_up, method=method)[1]
        ref_mag_down = round_uncertainty(unc_down, method=method)[1]
        ref_mag = min(ref_mag_up.min(), ref_mag_down.min())

        scale = 1. / 10.**ref_mag
        val_str = match_precision(scale * val, "1")
        up_str = match_precision(scale * unc_up, "1")
        down_str = match_precision(scale * unc_down, "1")

        return (val_str, [up_str, down_str] if asym else [up_str], ref_mag)