type Formula = (args: number[]) => number;
// An add-in the user enabled, and the functions it brings: [name, formula].
type AddIn = { id: string; functions: [string, Formula][] };
// The built-in functions every workbook has.
const sum: Formula = (args) => args.reduce((total, n) => total + n, 0);
const maxOf: Formula = (args) => Math.max(...args);
// The registry: each function under its name in capitals, so =sum and
// =SUM are one function.
class FunctionRegistry {
private entries = new Map<string, Formula>();
register(name: string, formula: Formula): void {
const key = name.toUpperCase();
if (this.entries.has(key)) throw new Error(`already registered: ${key}`);
this.entries.set(key, formula);
}
resolve(name: string): Formula | undefined {
return this.entries.get(name.toUpperCase());
}
names(): string[] {
return [...this.entries.keys()].sort();
}
}
// A workbook: setup fills its registry, and the formula engine looks names up.
class Workbook {
private functions = new FunctionRegistry();
// What setup refused, as "add-in: reason".
refused: string[] = [];
constructor(addIns: AddIn[]) {
this.functions.register('SUM', sum);
this.functions.register('MAX', maxOf);
for (const addIn of addIns) {
for (const [name, formula] of addIn.functions) {
try {
this.functions.register(name, formula);
} catch (error) {
this.refused.push(`${addIn.id}: ${(error as Error).message}`);
}
}
}
}
evaluate(name: string, args: number[]): string {
const formula = this.functions.resolve(name);
return formula ? String(formula(args)) : '#NAME?';
}
functionNames(): string[] {
return this.functions.names();
}
}
The switch knew every function by heart, so a function nobody wrote a branch for could never be found. The fix gives the workbook a registry: setup writes the built-ins and then each add-in’s functions into one map, and the engine’s only job is to look the formula’s name up. The engine now knows no function by name, which is why the checker’s own shipping add-in, mentioned in no brief, works too. Spreadsheet names ignore case, so the key is the name in capitals, used the same way when registering and when looking up; a map keyed by the spelling an add-in happened to use would hold SUM and Sum as two functions. That same key is what makes the duplicate check work: the inventory add-in’s Max is a second MAX, refused with a reason that names the add-in, and the built-in stays. Setup carries on after a refusal because an add-in’s one bad name should not cost the user every other function; the next level asks whether a half-installed add-in is really better.