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
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.
/*
* 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)
- Why must the redo stack be cleared after a new command?