class Spreadsheet {
constructor() {
this.cells = new Map();
this.values = new Map();
this.dependents = new Map();
}
setCell(label, rawValue) {
this.removeOldDependencies(label);
const parsedCell = this.parseValue(rawValue);
this.cells.set(label, parsedCell);
this.addDependencies(label, parsedCell.references);
const evaluatedValue = this.evaluate(parsedCell);
this.values.set(label, evaluatedValue);
this.recalculateDependents(label);
}
getCell(label) {
return this.values.get(label) ?? 0;
}
parseValue(rawValue) {
if (!rawValue.startsWith("=")) {
return {
constant: Number(rawValue),
references: new Map()
};
}
let constant = 0;
const references = new Map();
const operands = rawValue.slice(1).split("+");
for (const operand of operands) {
if (/^-?\d+$/.test(operand)) {
constant += Number(operand);
} else {
references.set(
operand,
(references.get(operand) ?? 0) + 1
);
}
}
return {
constant,
references
};
}
evaluate(parsedCell) {
let result = parsedCell.constant;
for (const [referencedCell, count] of parsedCell.references) {
const referencedValue = this.getCell(referencedCell);
result += referencedValue * count;
}
return result;
}
removeOldDependencies(label) {
const oldCell = this.cells.get(label);
if (!oldCell) {
return;
}
for (const referencedCell of oldCell.references.keys()) {
const dependentSet = this.dependents.get(referencedCell);
if (!dependentSet) {
continue;
}
dependentSet.delete(label);
if (dependentSet.size === 0) {
this.dependents.delete(referencedCell);
}
}
}
addDependencies(label, references) {
for (const referencedCell of references.keys()) {
if (!this.dependents.has(referencedCell)) {
this.dependents.set(referencedCell, new Set());
}
this.dependents.get(referencedCell).add(label);
}
}
recalculateDependents(changedLabel) {
const affected = this.collectAffectedCells(changedLabel);
const indegree = new Map();
for (const cell of affected) {
indegree.set(cell, 0);
}
for (const cell of affected) {
const parsedCell = this.cells.get(cell);
if (!parsedCell) {
continue;
}
for (const dependency of parsedCell.references.keys()) {
if (affected.has(dependency)) {
indegree.set(cell, indegree.get(cell) + 1);
}
}
}
const queue = [];
for (const [cell, degree] of indegree) {
if (degree === 0) {
queue.push(cell);
}
}
let front = 0;
while (front < queue.length) {
const cell = queue[front++];
if (cell !== changedLabel) {
const parsedCell = this.cells.get(cell);
const nextValue = this.evaluate(parsedCell);
this.values.set(cell, nextValue);
}
for (const dependent of this.dependents.get(cell) ?? []) {
if (!affected.has(dependent)) {
continue;
}
const nextDegree = indegree.get(dependent) - 1;
indegree.set(dependent, nextDegree);
if (nextDegree === 0) {
queue.push(dependent);
}
}
}
}
collectAffectedCells(changedLabel) {
const affected = new Set([changedLabel]);
const queue = [changedLabel];
let front = 0;
while (front < queue.length) {
const current = queue[front++];
for (const dependent of this.dependents.get(current) ?? []) {
if (affected.has(dependent)) {
continue;
}
affected.add(dependent);
queue.push(dependent);
}
}
return affected;
}
}