r"""
        Returns a readable string representiation of the number. *format* is used to format
        non-NumPy nominal and uncertainty values. It can be a string such as ``"%d"``, a function
        that is called with the value to format, or a rounding method as accepted by
        :py:meth:`round_value`. When *None* (the default), :py:attr:`default_format` is used. All
        keyword arguments except wildcard *kwargs* are only used to format non-NumPy values. In case
        of NumPy objects, *kwargs* are passed to `numpy.array2string
        <https://docs.scipy.org/doc/numpy/reference/generated/numpy.array2string.html>`_.

        When *unit* is set, it is appended to the end of the string. When *scientific* is *True*,
        all values are represented by their scientific notation. When *scientific* is *False* and
        *si* is *True*, the appropriate SI prefix is used. *labels* controls whether uncertainty
        labels are shown in the string. When *True*, uncertainty names are used, but it can also
        be a list of labels whose order should match the uncertainty dict traversal order. *style*
        can be ``"plain"``, ``"latex"``, or ``"root"``. *styles* can be a dict with fields
        ``"space"``, ``"label"``, ``"unit"``, ``"sym"``, ``"asym"``, ``"sci"`` to customize every
        aspect of the format style on top of :py:attr:`style_dict`. Unless *force_asymmetric* is
        *True*, an uncertainty is quoted symmetric if it yields identical values in both directions.

        Examples:

        .. code-block:: python

            n = Number(17.321, {"a": 1.158, "b": 0.453})
            n.str()               # -> '17.321 +- 1.158 (a) +- 0.453 (b)'
            n.str("%.1f")         # -> '17.3 +- 1.2 (a) +- 0.5 (b)'
            n.str("publication")  # -> '17.32 +- 1.16 (a) +- 0.45 (b)'
            n.str("pdg")          # -> '17.3 +- 1.2 (a) +- 0.5 (b)'

            n = Number(8848, 10)
            n.str(unit="m")                           # -> "8848.0 +- 10.0 m"
            n.str(unit="m", force_asymmetric=True)    # -> "8848.0 +10.0-10.0 m"
            n.str(unit="m", scientific=True)          # -> "8.848 +- 0.01 x 1E3 m"
            n.str("%.2f", unit="m", scientific=True)  # -> "8.85 +- 0.01 x 1E3 m"
            n.str(unit="m", si=True)                  # -> "8.848 +- 0.01 km"
            n.str("%.2f", unit="m", si=True)          # -> "8.85 +- 0.01 km"
            n.str(unit="m", style="latex")            # -> "8848.0 \pm 10.0\,m"
            n.str(unit="m", style="latex", si=True)   # -> "8.848 \pm 0.01\,km"
            n.str(unit="m", style="root")             # -> "8848.0 #pm 10.0 m"
            n.str(unit="m", style="root", si=True)    # -> "8.848 #pm 0.01 km"

def str(self, format=None, unit=None, scientific=False, si=False, labels=True, style="plain",
            styles=None, force_asymmetric=False, **kwargs):
        r"""
        Returns a readable string representiation of the number. *format* is used to format
        non-NumPy nominal and uncertainty values. It can be a string such as ``"%d"``, a function
        that is called with the value to format, or a rounding method as accepted by
        :py:meth:`round_value`. When *None* (the default), :py:attr:`default_format` is used. All
        keyword arguments except wildcard *kwargs* are only used to format non-NumPy values. In case
        of NumPy objects, *kwargs* are passed to `numpy.array2string
        <https://docs.scipy.org/doc/numpy/reference/generated/numpy.array2string.html>`_.

        When *unit* is set, it is appended to the end of the string. When *scientific* is *True*,
        all values are represented by their scientific notation. When *scientific* is *False* and
        *si* is *True*, the appropriate SI prefix is used. *labels* controls whether uncertainty
        labels are shown in the string. When *True*, uncertainty names are used, but it can also
        be a list of labels whose order should match the uncertainty dict traversal order. *style*
        can be ``"plain"``, ``"latex"``, or ``"root"``. *styles* can be a dict with fields
        ``"space"``, ``"label"``, ``"unit"``, ``"sym"``, ``"asym"``, ``"sci"`` to customize every
        aspect of the format style on top of :py:attr:`style_dict`. Unless *force_asymmetric* is
        *True*, an uncertainty is quoted symmetric if it yields identical values in both directions.

        Examples:

        .. code-block:: python

            n = Number(17.321, {"a": 1.158, "b": 0.453})
            n.str()               # -> '17.321 +- 1.158 (a) +- 0.453 (b)'
            n.str("%.1f")         # -> '17.3 +- 1.2 (a) +- 0.5 (b)'
            n.str("publication")  # -> '17.32 +- 1.16 (a) +- 0.45 (b)'
            n.str("pdg")          # -> '17.3 +- 1.2 (a) +- 0.5 (b)'

            n = Number(8848, 10)
            n.str(unit="m")                           # -> "8848.0 +- 10.0 m"
            n.str(unit="m", force_asymmetric=True)    # -> "8848.0 +10.0-10.0 m"
            n.str(unit="m", scientific=True)          # -> "8.848 +- 0.01 x 1E3 m"
            n.str("%.2f", unit="m", scientific=True)  # -> "8.85 +- 0.01 x 1E3 m"
            n.str(unit="m", si=True)                  # -> "8.848 +- 0.01 km"
            n.str("%.2f", unit="m", si=True)          # -> "8.85 +- 0.01 km"
            n.str(unit="m", style="latex")            # -> "8848.0 \pm 10.0\,m"
            n.str(unit="m", style="latex", si=True)   # -> "8.848 \pm 0.01\,km"
            n.str(unit="m", style="root")             # -> "8848.0 #pm 10.0 m"
            n.str(unit="m", style="root", si=True)    # -> "8.848 #pm 0.01 km"
        """
        if format is None:
            format = self.default_format or self.__class__.default_format

        if not self.is_numpy:
            # check style
            style = style.lower()
            if style not in style_dict.keys():
                raise ValueError("unknown style '{}'".format(style))
            d = style_dict[style]

            # extend by custom styles
            if styles:
                d.update(styles)

            # scientific or SI notation?
            prefix = ""
            transform = lambda x: x
            if scientific or si:
                if scientific:
                    mag = 0 if self.nominal == 0 else int(math.floor(math.log10(abs(self.nominal))))
                else:
                    prefix, mag = infer_si_prefix(self.nominal)
                transform = lambda x: x * 10.**(-mag)

            # gather and transform values
            nominal = transform(self.nominal)
            names, ups, downs = [], [], []
            for name, (up, down) in self.uncertainties.items():
                names.append(name)
                ups.append(transform(up))
                downs.append(transform(down))

            # special formats implemented by round_value
            if format in ("pub", "publication", "pdg", "one", "onedigit"):
                nominal, (ups, downs), _mag = round_value(self.nominal, ups, downs, method=format)
                fmt = lambda x: match_precision(float(x) * 10.**_mag, 10.**_mag)

            # string formatting
            elif not callable(format):
                fmt = lambda x: format % x

            # helper to build the ending consisting of scientific notation or SI prefix, and unit
            def ending():
                e = ""
                if scientific and mag:
                    e += d["space"] + d["sci"].format(mag=mag)
                _unit = (prefix or "") + (unit or "")
                if _unit:
                    e += d["unit"].format(unit=_unit)
                return e

            # start building the text
            text = fmt(nominal, **kwargs)

            # no uncertainties
            if len(names) == 0:
                text += ending()
                if style == "plain" and labels:
                    text += d["space"] + d["label"].format(label="no uncertainties")

            # one ore more uncertainties
            else:
                # special case: only the default uncertainty
                if len(names) == 1 and names[0] == self.DEFAULT:
                    labels = False

                for i, (name, up, down) in enumerate(zip(names, ups, downs)):
                    up = str(fmt(up))
                    down = str(fmt(down))

                    if up == down and not force_asymmetric:
                        text += d["space"] + d["sym"].format(unc=up)
                    else:
                        text += d["space"] + d["asym"].format(up=up, down=down)

                    if labels:
                        label = labels[i] if isinstance(labels, (list, tuple)) else name
                        text += d["space"] + d["label"].format(label=label)

                text += ending()

            return text

        else:
            # we are dealing with a numpy array here
            # start with nominal text
            text = np.array2string(self.nominal, **kwargs)

            # uncertainty text
            uncs = self.uncertainties
            if len(uncs) == 0:
                text += " (no uncertainties)"
            elif len(uncs) == 1 and list(uncs.keys())[0] == self.DEFAULT:
                up, down = self.get_uncertainty()
                text += "\n+ {}".format(np.array2string(up, **kwargs))
                text += "\n- {}".format(np.array2string(down, **kwargs))
            else:
                for name, (up, down) in uncs.items():
                    text += "\n+ {} {}".format(name, np.array2string(up, **kwargs))
                    text += "\n- {} {}".format(name, np.array2string(down, **kwargs))

            return text