← Command lesson
Lab

Command

Sign in to run your code

Read the brief and the starter here. With a free account you can edit it, run the sample, step through it and check your work, and your progress is kept.

type Levels = Record<string, number>;
type Change = { levels: Levels; inverse: Command };

// A request the controller runs. It checks itself when it runs: it throws when
// the request is invalid and returns null when there is nothing to change.
interface Command {
  label: string;
  execute(levels: Levels): Change | null;
}

// The level a light has now, from 0 (off) to 100.
function current(levels: Levels, light: string): number {
  const level = levels[light];
  if (level === undefined) throw new Error('unknown light: ' + light);
  return level;
}

function setLevel(light: string, level: number): Command {
  return {
    label: 'Set',
    execute(levels) {
      const before = current(levels, light);
      if (level < 0 || level > 100) throw new Error('level must be from 0 to 100: ' + level);
      if (level === before) return null;
      return { levels: { ...levels, [light]: level }, inverse: setLevel(light, before) };
    },
  };
}

// One notch of the dimmer wheel: dims a light by step, never below 0.
// Draft: Undo brightens the light back by the same step.
function dimBy(light: string, step: number): Command {
  return {
    label: 'Dim',
    execute(levels) {
      const level = Math.max(0, current(levels, light) - step);
      return { levels: { ...levels, [light]: level }, inverse: brightenBy(light, step) };
    },
  };
}

function brightenBy(light: string, step: number): Command {
  return {
    label: 'Brighten',
    execute(levels) {
      const level = Math.min(100, current(levels, light) + step);
      return { levels: { ...levels, [light]: level }, inverse: dimBy(light, step) };
    },
  };
}

type Entry = { forward: Command; inverse: Command };

// The controller runs every request (wall panel, phone app, schedule), one at
// a time, and keeps the home's history.
class Controller {
  levels: Levels;
  past: Entry[] = [];
  future: Entry[] = [];

  constructor(levels: Levels) {
    this.levels = { ...levels };
  }

  execute(command: Command): boolean {
    const change = command.execute(this.levels);
    if (!change) return false;
    this.levels = change.levels;
    this.past.push({ forward: command, inverse: change.inverse });
    this.future = [];
    return true;
  }

  undo(): boolean {
    const entry = this.past.at(-1);
    const change = entry?.inverse.execute(this.levels);
    if (!entry || !change) return false;
    this.levels = change.levels;
    this.future.push(this.past.pop()!);
    return true;
  }

  redo(): boolean {
    const entry = this.future.at(-1);
    const change = entry?.forward.execute(this.levels);
    if (!entry || !change) return false;
    this.levels = change.levels;
    this.past.push(this.future.pop()!);
    return true;
  }
}