diff --git a/README.md b/README.md index 1db9c80..75c7d6f 100644 --- a/README.md +++ b/README.md @@ -161,13 +161,14 @@ sheet = ps.Spreadsheet.create_new_sheet( ``` Other (keywords) arguments: -1. `rows_labels (List[str])`: _(optional)_ List of masks (aliases) -for row names. -2. `columns_labels (List[str])`: _(optional)_ List of masks (aliases) -for column names. +1. `rows_labels (List[Union[str, SkippedLabel]])`: _(optional)_ List of masks +(aliases) for row names. +2. `columns_labels (List[Union[str, SkippedLabel]])`: _(optional)_ List of +masks (aliases) for column names. If the instance of SkippedLabel is +used, the export skips this label. 3. `rows_help_text (List[str])`: _(optional)_ List of help texts for each row. 4. `columns_help_text (List[str])`: _(optional)_ List of help texts for each -column. +column. If the instance of SkippedLabel is used, the export skips this label. 5. `excel_append_row_labels (bool)`: _(optional)_ If True, one column is added on the beginning of the sheet as a offset for labels. 6. `excel_append_column_labels (bool)`: _(optional)_ If True, one row is @@ -303,6 +304,12 @@ or set of rows and columns). Following code, select the third column: ```python sheet.iloc[:,2] ``` +On the other hand +```python +sheet.loc[:,'Handy column'] +``` +selects all the rows in the columns with the label _'Handy column'_. + You can again set the values in the slice to some constant, or the array of constants, or to another cell, or to the result of some computation. ```python @@ -312,6 +319,33 @@ sheet.iloc[:,2] = sheet.iloc[1,3] # Just a reference to a cell ``` Technically the slice is the instance of `CellSlice` class. +There are two ways how to slice, either using `.loc` or `.iloc` attribute. +Where `iloc` uses integer position and `loc` uses label of the position +(as a string). + +By default the right-most value is excluded when defining slices. If you want +to use right-most value indexing, use one of the methods described below. + +#### Slicing using method (with the right-most value included option) +Sometimes, it is quite helpful to use a slice that includes the right-most +value. There are two functions for this purpose: +1. `sheet.iloc.get_slice(ROW_INDEX, COLUMN_INDEX, include_right=[True/False])`: +This way is equivalent to the one presented above with square brackets `[]`. +The difference is the key-value attribute `include_right` that enables the +possibility of including the right-most value of the slice (default value is +False). If you want to use slice as your index, you need to pass some `slice` +object to one (or both) of the indices. For example: +`sheet.iloc.get_slice(slice(0, 7), 3, include_right=True])` selects first nine +rows (because 8th row - right-most one - is included) from the fourth column +of the sheet _(remember, all is indexed from zero)_. + +2. `sheet.iloc.set_slice(ROW_INDEX, COLUMN_INDEX, VALUE, +include_right=[True/False])`: this command set slice to _VALUE_ in the similar +logic as when you call `get_slice` method (see the first point). + +There are again two possibilities, either to use `iloc` with integer position +or to use `loc` with labels. + #### Aggregate functions The slice itself can be used for computations using aggregate functions. @@ -677,7 +711,8 @@ sheet.to_excel( "value": "Value", "description": "Description" }), - values_only: bool = False + values_only: bool = False, + skipped_label_replacement: str = '' ) ``` The only required argument is the path to the destination file (positional @@ -699,6 +734,8 @@ for the sheet with variables (first row in the sheet). Dictionary should look like: `{"name": "Name", "value": "Value", "description": "Description"}`. * `values_only (bool)`: If true, only values (and not formulas) are exported. +* `skipped_label_replacement (str)`: Replacement for the SkippedLabel +instances. ##### Setting the format/style for Excel cells There is a possibility to set the style/format of each cell in the grid @@ -754,6 +791,9 @@ skipped, default value is false (NaN values are included). * `append_dict (dict)`: Append this dictionary to output. * `generate_schema (bool)`: If true, returns the JSON schema. +All the rows and columns with labels that are instances of SkippedLabel are +entirely skipped. + **The return value is:** Dictionary with keys: 1. column/row, 2. row/column, 3. language or @@ -996,7 +1036,8 @@ sheet.to_excel(*, sep: str = ',', line_terminator: str = '\n', na_rep: str = '', - skip_labels: bool = False + skip_labels: bool = False, + skipped_label_replacement: str = '' ) ``` Parameters are (all optional and key-value only): @@ -1011,6 +1052,8 @@ language in each cell instead of values. * `na_rep (str)`: Replacement for the missing data. * `skip_labels (bool)`: If true, first row and column with labels is skipped +* `skipped_label_replacement (str)`: Replacement for the SkippedLabel +instances. **The return value is:** @@ -1034,7 +1077,8 @@ sheet.to_markdown(*, spaces_replacement: str = ' ', top_right_corner_text: str = "Sheet", na_rep: str = '', - skip_labels: bool = False + skip_labels: bool = False, + skipped_label_replacement: str = '' ) ``` Parameters are (all optional, all key-value only): @@ -1047,6 +1091,8 @@ descriptions (labels) are replaced with this string. * `na_rep (str)`: Replacement for the missing data. * `skip_labels (bool)`: If true, first row and column with labels is skipped +* `skipped_label_replacement (str)`: Replacement for the SkippedLabel +instances. **The return value is:** @@ -1071,7 +1117,8 @@ sheet.to_html_table(*, top_right_corner_text: str = "Sheet", na_rep: str = '', language_for_description: str = None, - skip_labels: bool = False + skip_labels: bool = False, + skipped_label_replacement: str = '' ) ``` Parameters are (all optional, all key-value only): @@ -1085,6 +1132,8 @@ of each computational cell is inserted as word of this language (if the property description is not set). * `skip_labels (bool)`: If true, first row and column with labels is skipped +* `skipped_label_replacement (str)`: Replacement for the SkippedLabel +instances. **The return value is:** diff --git a/portable_spreadsheet/__init__.py b/portable_spreadsheet/__init__.py index 2d6d8e7..7de07a5 100644 --- a/portable_spreadsheet/__init__.py +++ b/portable_spreadsheet/__init__.py @@ -4,6 +4,7 @@ from .cell_slice import CellSlice # noqa from .grammars import GRAMMARS # noqa from .grammar_utils import GrammarUtils # noqa +from .skipped_label import SkippedLabel # noqa -__version__ = "0.1.15" +__version__ = "1.0.0" __status__ = "Production" diff --git a/portable_spreadsheet/cell_indices.py b/portable_spreadsheet/cell_indices.py index 497a546..6870ad1 100644 --- a/portable_spreadsheet/cell_indices.py +++ b/portable_spreadsheet/cell_indices.py @@ -129,8 +129,8 @@ def __init__(self, self.columns[language] = cols self.user_defined_languages.append(language) # Define user defined names for rows and columns - self.rows_labels: List[str] = copy.deepcopy(rows_labels) - self.columns_labels: List[str] = copy.deepcopy(columns_labels) + self.rows_labels: list = copy.deepcopy(rows_labels) + self.columns_labels: list = copy.deepcopy(columns_labels) # Or define auto generated aliases as an integer sequence from 0 if rows_labels is None: self.rows_labels = [str(_row_n) for _row_n in @@ -138,6 +138,11 @@ def __init__(self, if columns_labels is None: self.columns_labels = [str(_col_n) for _col_n in range(number_of_columns)] + # String representation of indices + self.rows_labels_str: List[str] = \ + [str(lb) for lb in self.rows_labels] + self.columns_labels_str: List[str] = \ + [str(lb) for lb in self.columns_labels] # assign the help texts self.rows_help_text: List[str] = copy.deepcopy(rows_help_text) self.columns_help_text: List[str] = copy.deepcopy(columns_help_text) @@ -282,14 +287,11 @@ def expand_size(self, expanded.number_of_columns + new_number_of_columns ) ] - # Or define auto generated aliases as an integer sequence from 0 - if new_columns_labels is None: - expanded.columns_labels = [ - str(i) - for i in range( - expanded.number_of_columns + new_number_of_columns - ) - ] + # String representation of indices + expanded.rows_labels_str: List[str] = \ + [str(lb) for lb in expanded.rows_labels] + expanded.columns_labels_str: List[str] = \ + [str(lb) for lb in expanded.columns_labels] # assign the help texts if expanded.rows_help_text is not None and new_number_of_rows > 0: if new_rows_help_text is None: diff --git a/portable_spreadsheet/serialization.py b/portable_spreadsheet/serialization.py index 9e47a7c..c6b0d10 100644 --- a/portable_spreadsheet/serialization.py +++ b/portable_spreadsheet/serialization.py @@ -10,6 +10,7 @@ from .cell import Cell, CellValueError from .cell_type import CellType from .cell_indices import CellIndices +from .skipped_label import SkippedLabel # ==== TYPES ==== # Type for the output dictionary with the logic: @@ -159,7 +160,8 @@ def to_excel(self, "value": "Value", "description": "Description" }), - values_only: bool = False + values_only: bool = False, + skipped_label_replacement: str = '' ) -> None: """Export the values inside Spreadsheet instance to the Excel 2010 compatible .xslx file @@ -180,6 +182,8 @@ def to_excel(self, for the sheet with variables (first row in the sheet). values_only (bool): If true, only values (and not formulas) are exported. + skipped_label_replacement (str): Replacement for the SkippedLabel + instances. """ # Quick sanity check if ".xlsx" not in file_path[-5:]: @@ -276,26 +280,32 @@ def to_excel(self, if self.cell_indices.excel_append_column_labels: # Add labels of column for col_idx in range(self.shape[1]): + col_lbl = self.cell_indices.columns_labels[ + # Reflect the export offset + col_idx + self.export_offset[1] + ].replace(' ', spaces_replacement) + if isinstance(col_lbl, SkippedLabel): + col_lbl = skipped_label_replacement worksheet.write(0, col_idx + int( self.cell_indices.excel_append_row_labels ), - self.cell_indices.columns_labels[ - # Reflect the export offset - col_idx + self.export_offset[1] - ].replace(' ', spaces_replacement), + col_lbl, col_label_format) if self.cell_indices.excel_append_row_labels: # Add labels for rows for row_idx in range(self.shape[0]): + # Reflect the export offset + row_lbl = self.cell_indices.rows_labels[ + row_idx + self.export_offset[0] + ].replace(' ', spaces_replacement) + if isinstance(row_lbl, SkippedLabel): + row_lbl = skipped_label_replacement worksheet.write(row_idx + int( self.cell_indices.excel_append_column_labels ), 0, - self.cell_indices.rows_labels[ - # Reflect the export offset - row_idx + self.export_offset[0] - ].replace(' ', spaces_replacement), + row_lbl, row_label_format) # Store results workbook.close() @@ -416,8 +426,14 @@ def to_dictionary(self, # Export the spreadsheet to the dictionary (that can by JSON-ified) values = {x_start_key: {}} for idx_x in range(x_range): + if isinstance(x[idx_x], SkippedLabel): + # Skip labels that are intended to be skipped + continue y_values = {y_start_key: {}} for idx_y in range(y_range): + if isinstance(y[idx_y], SkippedLabel): + # Skip labels that are intended to be skipped + continue # Select the correct cell if by_row: cell = self._get_cell_at(idx_x, idx_y) @@ -471,6 +487,9 @@ def to_dictionary(self, # Add row description (and labels) data['rows'] = [] for idx, x_label in enumerate(x): + if isinstance(x[idx_x], SkippedLabel): + # Skip labels that are intended to be skipped + continue metadata: dict = {"name": x_label} if x_helptext is not None: # Add the help text (description) for the row @@ -484,6 +503,9 @@ def to_dictionary(self, # Add column description (and labels) data['columns'] = [] for idx, y_label in enumerate(y): + if isinstance(y[idx_y], SkippedLabel): + # Skip labels that are intended to be skipped + continue metadata = {"name": y_label} if y_helptext is not None: # Add the help text (description) for the column @@ -753,6 +775,7 @@ def generate_json_schema() -> dict: def to_string_of_values(self) -> str: """Export values inside table to the Python array definition string. + (Mainly helpful for debugging purposes) Returns: str: Python list definition string. @@ -777,7 +800,8 @@ def to_2d_list(self, *, top_right_corner_text: str = "Sheet", skip_labels: bool = False, na_rep: Optional[object] = None, - spaces_replacement: str = ' ',) -> List[List[object]]: + spaces_replacement: str = ' ', + skipped_label_replacement: str = '') -> List[List[object]]: """Export values 2 dimensional Python array. Args: @@ -790,6 +814,8 @@ def to_2d_list(self, *, equals to None). skip_labels (bool): If true, first row and column with labels is skipped + skipped_label_replacement (str): Replacement for the SkippedLabel + instances. Returns: List[List[object]]: Python array. @@ -806,17 +832,21 @@ def to_2d_list(self, *, row.append(top_right_corner_text) # Insert labels of columns: for col_i in range(self.shape[1]): - col = self.cell_indices.columns_labels[ + col_lbl = self.cell_indices.columns_labels[ col_i + self.export_offset[1] - ] - value = col.replace(' ', spaces_replacement) - row.append(value) + ].replace(' ', spaces_replacement) + if isinstance(col_lbl, SkippedLabel): + col_lbl = skipped_label_replacement + row.append(col_lbl) else: if not skip_labels: # Insert labels of rows - row.append(self.cell_indices.rows_labels[ + row_lbl = self.cell_indices.rows_labels[ row_idx + self.export_offset[0] - ].replace(' ', spaces_replacement)) + ].replace(' ', spaces_replacement) + if isinstance(row_lbl, SkippedLabel): + row_lbl = skipped_label_replacement + row.append(row_lbl) for col_idx in range(self.shape[1]): # Append actual values: cell_at_position = self._get_cell_at(row_idx, col_idx) @@ -844,6 +874,7 @@ def to_csv(self, *, sep: str = ',', line_terminator: str = '\n', + skipped_label_replacement: str = '' ) -> str: """Export values to the string in the CSV logic @@ -858,14 +889,19 @@ def to_csv(self, *, na_rep (str): Replacement for the missing data. skip_labels (bool): If true, first row and column with labels is skipped + skipped_label_replacement (str): Replacement for the SkippedLabel + instances. Returns: str: CSV of the values """ sheet_as_array = self.to_2d_list( - top_right_corner_text=top_right_corner_text, na_rep=na_rep, - skip_labels=skip_labels, spaces_replacement=spaces_replacement, - language=language + top_right_corner_text=top_right_corner_text, + na_rep=na_rep, + skip_labels=skip_labels, + spaces_replacement=spaces_replacement, + language=language, + skipped_label_replacement=skipped_label_replacement ) export = "" for row_idx in range(len(sheet_as_array)): @@ -882,7 +918,8 @@ def to_markdown(self, *, top_right_corner_text: str = "Sheet", skip_labels: bool = False, na_rep: Optional[object] = '', - spaces_replacement: str = ' ' + spaces_replacement: str = ' ', + skipped_label_replacement: str = '' ): """Export values to the string in the Markdown (MD) file logic @@ -895,14 +932,19 @@ def to_markdown(self, *, na_rep (str): Replacement for the missing data. skip_labels (bool): If true, first row and column with labels is skipped + skipped_label_replacement (str): Replacement for the SkippedLabel + instances. Returns: str: Markdown (MD) compatible table of the values """ sheet_as_array = self.to_2d_list( - top_right_corner_text=top_right_corner_text, na_rep=na_rep, - skip_labels=skip_labels, spaces_replacement=spaces_replacement, - language=language + top_right_corner_text=top_right_corner_text, + na_rep=na_rep, + skip_labels=skip_labels, + spaces_replacement=spaces_replacement, + language=language, + skipped_label_replacement=skipped_label_replacement ) export = "" for row_idx in range(len(sheet_as_array)): @@ -979,7 +1021,8 @@ def to_html_table(self, *, top_right_corner_text: str = "Sheet", na_rep: str = '', language_for_description: str = None, - skip_labels: bool = False) -> str: + skip_labels: bool = False, + skipped_label_replacement: str = '') -> str: """Export values to the string in the HTML table logic Args: @@ -992,6 +1035,8 @@ def to_html_table(self, *, language (if the property description is not set). skip_labels (bool): If true, first row and column with labels is skipped + skipped_label_replacement (str): Replacement for the SkippedLabel + instances. Returns: str: HTML table definition @@ -1013,7 +1058,9 @@ def to_html_table(self, *, export += "" col = self.cell_indices.columns_labels[ col_i + self.export_offset[1] - ] + ].replace(' ', spaces_replacement) + if isinstance(col, SkippedLabel): + col = skipped_label_replacement if (help_text := # noqa 203 self.cell_indices.columns_help_text) is not None: title_attr = ' title="{}"'.format( @@ -1022,7 +1069,7 @@ def to_html_table(self, *, else: title_attr = "" export += f'' - export += col.replace(' ', spaces_replacement) + export += col export += "" export += "" else: @@ -1037,9 +1084,12 @@ def to_html_table(self, *, title_attr = "" export += "" export += f'' - export += self.cell_indices.rows_labels[ + row_lbl = self.cell_indices.rows_labels[ row_idx + self.export_offset[0] ].replace(' ', spaces_replacement) + if isinstance(row_lbl, SkippedLabel): + row_lbl = skipped_label_replacement + export += row_lbl export += "" export += "" @@ -1075,9 +1125,9 @@ def columns(self) -> List[str]: Returns: List[str]: List of column labels """ - return self.cell_indices.columns_labels[ - self.export_offset[1]:(self.export_offset[1] + self.shape[1]) - ] + return self.cell_indices.columns_labels_str[self.export_offset[1]:( + self.export_offset[1] + self.shape[1]) + ] @property def index(self) -> List[str]: @@ -1086,6 +1136,6 @@ def index(self) -> List[str]: Returns: List[str]: List of row labels. """ - return self.cell_indices.rows_labels[ - self.export_offset[0]:(self.export_offset[0] + self.shape[0]) - ] + return self.cell_indices.rows_labels_str[self.export_offset[0]:( + self.export_offset[0] + self.shape[0]) + ] diff --git a/portable_spreadsheet/skipped_label.py b/portable_spreadsheet/skipped_label.py new file mode 100644 index 0000000..e628b66 --- /dev/null +++ b/portable_spreadsheet/skipped_label.py @@ -0,0 +1,39 @@ +class SkippedLabel(object): + """This label of columns or rows are intended to be skipped when exported. + + Attributes: + label: the label that is used for selection. + empty_entity: if true, row or column is intended to be empty + and are skipped for some types of exports. + """ + + def __init__(self, + label: str = None, + empty_entity: bool = False): + """Initialise skipped label. + + Args: + label: the label that is used for selection. + empty_entity: if true, row or column is intended to be empty + and are skipped for some types of exports. + """ + self.label: str = label + self.empty_entity: bool = empty_entity + + def replace(self, old, new, count: int = -1): + """Replace the characters inside the label. + Args: + old: old string to be replaced. + new: new string that is inserted instead of old one. + count: how many times should be repeated. + """ + if self.label is None: + return + self.label = self.label.replace(old, new, count) + return self + + def __str__(self): + """Overload to string method.""" + if self.label is None: + return "" + return self.label diff --git a/portable_spreadsheet/spreadsheet.py b/portable_spreadsheet/spreadsheet.py index 8d3180f..bed3e1c 100644 --- a/portable_spreadsheet/spreadsheet.py +++ b/portable_spreadsheet/spreadsheet.py @@ -1,5 +1,5 @@ from numbers import Number -from typing import Tuple, List, Union, Optional, Callable +from typing import Tuple, List, Union, Optional, Callable, TYPE_CHECKING import copy from .cell import Cell @@ -8,6 +8,9 @@ from .spreadsheet_utils import _Location, _Functionality, _SheetVariables from .serialization import Serialization +if TYPE_CHECKING: + from .skipped_label import SkippedLabel + # ==== TYPES ==== # Type for the sheet (list of the list of the cells) T_sheet = List[List[Cell]] @@ -69,8 +72,8 @@ def create_new_sheet( number_of_columns: int, rows_columns: Optional[T_lg_col_row] = None, /, *, # noqa E999 - rows_labels: List[str] = None, - columns_labels: List[str] = None, + rows_labels: List[Union[str, 'SkippedLabel']] = None, + columns_labels: List[Union[str, 'SkippedLabel']] = None, rows_help_text: List[str] = None, columns_help_text: List[str] = None, excel_append_row_labels: bool = True, @@ -84,10 +87,12 @@ def create_new_sheet( number_of_columns (int): Number of columns. rows_columns (T_lg_col_row): List of all row names and column names for each user defined language. - rows_labels (List[str]): List of masks (aliases) for row - names. - columns_labels (List[str]): List of masks (aliases) for column - names. + rows_labels (List[Union[str, SkippedLabel]]): List of masks + (aliases) for row names. If the instance of SkippedLabel is + used, the export skips this label. + columns_labels (List[Union[str, SkippedLabel]]): List of masks + (aliases) for column names. If the instance of SkippedLabel is + used, the export skips this label. rows_help_text (List[str]): List of help texts for each row. columns_help_text (List[str]): List of help texts for each column. excel_append_row_labels (bool): If True, one column is added @@ -146,8 +151,8 @@ def _set_item(self, raise ValueError("Only one of parameters 'index_integer' and" "'index_label' has to be set!") if index_label is not None: - _x = self.cell_indices.rows_labels.index(index_label[0]) - _y = self.cell_indices.columns_labels.index(index_label[1]) + _x = self.cell_indices.rows_labels_str.index(index_label[0]) + _y = self.cell_indices.columns_labels_str.index(index_label[1]) index_integer = (_x, _y) if index_integer is not None: _x = index_integer[0] @@ -188,8 +193,8 @@ def _get_item(self, raise ValueError("Only one of parameters 'index_integer' and" "'index_label' has to be set!") if index_label is not None: - _x = self.cell_indices.rows_labels.index(index_label[0]) - _y = self.cell_indices.columns_labels.index(index_label[1]) + _x = self.cell_indices.rows_labels_str.index(index_label[0]) + _y = self.cell_indices.columns_labels_str.index(index_label[1]) index_integer = (_x, _y) if index_integer is not None: _x = index_integer[0] @@ -203,7 +208,9 @@ def _get_item(self, def _get_slice(self, index_integer: Tuple[slice, slice], - index_label: Tuple[slice, slice]) -> CellSlice: + index_label: Tuple[slice, slice], + *, + include_right: bool = False) -> CellSlice: """Get the values in the slice. Args: @@ -212,6 +219,7 @@ def _get_slice(self, index_label (Tuple[object, object]): The position of the slice in the spreadsheet. Mutually exclusive with parameter index_integer (only one can be set to not None). + include_right (bool): If True, right most value is included. Returns: CellSlice: Slice of the cells (aggregate). """ @@ -219,23 +227,28 @@ def _get_slice(self, raise ValueError("Only one of parameters 'index_integer' and" "'index_label' has to be set!") + # This value is added to the right-most index in the slice + # It makes sure that the right-most value is included if needed. + slice_offset: int = 1 if include_right else 0 + if index_label is not None: + # If sliced by labels (not by integer positions) if isinstance(index_label[0], slice): # If the first index is slice _x_start = 0 if index_label[0].start: - _x_start = self.cell_indices.rows_labels.index( + _x_start = self.cell_indices.rows_labels_str.index( index_label[0].start) - _x_end = self.shape[0] + _x_end = self.shape[0] # in the case of ':' if index_label[0].stop: - _x_end = self.cell_indices.rows_labels.index( - index_label[0].stop) + _x_end = self.cell_indices.rows_labels_str.index( + index_label[0].stop) + slice_offset _x_step = 1 if index_label[0].step: _x_step = int(index_label[0].step) else: # If the first index is scalar - _x_start = self.cell_indices.rows_labels.index( + _x_start = self.cell_indices.rows_labels_str.index( index_label[0]) _x_end = _x_start + 1 _x_step = 1 @@ -244,18 +257,18 @@ def _get_slice(self, # If the second index is slice _y_start = 0 if index_label[1].start: - _y_start = self.cell_indices.columns_labels.index( + _y_start = self.cell_indices.columns_labels_str.index( index_label[1].start) - _y_end = self.shape[1] + _y_end = self.shape[1] # in the case of ':' if index_label[1].stop: - _y_end = self.cell_indices.columns_labels.index( - index_label[1].stop) + _y_end = self.cell_indices.columns_labels_str.index( + index_label[1].stop) + slice_offset _y_step = 1 if index_label[1].step: _y_step = int(index_label[1].step) else: - # If the first index is scalar - _y_start = self.cell_indices.columns_labels.index( + # If the second index is scalar + _y_start = self.cell_indices.columns_labels_str.index( index_label[1]) _y_end = _y_start + 1 _y_step = 1 @@ -275,12 +288,18 @@ def _get_slice(self, # Negative index starts from end if _x_end < 0: _x_end = self.shape[0] + _x_end + else: + # If the right-most value is included + # Relevant only for positive slice indices + _x_end += slice_offset _x_step = 1 if index_integer[0].step: _x_step = int(index_integer[0].step) else: # If the first index is scalar _x_start = index_integer[0] + if _x_start < 0: + _x_start = self.shape[0] + _x_start _x_end = _x_start + 1 _x_step = 1 @@ -298,12 +317,18 @@ def _get_slice(self, # Negative index starts from end if _y_end < 0: _y_end = self.shape[1] + _y_end + else: + # If the right-most value is included + # Relevant only for positive slice indices + _y_end += slice_offset _y_step = 1 if index_integer[1].step: _y_step = int(index_integer[1].step) else: - # If the first index is scalar + # If the second index is scalar _y_start = index_integer[1] + if _y_start < 0: + _y_start = self.shape[1] + _y_start _y_end = _y_start + 1 _y_step = 1 @@ -321,7 +346,9 @@ def _get_slice(self, def _set_slice(self, value: T_cell_val, index_integer: Tuple[int, int], - index_label: Tuple[object, object]) -> None: + index_label: Tuple[object, object], + *, + include_right: bool = False) -> None: """Set the value of each cell in the slice Args: @@ -331,8 +358,10 @@ def _set_slice(self, index_label (Tuple[object, object]): The position of the slice in the spreadsheet. Mutually exclusive with parameter index_integer (only one can be set to not None). + include_right (bool): If True, right most value is included. """ - cell_slice: CellSlice = self._get_slice(index_integer, index_label) + cell_slice: CellSlice = self._get_slice(index_integer, index_label, + include_right=include_right) cell_slice.set(value) def expand(self, @@ -340,8 +369,8 @@ def expand(self, new_number_of_columns: int, new_rows_columns: Optional[T_lg_col_row] = {}, /, *, # noqa E225 - new_rows_labels: List[str] = None, - new_columns_labels: List[str] = None, + new_rows_labels: List[Union[str, 'SkippedLabel']] = None, + new_columns_labels: List[Union[str, 'SkippedLabel']] = None, new_rows_help_text: List[str] = None, new_columns_help_text: List[str] = None ): @@ -352,10 +381,12 @@ def expand(self, new_number_of_columns (int): Number of columns to be added. new_rows_columns (T_lg_col_row): List of all row names and column names for each language to be added. - new_rows_labels (List[str]): List of masks (aliases) for row - names to be added. - new_columns_labels (List[str]): List of masks (aliases) for - column names to be added. + new_rows_labels (List[Union[str, SkippedLabel]]): List of masks + (aliases) for row names to be added. If the instance of + SkippedLabel is used, the export skips this label. + new_columns_labels (List[Union[str, 'SkippedLabel']]): List of + masks (aliases) for column names to be added. If the instance + of SkippedLabel is used, the export skips this label. new_rows_help_text (List[str]): List of help texts for each row to be added. new_columns_help_text (List[str]): List of help texts for each diff --git a/portable_spreadsheet/spreadsheet_utils.py b/portable_spreadsheet/spreadsheet_utils.py index d345051..ce5ef76 100644 --- a/portable_spreadsheet/spreadsheet_utils.py +++ b/portable_spreadsheet/spreadsheet_utils.py @@ -1,4 +1,4 @@ -from typing import Dict, Union, Optional +from typing import Dict, Union, Optional, TYPE_CHECKING import re import copy from numbers import Number @@ -6,6 +6,9 @@ from .cell import Cell from .cell_type import CellType +if TYPE_CHECKING: + from .cell_slice import CellSlice + # ========== File with the functionality for internal purposes only =========== @@ -69,6 +72,55 @@ def __getitem__(self, index): else: return self.spreadsheet._get_slice(None, index) + def get_slice(self, + index_row: Union[slice, int], + index_column: Union[slice, int], + *, + include_right: bool = False) -> 'CellSlice': + """Get the slice directly using method. + + Args: + index_row (slice): Position of the row inside spreadsheet. + index_column (slice): Position of the column inside spreadsheet. + include_right (bool): If True, right most value (end parameter + value) is included. + + Returns: + CellSlice: slice of the cells + """ + if self.by_integer: + return self.spreadsheet._get_slice((index_row, index_column), None, + include_right=include_right) + else: + return self.spreadsheet._get_slice(None, (index_row, index_column), + include_right=include_right) + + def set_slice(self, + index_row: Union[slice, int], + index_column: Union[slice, int], + value, + *, + include_right: bool = False) -> None: + """Get the slice directly using method. + + Args: + index_row (slice): Position of the row inside spreadsheet. + index_column (slice): Position of the column inside spreadsheet. + include_right (bool): If True, right most value (end parameter + value) is included. + value: The new value to be set. + """ + if self.by_integer: + return self.spreadsheet._set_slice(value, + (index_row, index_column), + None, + include_right=include_right) + else: + return self.spreadsheet._set_slice(value, + None, + (index_row, index_column), + include_right=include_right) + class _Functionality(object): """Class encapsulating some shortcuts for functionality. diff --git a/setup.py b/setup.py index 837880d..79211e5 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="portable-spreadsheet", - version="0.1.15", + version="1.0.0", author="David Salac", author_email="info@davidsalac.eu", description="Simple spreadsheet that keeps tracks of each operation in " diff --git a/tests/test_serialization.py b/tests/test_serialization.py index be1ff0d..66372b6 100644 --- a/tests/test_serialization.py +++ b/tests/test_serialization.py @@ -8,6 +8,7 @@ import numpy as np from portable_spreadsheet.spreadsheet import Spreadsheet +from portable_spreadsheet.skipped_label import SkippedLabel class TestSerialization(unittest.TestCase): @@ -170,6 +171,27 @@ def test_to_numpy(self): self.assertTrue(np.allclose(self.sheet.to_numpy(), self.inserted_rand_values)) + def test_to_2d_list_skipped_labels(self): + """Test when some labels are passed as SkippedLabel""" + sheet = Spreadsheet.create_new_sheet( + 2, 3, + rows_labels=[SkippedLabel('A'), 'B'], + columns_labels=['C1', SkippedLabel('C2'), 'C3'] + ) + sheet.iloc[:, :] = [[2, 1, 3], [7, 3, 9]] + # Test the setter + val_t = 72.8 + sheet.loc['A', 'C2'] = val_t + self.assertTrue(np.allclose(sheet.to_numpy(), [[2., val_t, 3.], + [7., 3., 9.]])) + # Test the getter + self.assertAlmostEqual(sheet.loc['A', 'C2'].value, val_t) + # Test export (regression test) + self.assertListEqual([['Sheet', 'C1', 'HH', 'C3'], + ['HH', 2, 72.8, 3], + ['B', 7, 3, 9]], + sheet.to_2d_list(skipped_label_replacement="HH")) + def test_to_2d_list(self): """Test the serialization to 2D list""" # Check values diff --git a/tests/test_spreadsheet.py b/tests/test_spreadsheet.py index 7cb960d..5a45bf5 100644 --- a/tests/test_spreadsheet.py +++ b/tests/test_spreadsheet.py @@ -332,6 +332,22 @@ def test_slice(self): # Test getter self.assertAllClose2D(sheet.iloc[i_idx].to_numpy(), np_sheet[i_idx]) + def test_right_most_included_slices(self): + """Test the method selectors (get_slice and set_slice).""" + # A) Test the setter + self.sheet.iloc.set_slice(slice(0, 5), 0, [1., 2., 3., 4., 5., 6.], + include_right=True) + self.assertTrue(np.allclose(self.sheet.to_numpy()[:6, 0], + [1., 2., 3., 4., 5., 6.])) + # B) Test the getter + self.assertTrue( + np.allclose( + self.sheet.iloc.get_slice( + slice(0, 5), 0, include_right=True).to_numpy().transpose(), + [1., 2., 3., 4., 5., 6.] + ) + ) + def test_versions(self): """Tests if the version in __init__.py matches to the setup.py one. """