from typing import Iterable, Any, Optional, List, Union, Tuple
[docs]class Matrix:
    """
    # noqa: E501
    Poor man's implementation of a DataFrame/Matrix
    This is used to calculate similarities between HPO sets
    and is surprisingly much faster than using pandas DataFrames
    .. note::
        Pandas::
            ===== COMPARING SETS ======
            23806489 function calls (23770661 primitive calls) in 19.705 seconds
            ncalls  tottime  percall  cumtime  percall filename:lineno(function)
            ....
            9900    0.267    0.000   19.106    0.002 set.py:318(similarity)
            9900    1.124    0.000   14.330    0.001 set.py:477(_sim_score)
            ....
        Matrix::
            ===== COMPARING SETS ======
            12870433 function calls in 6.642 seconds
            ncalls  tottime  percall  cumtime  percall filename:lineno(function)
            ....
            9900    0.048    0.000    6.424    0.001 set.py:316(similarity)
            9900    0.928    0.000    5.112    0.001 set.py:432(_sim_score)
            ....
    .. warning::
        This `Matrix` should not be used as a public interface.
        It's only used internally for calculations.
    Parameters
    ----------
    rows: int
        The number of rows in the Matrix
    cols: int
        The number of columns in the Matrix
    data: list of values, default ``None``
        A list with values to fill the Matrix.
    Attributes
    ----------
    n_rows: int
        The number of rows in the Matrix
    n_cols: int
        The number of columns in the Matrix
    rows: iterator
        Iterator over all rows
        **Example:** ::
            print(matrix)
            >>    ||   0|   1|   2|   3|
            >> =========================
            >> 0  ||  11|  12|  13|  14|
            >> 1  ||  21|  22|  23|  24|
            >> 2  ||  31|  32|  33|  34|
            for row in matrix.rows:
                print(row)
            >> [11, 12, 13, 14]
            >> [21, 22, 23, 24]
            >> [31, 32, 33, 34]
    columns: iterator
        Iterator over all columns
        **Example:** ::
            print(matrix)
            >>    ||   0|   1|   2|   3|
            >> =========================
            >> 0  ||  11|  12|  13|  14|
            >> 1  ||  21|  22|  23|  24|
            >> 2  ||  31|  32|  33|  34|
            for col in matrix.columns:
                print(col)
            >> [11, 21, 31]
            >> [12, 22, 32]
            >> [13, 23, 33]
            >> [14, 24, 34]
    """
    def __init__(
        self,
        rows: int,
        cols: int,
        data: Optional[List[Any]] = None
    ):
        self.n_rows = rows
        self.n_cols = cols
        self._data: List[Any]
        if data is None:
            self._data = [None] * rows * cols
        elif len(data) == rows * cols:
            self._data = data
        else:
            raise RuntimeError('Wrong number of data items in `data`')
    def __getitem__(self, key: Tuple[Optional[int], Optional[int]]) -> Any:
        idx = self._get_key_indicies(key)
        return self._data[idx]
    def __setitem__(
        self,
        key: Tuple[Optional[int], Optional[int]],
        val: Any
    ) -> None:
        idx = self._get_key_indicies(key)
        if isinstance(idx, int) or len(self._data[idx]) == len(val):
            self._data[idx] = val
        else:
            raise ValueError('Different length of matrix subset and values')
    def _get_key_indicies(
        self,
        key: Tuple[Optional[int], Optional[int]]
    ) -> Union[int, slice]:
        row = key[0]
        col = key[1]
        if row is None and isinstance(col, int):
            # Return one column
            return slice(
                col,
                self.n_rows * (self.n_cols) + col,
                self.n_cols
            )
        if col is None and isinstance(row, int):
            # Return one row
            return slice(
                row * self.n_cols,
                row * self.n_cols + self.n_cols,
                1
            )
        if isinstance(row, int) and isinstance(col, int):
            if row > self.n_rows - 1:
                raise RuntimeError('Invalid row number: {}'.format(row))
            if col > self.n_cols - 1:
                raise RuntimeError('Invalid column number: {}'.format(col))
            return row * self.n_cols + col
        raise RuntimeError('Invalid arguments for Matrix subset')
    @property
    def rows(self) -> Iterable[Any]:
        for x in range(self.n_rows):
            yield self[x, None]
    @property
    def columns(self) -> Iterable[Any]:
        for x in range(self.n_cols):
            yield self[None, x]
    def __str__(self) -> str:
        maxlength = max(
            [len(str(self.n_cols))] +
            [len(str(x)) for x in self._data]
        ) + 2
        idxlength = len(str(self.n_rows)) + 2
        s = ''
        s += '{}||'.format(''.rjust(idxlength))
        s += ''.join([
            '{}|'.format(str(x).rjust(maxlength))
            for x in range(self.n_cols)
        ])
        s += '\n' + '=' * len(s)
        for idx, item in enumerate(self._data):
            if idx % self.n_cols == 0:
                s += '\n{}||'.format(
                    str(int(idx/self.n_cols)).ljust(idxlength)
                )
            s += '{}|'.format(str(item).rjust(maxlength))
        return s