peng/functions.py
def pprint_vector(vector, limit=False, width=None, indent=0, eng=False, frac_length=3):
    r"""
    Format a list of numbers (vector) or a Numpy vector for printing.

    If the argument **vector** is :code:`None` the string :code:`'None'` is
    returned

    :param vector: Vector to pretty print or None
    :type  vector: list of integers or floats, Numpy vector or None

    :param limit: Flag that indicates whether at most 6 vector items are
                  printed (all vector items if its length is equal or less
                  than 6, first and last 3 vector items if it is not) (True),
                  or the entire vector is printed (False)
    :type  limit: boolean

    :param width: Number of available characters per line. If None the vector
                  is printed in one line
    :type  width: integer or None

    :param indent: Flag that indicates whether all subsequent lines after the
                   first one are indented (True) or not (False). Only relevant
                   if **width** is not None
    :type  indent: boolean

    :param eng: Flag that indicates whether engineering notation is used
                (True) or not (False)
    :type  eng: boolean

    :param frac_length: Number of digits of fractional part (only applicable
                        if **eng** is True)
    :type  frac_length: integer

    :raises: ValueError (Argument \`width\` is too small)

    :rtype: string

    For example:

        >>> from __future__ import print_function
        >>> import peng
        >>> header = 'Vector: '
        >>> data = [1e-3, 20e-6, 300e+6, 4e-12, 5.25e3, -6e-9, 700, 8, 9]
        >>> print(
        ...     header+peng.pprint_vector(
        ...         data,
        ...         width=30,
        ...         eng=True,
        ...         frac_length=1,
        ...         limit=True,
        ...         indent=len(header)
        ...     )
        ... )
        Vector: [    1.0m,   20.0u,  300.0M,
                             ...
                   700.0 ,    8.0 ,    9.0  ]
        >>> print(
        ...     header+peng.pprint_vector(
        ...         data,
        ...         width=30,
        ...         eng=True,
        ...         frac_length=0,
        ...         indent=len(header)
        ...     )
        ... )
        Vector: [    1m,   20u,  300M,    4p,
                     5k,   -6n,  700 ,    8 ,
                     9  ]
        >>> print(peng.pprint_vector(data, eng=True, frac_length=0))
        [    1m,   20u,  300M,    4p,    5k,   -6n,  700 ,    8 ,    9  ]
        >>> print(peng.pprint_vector(data, limit=True))
        [ 0.001, 2e-05, 300000000.0, ..., 700, 8, 9 ]
    """
    # pylint: disable=R0912,R0913
    num_digits = 12
    approx = lambda x: float(x) if "." not in x else round(float(x), num_digits)

    def limstr(value):
        str1 = str(value)
        iscomplex = isinstance(value, complex)
        str1 = str1.lstrip("(").rstrip(")")
        if "." not in str1:
            return str1
        if iscomplex:
            sign = "+" if value.imag >= 0 else "-"
            regexp = re.compile(
                r"(.*(?:[Ee][\+-]\d+)?)"
                + (r"\+" if sign == "+" else "-")
                + r"(.*(?:[Ee][\+-]\d+)?j)"
            )
            rvalue, ivalue = regexp.match(str1).groups()
            return (
                str(complex(approx(rvalue), approx(sign + ivalue.strip("j"))))
                .lstrip("(")
                .rstrip(")")
            )
        str2 = str(round(value, num_digits))
        return str2 if len(str1) > len(str2) else str1

    def _str(*args):
        """
        Convert numbers to string, optionally represented in engineering notation.

        Numbers may be integers, float or complex
        """
        ret = [
            (limstr(element) if not eng else peng(element, frac_length, True))
            if not isinstance(element, complex)
            else (
                limstr(element)
                if not eng
                else "{real}{sign}{imag}j".format(
                    real=peng(element.real, frac_length, True),
                    imag=peng(abs(element.imag), frac_length, True),
                    sign="+" if element.imag >= 0 else "-",
                )
            )
            for element in args
        ]
        return ret[0] if len(ret) == 1 else ret

    if vector is None:
        return "None"
    lvector = len(vector)
    if (not limit) or (limit and (lvector < 7)):
        items = _str(*vector)
        uret = "[ {0} ]".format(", ".join(items))
    else:
        items = _str(*(vector[:3] + vector[-3:]))
        uret = "[ {0}, ..., {1} ]".format(", ".join(items[:3]), ", ".join(items[-3:]))
    if (width is None) or (len(uret) < width):
        return uret
    # -4 comes from the fact that an opening '[ ' and a closing ' ]'
    # are added to the multi-line vector string
    if any([len(item) > width - 4 for item in items]):
        raise ValueError("Argument `width` is too small")
    # Text needs to be wrapped in multiple lines
    # Figure out how long the first line needs to be
    wobj = textwrap.TextWrapper(initial_indent="[ ", width=width)
    # uret[2:] -> do not include initial '[ ' as this is specified as
    # the initial indent to the text wrapper
    rlist = wobj.wrap(uret[2:])
    first_line = rlist[0]
    first_line_elements = first_line.count(",")
    # Reconstruct string representation of vector excluding first line
    # Remove ... from text to be wrapped because it is placed in a single
    # line centered with the content
    uret_left = (",".join(uret.split(",")[first_line_elements:])).replace("...,", "")
    wobj = textwrap.TextWrapper(width=width - 2)
    wrapped_text = wobj.wrap(uret_left.lstrip())
    # Construct candidate wrapped and indented list of vector elements
    rlist = [first_line] + [
        (" " * (indent + 2)) + item.rstrip() for item in wrapped_text
    ]
    last_line = rlist[-1]
    last_line_elements = last_line.count(",") + 1
    # "Manually" format limit output so that it is either 3 lines, first and
    # last line with 3 elements and the middle with '...' or 7 lines, each with
    # 1 element and the middle with '...'
    # If numbers are not to be aligned at commas (variable width) then use the
    # existing results of the wrap() function
    if limit and (lvector > 6):
        if (first_line_elements < 3) or (
            (first_line_elements == 3) and (last_line_elements < 3)
        ):
            rlist = [
                "[ {0},".format(_str(vector[0])),
                _str(vector[1]),
                _str(vector[2]),
                "...",
                _str(vector[-3]),
                _str(vector[-2]),
                "{0} ]".format(_str(vector[-1])),
            ]
            first_line_elements = 1
        else:
            rlist = [
                "[ {0},".format(", ".join(_str(*vector[:3]))),
                "...",
                "{0} ]".format(", ".join(_str(*vector[-3:]))),
            ]
        first_line = rlist[0]
    elif limit:
        rlist = [item.lstrip() for item in rlist]
    first_comma_index = first_line.find(",")
    actual_width = len(first_line) - 2
    if not eng:
        if not limit:
            return "\n".join(rlist)
        num_elements = len(rlist)
        return "\n".join(
            [
                "{spaces}{line}{comma}".format(
                    spaces=(" " * (indent + 2)) if num > 0 else "",
                    line=(
                        line.center(actual_width).rstrip()
                        if line.strip() == "..."
                        else line
                    ),
                    comma=(
                        ","
                        if (
                            (num < num_elements - 1)
                            and (not line.endswith(","))
                            and (line.strip() != "...")
                        )
                        else ""
                    ),
                )
                if num > 0
                else line
                for num, line in enumerate(rlist)
            ]
        )
    # Align elements across multiple lines
    if limit:
        remainder_list = [line.lstrip() for line in rlist[1:]]
    else:
        remainder_list = _split_every(
            text=uret[len(first_line) :],
            sep=",",
            count=first_line_elements,
            lstrip=True,
        )
    new_wrapped_lines_list = [first_line]
    for line in remainder_list[:-1]:
        new_wrapped_lines_list.append(
            "{0},".format(line).rjust(actual_width)
            if line != "..."
            else line.center(actual_width).rstrip()
        )
    # Align last line on fist comma (if it exists) or
    # on length of field if does not
    if remainder_list[-1].find(",") == -1:
        marker = len(remainder_list[-1]) - 2
    else:
        marker = remainder_list[-1].find(",")
    new_wrapped_lines_list.append(
        "{0}{1}".format((first_comma_index - marker - 2) * " ", remainder_list[-1])
    )
    return "\n".join(
        [
            "{spaces}{line}".format(spaces=" " * (indent + 2), line=line)
            if num > 0
            else line
            for num, line in enumerate(new_wrapped_lines_list)
        ]
    )
def pprint_vector(vector, limit=False, width=None, indent=0, eng=False, frac_length=3):
    r"""
    Format a list of numbers (vector) or a Numpy vector for printing.

    If the argument **vector** is :code:`None` the string :code:`'None'` is
    returned

    :param vector: Vector to pretty print or None
    :type  vector: list of integers or floats, Numpy vector or None

    :param limit: Flag that indicates whether at most 6 vector items are
                  printed (all vector items if its length is equal or less
                  than 6, first and last 3 vector items if it is not) (True),
                  or the entire vector is printed (False)
    :type  limit: boolean

    :param width: Number of available characters per line. If None the vector
                  is printed in one line
    :type  width: integer or None

    :param indent: Flag that indicates whether all subsequent lines after the
                   first one are indented (True) or not (False). Only relevant
                   if **width** is not None
    :type  indent: boolean

    :param eng: Flag that indicates whether engineering notation is used
                (True) or not (False)
    :type  eng: boolean

    :param frac_length: Number of digits of fractional part (only applicable
                        if **eng** is True)
    :type  frac_length: integer

    :raises: ValueError (Argument \`width\` is too small)

    :rtype: string

    For example:

        >>> from __future__ import print_function
        >>> import peng
        >>> header = 'Vector: '
        >>> data = [1e-3, 20e-6, 300e+6, 4e-12, 5.25e3, -6e-9, 700, 8, 9]
        >>> print(
        ...     header+peng.pprint_vector(
        ...         data,
        ...         width=30,
        ...         eng=True,
        ...         frac_length=1,
        ...         limit=True,
        ...         indent=len(header)
        ...     )
        ... )
        Vector: [    1.0m,   20.0u,  300.0M,
                             ...
                   700.0 ,    8.0 ,    9.0  ]
        >>> print(
        ...     header+peng.pprint_vector(
        ...         data,
        ...         width=30,
        ...         eng=True,
        ...         frac_length=0,
        ...         indent=len(header)
        ...     )
        ... )
        Vector: [    1m,   20u,  300M,    4p,
                     5k,   -6n,  700 ,    8 ,
                     9  ]
        >>> print(peng.pprint_vector(data, eng=True, frac_length=0))
        [    1m,   20u,  300M,    4p,    5k,   -6n,  700 ,    8 ,    9  ]
        >>> print(peng.pprint_vector(data, limit=True))
        [ 0.001, 2e-05, 300000000.0, ..., 700, 8, 9 ]
    """
    # pylint: disable=R0912,R0913
    num_digits = 12
    approx = lambda x: float(x) if "." not in x else round(float(x), num_digits)

    def limstr(value):
        str1 = str(value)
        iscomplex = isinstance(value, complex)
        str1 = str1.lstrip("(").rstrip(")")
        if "." not in str1:
            return str1
        if iscomplex:
            sign = "+" if value.imag >= 0 else "-"
            regexp = re.compile(
                r"(.*(?:[Ee][\+-]\d+)?)"
                + (r"\+" if sign == "+" else "-")
                + r"(.*(?:[Ee][\+-]\d+)?j)"
            )
            rvalue, ivalue = regexp.match(str1).groups()
            return (
                str(complex(approx(rvalue), approx(sign + ivalue.strip("j"))))
                .lstrip("(")
                .rstrip(")")
            )
        str2 = str(round(value, num_digits))
        return str2 if len(str1) > len(str2) else str1

    def _str(*args):
        """
        Convert numbers to string, optionally represented in engineering notation.

        Numbers may be integers, float or complex
        """
        ret = [
            (limstr(element) if not eng else peng(element, frac_length, True))
            if not isinstance(element, complex)
            else (
                limstr(element)
                if not eng
                else "{real}{sign}{imag}j".format(
                    real=peng(element.real, frac_length, True),
                    imag=peng(abs(element.imag), frac_length, True),
                    sign="+" if element.imag >= 0 else "-",
                )
            )
            for element in args
        ]
        return ret[0] if len(ret) == 1 else ret

    if vector is None:
        return "None"
    lvector = len(vector)
    if (not limit) or (limit and (lvector < 7)):
        items = _str(*vector)
        uret = "[ {0} ]".format(", ".join(items))
    else:
        items = _str(*(vector[:3] + vector[-3:]))
        uret = "[ {0}, ..., {1} ]".format(", ".join(items[:3]), ", ".join(items[-3:]))
    if (width is None) or (len(uret) < width):
        return uret
    # -4 comes from the fact that an opening '[ ' and a closing ' ]'
    # are added to the multi-line vector string
    if any([len(item) > width - 4 for item in items]):
        raise ValueError("Argument `width` is too small")
    # Text needs to be wrapped in multiple lines
    # Figure out how long the first line needs to be
    wobj = textwrap.TextWrapper(initial_indent="[ ", width=width)
    # uret[2:] -> do not include initial '[ ' as this is specified as
    # the initial indent to the text wrapper
    rlist = wobj.wrap(uret[2:])
    first_line = rlist[0]
    first_line_elements = first_line.count(",")
    # Reconstruct string representation of vector excluding first line
    # Remove ... from text to be wrapped because it is placed in a single
    # line centered with the content
    uret_left = (",".join(uret.split(",")[first_line_elements:])).replace("...,", "")
    wobj = textwrap.TextWrapper(width=width - 2)
    wrapped_text = wobj.wrap(uret_left.lstrip())
    # Construct candidate wrapped and indented list of vector elements
    rlist = [first_line] + [
        (" " * (indent + 2)) + item.rstrip() for item in wrapped_text
    ]
    last_line = rlist[-1]
    last_line_elements = last_line.count(",") + 1
    # "Manually" format limit output so that it is either 3 lines, first and
    # last line with 3 elements and the middle with '...' or 7 lines, each with
    # 1 element and the middle with '...'
    # If numbers are not to be aligned at commas (variable width) then use the
    # existing results of the wrap() function
    if limit and (lvector > 6):
        if (first_line_elements < 3) or (
            (first_line_elements == 3) and (last_line_elements < 3)
        ):
            rlist = [
                "[ {0},".format(_str(vector[0])),
                _str(vector[1]),
                _str(vector[2]),
                "...",
                _str(vector[-3]),
                _str(vector[-2]),
                "{0} ]".format(_str(vector[-1])),
            ]
            first_line_elements = 1
        else:
            rlist = [
                "[ {0},".format(", ".join(_str(*vector[:3]))),
                "...",
                "{0} ]".format(", ".join(_str(*vector[-3:]))),
            ]
        first_line = rlist[0]
    elif limit:
        rlist = [item.lstrip() for item in rlist]
    first_comma_index = first_line.find(",")
    actual_width = len(first_line) - 2
    if not eng:
        if not limit:
            return "\n".join(rlist)
        num_elements = len(rlist)
        return "\n".join(
            [
                "{spaces}{line}{comma}".format(
                    spaces=(" " * (indent + 2)) if num > 0 else "",
                    line=(
                        line.center(actual_width).rstrip()
                        if line.strip() == "..."
                        else line
                    ),
                    comma=(
                        ","
                        if (
                            (num < num_elements - 1)
                            and (not line.endswith(","))
                            and (line.strip() != "...")
                        )
                        else ""
                    ),
                )
                if num > 0
                else line
                for num, line in enumerate(rlist)
            ]
        )
    # Align elements across multiple lines
    if limit:
        remainder_list = [line.lstrip() for line in rlist[1:]]
    else:
        remainder_list = _split_every(
            text=uret[len(first_line) :],
            sep=",",
            count=first_line_elements,
            lstrip=True,
        )
    new_wrapped_lines_list = [first_line]
    for line in remainder_list[:-1]:
        new_wrapped_lines_list.append(
            "{0},".format(line).rjust(actual_width)
            if line != "..."
            else line.center(actual_width).rstrip()
        )
    # Align last line on fist comma (if it exists) or
    # on length of field if does not
    if remainder_list[-1].find(",") == -1:
        marker = len(remainder_list[-1]) - 2
    else:
        marker = remainder_list[-1].find(",")
    new_wrapped_lines_list.append(
        "{0}{1}".format((first_comma_index - marker - 2) * " ", remainder_list[-1])
    )
    return "\n".join(
        [
            "{spaces}{line}".format(spaces=" " * (indent + 2), line=line)
            if num > 0
            else line
            for num, line in enumerate(new_wrapped_lines_list)
        ]
    )
r"""
    Format a list of numbers (vector) or a Numpy vector for printing.

    If the argument **vector** is :code:`None` the string :code:`'None'` is
    returned

    :param vector: Vector to pretty print or None
    :type  vector: list of integers or floats, Numpy vector or None

    :param limit: Flag that indicates whether at most 6 vector items are
                  printed (all vector items if its length is equal or less
                  than 6, first and last 3 vector items if it is not) (True),
                  or the entire vector is printed (False)
    :type  limit: boolean

    :param width: Number of available characters per line. If None the vector
                  is printed in one line
    :type  width: integer or None

    :param indent: Flag that indicates whether all subsequent lines after the
                   first one are indented (True) or not (False). Only relevant
                   if **width** is not None
    :type  indent: boolean

    :param eng: Flag that indicates whether engineering notation is used
                (True) or not (False)
    :type  eng: boolean

    :param frac_length: Number of digits of fractional part (only applicable
                        if **eng** is True)
    :type  frac_length: integer

    :raises: ValueError (Argument \`width\` is too small)

    :rtype: string

    For example:

        >>> from __future__ import print_function
        >>> import peng
        >>> header = 'Vector: '
        >>> data = [1e-3, 20e-6, 300e+6, 4e-12, 5.25e3, -6e-9, 700, 8, 9]
        >>> print(
        ...     header+peng.pprint_vector(
        ...         data,
        ...         width=30,
        ...         eng=True,
        ...         frac_length=1,
        ...         limit=True,
        ...         indent=len(header)
        ...     )
        ... )
        Vector: [    1.0m,   20.0u,  300.0M,
                             ...
                   700.0 ,    8.0 ,    9.0  ]
        >>> print(
        ...     header+peng.pprint_vector(
        ...         data,
        ...         width=30,
        ...         eng=True,
        ...         frac_length=0,
        ...         indent=len(header)
        ...     )
        ... )
        Vector: [    1m,   20u,  300M,    4p,
                     5k,   -6n,  700 ,    8 ,
                     9  ]
        >>> print(peng.pprint_vector(data, eng=True, frac_length=0))
        [    1m,   20u,  300M,    4p,    5k,   -6n,  700 ,    8 ,    9  ]
        >>> print(peng.pprint_vector(data, limit=True))
        [ 0.001, 2e-05, 300000000.0, ..., 700, 8, 9 ]