diff --git a/README.md b/README.md
index 06197e1..c1415c3 100644
--- a/README.md
+++ b/README.md
@@ -1,372 +1,34 @@
-# Persistent-Data-Structures
+# Undo-Redo
-Курсовой проект по дисциплине "Современные методы программирования" - "Persistent data structures"
-
-## Разработчики:
-* *Карицкая Полина, 24225.1*
-* *Пучков Дмитрий, 24225.1*
+Реализация дополнительного требования курсовой работы по дисциплине "Современные методы программирования" - "Persistent data structures"
---
## Оглавление
-- [Persistent-Data-Structures](#persistent-data-structures)
- - [Разработчики:](#разработчики)
- - [Оглавление](#оглавление)
- - [Описание задания](#описание-задания)
- - [Базовые требования](#базовые-требования)
- - [Дополнительные требования](#дополнительные-требования)
- - [Календарный план](#календарный-план)
- - [Ожидаемое решение](#ожидаемое-решение)
- - [Теоретическая часть](#теоретическая-часть)
- - [Персистентные структуры данных](#персистентные-структуры-данных)
- - [Fat node](#fat-node)
- - [Path copying](#path-copying)
- - [Более эффективное по скорости доступа представление структур данных](#более-эффективное-по-скорости-доступа-представление-структур-данных)
- - [API](#api)
- - [Примеры использования](#примеры-использования)
- - [Массив (Persistent Array)](#массив-persistent-array)
- - [Список (Persistent Linked List)](#список-persistent-linked-list)
- - [Aссоциативный массив (Persistent Map)](#aссоциативный-массив-persistent-map)
- - [Используемые источники](#используемые-источники)
-
----
-## Описание задания
-
-Реализовать библиотеку в Python со структурами данных в persistent-вариантах.
-
----
-## Базовые требования
-- [x] Массив (константное время доступа, переменная длина)
-- [x] Двусвязный список
-- [x] Ассоциативный массив (на основе Hash-таблицы, либо бинарного дерева)
-
----
-## Дополнительные требования
-- [x] Реализовать более эффективное по памяти представление структур данных;
-- [x] Реализовать универсальный undo-redo механизм для перечисленных структур с поддержкой каскадности (для вложенных структур);
-- [x] Реализовать поддержку транзакционной памяти (STM).
-
----
-## Календарный план
-
-| **Сроки** | **Этап работы** | **Разделение ответственностей** |
-|--------------------|----------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------|
-| **до 23.11.2024** | - Создание каркаса проекта
- Создание репозитория на GitHub | - Ответственный за настройку проекта и репозитория: *Полина*
- Ответственный за организацию структуры файлов и начальную настройку CI/CD: *Дмитрий* |
-| **до 07.12.2024** | **Реализация базовой функциональности:**
- Массив (Persistent Array)
- Двусвязный список (Persistent Linked List)
- Ассоциативный массив (Persistent Map)
- Создание единого API | - Ответственный за реализацию Persistent Array: *совместно*
- Ответственный за реализацию Persistent Linked List: *Полина*
- Ответственный за реализацию Persistent Map: *Дмитрий*
- Ответственный за создание API: *совместно* |
-| **до 21.12.2024** | **Реализация дополнительной функциональности:**
- Реализовать более эффективное по памяти представление структур данных
- Реализовать универсальный undo-redo механизм для перечисленных структур с поддержкой каскадности (для вложенных структур)
- Реализовать поддержку транзакционной памяти (STM) | - Ответственный за улучшение эффективности по памяти: *Полина*
- Ответственный за реализацию undo-redo механизма: *Дмитрий*
- Ответственный за поддержку транзакционной памяти (STM): *совместно* |
+- [Механизм Undo-Redo](#механизм-undo-redo)
+- [Механизм Undo-Redo с использованием персистентных структур данных](#механизм-undo-redo-с-использованием-персистентных-структур-данных)
---
-## Ожидаемое решение
-
-Ожидаемое решение состоит в разработке библиотеки, которая будет поддерживать следующие структуры данных:
-
-1. **Persistent Array** - массив с возможностью добавления элементов без изменения предыдущих состояний. Операции должны поддерживать константное время доступа.
-
-2. **Persistent Linked List** - двусвязный список с поддержкой добавления и удаления элементов, при этом сохраняется возможность обратиться к предыдущим состояниям списка.
-
-3. **Persistent Map** - ассоциативный массив, реализованный на основе хеш-таблицы или бинарного дерева поиска. Это будет позволять эффективно работать с данными, при этом поддерживать возможность обращения к предыдущим версиям данных.
-
-4. **Оптимизация по памяти** - предложим более эффективное по памяти представление данных операций доступа по сравнению с fat-node.
+## Механизм Undo-Redo
-5. **Undo/Redo Механизм** - реализуем механизм для всех структур данных, который позволит отменять и повторять изменения, причем для вложенных структур будет поддерживаться каскадность.
-
-6. **Транзакционная память (STM)** - реализуем механизм транзакционной памяти для обеспечения атомарности операций с данными.
-
-
-Каждая из структур данных будет поддерживать все основные операции: вставку, удаление, обновление, а также предоставлять возможность работы с предыдущими версиями данных через персистентность данных. Вся библиотека будет иметь единый API для работы с данными и их модификацией. Также будет обеспечена возможность взаимодействия с данными в виде вложенных структур.
+**Механизм undo-redo** — механизм, позволяющий пользователям отменять или повторно применять изменения. Он обеспечивает гибкость и возможность восстановления ошибок, что особенно важно для задач, требующих частого редактирования. В ветке рассматриваются реализация механизма undo-redo с использованием персистентных структур данных.
---
-## Теоретическая часть
-
-### Персистентные структуры данных
-
-**Персистентные структуры данных** сохраняют предыдущие версии при изменении. Структура называется *fully persistent*, если все её версии доступны для изменений. В *partially persistent* структурах можно изменять только последнюю версию, но доступ к предыдущим возможен. Эти структуры часто реализуются с использованием алгоритмов, таких как path copying, node copying и fat node, а также с применением бинарных деревьев поиска и красно-черных деревьев.
-
-Персистентные структуры используются в вычислительной геометрии, объектно-ориентированном программировании, текстовых редакторах, симуляциях и системах контроля версий, таких как Git.
-
-### Fat node
-
-*Fat node* используется для создания персистентных структур данных, где изменения сохраняются только в измененных узлах дерева, а сами узлы могут расширяться, чтобы хранить все версии. Основной принцип заключается в том, чтобы хранить обновления и версии данных в списках, что позволяет эффективно отслеживать изменения в структуре.
-
-**Преимущества:**
-
-* Экономия памяти, так как изменяются только те части данных, которые действительно изменились;
-* Быстрая работа с историей изменений.
-
-**Недостатки:**
-
-* Может быть менее эффективен для структур с частыми изменениями, так как увеличение размера узлов приводит к дополнительным затратам на память.
-
-Можно посмотреть [визуализацию метода *fat node*](https://kumom.io/persistent-bst).
-
-### Path copying
-
-Метод *path copying* используется для создания персистентных структур данных, где при изменении узла создается его копия, и нужно пройти по всем узлам от измененного до корня, чтобы обновить ссылки на новый узел. Этот метод упрощает доступ к данным, так как для поиска нужной версии достаточно пройти путь от корня.
-
-**Преимущества:**
-
-* Быстрый доступ к данным;
-* Простота в реализации.
-
-**Недостатки:**
-
-* Большие затраты на память, поскольку изменения могут потребовать копирования всей структуры;
-* Могут возникать дополнительные накладные расходы при частых модификациях.
-
-Метод *path copying* также используется для создания полностью персистентных структур данных.
-
-На иллюстрации продеминстрирован пример использования *path copying* на бинарном дереве поиска. При внесении изменения создается новый корень, при этом старый корень сохраняется для дальнейшего использования (он показан темно-серым цветом). Заметим также, что старое и новое деревья частично делят общую структуру.
-
-
-
-### Более эффективное по памяти представление структур данных
-
-Поскольку одно из требований — использование более эффективного подхода по сравнению с методом *fat-node*, в проекте будет применяться подход с использованием B-деревьев.
+## Механизм Undo-Redo с использованием персистентных структур данных
----
-## API
-
-Для работы с персистентными структурами данных используйте следующий API:
-
-Создание объекта:
-```python
-from persistent_data_structure import PersistentLinkedList, PersistentArray, PersistentMap
-
-lst = PersistentLinkedList()
-arr = PersistentLinkedList()
-dct = PersistentMap()
-```
-
-Обращение к элементу текщей версии:
-```python
-lst[index]
-arr[index]
-dct[key]
-```
-
-Обращение по индексу к элементу произвольной версии для массива и списка:
-```python
-arr.get(version, index)
-lst.get(version, index)
-```
-
-Обращение по ключу к элементу произвольной версии для мапы:
-```python
-dct.get(version, key)
-```
-
-Добавление элемента в конец в новую версию для массива и списка:
-```python
-arr.add(element)
-lst.add(element)
-```
-
-Добавление элемента в начало в новую версию списка:
-```python
-lst.add_first(element)
-```
-
-Удаление элемента по индексу для массива и списка в новой версии и возвращение элемента:
-```python
-arr.pop(index)
-lst.pop(index)
-```
-
-Удаление элемента по ключу для мапы в новой версии и возвращение элемента:
-```python
-dct.pop(key)
-```
-
-Обновление элемента по индексу в новой версии массива или списка:
-```python
-arr[index] = element
-lst[index] = element
-```
-
-Обновление элемента по ключу в новой версии мапы:
-```python
-dct['key'] = element
-```
-
-Вставка элемента в новую версию по указанному индексу для массива или списка:
-```python
-arr.insert(index, element)
-lst.insert(index, element)
-```
-
-Удаление элемента в новой версии массива или списка по индексу:
-```python
-arr.remove(index)
-lst.remove(index)
-```
-
-Удаление элемента в новой версии мапы по ключу:
-```python
-dct.remove(key)
-```
-
-Получение размера массива или списка для текущей версии:
-```python
-arr.get_size()
-lst.get_size()
-```
-
-Проверка на пустоту для массива или списка:
-```python
-arr.check_is_empty()
-lst.check_is_empty()
-```
-
-Возвращение состояние объекта для указанной версии:
-```python
-arr.get_version(version)
-lst.get_version(version)
-dct.get_version(version)
-```
-
-Обновление текущей версии объекта до указанной:
-```python
-arr.update_version(version)
-lst.update_version(version)
-dct.update_version(version)
-```
-
-Получение элемента текущей версии массива или списка по указанному индексу.
-```python
-arr.__getitem__(index)
-lst.__getitem__(index)
-```
-
-Получение элемента текущей версии мапы по указанному ключу.
-```python
-dct.__getitem__(key)
-```
-
-Обновление или создание элемента по указанному индексу в новой версии для массива или списка.
-```python
-arr.__setitem__(index)
-lst.__setitem__(index)
-```
-
-Обновление или создание элемента по указанному ключу в новой версии для мапы.
-```python
-dct.__setitem__(key)
-```
-
-Очистка объекта, при этом создается новая версия.
-```python
-arr.clear()
-lst.clear()
-dct.clear()
-```
-
----
-## Примеры использования
-
-### Массив (Persistent Array)
-
-**Примеры использования:**
-
-```
- >>> array = PersistentArray(size=5, default_value=0) # Создаем массив из 5 элементов, заполненных нулями
- >>> array[0] # Получаем значение первого элемента
- 0
- >>> array[0] = 10 # Обновляем значение первого элемента в новой версии
- >>> array[0] # Проверяем значение первого элемента в текущей версии
- 10
- >>> array.add(20) # Добавляем новый элемент в конец массива в новую версию
- >>> array[5] # Проверяем значение нового элемента
- 20
- >>> array.pop(1) # Удаляем элемент с индексом 1 в новой версии
- 0
- >>> array.insert(2, 15) # Вставляем значение 15 на индекс 2 в новой версии
- >>> array[2] # Проверяем вставленное значение
- 15
- >>> array.remove(3) # Удаляем элемент с индексом 3
- >>> array.get(1, 0) # Получаем значение элемента с индексом 0 из версии 1
- 0
- >>> array.clear() # Очищаем массив, создавая новую версию
- >>> array.get_size() # Проверяем размер массива в текущей версии
- 0
- >>> array.check_is_empty() # Проверяем, пуст ли массив
- True
- >>> array.get(0, 2) # Пытаемся получить элемент из очищенного массива в версии 0
- Traceback (most recent call last):
- ...
- ValueError: Invalid index
-```
-
-### Список (Persistent Linked List)
-
-**Примеры использования:**
-
-```
- >>> linked_list = PersistentLinkedList([1, 2, 3]) # Создаем список с начальными элементами
- >>> linked_list.add(4) # Добавляем элемент в конец списка
- >>> linked_list[3] # Получаем элемент по индексу в текущей версии
- 4
- >>> linked_list.add_first(0) # Добавляем элемент в начало списка
- >>> linked_list[0] # Получаем элемент в текущей версии по индексу
- 0
- >>> linked_list.insert(2, 1.5) # Вставляем элемент на индекс 2
- >>> linked_list[2] # Проверяем значение вставленного элемента
- 1.5
- >>> linked_list.get(1, 2) # Получаем элемент из версии 1 по индексу 2
- 3
- >>> linked_list.pop(1) # Удаляем элемент по индексу 1 в новой версии
- 2
- >>> linked_list.remove(3) # Удаляем элемент
- >>> linked_list.clear() # Очищаем список, создавая новую версию
- >>> linked_list.get(0, 2) # Пытаемся получить элемент из очищенной версии
- Traceback (most recent call last):
- ...
- IndexError: Index out of range
- >>> linked_list.update_version(2) # Возвращаемся к версии 2
- >>> linked_list[0] # Получаем элемент по индексу в версии 2
- 1
- >>> linked_list.get_size() # Получаем размер списка в текущей версии
- 4
- >>> linked_list.check_is_empty() # Проверяем, пуст ли список в текущей версии
- False
-```
-
-### Aссоциативный массив (Persistent Map)
-
-**Примеры использования:**
-
-```
- >>> map = PersistentMap({'foo': 'bar'}) # Создаем пустой ассоциативный массив
- >>> map['key'] = 'value' # Добавляем элемент
- >>> map['key'] = 'value2' # Изменяем элемент в следующей версии
- >>> map['key'] # Получаем элемент последней версии
- 'value2'
- >>> map.get(1, 'key')
- {'key': 'value'}
- >>> map.remove('key') # Удаляем элемент в новой версии
- >>> map.clear() # Очищаем ассоциативный массив в новой версии
- >>> map.get(0, 'key') # Пытаемся получить элемент отсутствующий в переданной версии
- Traceback (most recent call last):
- ...
- KeyError: Key "key" does not exist
-```
-
----
-## Используемые источники
+Функциональность undo-redo опирается на следующие основные принципы персистентных структур данных:
-1. Milan Straka, "Functional Data Structures and Algorithms", Computer Science Institute of Charles University, Prague 2013
+1. **Ведение истории версий:**
-2. Крис Окасаки, "Чисто функциональные структуры данных", 2018
+ * Изменения в структуре данных записываются как новые версии;
-3. Статья на Geeks for geeks ["Persistent data structures"](https://www.geeksforgeeks.org/persistent-data-structures/)
+ * История версий хранится в виде словаря, где ключами являются номера версий, а значениями — соответствующие состояния.
-4. Несколько статей по персистентным векторам:
+2. **Восстановление состояния:**
- * [Understanding Clojure's Persistent Vectors, pt. 1](https://hypirion.com/musings/understanding-persistent-vector-pt-1)
- * [Understanding Clojure's Persistent Vectors, pt. 2](https://hypirion.com/musings/understanding-persistent-vector-pt-2)
- * [Understanding Clojure's Persistent Vectors, pt. 3](https://hypirion.com/musings/understanding-persistent-vector-pt-3)
+ * Операция undo уменьшает индекс текущей версии, возвращая предыдущее состояние;
-5. Driscoll J. R. et al. Making data structures persistent //Proceedings of the eighteenth annual ACM symposium on Theory of computing. – 1986. – С. 109-121.
+ * Операция redo увеличивает индекс версии, повторяя ранее отменённое состояние.
-6. Серия видео-лекций по персистентным структурам данных:
+3. **Нерушимость изменений:**
- * ["Visualizing Persistent Data Structures" by Dann Toliver](https://www.youtube.com/watch?v=2XH_q494U3U)
- * [Persistent Data Structures, MIT](https://www.youtube.com/watch?v=T0yzrZL1py0)
\ No newline at end of file
+ * Модификации не перезаписывают существующие данные, а создают новую версию с обновлённым состоянием.
\ No newline at end of file
diff --git a/persistent_data_structures/base_persistent.py b/persistent_data_structures/base_persistent.py
index a4b967b..2cd4f4f 100644
--- a/persistent_data_structures/base_persistent.py
+++ b/persistent_data_structures/base_persistent.py
@@ -15,6 +15,8 @@ def __init__(self, initial_state=None) -> None:
self._history = {0: initial_state}
self._current_state = 0
self._last_state = 0
+ self._container = None
+ self._location = None
def get_version(self, version):
"""Возвращает состояние персистентной структуры данных на указанной версии.
@@ -27,6 +29,22 @@ def get_version(self, version):
raise ValueError(f'Version "{version}" does not exist')
return self._history[version]
+ def getcopy(self, version: int, key: any) -> any:
+ """Возвращает копию элемента с указанной версией и ключом/индексом.
+
+ :param version: Номер версии
+ :param key: Ключ/индекс
+ :return: Копия значения сответствующее указанному ключу или None,
+ если ключ/индекс не существует.
+ :raises ValueError: Если версия не существует
+ :raises KeyError: Если ключ/индекс не существует
+ """
+ value = deepcopy(self.get(version, key))
+ if isinstance(value, BasePersistent):
+ value._container = None
+ value._location = None
+ return value
+
def update_version(self, version):
"""Обновляет текущую версию персистентной структуры данных до указанной.
@@ -37,8 +55,38 @@ def update_version(self, version):
raise ValueError(f'Version "{version}" does not exist')
self._current_state = version
+ def undo(self):
+ """Отменяет последнее изменение."""
+ if self._container is not None:
+ raise NotImplementedError(f'Cannot undo inside container "{self._container}"')
+ if self._current_state == 0:
+ raise ValueError("No actions to undo")
+ if self._current_state > 0:
+ self._current_state -= 1
+
+ def redo(self):
+ """Повторяет последнее отмененное изменение.
+
+ :raises ValueError: Если нет операций."""
+ if self._container is not None:
+ raise NotImplementedError(f'Cannot redo inside container "{self._container}"')
+ if self._current_state >= self._last_state:
+ raise ValueError("No operations to redo")
+ self._current_state += 1
+
def _create_new_state(self) -> None:
"""Создает новую версию."""
+ # Персистентные структуры могут быть вложенными,
+ # поэтому нужно сохранять историю изменений в родительской персистентной структуре
+ if self._container is not None:
+ # В текущей версии родительской структуры подменяем имеющуюся вложенную структуру
+ # на ее копию, таким образом изменения во вложенной структуре не будут отражаться на
+ # прощлых версиях родительской структуры
+ self._container._history[
+ self._container._current_state
+ ][self._location] = deepcopy(self)
self._last_state += 1
self._history[self._last_state] = deepcopy(self._history[self._current_state])
self._current_state = self._last_state
+ if self._container is not None:
+ self._container[self._location] = self
diff --git a/persistent_data_structures/persistent_array.py b/persistent_data_structures/persistent_array.py
index cee913f..3c19d96 100644
--- a/persistent_data_structures/persistent_array.py
+++ b/persistent_data_structures/persistent_array.py
@@ -1,6 +1,6 @@
import numpy as np
-from persistent_data_structures.base_persistent import BasePersistent
+from base_persistent import BasePersistent
class PersistentArray(BasePersistent):
@@ -54,6 +54,9 @@ def add(self, value: any) -> None:
:param value (int): Значение нового элемента, который добавляется в массив.
"""
self._create_new_state()
+ if isinstance(value, BasePersistent):
+ value._container = self
+ value._location = self.size
self._history[self._last_state] = np.append(self._history[self._last_state], value)
self.size += 1
@@ -70,6 +73,10 @@ def pop(self, index: int) -> any:
self._create_new_state()
self._history[self._last_state] = np.delete(self._history[self._last_state], index)
self.size -= 1
+ # Сдвигаем индексы всех элементов BasePersistent после удаленного элемента
+ for i in range(index, self.size):
+ if isinstance(self._history[self._current_state][i], BasePersistent):
+ self._history[self._current_state][i]._location -= 1
return removed_element
def __setitem__(self, index: int, value: any) -> None:
@@ -83,6 +90,9 @@ def __setitem__(self, index: int, value: any) -> None:
"""
if index < 0 or index >= self.size:
raise ValueError("Invalid index")
+ if isinstance(value, BasePersistent):
+ value._container = self
+ value._location = index
self._create_new_state()
self._history[self._last_state][index] = value
@@ -97,9 +107,16 @@ def insert(self, index: int, value: any) -> None:
"""
if index < 0 or index > self.size:
raise ValueError("Invalid index")
+ if isinstance(value, BasePersistent):
+ value._container = self
+ value._location = index
self._create_new_state()
self._history[self._last_state] = np.insert(self._history[self._last_state], index, value)
self.size += 1
+ # Сдвигаем индексы всех элементов BasePersistent после добавленного элемента
+ for i in range(index+1, self.size):
+ if isinstance(self._history[self._current_state][i], BasePersistent):
+ self._history[self._current_state][i]._location += 1
def remove(self, index: int) -> None:
"""Удаление элемента в новой версии массива по индексу.
diff --git a/persistent_data_structures/persistent_list.py b/persistent_data_structures/persistent_list.py
index 75b4fa0..58f043b 100644
--- a/persistent_data_structures/persistent_list.py
+++ b/persistent_data_structures/persistent_list.py
@@ -1,4 +1,6 @@
-from persistent_data_structures.base_persistent import BasePersistent
+from copy import deepcopy
+
+from base_persistent import BasePersistent
class Node:
@@ -57,6 +59,9 @@ def add(self, data: any) -> None:
"""
self._create_new_state()
head, tail = self._history[self._last_state]
+ if isinstance(data, BasePersistent):
+ data._container = self
+ data._location = self.size
new_node = Node(data)
if tail is None:
head = tail = new_node
@@ -76,6 +81,9 @@ def add_first(self, data: any) -> None:
"""
self._create_new_state()
head, tail = self._history[self._last_state]
+ if isinstance(data, BasePersistent):
+ data._container = self
+ data._location = 0
new_node = Node(data, next_node=head)
if head:
head.prev = new_node
@@ -83,6 +91,13 @@ def add_first(self, data: any) -> None:
if tail is None:
tail = new_node
self.size += 1
+ # сдвигаем индексы персистентных элементов
+ current = head
+ while current.next_node:
+ current = current.next_node
+ if isinstance(current.value, BasePersistent):
+ current.value._location += 1
+
self._history[self._last_state] = (head, tail)
def insert(self, index: int, data: any) -> None:
@@ -94,8 +109,15 @@ def insert(self, index: int, data: any) -> None:
:return: None
:raises IndexError: Если индекс выходит за пределы списка.
"""
+ if index < 0 or index > self.size:
+ raise IndexError("Index out of range")
+
self._create_new_state()
head, tail = self._history[self._last_state]
+ if isinstance(data, BasePersistent):
+ data._container = self
+ data._location = index
+
current = head
count = 0
while current:
@@ -107,11 +129,12 @@ def insert(self, index: int, data: any) -> None:
if current == head:
head = new_node
self.size += 1
- break
+ count += 1
+
+ if isinstance(current.value, BasePersistent) and count > index:
+ current.value._location += 1
count += 1
current = current.next_node
- else:
- raise IndexError("Index out of range")
self._history[self._last_state] = (head, tail)
def pop(self, index: int) -> any:
@@ -122,12 +145,15 @@ def pop(self, index: int) -> any:
:return: Значение удаленного элемента.
:raises IndexError: Если индекс выходит за пределы списка.
"""
- head, tail = self._history[self._current_state]
+ if index < 0 or index >= self.size:
+ raise IndexError("Index out of range")
+ self._create_new_state()
+ head, tail = self._history[self._last_state]
+ popped_value = self.__getitem__(index)
current = head
count = 0
while current:
if count == index:
- value = current.value
if current.prev:
current.prev.next_node = current.next_node
if current.next_node:
@@ -136,40 +162,25 @@ def pop(self, index: int) -> any:
head = current.next_node
if current == tail:
tail = current.prev
- self._create_new_state()
+
self.size -= 1
self._history[self._last_state] = (head, tail)
- return value
+
+ if isinstance(current.value, BasePersistent) and count > index:
+ current.value._location -= 1
count += 1
current = current.next_node
- raise IndexError("Index out of range")
+ return popped_value
- def remove(self, value: any) -> None:
+ def remove(self, index: int) -> None:
"""
- Удаляет элемент из списка в новой версии.
+ Удаляет элемент по индексу из списка в новой версии.
- :param data: Данные элемента для удаления.
+ :param index: Индекс элемента для удаления.
:return: None
- :raises ValueError: Если элемент не найден в списке.
+ :raises IndexError: Если индекс выходит за пределы списка.
"""
- head, tail = self._history[self._current_state]
- current = head
- while current:
- if current.value == value:
- if current.prev:
- current.prev.next_node = current.next_node
- if current.next_node:
- current.next_node.prev = current.prev
- if current == head:
- head = current.next_node
- if current == tail:
- tail = current.prev
- self._create_new_state()
- self.size -= 1
- self._history[self._last_state] = (head, tail)
- return
- current = current.next_node
- raise ValueError(f"Value {value} not found in the list")
+ self.pop(index)
def get(self, version: int = None, index: int = None) -> any:
"""
@@ -234,6 +245,9 @@ def __setitem__(self, index: int, value: any) -> None:
:raises IndexError: Если индекс выходит за пределы списка.
"""
self._create_new_state()
+ if isinstance(value, BasePersistent):
+ value._container = self
+ value._location = index
head, tail = self._history[self._last_state]
current = head
count = 0
@@ -255,6 +269,20 @@ def get_size(self) -> int:
"""
return self.size
+ def calc_size(self) -> int:
+ """
+ Расчет текущего размера списка.
+
+ :return: Количество элементов в текущей версии списка.
+ """
+ size = 0
+ head, tail = self._history[self._current_state]
+ current = head
+ while current:
+ size += 1
+ current = current.next_node
+ return size
+
def check_is_empty(self) -> bool:
"""
Проверяет, пуст ли список.
@@ -263,3 +291,56 @@ def check_is_empty(self) -> bool:
"""
head, tail = self._history[self._current_state]
return head is None
+
+ def update_version(self, version) -> None:
+ """Обновляет текущую версию персистентной структуры данных до указанной.
+
+ :param version: Номер версии.
+ :raises ValueError: Если указанная версия не существует.
+ """
+ if version < 0 or version >= len(self._history):
+ raise ValueError(f'Version "{version}" does not exist')
+ self._current_state = version
+ self.size = self.calc_size()
+
+ def undo(self) -> None:
+ """Отменяет последнее изменение."""
+ if self._container is not None:
+ raise NotImplementedError(f'Cannot undo inside container "{self._container}"')
+ if self._current_state == 0:
+ raise ValueError("No actions to undo")
+ if self._current_state > 0:
+ self._current_state -= 1
+ self.size = self.calc_size()
+
+ def redo(self) -> None:
+ """Повторяет последнее отмененное изменение.
+
+ :raises ValueError: Если нет операций."""
+ if self._container is not None:
+ raise NotImplementedError(f'Cannot redo inside container "{self._container}"')
+ if self._current_state >= self._last_state:
+ raise ValueError("No operations to redo")
+ self._current_state += 1
+ self.size = self.calc_size()
+
+ def _create_new_state(self) -> None:
+ """Создает новую версию."""
+ # Персистентные структуры могут быть вложенными,
+ # поэтому нужно сохранять историю изменений в родительской персистентной структуре
+ if self._container is not None:
+ # В текущей версии родительской структуры подменяем имеющуюся вложенную структуру
+ # на ее копию, таким образом изменения во вложенной структуре не будут отражаться на
+ # прощлых версиях родительской структуры
+ head, tail = self._container._history[self._container._current_state]
+ current = head
+ while current:
+ if current.value is self:
+ current.value = deepcopy(self)
+ break
+ current = current.next_node
+ self._last_state += 1
+ self._history[self._last_state] = deepcopy(self._history[self._current_state])
+ self._current_state = self._last_state
+ if self._container is not None:
+ self._container[self._location] = self
diff --git a/persistent_data_structures/persistent_map.py b/persistent_data_structures/persistent_map.py
index 9efb825..f469c57 100644
--- a/persistent_data_structures/persistent_map.py
+++ b/persistent_data_structures/persistent_map.py
@@ -1,4 +1,4 @@
-from persistent_data_structures.base_persistent import BasePersistent
+from base_persistent import BasePersistent
class PersistentMap(BasePersistent):
@@ -20,6 +20,9 @@ def __setitem__(self, key: any, value: any) -> None:
:param value: Значение
"""
self._create_new_state()
+ if isinstance(value, BasePersistent):
+ value._container = self
+ value._location = key
self._history[self._last_state][key] = value
def __getitem__(self, key: any) -> any:
@@ -42,7 +45,7 @@ def get(self, version: int, key: any) -> any:
raise ValueError(f'Version "{version}" does not exist')
if key not in self._history[version]:
raise KeyError(f'Key "{key}" does not exist')
- return self._history[version]
+ return self._history[version][key]
def pop(self, key: any) -> any:
"""Удаляет элемент по указанному ключу и возвращает его.
@@ -50,8 +53,11 @@ def pop(self, key: any) -> any:
:param key: Ключ
:return: Удаленный элемент
"""
+ if key not in self._history[self._current_state]:
+ raise KeyError(f'Key "{key}" does not exist')
self._create_new_state()
- return self._history[self._last_state].pop(key)
+ popped_item = self._history[self._last_state].pop(key)
+ return popped_item
def remove(self, key: any) -> None:
"""Удаляет элемент по указанному ключу в новой версии.
@@ -63,4 +69,4 @@ def remove(self, key: any) -> None:
def clear(self) -> None:
"""Очищает ассоциативный массив в новой версии."""
self._create_new_state()
- self._history[self._current_state] = {}
+ self._history[self._last_state] = {}
diff --git a/tests/test_array.py b/tests/test_array.py
index 00b2cca..5a89858 100644
--- a/tests/test_array.py
+++ b/tests/test_array.py
@@ -1,109 +1,193 @@
import pytest
-
from persistent_array import PersistentArray
-# Тестирование методов класса PersistentArray
@pytest.fixture
def persistent_array():
- """Фикстура для создания PersistentArray"""
+ """Фикстура для создания персистентного массива с размером 5 и значением по умолчанию 0."""
return PersistentArray(size=5, default_value=0)
def test_initial_state(persistent_array):
- """Тест1. Проверка начального состояния массива"""
+ """Тест 1. Проверка начального состояния массива"""
assert persistent_array.get_size() == 5
- assert persistent_array[0] == 0
- assert persistent_array[4] == 0
+ for i in range(5):
+ assert persistent_array[i] == 0
-def test_get_version(persistent_array):
- """Тест 2. Проверка получения состояния на определенной версии"""
- persistent_array.add(1)
- persistent_array.add(2)
+def test_get_item(persistent_array):
+ """Тест 2. Проверка метода получения элемента массива"""
+ persistent_array[1] = 10
+ assert persistent_array[1] == 10
- assert persistent_array.get(0, 0) == 0
- assert persistent_array.get(1, 5) == 1
- assert persistent_array.get(2, 6) == 2
+def test_set_item(persistent_array):
+ """Тест 3. Проверка метода установки элемента массива"""
+ persistent_array[2] = 20
+ assert persistent_array[2] == 20
+ assert persistent_array.get_version(0)[2] == 0
-def test_update_version(persistent_array):
- """Тест 3. Проверка обновления текущей версии"""
- persistent_array.add(1)
- persistent_array.add(2)
- persistent_array.update_version(1)
- assert persistent_array[5] == 1
- persistent_array.update_version(2)
- assert persistent_array[6] == 2
+def test_add(persistent_array):
+ """Тест 4. Проверка добавления нового элемента в массив"""
+ persistent_array.add(42)
+ assert persistent_array.get_size() == 6
+ assert persistent_array[5] == 42
-def test_add_element(persistent_array):
- """Тест 4. Проверка добавления элемента в массив"""
- persistent_array.add(10)
+def test_pop(persistent_array):
+ """Тест 5. Проверка удаления элемента из массива"""
+ persistent_array[2] = 30
+ removed = persistent_array.pop(2)
+ assert removed == 30
+ assert persistent_array.get_size() == 4
+
+
+def test_insert(persistent_array):
+ """Тест 6. Проверка вставки элемента в массив по индексу"""
+ persistent_array.insert(2, 50)
+ assert persistent_array[2] == 50
assert persistent_array.get_size() == 6
- assert persistent_array[5] == 10
-def test_pop_element(persistent_array):
- """Тест 5. Проверка удаления элемента из массива"""
+def test_remove(persistent_array):
+ """Тест 7. Проверка удаления элемента массива по индексу"""
+ persistent_array.remove(3)
+ assert persistent_array.get_size() == 4
+
+
+def test_undo_redo(persistent_array):
+ """Тест 8. Проверка функционала отмены и повторения изменений"""
+ persistent_array[1] = 10
+ persistent_array.undo()
+ assert persistent_array[1] == 0
+ persistent_array.redo()
+ assert persistent_array[1] == 10
+
+
+def test_check_is_empty():
+ """Тест 9. Проверка метода проверки пустоты массива"""
+ array = PersistentArray(size=0)
+ assert array.check_is_empty()
+ array.add(5)
+ assert not array.check_is_empty()
+
+
+def test_get_version(persistent_array):
+ """Тест 10. Проверка получения версии массива"""
+ persistent_array[1] = 10
+ assert persistent_array.get_version(0)[1] == 0
+ assert persistent_array.get_version(1)[1] == 10
+ with pytest.raises(ValueError):
+ persistent_array.get_version(3)
+
+
+def test_update_version(persistent_array):
+ """Тест 11. Проверка метода обновления версии массива"""
+ persistent_array[1] = 10
+ persistent_array.update_version(1)
+ assert persistent_array[1] == 10
+ with pytest.raises(ValueError):
+ persistent_array.update_version(2)
+
+
+def test_undo(persistent_array):
+ """Тест 12. Проверка отмены изменений (undo)"""
persistent_array.add(10)
persistent_array.add(20)
- removed_value = persistent_array.pop(1)
- assert removed_value == 0
- assert persistent_array.get_size() == 6
- assert persistent_array[1] == 0
+ persistent_array.add(30)
+ state_after_add = persistent_array._history[persistent_array._current_state].tolist()
+ assert state_after_add == [0, 0, 0, 0, 0, 10, 20, 30]
+ persistent_array.undo()
+ state_after_undo = persistent_array._history[persistent_array._current_state].tolist()
+ assert state_after_undo == [0, 0, 0, 0, 0, 10, 20]
+ persistent_array.undo()
+ state_after_second_undo = persistent_array._history[persistent_array._current_state].tolist()
+ assert state_after_second_undo == [0, 0, 0, 0, 0, 10]
-def test_insert_element(persistent_array):
- """Тест 6. Проверка вставки элемента в массив по индексу"""
+def test_redo(persistent_array):
+ """Тест 13. Проверка возврата изменений после undo (redo)"""
persistent_array.add(10)
- persistent_array.insert(2, 15)
- assert persistent_array.get_size() == 7
- assert persistent_array[2] == 15
+ persistent_array.add(20)
+ persistent_array.add(30)
+ persistent_array.undo()
+ persistent_array.undo()
+ state_after_undo = persistent_array._history[persistent_array._current_state].tolist()
+ assert state_after_undo == [0, 0, 0, 0, 0, 10]
+ persistent_array.redo()
+ state_after_redo = persistent_array._history[persistent_array._current_state].tolist()
+ assert state_after_redo == [0, 0, 0, 0, 0, 10, 20]
+ persistent_array.redo()
+ state_after_second_redo = persistent_array._history[persistent_array._current_state].tolist()
+ assert state_after_second_redo == [0, 0, 0, 0, 0, 10, 20, 30]
-def test_remove_element(persistent_array):
- """Тест 7. Проверка удаления элемента по индексу"""
+def test_undo_redo_integrity(persistent_array):
+ """Тест 14. Проверка целостности данных при использовании undo и redo"""
persistent_array.add(10)
persistent_array.add(20)
- persistent_array.remove(0)
- assert persistent_array.get_size() == 6
- assert persistent_array[0] == 0
+ state_after_add = persistent_array._history[persistent_array._current_state].tolist()
+ assert state_after_add == [0, 0, 0, 0, 0, 10, 20]
+ persistent_array.undo()
+ state_after_undo = persistent_array._history[persistent_array._current_state].tolist()
+ assert state_after_undo == [0, 0, 0, 0, 0, 10]
+ persistent_array.redo()
+ state_after_redo = persistent_array._history[persistent_array._current_state].tolist()
+ assert state_after_redo == [0, 0, 0, 0, 0, 10, 20]
-def test_set_item(persistent_array):
- """Тест 8. Проверка обновления элемента в массиве по индексу"""
- persistent_array[0] = 99
- assert persistent_array[0] == 99
+def test_remove_nested_persistent_array(persistent_array):
+ """Тест 15. Проверка удаления вложенной структуры из массива"""
+ nested_array = PersistentArray(size=3, default_value=5)
+ persistent_array.add(nested_array)
+ persistent_array.remove(5)
assert persistent_array.get_size() == 5
+ with pytest.raises(ValueError):
+ persistent_array[5]
+
+def test_nested_persistent_array_get(persistent_array):
+ """Тест 16. Проверка получения элемента вложенной структуры"""
+ nested_array = PersistentArray(size=3, default_value=5)
+ persistent_array.add(nested_array)
+ assert persistent_array[5][1] == 5
-def test_check_is_empty(persistent_array):
- """Тест 9. Проверка, является ли массив пустым"""
- assert not persistent_array.check_is_empty()
- persistent_array.remove(0)
- persistent_array.remove(0)
- persistent_array.remove(0)
- persistent_array.remove(0)
- persistent_array.remove(0)
- assert persistent_array.check_is_empty()
+
+def test_size_after_pop(persistent_array):
+ """Тест 17. Проверка размера после удаления элемента"""
+ persistent_array.add(10)
+ persistent_array.add(20)
+ assert persistent_array.get_size() == 7
+ persistent_array.pop(1)
+ assert persistent_array.get_size() == 6
+
+
+def test_multiple_versions(persistent_array):
+ """Тест 18. Проверка сохранения нескольких версий"""
+ persistent_array.add(10)
+ persistent_array.add(20)
+ persistent_array.add(30)
+ assert persistent_array.get(0, 0) == 0
+ assert persistent_array.get(1, 5) == 10
+ assert persistent_array.get(2, 6) == 20
+ assert persistent_array.get(3, 7) == 30
def test_invalid_index_get(persistent_array):
- """Тест 10. Проверка на исключение для недопустимого индекса при получении"""
+ """Тест 20. Проверка на исключение для недопустимого индекса при получении"""
with pytest.raises(ValueError):
persistent_array[10]
def test_invalid_index_set(persistent_array):
- """Тест 11. Проверка на исключение для недопустимого индекса при обновлении"""
+ """Тест 21. Проверка на исключение для недопустимого индекса при обновлении"""
with pytest.raises(ValueError):
persistent_array[10] = 5
def test_invalid_version_get(persistent_array):
- """Тест 12. Проверка на исключение для недопустимой версии при получении"""
+ """Тест 22. Проверка на исключение для недопустимой версии при получении"""
persistent_array.add(1)
persistent_array.add(2)
with pytest.raises(ValueError):
@@ -111,8 +195,87 @@ def test_invalid_version_get(persistent_array):
def test_invalid_version_update(persistent_array):
- """Тест 13. Проверка на исключение для недопустимой версии при обновлении"""
+ """Тест 23. Проверка на исключение для недопустимой версии при обновлении"""
persistent_array.add(1)
persistent_array.add(2)
with pytest.raises(ValueError):
persistent_array.update_version(10)
+
+
+def test_nested_array(persistent_array):
+ """Тест 24. Проверка вложенности персистентных массивов"""
+ nested_array = PersistentArray(size=3, default_value=1)
+ persistent_array.add(nested_array)
+ assert persistent_array.get_size() == 6
+ assert persistent_array[5] == nested_array
+ assert persistent_array[5][1] == 1
+
+
+def test_nested_array_undo_redo(persistent_array):
+ """Тест 25. Проверка отмены и повторения изменений во вложенных массивах"""
+ nested_array = PersistentArray(size=3, default_value=1)
+ persistent_array.add(nested_array)
+ persistent_array[5][0] = 42
+ persistent_array.undo()
+ assert persistent_array[5][0] == 1
+ persistent_array.redo()
+ assert persistent_array[5][0] == 42
+
+
+def test_nested_array_deepcopy(persistent_array):
+ """Тест 26. Проверка правильности работы deepcopy для вложенных массивов"""
+ nested_array = PersistentArray(size=3, default_value=1)
+ persistent_array.add(nested_array)
+ persistent_array[5][0] = 42
+ nested_array_copy = persistent_array.get_version(1)[5]
+ assert nested_array_copy[0] == 1
+ assert persistent_array[5][0] == 42
+
+
+def test_remove_nested_array(persistent_array):
+ """Тест 27. Проверка удаления вложенного массива"""
+ nested_array = PersistentArray(size=3, default_value=1)
+ persistent_array.add(nested_array)
+ persistent_array.remove(5)
+ assert persistent_array.get_size() == 5
+ with pytest.raises(ValueError):
+ persistent_array[5]
+
+
+def test_undo_redo_with_nested(persistent_array):
+ """Тест 28. Проверка функционала отмены и повторения изменений с вложенной структурой"""
+ nested_array = PersistentArray(size=3, default_value=1)
+ persistent_array.add(nested_array)
+ persistent_array[5][1] = 20
+ persistent_array.undo()
+ assert persistent_array[5][1] == 1
+ persistent_array.redo()
+ assert persistent_array[5][1] == 20
+
+
+def test_triple_nested_persistent_array(persistent_array):
+ """Тест 29. Проверка удаления вложенной структуры из массива (тройная вложенность)"""
+ nested_array_1 = PersistentArray(size=3, default_value=1)
+ nested_array_1[1] = 42
+ nested_array_2 = PersistentArray(size=2, default_value=5)
+ nested_array_2[0] = 99
+ nested_array_3 = PersistentArray(size=1, default_value=7)
+ nested_array_3[0] = 77
+ persistent_array.add(nested_array_1)
+ persistent_array.add(nested_array_2)
+ persistent_array.add(nested_array_3)
+ assert persistent_array.get_size() == 8
+ assert isinstance(persistent_array[5], PersistentArray)
+ assert isinstance(persistent_array[6], PersistentArray)
+ assert isinstance(persistent_array[7], PersistentArray)
+ removed_element = persistent_array.pop(7)
+ assert removed_element == nested_array_3
+ assert persistent_array.get_size() == 7
+ assert persistent_array[5][0] == 1
+ assert persistent_array[5][1] == 42
+ assert persistent_array[6][0] == 99
+ persistent_array.undo()
+ assert persistent_array.get_size() == 7
+ persistent_array[6] = 88
+ persistent_array.undo()
+ assert persistent_array[6][0] == 99
diff --git a/tests/test_list.py b/tests/test_list.py
index 75f6742..2723d8a 100644
--- a/tests/test_list.py
+++ b/tests/test_list.py
@@ -5,93 +5,178 @@
# Тестирование методов класса PersistentLinkedList
@pytest.fixture
-def linked_list():
- """
- Фикстура для создания экземпляра персистентного двусвязного списка.
- """
- return PersistentLinkedList([1, 2, 3, 4, 5])
+def persistent_list():
+ """Создает фикстуру для тестирования PersistentLinkedList."""
+ initial_data = [1, 2, 3]
+ return PersistentLinkedList(initial_data)
-def test_add(linked_list):
- """Тест 1. Проверка добавления элемента в конец списка"""
- linked_list.add(6)
- assert linked_list.get(index=5) == 6
+def test_initial_state(persistent_list):
+ """Тест 1. Проверка, что начальное состояние списка корректно"""
+ assert persistent_list.get(version=0, index=0) == 1
+ assert persistent_list.get(version=0, index=1) == 2
+ assert persistent_list.get(version=0, index=2) == 3
+ assert persistent_list.size == 3
-def test_add_first(linked_list):
- """Тест 2. Проверка добавления элемента в начало списка"""
- linked_list.add_first(0)
- assert linked_list.get(index=0) == 0
+def test_add_element(persistent_list):
+ """Тест 2. Проверка на добавление нового элемента в конец списка"""
+ persistent_list.add(4)
+ assert persistent_list.get(version=1, index=3) == 4
+ assert persistent_list.size == 4
-def test_insert(linked_list):
- """Тест 3. Проверка вставки элемента по индексу"""
- linked_list.insert(2, 10)
- assert linked_list.get(index=2) == 10
- assert linked_list.get(index=3) == 3
+def test_add_first_element(persistent_list):
+ """Тест 3. Проверка на добавление элемента в начало списка"""
+ persistent_list.add_first(0)
+ assert persistent_list.get(version=1, index=0) == 0
+ assert persistent_list.get(version=1, index=1) == 1
+ assert persistent_list.size == 4
-def test_pop(linked_list):
- """Тест 4. Проверка удаления элемента по индексу"""
- removed_value = linked_list.pop(2)
- assert removed_value == 3
- assert linked_list.get(index=2) == 4
+def test_insert_element(persistent_list):
+ """Тест 4. Проверка на вставку элемента на заданный индекс"""
+ persistent_list.insert(1, 10)
+ assert persistent_list.get(version=1, index=1) == 10
+ assert persistent_list.get(version=1, index=2) == 2
+ assert persistent_list.size == 4
-def test_remove(linked_list):
- """Тест 5. Проверка удаления элемента по значению"""
- linked_list.remove(4)
+def test_pop_element(persistent_list):
+ """Тест 5. Проверка на удаление элемента по индексу"""
+ value = persistent_list.pop(1)
+ assert value == 2
+ assert persistent_list.get(version=1, index=1) == 3
+ assert persistent_list.size == 2
+
+
+def test_remove_element(persistent_list):
+ """Тест 6. Проверка на удаление элемента с использованием remove()"""
+ persistent_list.remove(0)
+ assert persistent_list.get(version=1, index=0) == 2
+ assert persistent_list.size == 2
+
+
+def test_undo_operation(persistent_list):
+ """Тест 7. Проверка на возможность отмены последней операции"""
+ persistent_list.add(4)
+ persistent_list.undo()
+ assert persistent_list.size == 3
+ assert persistent_list.get(version=0, index=2) == 3
+
+
+def test_redo_operation(persistent_list):
+ """Тест 8. Проверка на возможность повторения отмененной операции"""
+ persistent_list.add(4)
+ persistent_list.undo()
+ persistent_list.redo()
+ assert persistent_list.get(version=1, index=3) == 4
+ assert persistent_list.size == 4
+
+
+def test_version_retrieval(persistent_list):
+ """Тест 9. Проверка на восстановление предыдущей версии списка"""
+ persistent_list.add(4)
+ persistent_list.add(5)
+ persistent_list.update_version(0)
+ assert persistent_list.size == 3
+ assert persistent_list.get(version=0, index=2) == 3
+
+
+def test_invalid_version(persistent_list):
+ """Тест 10. Проверка на обработку неверного номера версии"""
with pytest.raises(ValueError):
- linked_list.remove(4)
-
-
-def test_get(linked_list):
- """Тест 6. Проверка получения элемента по индексу"""
- assert linked_list.get(index=0) == 1
- assert linked_list.get(index=4) == 5
-
-
-def test_get_version(linked_list):
- """Тест 7. Проверка получения версии списка"""
- linked_list.add(6)
- linked_list.add(7)
- version_1 = linked_list.get_version(1)
- assert version_1[0] is not None
- version_0_values = []
- current = version_1[0]
- while current:
- version_0_values.append(current.value)
- current = current.next_node
- assert version_0_values == [1, 2, 3, 4, 5, 6]
-
-
-def test_update_version(linked_list):
- """Тест 8. Проверка обновления версии списка"""
- linked_list.add(6)
- linked_list.add(7)
- linked_list.update_version(1)
- assert linked_list.get(index=5) == 6
- linked_list.update_version(0)
- assert linked_list.get(index=4) == 5
-
-
-def test_clear(linked_list):
- """Тест 9. Проверка очистки списка"""
- linked_list.clear()
- assert linked_list.get_size() == 0
- assert linked_list.check_is_empty()
-
-
-def test_get_size(linked_list):
- """Тест 10. Проверка получения размера списка"""
- assert linked_list.get_size() == 5
- linked_list.add(6)
- assert linked_list.get_size() == 6
-
-
-def test_check_is_empty(linked_list):
- """Тест 11. Проверка метода для проверки пустоты списка"""
- linked_list.clear()
- assert linked_list.check_is_empty() is True
- linked_list.add(10)
- assert linked_list.check_is_empty() is False
+ persistent_list.get_version(10)
+
+
+def test_index_out_of_range(persistent_list):
+ """Тест 11. Проверка на обработку индекса за пределами диапазона"""
+ with pytest.raises(IndexError):
+ persistent_list.get(version=0, index=10)
+
+
+@pytest.fixture
+def setup_nested_persistent_list():
+ """Фикстура для создания вложенной персистентной структуры"""
+ inner_list = PersistentLinkedList([4, 5])
+ outer_list = PersistentLinkedList([1, inner_list, 3])
+ return outer_list, inner_list
+
+
+def test_nested_persistence(setup_nested_persistent_list):
+ """Тест 12. Проверка на вложенность и персистентность вложенных структур"""
+ outer_list, inner_list = setup_nested_persistent_list
+ assert outer_list.get(index=0) == 1
+ assert isinstance(outer_list.get(index=1), PersistentLinkedList)
+ assert outer_list.get(index=2) == 3
+ nested_list = outer_list.get(index=1)
+ assert nested_list.get(index=0) == 4
+ assert nested_list.get(index=1) == 5
+ nested_list.add(6)
+ assert nested_list.get(version=1, index=2) == 6
+ updated_nested = outer_list.get(index=1)
+ assert updated_nested.get(version=1, index=2) == 6
+ assert outer_list.get(version=0, index=1).get(index=1) == 5
+
+
+def test_nested_undo_redo(setup_nested_persistent_list):
+ """Тест 13. Проверка работы undo и redo для вложенных структур"""
+ outer_list, inner_list = setup_nested_persistent_list
+ inner_list.add(7)
+ assert inner_list.get(version=1, index=2) == 7
+ inner_list.undo()
+ assert inner_list.get(index=1) == 5
+ with pytest.raises(IndexError):
+ inner_list.get(index=2)
+ inner_list.redo()
+ assert inner_list.get(index=2) == 7
+
+
+def test_deep_nested_persistence():
+ """Тест 14. Проверка на многослойную вложенность персистентных структур"""
+ inner_list = PersistentLinkedList([1, 2])
+ middle_list = PersistentLinkedList([inner_list])
+ outer_list = PersistentLinkedList([middle_list])
+ inner_list.add(3)
+ assert inner_list.get(index=2) == 3
+ assert middle_list.get(index=0).get(index=2) == 3
+ assert outer_list.get(index=0).get(index=0).get(index=2) == 3
+ assert inner_list.get(version=0, index=1) == 2
+ assert middle_list.get(version=0, index=0).get(version=0, index=1) == 2
+ assert outer_list.get(version=0, index=0).get(version=0, index=0).get(version=0, index=1) == 2
+
+
+def test_cascade_undo_redo():
+ """Тест 15. Проверка каскадную вложенность"""
+ list1 = PersistentLinkedList([1, 2, 3])
+ list2 = PersistentLinkedList([4, 5])
+ list3 = PersistentLinkedList([6, 7])
+ list1.add(list2)
+ list2.add(list3)
+ assert list1[3] == list2
+ assert list1[3][2] == list3
+
+
+def test_triple_nested_undo_redo():
+ """Тест 16. Проверка undo-redo с тройной вложенностью"""
+ list1 = PersistentLinkedList([1, 2, 3])
+ list2 = PersistentLinkedList([4, 5])
+ list3 = PersistentLinkedList([6, 7])
+ list1.add(list2)
+ list2.add(list3)
+ list3.add(8)
+ list1.undo()
+ with pytest.raises(IndexError):
+ list1[3][2][2]
+ assert list1[3][2][1] == 7
+ assert list1[3][2][0] == 6
+ list1.undo()
+ with pytest.raises(IndexError):
+ list1[3][2]
+ list1.redo()
+ with pytest.raises(IndexError):
+ list1[3][2][2]
+ assert list1[3][2][1] == 7
+ assert list1[3][2][0] == 6
+ with pytest.raises(NotImplementedError):
+ list2.redo()
diff --git a/tests/test_map.py b/tests/test_map.py
index 580e6b6..115cab9 100644
--- a/tests/test_map.py
+++ b/tests/test_map.py
@@ -1,83 +1,204 @@
import pytest
-
from persistent_map import PersistentMap
-# Тестирование методов класса PersistentMap
@pytest.fixture
-def persistent_map():
- """Фикстура для создания PersistentMap"""
- return PersistentMap({'a': 1, 'b': 2})
+def setup_persistent_map():
+ """Фикстура для создания персистентного ассоциативного массива."""
+ return PersistentMap({"a": 1, "b": 2, "c": 3})
+
+def test_get_version(setup_persistent_map):
+ """Тест 1. Проверка получения версии персистентного массива"""
+ persistent_map = setup_persistent_map
+ assert persistent_map.get_version(0) == {"a": 1, "b": 2, "c": 3}
-def test_get_version(persistent_map):
- """Тест 1. Проверка получения состояния на определенной версии"""
- persistent_map['c'] = 3
- persistent_map.update_version(1)
- assert persistent_map['c'] == 3
+def test_update_version(setup_persistent_map):
+ """Тест 2. Проверка обновления версии массива"""
+ persistent_map = setup_persistent_map
+ persistent_map["d"] = 4
persistent_map.update_version(0)
- with pytest.raises(KeyError, match='Key "c" does not exist'):
- persistent_map.get(0, 'c')
+ assert persistent_map.get_version(0) == {"a": 1, "b": 2, "c": 3}
-def test_update_version(persistent_map):
- """Тест 2. Проверка обновления версии"""
- persistent_map['c'] = 3
- persistent_map.update_version(1)
- assert persistent_map['c'] == 3
+def test_setitem_add_new_key(setup_persistent_map):
+ """Тест 3. Проверка добавления нового ключа"""
+ persistent_map = setup_persistent_map
+ persistent_map["d"] = 4
+ assert persistent_map["d"] == 4
+ assert persistent_map.get_version(1) == {"a": 1, "b": 2, "c": 3, "d": 4}
-def test_invalid_version_get(persistent_map):
- """Тест 3. Проверка на исключение для недопустимой версии при получении"""
- with pytest.raises(ValueError, match='Version "2" does not exist'):
- persistent_map.get(2, 'a')
+def test_setitem_update_key(setup_persistent_map):
+ """Тест 4. Проверка изменения значения существующего ключа"""
+ persistent_map = setup_persistent_map
+ persistent_map["a"] = 10
+ assert persistent_map["a"] == 10
+ assert persistent_map.get_version(1) == {"a": 10, "b": 2, "c": 3}
-def test_invalid_key_get(persistent_map):
- """Тест 4. Проверка на исключение для недопустимого ключа при получении"""
- with pytest.raises(KeyError, match='Key "c" does not exist'):
- persistent_map.get(0, 'c')
+def test_getitem(setup_persistent_map):
+ """Тест 5. Проверка получения значения по ключу"""
+ persistent_map = setup_persistent_map
+ assert persistent_map["a"] == 1
+ assert persistent_map["b"] == 2
+ assert persistent_map["c"] == 3
-def test_setitem(persistent_map):
- """Тест 5. Проверка обновления элемента с использованием __setitem__"""
- persistent_map['c'] = 3
- assert persistent_map['c'] == 3
+def test_get_existing_version_key(setup_persistent_map):
+ """Тест 6. Проверка получения значения существующего ключа из версии"""
+ persistent_map = setup_persistent_map
+ assert persistent_map.get(0, "a") == 1
+ with pytest.raises(KeyError):
+ persistent_map.get(0, "d")
-def test_getitem(persistent_map):
- """Тест 6. Проверка получения элемента с использованием __getitem__"""
- assert persistent_map['a'] == 1
- assert persistent_map['b'] == 2
+def test_get_invalid_version(setup_persistent_map):
+ """Тест 7. Проверка ошибки при запросе несуществующей версии"""
+ persistent_map = setup_persistent_map
+ with pytest.raises(ValueError):
+ persistent_map.get(10, "a")
-def test_pop(persistent_map):
- """Тест 7. Проверка удаления элемента с использованием pop"""
- value = persistent_map.pop('a')
- assert value == 1
- assert 'a' not in persistent_map._history[persistent_map._current_state]
+def test_pop_existing_key(setup_persistent_map):
+ """Тест 8. Проверка удаления ключа методом pop"""
+ persistent_map = setup_persistent_map
+ popped_value = persistent_map.pop("a")
+ assert popped_value == 1
+ assert "a" not in persistent_map.get_version(1)
-def test_remove(persistent_map):
- """Тест 8. Проверка удаления элемента с использованием remove"""
- persistent_map.remove('a')
- assert 'a' not in persistent_map._history[persistent_map._current_state]
+def test_pop_non_existing_key(setup_persistent_map):
+ """Тест 9. Проверка ошибки при pop несуществующего ключа"""
+ persistent_map = setup_persistent_map
+ with pytest.raises(KeyError):
+ persistent_map.pop("d")
-def test_clear(persistent_map):
- """Тест 9. Проверка очистки структуры данных"""
- persistent_map.clear()
- assert persistent_map._history[persistent_map._current_state] == {}
+def test_remove_existing_key(setup_persistent_map):
+ """Тест 10. Проверка удаления ключа методом remove"""
+ persistent_map = setup_persistent_map
+ persistent_map.remove("b")
+ assert "b" not in persistent_map.get_version(1)
-def test_version_history(persistent_map):
- """Тест 10. Проверка истории версий"""
- persistent_map['c'] = 3
- persistent_map.update_version(1)
- persistent_map['d'] = 4
- persistent_map.update_version(2)
+def test_remove_non_existing_key(setup_persistent_map):
+ """Тест 11. Проверка ошибки при remove несуществующего ключа"""
+ persistent_map = setup_persistent_map
+ with pytest.raises(KeyError):
+ persistent_map.remove("d")
- assert persistent_map.get_version(0) == {'a': 1, 'b': 2}
- assert persistent_map.get_version(1) == {'a': 1, 'b': 2, 'c': 3}
- assert persistent_map.get_version(2) == {'a': 1, 'b': 2, 'c': 3, 'd': 4}
+
+def test_clear(setup_persistent_map):
+ """Тест 12. Проверка очистки массива методом clear"""
+ persistent_map = setup_persistent_map
+ persistent_map.clear()
+ assert persistent_map.get_version(1) == {}
+
+
+def test_undo(setup_persistent_map):
+ """Тест 13. Проверка отмены последней операции"""
+ persistent_map = setup_persistent_map
+ persistent_map["d"] = 4
+ persistent_map.undo()
+ assert persistent_map.get_version(0) == {"a": 1, "b": 2, "c": 3}
+
+
+def test_redo(setup_persistent_map):
+ """Тест 14. Проверка повторения последней отмененной операции"""
+ persistent_map = setup_persistent_map
+ persistent_map["d"] = 4
+ persistent_map.undo()
+ persistent_map.redo()
+ assert persistent_map.get_version(1) == {"a": 1, "b": 2, "c": 3, "d": 4}
+
+
+def test_undo_without_changes(setup_persistent_map):
+ """Тест 15. Проверка ошибки отмены операции без изменений"""
+ persistent_map = setup_persistent_map
+ with pytest.raises(ValueError):
+ persistent_map.undo()
+
+
+def test_redo_without_undo(setup_persistent_map):
+ """Тест 16. Проверка ошибки повтора без отмены операции"""
+ persistent_map = setup_persistent_map
+ persistent_map["d"] = 4
+ with pytest.raises(ValueError):
+ persistent_map.redo()
+
+
+def test_setitem_nested_structure(setup_persistent_map):
+ """Тест 17. Проверка добавления вложенной структуры в персистентный массив"""
+ persistent_map = setup_persistent_map
+ nested_map = PersistentMap({"nested_key": 1})
+ persistent_map["nested"] = nested_map
+ assert persistent_map["nested"].get_version(0) == {"nested_key": 1}
+ assert persistent_map.get_version(1) == {"a": 1, "b": 2, "c": 3, "nested": nested_map}
+
+
+def test_nested_undo(setup_persistent_map):
+ """Тест 18. Проверка отмены изменений в вложенной структуре"""
+ persistent_map = setup_persistent_map
+ nested_map = PersistentMap({"nested_key": 1})
+ persistent_map["nested"] = nested_map
+ nested_map["nested_key"] = 2
+ persistent_map.undo()
+ assert persistent_map.get_version(0) == {"a": 1, "b": 2, "c": 3}
+ assert persistent_map.get_version(1)["nested"]["nested_key"] == 1
+
+
+def test_nested_redo(setup_persistent_map):
+ """Тест 19. Проверка повтора изменений в вложенной структуре"""
+ persistent_map = setup_persistent_map
+ nested_map = PersistentMap({"nested_key": 1})
+ persistent_map["nested"] = nested_map
+ nested_map["nested_key"] = 2
+ persistent_map.undo()
+ persistent_map.redo()
+ assert persistent_map.get_version(2)["nested"]["nested_key"] == 2
+
+
+def test_remove_nested_structure(setup_persistent_map):
+ """Тест 20. Проверка удаления вложенной структуры методом remove"""
+ persistent_map = setup_persistent_map
+ nested_map = PersistentMap({"nested_key": 1})
+ persistent_map["nested"] = nested_map
+ persistent_map.remove("nested")
+ with pytest.raises(KeyError):
+ persistent_map["nested"]
+
+
+def test_cascade_undo_redo_nested():
+ """Тест 13. Проверка каскадного undo/redo с вложенными структурами"""
+ persistent_map = PersistentMap({
+ "level1": PersistentMap({
+ "level2": PersistentMap({
+ "level3": 42
+ })
+ })
+ })
+ persistent_map["level1"]["level2"]["level3"] = 100
+ assert persistent_map["level1"]["level2"]["level3"] == 100
+ persistent_map["level1"]["level2"].undo()
+ assert persistent_map["level1"]["level2"]["level3"] == 42
+ persistent_map["level1"]["level2"].redo()
+ assert persistent_map["level1"]["level2"]["level3"] == 100
+
+
+def test_triple_nested_undo_redo():
+ """Тест 14. Проверка undo/redo с тройной вложенностью"""
+ persistent_map = PersistentMap({
+ "level1": PersistentMap({
+ "level2": PersistentMap({
+ "level3": PersistentMap({"key": 333})
+ })
+ })
+ })
+ persistent_map["level1"]["level2"]["level3"]["key"] = 200
+ assert persistent_map["level1"]["level2"]["level3"]["key"] == 200
+ persistent_map["level1"]["level2"]["level3"].undo()
+ assert persistent_map["level1"]["level2"]["level3"]["key"] == 333
+ persistent_map["level1"]["level2"]["level3"].redo()
+ assert persistent_map["level1"]["level2"]["level3"]["key"] == 200