countings should work

This commit is contained in:
Kyattsukuro 2026-03-14 15:55:21 +01:00
parent 94cb08e321
commit 3891c9c0bf
17 changed files with 95 additions and 48 deletions

View File

@ -13,7 +13,8 @@ EXPECTED_INPUT_TABLE_SCHEMA = [
"Bemerkung" "Bemerkung"
] ]
EXPECTED_OUTPUT_TABLE_SCHEMA = deepcopy(EXPECTED_INPUT_TABLE_SCHEMA).remove("Geschlecht") EXPECTED_OUTPUT_TABLE_SCHEMA = deepcopy(EXPECTED_INPUT_TABLE_SCHEMA)
EXPECTED_OUTPUT_TABLE_SCHEMA.remove("Geschlecht")
COL_INSTRUCTIONS = { COL_INSTRUCTIONS = {
("Art",): { ("Art",): {
@ -22,15 +23,20 @@ COL_INSTRUCTIONS = {
}, },
("Ort",): { ("Ort",): {
"command": "REGEX_MOVE_TO_COL", "command": "REGEX_MOVE_TO_COL",
"with_input_iteration": CATCHED_BY, # Moves matching group {{% 1 %}} to "to" "with_input_iteration": CATCHED_BY, # Moves matching group {{% 0 %}} to "to"
"match": r"^(?:[^ ]* +)*({{% 1 %}}),?(?: .*+)*$", "match": r"^(?:[^ ]* +)*({{% 0 %}}),?(?: .*+)*$",
"to": "Fangart" "to": "Fangart"
}, },
("Fangart",): { ("Anzahl",): {
"command": "REGEX_EDIT_COL", "command": "REGEX_EDIT_COL",
"match": r"^ *$", "match": r"^ *$",
"replace_with": "1" "replace_with": "1"
}, },
("Anzahl",): {
"command": "REGEX_GROUP_VALUEBASED_CHOOS",
"match": r"^ *([0-9]+) *oder *([0-9]+) *$",
"highest": True
},
} }
ROW_INSTRUCTION = [ ROW_INSTRUCTION = [
@ -38,7 +44,7 @@ ROW_INSTRUCTION = [
"command": "CONDITIONAL_COPY", "command": "CONDITIONAL_COPY",
"when_regex": { "when_regex": {
"Geschlecht": r"^(?:[^ ]* +)*(m),?(?: .*+)*$", "Geschlecht": r"^(?:[^ ]* +)*(m),?(?: .*+)*$",
"Anzahl": r"/d" "Anzahl": r"([0-9]+)"
}, },
"source": "Anzahl", "source": "Anzahl",
"target": "Davon m" "target": "Davon m"
@ -47,7 +53,7 @@ ROW_INSTRUCTION = [
"command": "CONDITIONAL_COPY", "command": "CONDITIONAL_COPY",
"when_regex": { "when_regex": {
"Geschlecht": r"^(?:[^ ]* +)*(w),?(?: .*+)*$", "Geschlecht": r"^(?:[^ ]* +)*(w),?(?: .*+)*$",
"Anzahl": r"/d" "Anzahl": r"([0-9]+)"
}, },
"source": "Anzahl", "source": "Anzahl",
"target": "Davon w" "target": "Davon w"

View File

@ -8,6 +8,6 @@ class Copy:
def execute_on_row(self, row=dict[str, str], source_col=None): def execute_on_row(self, row=dict[str, str], source_col=None):
return { return {
**({} if not self.replace else {source_col: ""}), **({} if not self.replace else {self.source_col: ""}),
self.target: row[source_col] self.target: row[self.source].value
} }

View File

@ -5,7 +5,9 @@ class FindAndReplace:
self.map = map self.map = map
def execute_on_cell(self, entry) -> str: def execute_on_cell(self, entry) -> str:
res = deepcopy(entry.value)
for species, alternatives in self.map.items(): for species, alternatives in self.map.items():
for old_version in alternatives: for old_version in alternatives:
if old_version in entry.value: if old_version in entry.value:
return deepcopy(entry.value).replace(old_version, species) res = res.replace(old_version, species)
return res

View File

@ -5,7 +5,7 @@ class RegexEditCol:
self.match = match self.match = match
self.replace_with = replace_with self.replace_with = replace_with
def execute_on_cell(self, value) -> str: def execute_on_cell(self, entry) -> str:
if value is None: if entry.value is None:
return value return entry.value
return re.sub(self.match, self.replace_with, str(value)) return re.sub(self.match, self.replace_with, entry.value)

View File

@ -0,0 +1,21 @@
import regex
class RegexGroupValuebasedChoose:
def __init__(self, match: str, highest: bool):
self.match = match
self.highest = highest
def execute_on_cell(self, entry):
matches = regex.match(self.match, entry.value)
if not matches:
return None
groups = matches.groups(default=[])
values = sorted(map(lambda x: int(x), groups))
if not len(values):
return None
if self.highest:
return str(values[-1])
else:
return str(values[0])

View File

@ -1,12 +1,20 @@
import regex import regex
import re
class RegexMoveToCol: class RegexMoveToCol:
def __init__(self, match: str, to: str, with_input_iteration: list[str]|None = None): def __init__(self, match: str, to: str, with_input_iteration: list[str]|None = None):
self.match = match self.match = match
self.to = to self.to = to
if with_input_iteration is not None and type(with_input_iteration) != list: if with_input_iteration is not None:
with_input_iteration = list(map([e] for e in with_input_iteration)) with_input_iteration = (
[[with_input_iteration]]
if isinstance(with_input_iteration, str)
else [
x if isinstance(x, list) else [x]
for x in (with_input_iteration if isinstance(with_input_iteration, (list, tuple)) else [with_input_iteration])
]
)
self.with_input_iteration = with_input_iteration self.with_input_iteration = with_input_iteration
self.__operation_input_depth = self.__find_operation_input_depth() self.__operation_input_depth = self.__find_operation_input_depth()
@ -14,48 +22,54 @@ class RegexMoveToCol:
def __find_operation_input_depth(self): def __find_operation_input_depth(self):
if not self.with_input_iteration: if not self.with_input_iteration:
return None return None
return max(len(option) for option in self.__iteration_input_depth()) return max(len(option) for option in self.with_input_iteration)
@staticmethod @staticmethod
def _get_merged_groups(regex_pattern: str, entry: str) -> str: def _get_merged_groups(regex_pattern: str, value: str) -> str:
match = regex.search(regex_pattern, entry.value) match = regex.search(regex_pattern, value)
if not match: if not match:
return "" return None
return "".join(g for g in match.groups() if g is not None) return "".join(g for g in match.groups() if g is not None)
@staticmethod @staticmethod
def _remove_regex_groups(regex_pattern: str, entry: str) -> str: def _remove_regex_groups(pattern, value):
match = regex.search(regex_pattern, entry.value) regex = re.compile(pattern)
if not match:
return entry.value
spans = [s for s in match.spans()[1:] if s != (-1, -1)] def repl(m):
s = m.group(0)
# remove all group spans from the full match
spans = [m.span(i) for i in range(1, len(m.groups()) + 1) if m.span(i) != (-1, -1)]
spans.sort(reverse=True)
result = [] for start, end in spans:
last = 0 rel_start = start - m.start()
rel_end = end - m.start()
s = s[:rel_start] + s[rel_end:]
for start, end in sorted(spans): return s
result.append(entry.value[last:start])
last = end
result.append(entry.value[last:]) return regex.sub(repl, value)
return "".join(result)
def do_changes(self, source_coll: str, used_regex: str, row: dict[str, str], changes={}): def do_changes(self, source_coll: str, used_regex: str, row: dict[str, str], changes={}):
changes[self.to] = self._get_merged_groups(used_regex, row[source_coll]) current_to = changes.get(self.to, row[source_coll].value)
changes[source_coll] = self._remove_regex_groups(used_regex, row[source_coll]) current_source = changes.get(source_coll, row[source_coll].value)
pending_res_to = self._get_merged_groups(used_regex, current_to)
if pending_res_to is not None:
changes[self.to] = pending_res_to
changes[source_coll] = self._remove_regex_groups(used_regex, current_source)
return changes return changes
def execute_on_row(self, row: dict[str, str], source_coll: str = None): def execute_on_row(self, row: dict[str, str], source_coll: str = None):
if not source_coll: if not source_coll:
raise ValueError("source_col required") raise ValueError("source_col required")
if self.with_input_iteration: if self.with_input_iteration:
changes = None changes = {}
used_regex = self.match
for iteration in self.with_input_iteration: for iteration in self.with_input_iteration:
for operation_id in range(0, self.__operation_input_depth-1): used_regex = self.match
used_regex.replace(f"{{{{ {operation_id} }}}}", iteration.get(operation_id)) for operation_id in range(0, self.__operation_input_depth):
used_regex = used_regex.replace(f"{{{{% {operation_id} %}}}}", iteration[operation_id])
changes = self.do_changes(source_coll, used_regex, row, changes) changes = self.do_changes(source_coll, used_regex, row, changes)
return changes return changes
else: else:

View File

@ -6,7 +6,7 @@ class RegexCellCondition:
def check_on_row(self, row: dict[str, any]): def check_on_row(self, row: dict[str, any]):
for apply_coll, condition in self.map.items(): for apply_coll, condition in self.map.items():
entry_value = str(row.get(apply_coll).value) entry_value = row.get(apply_coll).value
if not regex.match(condition, entry_value if entry_value else ""): if not regex.match(condition, entry_value if entry_value else ""):
return False return False
return True return True

View File

@ -9,6 +9,7 @@ from .commands.find_and_replace import FindAndReplace
from .commands.regex_edit_col import RegexEditCol from .commands.regex_edit_col import RegexEditCol
from .commands.regex_move_to_col import RegexMoveToCol from .commands.regex_move_to_col import RegexMoveToCol
from .commands.copy import Copy from .commands.copy import Copy
from .commands.regex_group_valuebased_choose import RegexGroupValuebasedChoose
from .conditions.regex_cell import RegexCellCondition from .conditions.regex_cell import RegexCellCondition
@ -53,9 +54,9 @@ class TableSerializer:
for expected_col, ws_idx in map_expected_with_ws.items(): for expected_col, ws_idx in map_expected_with_ws.items():
cell = row[ws_idx] cell = row[ws_idx]
data[expected_col][rid] = Entry( data[expected_col][rid] = Entry(
value=cell.value, value=str(cell.value) if not cell.value is None else None,
bg_color=cell.fill.start_color.rgb if cell.fill and cell.fill.start_color else "00000000", bg_color=cell.fill.start_color.rgb if cell.fill and cell.fill.start_color else (0,0,0),
fg_color=cell.font.color.rgb if cell.font and cell.font.color else "00000000" fg_color=cell.font.color.rgb if cell.font and cell.font.color else (0,0,0)
) )
return data return data
@ -64,7 +65,7 @@ class TableSerializer:
raise ValueError("There is nothing to write, serialize first") raise ValueError("There is nothing to write, serialize first")
# write header # write header
owb = load_workbook(destination_file) owb = Workbook()
ows = owb.active ows = owb.active
if self.output_header: if self.output_header:
@ -94,17 +95,20 @@ class TableSerializer:
def _extecute_instruct(self, source_cell: str, instruct: dict, row: dict[str, any], working_row: dict[str, any]): def _extecute_instruct(self, source_cell: str, instruct: dict, row: dict[str, any], working_row: dict[str, any]):
# execute changes # execute changes
res = "" res = None
res_row = {} res_row = {}
match instruct.get("command"): match instruct.get("command"):
case "FIND_AND_REPLACE": case "FIND_AND_REPLACE":
res = FindAndReplace(instruct["map"]).execute_on_cell(row[source_cell]) res = FindAndReplace(instruct["map"]).execute_on_cell(row[source_cell])
case "REGEX_MOVE_TO_COL": case "REGEX_MOVE_TO_COL":
res_row = RegexMoveToCol(instruct["match"], instruct["to"], instruct.get("with_iteration_input")) \ res_row = RegexMoveToCol(instruct["match"], instruct["to"], instruct.get("with_input_iteration")) \
.execute_on_row(row, source_cell) .execute_on_row(row, source_cell)
case "REGEX_EDIT_COL": case "REGEX_EDIT_COL":
res = RegexEditCol(instruct["match"], instruct["replace_with"])\ res = RegexEditCol(instruct["match"], instruct["replace_with"])\
.execute_on_cell(row[source_cell]) .execute_on_cell(row[source_cell])
case "REGEX_GROUP_VALUEBASED_CHOOS":
res = RegexGroupValuebasedChoose(instruct["match"], instruct["highest"])\
.execute_on_cell(row[source_cell])
case "CONDITIONAL_COPY": case "CONDITIONAL_COPY":
res_row = Copy(instruct["source"], instruct["target"], replace=instruct.get("replace"))\ res_row = Copy(instruct["source"], instruct["target"], replace=instruct.get("replace"))\
.execute_on_row(row) .execute_on_row(row)
@ -114,7 +118,7 @@ class TableSerializer:
working_row[target_cel] = self._merge_change_to_cel(working_row.get(target_cel), change) working_row[target_cel] = self._merge_change_to_cel(working_row.get(target_cel), change)
# Merge changes in working row # Merge changes in working row
if source_cell: if source_cell and res is not None:
working_row[source_cell] = self._merge_change_to_cel(working_row.get(source_cell), res) working_row[source_cell] = self._merge_change_to_cel(working_row.get(source_cell), res)
return working_row return working_row
@ -128,7 +132,7 @@ class TableSerializer:
def run(self, col_instructions: dict[tuple[str], dict], row_instructions: dict[tuple[str], dict]): def run(self, col_instructions: dict[tuple[str], dict], row_instructions: dict[tuple[str], dict]):
self.serialized_output = {} self.serialized_output = {}
for rid in range(2, self.vertical_len + 1): for rid in range(2, self.vertical_len + 2):
working_row = {} working_row = {}
source_row = self.get_row_as_dict(rid) source_row = self.get_row_as_dict(rid)
for source_cols, instruct in list(col_instructions.items()) + [ for source_cols, instruct in list(col_instructions.items()) + [
@ -143,5 +147,5 @@ class TableSerializer:
for col in self.expected_output_schema: for col in self.expected_output_schema:
if col in working_row: if col in working_row:
continue continue
working_row[col] = source_row.get(col) working_row[col] = source_row.get(col).value
self.serialized_output[rid] = working_row self.serialized_output[rid] = working_row