From f40792c523065464d00c0c65c3faf7bf0296c48e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Sun, 14 Jun 2026 14:27:36 +0400 Subject: [PATCH] =?UTF-8?q?feat(differ):=20differ.py=20=E2=80=94=20=D1=81?= =?UTF-8?q?=D1=80=D0=B0=D0=B2=D0=BD=D0=B5=D0=BD=D0=B8=D0=B5=20=D1=81=D1=82?= =?UTF-8?q?=D1=80=D0=BE=D0=BA=20=D0=BC=D0=B5=D0=B6=D0=B4=D1=83=20=D0=B4?= =?UTF-8?q?=D0=BE=D0=BF=D0=BD=D0=B8=D0=BA=D0=B0=D0=BC=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit diff(rows_old, rows_new) → {changes, ambiguous, summary} change_type: added | deleted | changed | unchanged changed_fields: только изменённые поля {old, new} --- site/differ.py | 107 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 site/differ.py diff --git a/site/differ.py b/site/differ.py new file mode 100644 index 0000000..4ca1978 --- /dev/null +++ b/site/differ.py @@ -0,0 +1,107 @@ +""" +differ.py — Сравнение строк спецификаций между версиями договора. + +Чистая функция: diff(rows_old, rows_new) → список изменений. +Не зависит от БД, LLM, Flask. Только от данных. + +change_type: + added — новая строка (была в new, нет в old) + deleted — удалённая строка (была в old, нет в new) + changed — изменённая строка (совпали по row_num, но значения разные) + unchanged — без изменений +""" + + +# Поля для сравнения (все кроме row_num) +_COMPARE_FIELDS = ["name", "price", "qty", "sum", "date_start"] + + +def diff(rows_old: list, rows_new: list) -> dict: + """ + Сравнить два списка строк спецификации. + + Args: + rows_old: строки из предыдущего допника + rows_new: строки из нового допника + + Returns: + {"changes": [...], "ambiguous": [...]} + changes: [{row_num, change_type, old_values, new_values}, ...] + ambiguous: строки которые не удалось сопоставить + """ + # Индексация по row_num + old_by_num = {r["row_num"]: r for r in rows_old} + new_by_num = {r["row_num"]: r for r in rows_new} + + all_nums = sorted(set(list(old_by_num.keys()) + list(new_by_num.keys()))) + + changes = [] + ambiguous = [] + + for num in all_nums: + in_old = num in old_by_num + in_new = num in new_by_num + + if in_old and not in_new: + # Строка удалена + changes.append({ + "row_num": num, + "change_type": "deleted", + "old_values": _row_values(old_by_num[num]), + "new_values": None, + }) + + elif not in_old and in_new: + # Новая строка + changes.append({ + "row_num": num, + "change_type": "added", + "old_values": None, + "new_values": _row_values(new_by_num[num]), + }) + + else: + # Строка есть в обоих — сравниваем значения + old_vals = _row_values(old_by_num[num]) + new_vals = _row_values(new_by_num[num]) + + if old_vals == new_vals: + changes.append({ + "row_num": num, + "change_type": "unchanged", + "old_values": old_vals, + "new_values": new_vals, + }) + else: + # Частичное изменение — показываем что именно поменялось + changed_fields = {} + for field in _COMPARE_FIELDS: + if old_vals.get(field) != new_vals.get(field): + changed_fields[field] = { + "old": old_vals.get(field), + "new": new_vals.get(field), + } + + changes.append({ + "row_num": num, + "change_type": "changed", + "old_values": old_vals, + "new_values": new_vals, + "changed_fields": changed_fields, + }) + + return { + "changes": changes, + "ambiguous": ambiguous, + "summary": { + "added": sum(1 for c in changes if c["change_type"] == "added"), + "deleted": sum(1 for c in changes if c["change_type"] == "deleted"), + "changed": sum(1 for c in changes if c["change_type"] == "changed"), + "unchanged": sum(1 for c in changes if c["change_type"] == "unchanged"), + }, + } + + +def _row_values(row: dict) -> dict: + """Извлечь только значимые поля из строки спецификации.""" + return {f: row.get(f) for f in _COMPARE_FIELDS}