Command Pattern: Undo and Redo Editor

Problem

Build a tiny text editor. Commands: type WORD (adds the word at the end), delete N (removes the last N characters, or all characters if there are fewer), undo, redo and show. show prints the text, or (empty) if there is no text. undo does nothing if there is nothing to undo. redo does nothing if there is nothing to redo. A new type or delete removes all redo steps. undo and redo are not commands themselves.

Constraints

  • 1 <= lines <= 10^4

Examples

Input

type ab
type cd
delete 3
show
undo
show

Output

a
abcd

Solutions

Best approachTime O(total characters)Space O(total characters)

Each action becomes an object with execute and undo. Two stacks give undo and redo. A delete must remember what it removed. A new action clears the redo stack, because the old future no longer exists.

Same code works in Java810172125Basic features only, so nothing changes between versions.
/*
 * Problem : Process lines.
 * Input   : Several lines.
 * Output  : The shown texts.
 * Approach: Command objects and two stacks.
 * Time    : O(total characters)
 * Space   : O(total characters)
 * Java    : 8 or newer.
 */
import java.util.Scanner;

public class Main {
        // Step 1: Every action is a command object that knows how to do itself and how to undo itself.
        interface Command {
            String execute(String text);

            String undo(String text);
        }

        static class TypeCommand implements Command {
            private final String added;

            TypeCommand(String added) {
                this.added = added;
            }

            public String execute(String text) {
                return text + added;
            }

            public String undo(String text) {
                return text.substring(0, text.length() - added.length());
            }
        }

        // Step 2: A delete command must remember the removed characters, because undo has to put them back.
        static class DeleteCommand implements Command {
            private final int count;
            private String removed = "";

            DeleteCommand(int count) {
                this.count = count;
            }

            public String execute(String text) {
                int keep = Math.max(0, text.length() - count);
                removed = text.substring(keep);
                return text.substring(0, keep);
            }

            public String undo(String text) {
                return text + removed;
            }
        }

    // This method solves the task. It receives the input and returns the answer.
    static String[] solve(String[] lines) {
        // Step 3: Two stacks: commands that can be undone, and commands that can be redone.
        java.util.ArrayDeque<Command> done = new java.util.ArrayDeque<>();
        java.util.ArrayDeque<Command> undone = new java.util.ArrayDeque<>();
        String text = "";
        java.util.List<String> output = new java.util.ArrayList<>();

        for (String line : lines) {
            String[] part = line.split(" ");
            if (part[0].equals("type") || part[0].equals("delete")) {
                // Step 4: A new command runs and is pushed on the undo stack. A new action makes the redo stack invalid.
                Command command = part[0].equals("type") ? new TypeCommand(part[1]) : new DeleteCommand(Integer.parseInt(part[1]));
                text = command.execute(text);
                done.push(command);
                undone.clear();
            } else if (part[0].equals("undo")) {
                // Step 5: Undo takes the last command, reverses it and moves it to the redo stack.
                if (!done.isEmpty()) {
                    Command command = done.pop();
                    text = command.undo(text);
                    undone.push(command);
                }
            } else if (part[0].equals("redo")) {
                if (!undone.isEmpty()) {
                    Command command = undone.pop();
                    text = command.execute(text);
                    done.push(command);
                }
            } else {
                output.add(text.isEmpty() ? "(empty)" : text);
            }
        }
        return output.toArray(new String[0]);
    }

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        java.util.List<String> all = new java.util.ArrayList<>();
        while (scanner.hasNextLine()) {
            String line = scanner.nextLine().trim();
            if (!line.isEmpty()) {
                all.add(line);
            }
        }
        String[] lines = all.toArray(new String[0]);
        System.out.println(String.join("\n", solve(lines)));
    }
}
Hints (1)
  1. Why must the redo stack be cleared after a new command?