Files
VSTU_Schedule_Parser/translations.py
2026-03-18 22:15:49 +03:00

325 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Copyright Stanislav Mironov
from abc import ABC, abstractmethod
from datetime import time
import openpyxl
import xlrd
from coord import Coord, Merged
EMPTY_CTYPES = [xlrd.XL_CELL_EMPTY, xlrd.XL_CELL_BLANK]
class TranschendentnostCell:
def __init__(self, value, is_empty, is_time=False):
self.value = value
self.is_time = isinstance(value, time) or is_time
self._is_empty = is_empty
def is_nospace_nocase_same(self, query):
try:
if self.value.lower().replace(" ", "").strip() == query.lower().replace(" ", "").strip():
return True
except: pass
return False
def is_empty(self):
return self._is_empty
# --- Абстрактный базовый класс (Контракт) ---
class ExcelSheetReader(ABC):
"""
Абстрактный базовый класс, определяющий интерфейс для чтения данных из листа Excel.
Использует 0-индексированную систему координат, как в xlrd.
"""
def __init__(self, file_path):
self.file_path = file_path
@abstractmethod
def get_sheet_index(self):
pass
@abstractmethod
def get_sheet_name(self):
pass
@abstractmethod
def has_next_sheet(self):
pass
@abstractmethod
def next_sheet(self):
pass
@abstractmethod
def get_cell_value(self, row, col):
"""Возвращает значение ячейки по 0-индексированным координатам."""
pass
@abstractmethod
def get_border_style(self, coord: Coord, side):
"""
Возвращает числовой стиль границы (как в xlrd).
side: 'left', 'right', 'top', 'bottom'
"""
pass
@abstractmethod
def get_merged_cells(self):
"""Возвращает список объединенных ячеек в формате xlrd: [(rlo, rhi, clo, chi), ...]."""
pass
@abstractmethod
def get_row_count(self):
"""Возвращает общее количество строк на листе."""
pass
@abstractmethod
def get_row_values(self, row_index):
"""Возвращает список значений всех ячеек в строке."""
pass
#@abstractmethod
def info(self):
"""Возвращает строку информации"""
return "TODO: info"
@abstractmethod
def cell(self, row, col) -> TranschendentnostCell:
"""Возвращает абстрактную клетку"""
pass
def find(self, query = None, startswith=False, nospace=False):
return self.find_any([query], startswith=startswith, nospace=nospace)
def find_any(self, query = None, startswith=False, nospace=False):
for rx in range(self.get_row_count()):
i = 0
for x in self.get_row_values(rx):
if nospace:
x = str(x).replace(" ", "").strip()
for query_selected in query:
if x == query_selected:
return Coord(rx, i)
elif startswith:
try:
if str(x).lower().startswith(query_selected.lower()):
return Coord(rx, i)
except: pass
i += 1
return None
def get_merged(self, rowx: int, colx: int):
"""Даём ей координаты ячейки таблицы а она выдаёт её границы если переданные координаты находятся 'внутри' объединённой ячейки"""
for crange in self.get_merged_cells():
rlo, rhi, clo, chi = crange
chi -= 1
rhi -= 1
if rlo <= rowx <= rhi and chi >= colx >= clo:
return rlo, clo, rhi, chi
# если ячейка не часть объединённых то начала и концы у неё равны.
return rowx, colx, rowx, colx
def get_merged_coord(self, coord: "Coord") -> Merged:
merged = self.get_merged(coord.row, coord.col)
return Merged(coord1=Coord(merged[0], merged[1]), coord2=Coord(merged[2], merged[3]))
# --- Реализация №1: Обертка для xlrd ---
class XlrdSheetReader(ExcelSheetReader):
def __init__(self, file_path, sheet_index=0):
super().__init__(file_path)
self.sheet_index = sheet_index
self.book = xlrd.open_workbook(file_path, formatting_info=True)
self.init_sheet()
def get_sheet_index(self):
return self.sheet_index
def init_sheet(self):
self.sheet = self.book.sheet_by_index(self.sheet_index)
def get_sheet_name(self):
return self.sheet.name
def has_next_sheet(self):
return self.sheet_index < len(self.book.sheet_names())-1
def next_sheet(self):
if self.has_next_sheet():
self.sheet_index += 1
self.init_sheet()
def get_cell_value(self, row, col):
# Проверка на выход за пределы таблицы, чтобы избежать ошибок
if row < self.sheet.nrows and col < self.sheet.ncols:
return self.sheet.cell_value(row, col)
return None
def info(self):
return """[XLRD (.xls)] The number of worksheets is {0}
Worksheet name(s): {1}
'{2}': size: {3}x{4}""".format(self.book.nsheets, self.book.sheet_names(), self.sheet.name, self.sheet.nrows, self.sheet.ncols)
def cell(self, row, col):
"""Возвращает абстрактную клетку"""
c = self.sheet.cell(row, col)
is_empty = c.ctype in EMPTY_CTYPES
is_time = c.ctype == xlrd.XL_CELL_DATE
value = c.value
if is_empty:
value = ""
elif is_time:
if isinstance(value, float):
if value <= 1:
seconds = round(value * 86400)
minutes, seconds = divmod(seconds, 60)
hours, minutes = divmod(minutes, 60)
value = time(hour=hours, second=seconds, minute=minutes)
else:
print(f"TODO: value is {value} its unix? not 0.xxxxxxxx")
else:
is_time = False
print("IsTime but not float!")
return TranschendentnostCell(value, is_empty, is_time=is_time)
def get_border_style(self, coord: Coord, side):
row = coord.row
col = coord.col
if row >= self.sheet.nrows or col >= self.sheet.ncols:
return 0 # Нет границы за пределами листа
cell = self.sheet.cell(row, col)
xf = self.book.xf_list[cell.xf_index]
border = xf.border
style_map = {
'left': border.left_line_style,
'right': border.right_line_style,
'top': border.top_line_style,
'bottom': border.bottom_line_style,
}
return style_map.get(side, 0)
def get_merged_cells(self):
return self.sheet.merged_cells
def get_row_count(self):
return self.sheet.nrows
def get_row_values(self, row_index):
if row_index < self.sheet.nrows:
return self.sheet.row_values(row_index)
return []
# --- Реализация №2: Обертка-транслятор для openpyxl ---
class OpenpyxlSheetReader(ExcelSheetReader):
def __init__(self, file_path, sheet_name=None):
super().__init__(file_path)
self.sheet_index = 0
self.workbook = openpyxl.load_workbook(file_path, data_only=True)
self.init_sheet()
# Словарь для трансляции стилей границ openpyxl в числовые коды xlrd
self.BORDER_STYLE_MAP = {
'thin': 1, 'medium': 2, 'dashed': 3, 'dotted': 4, 'thick': 5,
'double': 6, 'hair': 7, 'mediumDashed': 8, 'dashDot': 9,
'mediumDashDot': 10, 'dashDotDot': 11, 'mediumDashDotDot': 12,
'slantDashDot': 13
}
def get_sheet_index(self):
return self.sheet_index
def get_sheet_name(self):
return self.workbook.sheetnames[self.sheet_index]
def has_next_sheet(self):
return self.sheet_index < len(self.workbook.sheetnames)-1
def next_sheet(self):
if self.has_next_sheet():
self.sheet_index += 1
self.init_sheet()
def init_sheet(self):
self.sheet = self.workbook[self.workbook.sheetnames[self.sheet_index]]
def info(self):
return """[OpenPyXL (.xlsx)] The number of worksheets is {0}
Worksheet name(s): {1}
'{2}': size: {3}x{4}""".format(len(self.workbook.sheetnames), self.workbook.sheetnames, self.sheet, self.sheet.max_row, self.sheet.max_column)
def _get_cell(self, row, col):
"""Внутренний метод для получения ячейки с преобразованием координат."""
# openpyxl использует 1-индексированную систему
if row < self.sheet.max_row and col < self.sheet.max_column:
return self.sheet.cell(row=row + 1, column=col + 1)
return None
def cell(self, row, col):
"""Возвращает абстрактную клетку"""
c = self._get_cell(row, col)
is_empty = (c.value is None)
return TranschendentnostCell("" if is_empty else c.value, is_empty, is_time=isinstance(c.value, time))
def get_cell_value(self, row, col):
cell = self._get_cell(row, col)
return cell.value if cell else None
def get_border_style(self, coord: Coord, side):
cell = self._get_cell(coord.row, coord.col)
if not cell:
return 0
border_side = getattr(cell.border, side, None)
if border_side:
return self.BORDER_STYLE_MAP.get(border_side.style, 0)
return 0
def get_merged_cells(self):
# Преобразуем формат объединенных ячеек openpyxl в формат xlrd
merged_list = []
for merged_range in self.sheet.merged_cells.ranges:
# Преобразуем 1-индексированные границы в 0-индексированный формат xlrd (rlo, rhi, clo, chi)
# rhi и chi в xlrd являются эксклюзивными (до, но не включая)
rlo = merged_range.min_row - 1
rhi = merged_range.max_row
clo = merged_range.min_col - 1
chi = merged_range.max_col
merged_list.append((rlo, rhi, clo, chi))
return merged_list
def get_row_count(self):
return self.sheet.max_row
def get_row_values(self, row_index):
if row_index < self.sheet.max_row:
# Получаем значения из генератора
return [cell.value for cell in self.sheet[row_index + 1]]
return []
# --- Фабричная функция (единственная точка входа) ---
def create_reader(file_path, **kwargs) -> ExcelSheetReader:
"""
Создает и возвращает подходящий экземпляр ридера в зависимости от расширения файла.
"""
if file_path.lower().endswith('.xlsx'):
return OpenpyxlSheetReader(file_path, **kwargs)
elif file_path.lower().endswith('.xls'):
return XlrdSheetReader(file_path, **kwargs)
else:
raise ValueError("Неподдерживаемый формат файла. Используйте .xls или .xlsx")