Source code for ooodev.formatters.formatter_table
from __future__ import annotations
from typing import List, Set, Any, Tuple, TYPE_CHECKING
from ooodev.formatters.table_rule_kind import TableRuleKind as TableRuleKind
from ooodev.formatters.format_table_item import FormatTableItem as FormatTableItem
from ooodev.formatters.format_string import FormatString as FormatString
from ooodev.formatters.table_item_processor import TableItemProcessor
if TYPE_CHECKING:
from ..utils.type_var import Row
[docs]class FormatterTable:
"""
Formatter for formatting 2d sequences.
See Also:
:py:meth:`.Calc.print_array`
"""
[docs] def __init__(
self,
format: str | Tuple[str, ...],
idx_rule: TableRuleKind = TableRuleKind.IGNORE,
idxs: Tuple[int, ...] | None = None,
**kwargs,
) -> None:
"""
Constructor
Args:
format (str | Tuple[str, ...]): Formatting that is applied to specified data.
idx_rule (FormatterTableRuleKind, optional): Flag Options that determine behaviors. Defaults to FormatterTableRuleKind.IGNORE.
idxs (Tuple[int, ...] | None, optional): Row Indexes that specify which rows are affect. Defaults to None.
If ``idx_rule`` contains ``FormatterTableRuleKind.IGNORE`` then ``idxs`` are the rows that are excluded from formatting.
If ``idx_rule`` contains ``FormatterTableRuleKind.ONLY`` then ``idxs`` are the rows that are formatted.
**Kwargs: Expandable list or Key, value args that are auto assigned as class Attributes. Useful for child classes.
"""
self._format = format
self._idx_rule = idx_rule
self._idxs = () if idxs is None else idxs
self._row_formats: List[FormatTableItem] = []
self._custom_formats_str: List[FormatString] = []
# self._custom_formats_col: List[FormatterTableItem] = []
self._col_formats: List[FormatTableItem] = []
self._cols = None
self._idx_col_last = -1
# for classes that inherit FormatterTable and need to expand constructor
for key, value in kwargs.items():
setattr(self, key, value)
def _get_cols(self) -> Tuple[int, ...]:
if self._cols is None:
cols: Set[int] = set()
for cf in self.col_formats:
cols.update(cf.idxs_inc)
self._cols = tuple(cols)
return self._cols
def _has_col(self, idx_col: int) -> bool:
cols = self._get_cols()
return idx_col in cols
def _custom_row_item_format(self, idx_row: int) -> FormatTableItem | None:
if idx_row < 0:
return None
return next((cf for cf in self._row_formats if cf.is_index(idx_row)), None)
def _custom_row_format(self, idx_row: int) -> FormatString | None:
if idx_row < 0:
return None
return next((cf for cf in self._custom_formats_str if cf.has_index(idx_row)), None)
def _get_col_formatter(self, idx_col: int) -> FormatTableItem | None:
if idx_col < 0:
return None
return next((cf for cf in self._col_formats if cf.is_index(idx_col)), None)
def _format_col(self, idx_row: int, idx_col: int, val: Any) -> Tuple[bool, Any]:
# returns original value or formatting string.
cf = self._get_col_formatter(idx_col=idx_col)
do_col_format = True
if not self._is_format_row(idx_row=idx_row):
# this row may be ignored, check for col override:
do_col_format = TableRuleKind.COL_OVER_ROW in self._idx_rule
if not do_col_format and cf and TableRuleKind.COL_OVER_ROW in self._idx_rule:
return TableItemProcessor.process_col(
itm=cf, idx_row=idx_row, idx_col=idx_col, idx_col_last=self._idx_col_last, value=val
)
if cf:
# there is a custom column format and column expects formatting.
# ignore column formatting and apply only custom formatting.
return TableItemProcessor.process_col(
itm=cf, idx_row=idx_row, idx_col=idx_col, idx_col_last=self._idx_col_last, value=val
)
# no column formatting
return (False, val)
def _is_format_row(self, idx_row: int) -> bool:
if TableRuleKind.IGNORE in self._idx_rule:
return idx_row not in self._idxs
return idx_row in self._idxs if TableRuleKind.ONLY in self._idx_rule else False
def _format_row(self, idx_row, row_data: List[str], join_str: str) -> str:
if len(self._custom_formats_str) == 0:
return join_str.join(row_data).rstrip()
if custom_fmt_row := self._custom_row_format(idx_row=idx_row):
row_str = join_str.join(row_data)
return custom_fmt_row.get_formatted(row_str).rstrip()
return join_str.join(row_data).rstrip()
def _format_row_items(self, idx_row: int, row_data: Row, join_str: str) -> str:
is_row_format = self._is_format_row(idx_row=idx_row)
fmt_rows_idxs = set()
# do not apply column formatting to rows that are ignored.
if is_row_format:
fmt_rows = []
for i, val in enumerate(row_data):
col_formatted, col_val = self._format_col(idx_row=idx_row, idx_col=i, val=val)
fmt_rows.append(col_val)
if col_formatted:
fmt_rows_idxs.add(i)
else:
fmt_rows = row_data
if custom_row_fmt_itm := self._custom_row_item_format(idx_row=idx_row):
# there is a custom format for this row.
# ignore other format for row and apply custom.
#
# perhaps later other flag such as CUSTOM_COL_OVER_CUSTOM_ROW should be added.
# if col over rows then rows should ignore cols that have been formatted
# for now keep it a simple as possible.
s_row = []
for i, val in enumerate(fmt_rows):
# if self._has_col(idx_col=i):
# s_row.append(str(val))
# else:
p_row_state, p_row_val = TableItemProcessor.process_row(
itm=custom_row_fmt_itm, idx_row=idx_row, idx_col=i, idx_col_last=self._idx_col_last, value=val
)
if p_row_state:
s_row.append(p_row_val)
else:
s_row.append(str(p_row_val))
return self._format_row(idx_row=idx_row, row_data=s_row, join_str=join_str)
if is_row_format:
# if col over rows then rows should ignore cols that have been formatted
s_row = []
for i, val in enumerate(fmt_rows):
if i in fmt_rows_idxs:
# if self._has_col(idx_col=i):
s_row.append(val) # will be string because it is formatted
else:
s_row.append(self._apply_all_formats(val, self._format))
else:
s_row = [str(v) for v in fmt_rows]
return self._format_row(idx_row=idx_row, row_data=s_row, join_str=join_str)
def _apply_single_format(self, value, fmt: str) -> str:
try:
return format(value, fmt)
except Exception:
return str(value)
def _apply_all_formats(self, value, formats: str | Tuple[str, ...]) -> str:
if not isinstance(formats, tuple):
return self._apply_single_format(value, formats)
v = value
for fmt in formats:
v = self._apply_single_format(v, fmt)
return str(v)
[docs] def get_formatted(self, idx_row: int, row_data: Row, join_str: str = " ") -> str:
"""
Applies formatting to rows and columns.
Args:
val (Any): Any value
idx_row (int): current row index being processed
row_data: (Row): List of data to format
Returns:
str: formatted row
"""
self._idx_col_last = len(row_data) - 1 # maybe -1
return self._format_row_items(idx_row=idx_row, row_data=row_data, join_str=join_str)
# region Properties
@property
def format(self) -> str | Tuple[str, ...]:
"""
Gets format
Format option such as ``.2f``
Multiple formats can be added such as ``(".2f", "<10")``.
Formats are applied in the order they are added.
In this case first float is formatted as string with two decimal places, and
then value is padded to the right with spaces.
"""
return self._format
@property
def col_formats(self) -> List[FormatTableItem]:
"""
Gets Column formats
New formats can be added.
Applies to any column that matches.
Example:
.. code-block:: python
fl.col_formats.append(FormatTableItem(format=(".0%", ">9"), idxs=(4,)))
"""
return self._col_formats
@property
def custom_formats_str(self) -> List[FormatString]:
"""
Gets Custom formats string value.
If you need to apply extra formatting to a row after is has been formatted as string
then ``FormatterString`` instances can be added.
"""
return self._custom_formats_str
@property
def row_formats(self) -> List[FormatTableItem]:
"""
Gets Row formats
New formats can be added.
Applies to any column that matches.
Example:
.. code-block:: python
fl.row_formats.append(FormatTableItem(format=">9", idxs_inc=(0, 9)))
"""
return self._row_formats
# endregion Properties
__all__ = ["FormatterTable"]