Учебный проект: электронная таблица
C++ проект: движок электронной таблицы с поддержкой формул, ссылок между ячейками, графа зависимостей и кэширования.
Проект реализует упрощённую модель Excel с формулами, AST и системой пересчёта зависимых ячеек.
В репозитории находятся следующие основные файлы:
common.h
structures.cpp
formula.h
formula.cpp
FormulaAST.h
FormulaAST.cpp
Formula.g4
cell.h
cell.cpp
sheet.h
sheet.cpp
main.cpp
test_runner_p.h
Описание компонентов:
| файл | назначение |
|---|---|
common.h |
интерфейсы проекта (SheetInterface, CellInterface, Position, FormulaError) |
structures.cpp |
реализация базовых структур |
formula.h / formula.cpp |
интерфейс и реализация формулы |
FormulaAST.h / FormulaAST.cpp |
AST формулы и вычисление выражений |
Formula.g4 |
grammar ANTLR для парсинга формул |
cell.h / cell.cpp |
реализация ячейки |
sheet.h / sheet.cpp |
реализация таблицы и графа зависимостей |
main.cpp |
unit-тесты |
test_runner_p.h |
минимальный фреймворк для тестирования |
Поддерживаются:
- текстовые ячейки
- формульные ячейки
- ссылки на другие ячейки
- арифметические операции
- скобки
- dependency graph
- lazy caching
- проверка циклических зависимостей
- selective cache invalidation
- strong exception guarantee для изменения ячеек
- печать значений таблицы
- печать текстов таблицы
Формулы поддерживают:
+
-
*
/
()
Примеры:
1+2
A1+B2
-(A1/2)
2*(B3+4)
Формулы могут возвращать:
| ошибка | причина |
|---|---|
#REF! |
некорректная ссылка |
#VALUE! |
значение нельзя преобразовать в число |
#ARITHM! |
арифметическая ошибка |
Sheet
├── Cell
│ ├── Impl
│ │ ├── EmptyImpl
│ │ ├── TextImpl
│ │ └── FormulaImpl
│ │
│ ├── referenced_cells_
│ ├── dependents_
│ └── cache_
│
└── Graph logic
├── cycle detection
├── dependency rebuild
└── cache invalidation
Cell представляет одну ячейку таблицы.
Она хранит:
referenced_cells_ — graph-state: ячейки, на которые ссылается данный узел
dependents_ — ячейки, которые зависят от данного узла
cache_ — кэш вычисленного значения
Реализации содержимого:
EmptyImpl
TextImpl
FormulaImpl
Cell отвечает за:
- хранение локального содержимого
- вычисление видимого значения
- кэширование результата
- хранение graph-state конкретного узла
При этом логикой управления dependency graph всей таблицы занимается Sheet.
Sheet — основной orchestrator таблицы.
Отвечает за:
- хранение ячеек
- управление графом зависимостей
- проверку циклов
- перестройку зависимостей
- инвалидацию кэша
- печать таблицы
- управление printable size
Ячейки хранятся как:
unordered_map<Key, unique_ptr<Cell>>
Это позволяет эффективно работать с разреженными таблицами.
Каждая ячейка является вершиной графа.
Используются два типа рёбер:
referenced_cells_ (outgoing edges)
dependents_ (incoming edges)
Это позволяет:
- быстро проверять циклы
- инвалидировать кэш зависимых ячеек
- не пересчитывать всю таблицу целиком после каждого изменения
Формулы:
- парсятся через ANTLR
- строят AST
- вычисляются через callback получения значений ячеек
double Execute(CellValueGetter)
Это позволяет AST не зависеть напрямую от Sheet.
Кроме дерева выражения FormulaAST также хранит
подготовленный список ячеек, использованных в формуле.
Поэтому GetReferencedCells() работает за O(1).
Перед установкой формулы выполняется проверка:
WouldIntroduceCycle(...)
Используется поиск пути:
HasPathToTarget(...)
Если путь существует, выбрасывается:
CircularDependencyException
После изменения ячейки:
- сбрасывается кэш текущей ячейки
- сбрасывается кэш всех зависимых от неё ячеек
Используется обход графа:
InvalidateCacheFrom(...)
Это позволяет инвалидировать только затронутую часть таблицы.
Установка значения ячейки выполняется в два этапа.
tmp_cell.Set(text)
ExtractReferencedCells(tmp_cell)
WouldIntroduceCycle(...)
Во временной ячейке подготавливается новое содержимое. Если это формула, ссылки извлекаются из уже подготовленного состояния, без повторного парсинга выражения.
cell.Set(text)
RebuildDependencies(...)
InvalidateCacheFrom(...)
Сначала применяется новое содержимое, после чего перестраивается graph-state таблицы и инвалидируется кэш затронутых ячеек.
Это позволяет сохранить strong exception guarantee для операции изменения ячейки.
Если возникает ошибка:
FormulaExceptionCircularDependencyException
состояние таблицы остаётся прежним.
Основные идеи:
GetValue() O(1) при наличии кэша
cache invalidation O(K)
cycle detection O(K)
где K — число затронутых зависимых ячеек.
В main.cpp реализованы unit-тесты, проверяющие:
- преобразование
Position <-> string - граничные случаи для координат ячеек
- арифметику формул
- форматирование выражений
- ссылки на ячейки
- ссылки на отсутствующие ячейки
- ошибки формул (
#REF!,#VALUE!,#ARITHM!) - циклические зависимости
- rollback при циклической зависимости
- rollback при синтаксически неверной формуле
- пересчёт зависимых ячеек
- очистку ячеек
- printable size
- печать текстов и значений таблицы
Проект использует:
- C++17
- ANTLR4
- AST
- dependency graph
- lazy caching
- selective invalidation
Проект демонстрирует реализацию mini Excel-движка на C++ с архитектурой, близкой к реальным spreadsheet-системам.
Основная сложность проекта заключается не только в парсинге формул, но и в корректной реализации:
- dependency graph
- cycle detection
- cache invalidation
- согласованности состояния таблицы
- strong exception guarantee
Теперь:
- AST не зависит напрямую от
Sheet - управление dependency graph сосредоточено в
Sheet Cellотвечает за локальное содержимое и кэш узла- устранено дублирование ответственности между
CellиSheet - добавлен two-phase commit при
SetCell - ссылки из временной ячейки извлекаются без повторного парсинга
- кэш инвалидируется только там, где это необходимо
Sheet
│
▼
Cell
│
▼
FormulaImpl
│
▼
FormulaAST
│
▼
Execute(CellValueGetter)