Skip to content

Commit 1d21f0c

Browse files
Pieter12345facchinm
authored andcommitted
CommandHistory optimization
- Use LinkedList with ListIterator to make all methods except for `clear()` run in `O(1)` (constant runtime) instead of `O(n)` (linear runtime). - No longer store executed commands that are executed multiple times (executing {1, 1, 1, 1, 2} now only adds {1, 2} to the history).
1 parent f5b3831 commit 1d21f0c

File tree

1 file changed

+77
-28
lines changed

1 file changed

+77
-28
lines changed
+77-28
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,60 @@
11
package processing.app;
22

3-
import java.util.ArrayList;
4-
import java.util.List;
3+
import java.util.LinkedList;
4+
import java.util.ListIterator;
55

66
/**
77
* Keeps track of command history in console-like applications.
88
* @author P.J.S. Kools
99
*/
1010
public class CommandHistory {
1111

12-
private List<String> commandHistory = new ArrayList<String>();
13-
private int selectedCommandIndex = 0;
12+
private final LinkedList<String> commandHistory = new LinkedList<String>();
1413
private final int maxHistorySize;
14+
private ListIterator<String> iterator = null;
15+
private boolean iteratorAsc;
1516

1617
/**
1718
* Create a new {@link CommandHistory}.
1819
* @param maxHistorySize - The max command history size.
1920
*/
2021
public CommandHistory(int maxHistorySize) {
2122
this.maxHistorySize = (maxHistorySize < 0 ? 0 : maxHistorySize);
22-
this.commandHistory.add(""); // Current command placeholder.
23+
this.commandHistory.addLast(""); // Current command placeholder.
2324
}
2425

2526
/**
2627
* Adds the given command to the history and resets the history traversal
27-
* position to the latest command. If the max history size is exceeded,
28-
* the oldest command will be removed from the history.
28+
* position to the latest command. If the latest command in the history is
29+
* equal to the given command, it will not be added to the history.
30+
* If the max history size is exceeded, the oldest command will be removed
31+
* from the history.
2932
* @param command - The command to add.
3033
*/
3134
public void addCommand(String command) {
35+
if (this.maxHistorySize == 0) {
36+
return;
37+
}
38+
39+
// Remove 'current' command.
40+
this.commandHistory.removeLast();
41+
42+
// Add new command if it differs from the latest command.
43+
if (this.commandHistory.isEmpty()
44+
|| !this.commandHistory.getLast().equals(command)) {
45+
46+
// Remove oldest command if max history size is exceeded.
47+
if (this.commandHistory.size() >= this.maxHistorySize) {
48+
this.commandHistory.removeFirst();
49+
}
3250

33-
// Remove the oldest command if the max history size is exceeded.
34-
if(this.commandHistory.size() >= this.maxHistorySize + 1) {
35-
this.commandHistory.remove(0);
51+
// Add new command and reset 'current' command.
52+
this.commandHistory.addLast(command);
3653
}
3754

38-
// Add the new command, reset the 'current' command and reset the index.
39-
this.commandHistory.set(this.commandHistory.size() - 1, command);
40-
this.commandHistory.add(""); // Current command placeholder.
41-
this.selectedCommandIndex = this.commandHistory.size() - 1;
55+
// Re-add 'current' command and reset command iterator.
56+
this.commandHistory.addLast(""); // Current command placeholder.
57+
this.iterator = null;
4258
}
4359

4460
/**
@@ -47,16 +63,35 @@ public void addCommand(String command) {
4763
* returns {@code false} otherwise.
4864
*/
4965
public boolean hasNextCommand() {
50-
return this.selectedCommandIndex + 1 < this.commandHistory.size();
66+
if (this.iterator == null) {
67+
return false;
68+
}
69+
if (!this.iteratorAsc) {
70+
this.iterator.next(); // Current command, ascending.
71+
this.iteratorAsc = true;
72+
}
73+
return this.iterator.hasNext();
5174
}
5275

5376
/**
5477
* Gets the next (more recent) command from the history.
5578
* @return The next command or {@code null} if no next command is available.
5679
*/
5780
public String getNextCommand() {
58-
return this.hasNextCommand()
59-
? this.commandHistory.get(++this.selectedCommandIndex) : null;
81+
82+
// Return null if there is no next command available.
83+
if (!this.hasNextCommand()) {
84+
return null;
85+
}
86+
87+
// Get next command.
88+
String next = this.iterator.next();
89+
90+
// Reset 'current' command when at the end of the list.
91+
if (this.iterator.nextIndex() == this.commandHistory.size()) {
92+
this.iterator.set(""); // Reset 'current' command.
93+
}
94+
return next;
6095
}
6196

6297
/**
@@ -65,15 +100,22 @@ public String getNextCommand() {
65100
* returns {@code false} otherwise.
66101
*/
67102
public boolean hasPreviousCommand() {
68-
return this.selectedCommandIndex > 0;
103+
if (this.iterator == null) {
104+
return this.commandHistory.size() > 1;
105+
}
106+
if (this.iteratorAsc) {
107+
this.iterator.previous(); // Current command, descending.
108+
this.iteratorAsc = false;
109+
}
110+
return this.iterator.hasPrevious();
69111
}
70112

71113
/**
72114
* Gets the previous (older) command from the history.
73115
* When this method is called while the most recent command in the history is
74116
* selected, this will store the current command as temporary latest command
75-
* so that {@link #getNextCommand()} will return it. This temporary latest
76-
* command gets reset when this case occurs again or when
117+
* so that {@link #getNextCommand()} will return it once. This temporary
118+
* latest command gets reset when this case occurs again or when
77119
* {@link #addCommand(String)} is invoked.
78120
* @param currentCommand - The current unexecuted command.
79121
* @return The previous command or {@code null} if no previous command is
@@ -86,14 +128,21 @@ public String getPreviousCommand(String currentCommand) {
86128
return null;
87129
}
88130

89-
// Store current unexecuted command if not traversing already.
90-
if (this.selectedCommandIndex == this.commandHistory.size() - 1) {
91-
this.commandHistory.set(this.commandHistory.size() - 1,
92-
(currentCommand == null ? "" : currentCommand));
131+
// Store current unexecuted command and create iterator if not traversing.
132+
if (this.iterator == null) {
133+
this.iterator =
134+
this.commandHistory.listIterator(this.commandHistory.size());
135+
this.iterator.previous(); // Last element, descending.
136+
this.iteratorAsc = false;
137+
}
138+
139+
// Store current unexecuted command if on 'current' index.
140+
if (this.iterator.nextIndex() == this.commandHistory.size() - 1) {
141+
this.iterator.set(currentCommand == null ? "" : currentCommand);
93142
}
94143

95144
// Return the previous command.
96-
return this.commandHistory.get(--this.selectedCommandIndex);
145+
return this.iterator.previous();
97146
}
98147

99148
/**
@@ -103,16 +152,16 @@ public String getPreviousCommand(String currentCommand) {
103152
* was set.
104153
*/
105154
public String resetHistoryLocation() {
106-
this.selectedCommandIndex = this.commandHistory.size() - 1;
155+
this.iterator = null;
107156
return this.commandHistory.set(this.commandHistory.size() - 1, "");
108157
}
109158

110159
/**
111160
* Clears the command history.
112161
*/
113162
public void clear() {
163+
this.iterator = null;
114164
this.commandHistory.clear();
115-
this.commandHistory.add(""); // Current command placeholder.
116-
this.selectedCommandIndex = 0;
165+
this.commandHistory.addLast(""); // Current command placeholder.
117166
}
118167
}

0 commit comments

Comments
 (0)