From c0d9c16dc2a9a8173d8857f94b062c25fb485bee Mon Sep 17 00:00:00 2001 From: PH Tools Date: Wed, 7 Dec 2022 18:26:20 -0500 Subject: [PATCH] fix(xl_app): Refactor row and column XL-read --- PHX/PHPP/phpp_app.py | 6 +-- PHX/PHPP/sheet_io/io_areas.py | 45 +++++++++++------- PHX/xl/xl_app.py | 90 +++++++++++++++++++++++++++-------- PHX/xl/xl_data.py | 9 +++- PHX/xl/xl_typing.py | 18 ++++++- _testing_to_PHPP.py | 12 ++--- 6 files changed, 131 insertions(+), 49 deletions(-) diff --git a/PHX/PHPP/phpp_app.py b/PHX/PHPP/phpp_app.py index c6843a2..51c4e5c 100644 --- a/PHX/PHPP/phpp_app.py +++ b/PHX/PHPP/phpp_app.py @@ -36,7 +36,7 @@ def get_data_worksheet(_xl: xl_app.XLConnection) -> Sheet: def get_phpp_version(_xl: xl_app.XLConnection, _search_col: str = "A", _row_start: int = 1, - _row_end: int = 5) -> Tuple[str, str]: + _row_end: int = 10) -> Tuple[str, str]: """Find the PHPP Version and Language of the active xl-file. Arguments: @@ -44,7 +44,7 @@ def get_phpp_version(_xl: xl_app.XLConnection, * _xl (xl_app.XLConnection): * _search_col (str) * _row_start (int) default=1 - * _row_end (int) default=5 + * _row_end (int) default=10 Returns: -------- @@ -74,7 +74,7 @@ def get_phpp_version(_xl: xl_app.XLConnection, # -- Pull the search row data from the Active XL Instance data = _xl.get_single_row_data(data_worksheet.name, data_row) - data = [_ for _ in data if _ is not None] + data = [_ for _ in data if _ is not None and _ is not ""] # -- Find the right Versions number version = str(data[1]).upper().strip().replace(" ", "").replace(".", "_") diff --git a/PHX/PHPP/sheet_io/io_areas.py b/PHX/PHPP/sheet_io/io_areas.py index 8cd8fe8..a74381e 100644 --- a/PHX/PHPP/sheet_io/io_areas.py +++ b/PHX/PHPP/sheet_io/io_areas.py @@ -79,7 +79,7 @@ def find_section_header_row(self, _row_start: int = 1, _row_end: int = 100) -> i _row_end=_row_end, ) - for i, val in enumerate(xl_data): + for i, val in enumerate(xl_data, start=1): if val == self.shape.surface_rows.locator_string_header: return i @@ -102,7 +102,7 @@ def find_section_first_entry_row(self) -> int: for i, val in enumerate(xl_data, start=self.section_header_row): try: - val = str(int(val)) # Value comes in as "1.0" from Excel? + val = str(int(val)) # type: ignore - Value comes in as "1.0" from Excel? except: continue @@ -142,8 +142,8 @@ def get_surface_phpp_id_by_name(self, _name: str, _use_cache: bool = False) -> s self.shape.name, f"{col_offset(str(self.shape.surface_rows.inputs.description.column), -1)}{row}", ) - print(f"Getting PHPP Surface id for {_name}") - name = f"{int(prefix)}-{_name}" + self.xl.output(f"Getting PHPP Surface id for {_name}") + name = f"{int(str(prefix))}-{_name}" # -- Save in cache self.surface_cache[_name] = name @@ -155,8 +155,20 @@ class ThermalBridges: def __init__(self, _xl: xl_app.XLConnection, _shape: shape_model.Areas): self.xl = _xl self.shape = _shape - self.section_header_row: Optional[int] = None - self.section_first_entry_row: Optional[int] = None + self._section_header_row: Optional[int] = None + self._section_first_entry_row: Optional[int] = None + + @property + def section_header_row(self) -> int: + if not self._section_header_row: + self._section_header_row = self.find_section_header_row() + return self._section_header_row + + @property + def section_first_entry_row(self) -> int: + if not self._section_first_entry_row: + self._section_first_entry_row = self.find_section_first_entry_row() + return self._section_first_entry_row def find_section_header_row(self, _row_start: int = 100, _row_end: int = 500) -> int: """Return the row number of the 'Thermal Bridge input' section header.""" @@ -189,9 +201,6 @@ def find_section_header_row(self, _row_start: int = 100, _row_end: int = 500) -> def find_section_first_entry_row(self) -> int: """Return the row number of the very first user-input entry row in the 'Thermal Bridge input' section.""" - if not self.section_header_row: - self.section_header_row = self.find_section_header_row() - xl_data = self.xl.get_single_column_data( _sheet_name=self.shape.name, _col=self.shape.thermal_bridge_rows.locator_col_entry, @@ -201,7 +210,7 @@ def find_section_first_entry_row(self) -> int: for i, val in enumerate(xl_data, start=self.section_header_row): try: - val = str(int(val)) # Value comes in as "1.0" from Excel? + val = str(int(val)) # type: ignore - Value comes in as "1.0" from Excel? except: continue @@ -225,20 +234,20 @@ def __init__(self, _xl: xl_app.XLConnection, _shape: shape_model.Areas): def write_thermal_bridges( self, _tbs: List[areas_thermal_bridges.ThermalBridgeRow] ) -> None: - if not self.thermal_bridges.section_first_entry_row: - self.thermal_bridges.section_first_entry_row = ( - self.thermal_bridges.find_section_first_entry_row() - ) + # if not self.thermal_bridges.section_first_entry_row: + # self.thermal_bridges.section_first_entry_row = ( + # self.thermal_bridges.find_section_first_entry_row() + # ) for i, tb in enumerate(_tbs, start=self.thermal_bridges.section_first_entry_row): for item in tb.create_xl_items(self.shape.name, _row_num=i): self.xl.write_xl_item(item) def write_surfaces(self, _surfaces: List[areas_surface.SurfaceRow]) -> None: - if not self.surfaces.section_first_entry_row: - self.surfaces.section_first_entry_row = ( - self.surfaces.find_section_first_entry_row() - ) + # if not self.surfaces.section_first_entry_row: + # self.surfaces.section_first_entry_row = ( + # self.surfaces.find_section_first_entry_row() + # ) for i, surface in enumerate( _surfaces, start=self.surfaces.section_first_entry_row diff --git a/PHX/xl/xl_app.py b/PHX/xl/xl_app.py index 20e6cac..31a3e01 100644 --- a/PHX/xl/xl_app.py +++ b/PHX/xl/xl_app.py @@ -60,6 +60,8 @@ def __init__(self, _value, _range, _worksheet, _e): # ----------------------------------------------------------------------------- + + def silent_print(_input: Any) -> None: """Default 'output' for XLConnection.""" return @@ -198,12 +200,31 @@ def create_new_worksheet(self, _sheet_name: str) -> None: self.get_sheet_by_name(_sheet_name).clear() + def get_last_used_row_num_in_column(self, _sheet_name: str, _col: str) -> int: + """Return the row number of the last cell in a column with a value in it. + + Arguments: + ---------- + * _sheet_name (str): The name of the Worksheet to read from. + * _col (str): The Alpha character of the column to read. + + Returns: + -------- + * (int): The number of the last row in the column with a value. + """ + sheet = self.get_sheet_by_name(_sheet_name) + sheet.activate() + col_range = sheet.range(f"{_col}:{_col}") + col_last_cell_range = sheet.range(col_range.last_cell.address) + group_last_cell_range = col_last_cell_range.end("up") # same as 'Ctrl-Up' + return group_last_cell_range.row + def get_single_column_data( self, _sheet_name: str, _col: str, - _row_start: int = 1, - _row_end: int = 100, + _row_start: Optional[int] = None, + _row_end: Optional[int] = None, ) -> List[xl_data.xl_range_value]: """Return a list with the values read from a single column of the excel document. @@ -211,36 +232,66 @@ def get_single_column_data( ---------- * _sheet_name: (str) The Excel Worksheet to read from. * _col: (str) The Column letter to read. - * _row_start: (int) default=1 - * _row_end: (int) default=100 + * _row_start: (Optional[int]) default=None + * _row_end: (Optional[int]) default=None Returns: -------- (List[xl_range_value]): The data from Excel worksheet, as a list. """ - return ( - self.get_sheet_by_name(_sheet_name) - .range(f"{_col}{_row_start}:{_col}{_row_end}") - .value - ) + if not _row_start or not _row_end: + _row_start = 1 + _row_end = self.get_last_used_row_num_in_column(_sheet_name, _col) + + address = f"{_col}{_row_start}:{_col}{_row_end}" + self.output(f"Reading: '{address}' data on sheet: '{_sheet_name}'") + + sheet = self.get_sheet_by_name(_sheet_name) + sheet.activate() + col_range = sheet.range(f"{address}") + return col_range.value # type: ignore + + def get_last_used_column_in_row(self, _sheet_name: str, _row: int) -> str: + """Return the column letter of the last cell in a column with a value in it. + + Arguments: + ---------- + * _sheet_name (str): The name of the Worksheet to read from. + * _col (str): The Alpha character of the column to read. + + Returns: + -------- + * (str): The Letter of the last column in the row with a value. + """ + sheet = self.get_sheet_by_name(_sheet_name) + sheet.activate() + row_range = sheet.range(f"{_row}:{_row}") + row_last_cell_range = sheet.range(row_range.last_cell.address) + group_last_cell_range = row_last_cell_range.end("left") # same as 'Ctrl-Left' + return xl_data.xl_chr(xl_data.xl_ord("A") + group_last_cell_range.column - 1) def get_single_row_data( self, _sheet_name: str, _row_number: int - ) -> List[xl_data.xl_range_value]: - """ + ) -> List[xl_data.xl_range_single_value]: + """Return all the data from a single Row in the Excel Workbook. Arguments: ---------- * _sheet_name (str): The name of the sheet to read - * _row_number (int): The row to read + * _row_number (int): The row number to read Returns: -------- - * (List[xl_data.xl_range_value]) The data read from XL. + * (List[xl_data.xl_range_single_value]) A List of the data read from XL. """ - sht = self.get_sheet_by_name(_sheet_name) - return sht.range((_row_number, 1), (_row_number, 500)).value + self.output(f"Reading: Row-{_row_number} on sheet: '{_sheet_name}'") + + sheet = self.get_sheet_by_name(_sheet_name) + sheet.activate() + last_col_letter = self.get_last_used_column_in_row(_sheet_name, _row_number) + row_range = sheet.range(f"A{_row_number}:{last_col_letter}{_row_number}") + return row_range.value # type: ignore def get_multiple_column_data( self, @@ -249,8 +300,8 @@ def get_multiple_column_data( _col_end: str, _row_start: int = 1, _row_end: int = 100, - ) -> List[List[xl_data.xl_range_value]]: - """Return a list with the values read from a specified block of the xl document. + ) -> xl_data.xl_range_value: + """Return a list of lists with the values read from a specified block of the xl document. Arguments: ---------- @@ -268,8 +319,7 @@ def get_multiple_column_data( if _col_start == _col_end: raise ReadMultipleColumnsError(_col_start, _col_end) - # -- Use xl.Range() instead of ord() since ord('KL') and similar will fail - # rng = xw.Range(f"{_col_end}1:{_col_start}1") + # -- Use xl.Range() instead of ord() since ord('KL') will fail rng: xl_Range_Protocol = self.xl.Range(f"{_col_end}1:{_col_start}1") _ndim = len(rng.columns) @@ -306,7 +356,7 @@ def write_xl_item(self, _xl_item: xl_data.XlItem) -> None: ) xl_sheet = self.get_sheet_by_name(_xl_item.sheet_name) xl_range = xl_sheet.range(_xl_item.xl_range) - xl_range.value = _xl_item.write_value + xl_range.value = _xl_item.write_value # type: ignore if _xl_item.font_color or _xl_item.range_color: xl_range.color = _xl_item.range_color diff --git a/PHX/xl/xl_data.py b/PHX/xl/xl_data.py index 13cc94e..542aede 100644 --- a/PHX/xl/xl_data.py +++ b/PHX/xl/xl_data.py @@ -9,7 +9,14 @@ from ph_units import converter xl_writable = Optional[Union[str, float, int, List, Tuple]] -xl_range_value = Optional[Union[str, float, int]] +xl_range_single_value = Union[str, float, int, None] +xl_range_list1D_value = Union[List[str], List[float], List[int], List[None]] +xl_range_list2D_value = Union[ + List[List[str]], List[List[float]], List[List[int]], List[List[None]] +] +xl_range_value = Union[ + xl_range_single_value, xl_range_list1D_value, xl_range_list2D_value +] def xl_ord(_col: str) -> int: diff --git a/PHX/xl/xl_typing.py b/PHX/xl/xl_typing.py index 82f97f1..39eb35d 100644 --- a/PHX/xl/xl_typing.py +++ b/PHX/xl/xl_typing.py @@ -31,12 +31,23 @@ def __len__(self) -> int: ... +class xl_CellRange_Protocol: + def __init__(self): + self.address: str + + class xl_Range_Protocol: def __init__(self): - self.value: xl_data.xl_writable + self.value: xl_data.xl_range_value self.color: Optional[Tuple[int, ...]] self.font: xl_Range_Font = xl_Range_Font() self.columns: xl_RangeColumns_Protocol = xl_RangeColumns_Protocol() + self.last_cell: xl_CellRange_Protocol + self.row: int + self.column: int + + def end(self, *args, **kwargs) -> "xl_Range_Protocol": + return xl_Range_Protocol() def options(self, ndim: int) -> "xl_Range_Protocol": return xl_Range_Protocol() @@ -48,6 +59,8 @@ def offset(self, row_offset: int = 0, column_offset: int = 0) -> "xl_Range_Proto class xl_API_Protocol: def __init__(self, sheet): self.sheet: "xl_Sheet_Protocol" = sheet + self.rows: Dict + self.Rows: Dict def unprotect(self): self.sheet.protected = False @@ -80,6 +93,9 @@ def clear_formats(self) -> None: def clear(self) -> None: return None + def activate(self) -> None: + ... + class xl_Sheets_Protocol: def __init__(self): diff --git a/_testing_to_PHPP.py b/_testing_to_PHPP.py index cb35e42..efc8b09 100644 --- a/_testing_to_PHPP.py +++ b/_testing_to_PHPP.py @@ -45,9 +45,9 @@ phpp_conn = phpp_app.PHPPConnection(xl, phpp_shape) try: - xl.output( - f"[bold green]> connected to excel doc: {phpp_conn.xl.wb.name}[/bold green]" - ) + clr = "bold green" + msg = f"[{clr}]> connected to excel doc: {phpp_conn.xl.wb.name}[/{clr}]" + xl.output(msg) except xl_app.NoActiveExcelRunningError as e: raise e @@ -56,10 +56,10 @@ # phpp_conn.write_certification_config(phx_project) # phpp_conn.write_climate_data(phx_project) # phpp_conn.write_project_constructions(phx_project) - # phpp_conn.write_project_tfa(phx_project) - # phpp_conn.write_project_opaque_surfaces(phx_project) + phpp_conn.write_project_tfa(phx_project) + phpp_conn.write_project_opaque_surfaces(phx_project) # phpp_conn.write_project_thermal_bridges(phx_project) - phpp_conn.write_project_window_components(phx_project) + # phpp_conn.write_project_window_components(phx_project) # phpp_conn.write_project_window_surfaces(phx_project) # phpp_conn.write_project_window_shading(phx_project) # phpp_conn.write_project_ventilation_components(phx_project)