{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "cell-23-18-00",
   "metadata": {},
   "source": [
    "# 23-18 · Группируем файлы в дубликаты\n",
    "\n",
    "Практика к разделу [«Находим группы дубликатов»](../../site/chapters/glava-23/23-19-gruppy-dublikatov.html). Настоящий файл — `projects/python/safesort/src/safesort/duplicates.py`."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-23-18-01",
   "metadata": {},
   "source": [
    "## Цель\n",
    "\n",
    "Воспроизвести двухэтапную логику `find_duplicates()` (сначала группировка по размеру, потом по дайджесту) на синтетических записях, чтобы не создавать настоящие файлы на диске."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-23-18-02",
   "metadata": {},
   "source": [
    "## Example"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-23-18-03",
   "metadata": {},
   "outputs": [],
   "source": [
    "import hashlib\n",
    "from collections import defaultdict\n",
    "from dataclasses import dataclass\n",
    "\n",
    "\n",
    "@dataclass(frozen=True)\n",
    "class FileInfo:\n",
    "    name: str\n",
    "    size: int\n",
    "    content: bytes\n",
    "\n",
    "\n",
    "def find_duplicates(files):\n",
    "    by_size = defaultdict(list)\n",
    "    for file in files:\n",
    "        by_size[file.size].append(file)\n",
    "\n",
    "    groups = []\n",
    "    for size, candidates in by_size.items():\n",
    "        if len(candidates) < 2:\n",
    "            continue\n",
    "        by_digest = defaultdict(list)\n",
    "        for candidate in candidates:\n",
    "            digest = hashlib.sha256(candidate.content).hexdigest()\n",
    "            by_digest[digest].append(candidate)\n",
    "        for digest, matched in by_digest.items():\n",
    "            if len(matched) >= 2:\n",
    "                groups.append({\"size\": size, \"digest\": digest, \"files\": tuple(matched)})\n",
    "    return groups\n",
    "\n",
    "\n",
    "fajly = [\n",
    "    FileInfo(\"notes.txt\", 8, b\"AAAAAAAA\"),\n",
    "    FileInfo(\"copy_of_notes.txt\", 8, b\"AAAAAAAA\"),\n",
    "    FileInfo(\"unikalnyj.txt\", 8, b\"BBBBBBBB\"),   # тот же размер, другое содержимое\n",
    "    FileInfo(\"photo1.jpg\", 100, b\"J\" * 100),\n",
    "    FileInfo(\"photo2.jpg\", 100, b\"J\" * 100),\n",
    "    FileInfo(\"odinokij.pdf\", 55, b\"P\" * 55),      # уникальный размер — не может быть дубликатом\n",
    "]\n",
    "\n",
    "gruppy = find_duplicates(fajly)\n",
    "for g in gruppy:\n",
    "    print(g[\"size\"], g[\"digest\"][:12], [f.name for f in g[\"files\"]])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-23-18-04",
   "metadata": {},
   "source": [
    "## Проверка результата"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-23-18-05",
   "metadata": {},
   "outputs": [],
   "source": [
    "imena_v_gruppah = {frozenset(f.name for f in g[\"files\"]) for g in gruppy}\n",
    "\n",
    "assert len(gruppy) == 2\n",
    "assert frozenset({\"notes.txt\", \"copy_of_notes.txt\"}) in imena_v_gruppah\n",
    "assert frozenset({\"photo1.jpg\", \"photo2.jpg\"}) in imena_v_gruppah\n",
    "assert not any(\"odinokij.pdf\" in imena for imena in imena_v_gruppah)\n",
    "print(\"Верно: найдены ровно две группы дубликатов, уникальные файлы не попали ни в одну.\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-23-18-06",
   "metadata": {},
   "source": [
    "## Starter\n",
    "\n",
    "Заполните отмеченное место. Неизменённый starter не проходит tests."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "task-23-18",
   "metadata": {
    "tags": [
     "exercise",
     "starter"
    ]
   },
   "outputs": [],
   "source": [
    "def gruppy_s_pustoj_paroj(files):\n",
    "    # TODO: append two zero-byte FileInfo values, then find duplicates.\n",
    "    raise NotImplementedError\n",
    "\n",
    "\n",
    "gruppy2 = gruppy_s_pustoj_paroj(fajly)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-23-18-08",
   "metadata": {},
   "source": [
    "## Task\n",
    "\n",
    "Добавьте к списку два пустых файла и верните результат `find_duplicates()`."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-23-18-09",
   "metadata": {},
   "source": [
    "## Tests\n",
    "\n",
    "Запустите после task cell: есть основной пример и хотя бы один крайний случай."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "tests-23-18",
   "metadata": {
    "tags": [
     "exercise-tests"
    ]
   },
   "outputs": [],
   "source": [
    "gruppa_pustyh = next(g for g in gruppy2 if g[\"size\"] == 0)\n",
    "assert {f.name for f in gruppa_pustyh[\"files\"]} == {\"pustoj_a.txt\", \"pustoj_b.txt\"}\n",
    "assert gruppa_pustyh[\"digest\"] == hashlib.sha256(b\"\").hexdigest()\n",
    "assert not any(g[\"size\"] == 0 for g in find_duplicates(fajly + [FileInfo(\"one\", 0, b\"\")]))\n",
    "print(\"Tests passed\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-23-18-11",
   "metadata": {},
   "source": [
    "## Hint\n",
    "\n",
    "Создайте два `FileInfo` с size 0 и content `b\"\"`."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-23-18-12",
   "metadata": {},
   "source": [
    "## Solution\n",
    "\n",
    "<details><summary>Показать решение после собственной попытки</summary>\n",
    "\n",
    "```python\n",
    "def gruppy_s_pustoj_paroj(files):\n",
    "    return find_duplicates(files + [\n",
    "        FileInfo(\"pustoj_a.txt\", 0, b\"\"),\n",
    "        FileInfo(\"pustoj_b.txt\", 0, b\"\"),\n",
    "    ])\n",
    "\n",
    "\n",
    "gruppy2 = gruppy_s_pustoj_paroj(fajly)\n",
    "```\n",
    "\n",
    "</details>"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Cartesian Python 3.14",
   "language": "python",
   "name": "cartesian-python314"
  },
  "language_info": {
   "name": "python",
   "version": "3.14.6"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
