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* на бинарном дереве поиска. При внесении изменения создается новый корень, при этом старый корень сохраняется для дальнейшего использования (он показан темно-серым цветом). Заметим также, что старое и новое деревья частично делят общую структуру. - -![Пример использования path copying](path_copying.png) - -### Более эффективное по памяти представление структур данных - -Поскольку одно из требований — использование более эффективного подхода по сравнению с методом *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..c9fdf56 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): """Возвращает состояние персистентной структуры данных на указанной версии. @@ -37,8 +39,35 @@ 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): + """Отменяет отмененное изменение.""" + if self._container is not None: + raise NotImplementedError(f'Cannot redo inside container "{self._container}"') + if self._current_state < self._last_state: + 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..d2e7da4 100644 --- a/persistent_data_structures/persistent_array.py +++ b/persistent_data_structures/persistent_array.py @@ -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..1943dc7 100644 --- a/persistent_data_structures/persistent_list.py +++ b/persistent_data_structures/persistent_list.py @@ -57,6 +57,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 +79,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 +89,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 +107,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 +127,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 +143,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 +160,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 +243,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 diff --git a/persistent_data_structures/persistent_map.py b/persistent_data_structures/persistent_map.py index 9efb825..7607571 100644 --- a/persistent_data_structures/persistent_map.py +++ b/persistent_data_structures/persistent_map.py @@ -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: @@ -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..2ad0766 100644 --- a/tests/test_array.py +++ b/tests/test_array.py @@ -116,3 +116,87 @@ def test_invalid_version_update(persistent_array): persistent_array.add(2) with pytest.raises(ValueError): persistent_array.update_version(10) + + +def test_undo(persistent_array): + """Тест 14. Проверка отмены изменений (undo)""" + persistent_array.add(10) + persistent_array.add(20) + 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_redo(persistent_array): + """Тест 15. Проверка возврата изменений после undo (redo)""" + persistent_array.add(10) + 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_undo_redo_integrity(persistent_array): + """Тест 16. Проверка целостности данных при использовании undo и redo""" + persistent_array.add(10) + persistent_array.add(20) + 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_remove_nested_persistent_array(persistent_array): + """Тест 17. Проверка удаления вложенной структуры из массива""" + 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): + """Тест 18. Проверка получения элемента вложенной структуры""" + nested_array = PersistentArray(size=3, default_value=5) + persistent_array.add(nested_array) + assert persistent_array[5][1] == 5 + + +def test_size_after_pop(persistent_array): + """Тест 19. Проверка размера после удаления элемента""" + 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): + """Тест 20. Проверка сохранения нескольких версий""" + 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 diff --git a/tests/test_list.py b/tests/test_list.py index 75f6742..5641a7f 100644 --- a/tests/test_list.py +++ b/tests/test_list.py @@ -40,9 +40,9 @@ def test_pop(linked_list): def test_remove(linked_list): """Тест 5. Проверка удаления элемента по значению""" - linked_list.remove(4) - with pytest.raises(ValueError): - linked_list.remove(4) + linked_list.remove(1) + with pytest.raises(IndexError): + linked_list.remove(8) def test_get(linked_list): @@ -95,3 +95,32 @@ def test_check_is_empty(linked_list): assert linked_list.check_is_empty() is True linked_list.add(10) assert linked_list.check_is_empty() is False + + +def test_undo(linked_list): + """Тест 12. Проверка отмены последнего изменения""" + linked_list.add(6) + linked_list.add(7) + assert linked_list.get(index=5) == 6 + assert linked_list.get(index=6) == 7 + + linked_list.undo() + assert linked_list.get(index=5) == 6 + try: + linked_list.get(index=6) + except IndexError: + pass + + +def test_redo(linked_list): + """Тест 13. Проверка отмены отмены изменений (redo)""" + linked_list.add(6) + linked_list.add(7) + assert linked_list.get(index=5) == 6 + assert linked_list.get(index=6) == 7 + linked_list.undo() + linked_list.undo() + linked_list.redo() + assert linked_list.get(index=5) == 6 + linked_list.redo() + assert linked_list.get(index=6) == 7 diff --git a/tests/test_map.py b/tests/test_map.py index 580e6b6..6f66fd5 100644 --- a/tests/test_map.py +++ b/tests/test_map.py @@ -81,3 +81,35 @@ def test_version_history(persistent_map): 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_undo_redo_basic(persistent_map): + """Тест 11. Проверка undo-redo""" + persistent_map['c'] = 3 + assert persistent_map.get_version(1) == {'a': 1, 'b': 2, 'c': 3} + persistent_map['a'] = 10 + assert persistent_map.get_version(2) == {'a': 10, 'b': 2, 'c': 3} + persistent_map.undo() + assert persistent_map.get_version(1) == {'a': 1, 'b': 2, 'c': 3} + persistent_map.redo() + assert persistent_map.get_version(2) == {'a': 10, 'b': 2, 'c': 3} + + +def test_undo_no_changes(persistent_map): + """Тест 12. отмены без изменений (начальная версия)""" + with pytest.raises(ValueError): + persistent_map.undo() + + +def test_undo_and_redo_multiple_times(persistent_map): + """Тест 13. Проверка нескольких операций undo и redo""" + persistent_map['c'] = 3 + persistent_map['d'] = 4 + persistent_map['e'] = 5 + assert persistent_map.get_version(3) == {'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5} + persistent_map.undo() + persistent_map.undo() + assert persistent_map.get_version(1) == {'a': 1, 'b': 2, 'c': 3} + persistent_map.redo() + persistent_map.redo() + assert persistent_map.get_version(3) == {'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5}