Created
March 15, 2022 19:16
-
-
Save ibeauregard/2002a8bbb723e018f0934b3578f46f52 to your computer and use it in GitHub Desktop.
A simple text editor, along with tests
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import functools | |
class TextEditor: | |
def __init__(self): | |
self.text = '' | |
self.left = 0 | |
self.right = 0 | |
self.clipboard = '' | |
self.operation_stack = OperationStack() | |
def return_text(method): | |
@functools.wraps(method) | |
def wrapper(self, *args, **kwargs): | |
method(self, *args, **kwargs) | |
return self.text | |
return wrapper | |
@return_text | |
def append(self, string): | |
if not string: | |
return | |
self._store_and_perform(AppendOperation(self, string)) | |
@return_text | |
def move(self, position): | |
self.left = self.right = int(position) | |
@return_text | |
def backspace(self): | |
if self.left == self.right == 0: | |
return | |
self._store_and_perform(BackspaceOperation(self)) | |
@return_text | |
def select(self, left, right): | |
self.left, self.right = max(0, int(left)), min(len(self.text), int(right)) | |
@return_text | |
def copy(self): | |
self.clipboard = self.text[self.left:self.right] | |
@return_text | |
def paste(self): | |
self.append(self.clipboard) | |
@return_text | |
def undo(self): | |
try: | |
self.operation_stack.last_done().undo() | |
except IndexError: | |
pass | |
@return_text | |
def redo(self): | |
try: | |
self.operation_stack.last_undone().do() | |
except IndexError: | |
pass | |
def _store_and_perform(self, operation): | |
self.operation_stack.store(operation) | |
operation.do() | |
del return_text | |
class BackspaceOperation: | |
def __init__(self, editor): | |
self.editor = editor | |
self.left = self.right = None | |
def do(self): | |
editor = self.editor | |
self.left = self.left or editor.left | |
self.right = self.right or editor.right | |
left = self.left - (self.left == self.right) | |
self.removed = editor.text[left:self.right] | |
editor.text = f'{editor.text[:left]}{editor.text[self.right:]}' | |
editor.left = editor.right = left | |
def undo(self): | |
editor = self.editor | |
left = self.left - (self.left == self.right) | |
editor.text = f'{editor.text[:left]}{self.removed}{editor.text[left:]}' | |
editor.left, editor.right = self.left, self.right | |
class AppendOperation: | |
def __init__(self, editor, string): | |
self.editor = editor | |
self.string = string | |
self.left = self.right = None | |
def do(self): | |
editor = self.editor | |
self.left = self.left or editor.left | |
self.right = self.right or editor.right | |
self.removed = editor.text[self.left:self.right] | |
editor.text = f'{editor.text[:self.left]}{self.string}{editor.text[self.right:]}' | |
editor.left = editor.right = editor.left + len(self.string) | |
def undo(self): | |
editor = self.editor | |
editor.text = f'{editor.text[:self.left]}{self.removed}{editor.text[self.left + len(self.string):]}' | |
editor.left, editor.right = self.left, self.right | |
class OperationStack: | |
def __init__(self): | |
self.stack = [] | |
self.cursor = 0 | |
def store(self, operation): | |
del self.stack[self.cursor:] | |
self.stack.append(operation) | |
self.cursor += 1 | |
def last_done(self): | |
self.cursor = max(0, self.cursor - 1) | |
return self.stack[self.cursor] | |
def last_undone(self): | |
last = self.stack[self.cursor] | |
self.cursor += 1 | |
return last | |
operations = {'APPEND': TextEditor.append, | |
'MOVE': TextEditor.move, | |
'BACKSPACE': TextEditor.backspace, | |
'SELECT': TextEditor.select, | |
'COPY': TextEditor.copy, | |
'PASTE': TextEditor.paste, | |
'UNDO': TextEditor.undo, | |
'REDO': TextEditor.redo} | |
def process_queries(*queries, text_editor=None): | |
text_editor = text_editor or TextEditor() | |
return [operations[operation](text_editor, *parameters) for operation, *parameters in queries] | |
def test(name, *, queries, expected): | |
output = process_queries(*queries) | |
for i, (query, out, exp) in enumerate(zip(queries, output, expected), start=1): | |
assert out == exp, '\n'.join(("\n" | |
f"Test {name} failed at query {i} of {len(queries)}", | |
f"{'Query:':9} {query}", | |
f"Expected: '{exp}'", | |
f"{'Got:':9} '{out}'")) | |
test(name='1.1', | |
queries=[["APPEND", "Hey"], ["APPEND", " there"],["APPEND", "!"]], | |
expected=['Hey', 'Hey there', 'Hey there!']) | |
test(name='1.2', | |
queries=[["APPEND", "Hey"], ["APPEND", " you"], ["APPEND", ", don't"], ["APPEND", " "],["APPEND", "let me down"]], | |
expected=["Hey", "Hey you", "Hey you, don't", "Hey you, don't ", "Hey you, don't let me down"]) | |
test(name='2', | |
queries=[["APPEND", "Hey you"],["MOVE", "3"], ["APPEND", ","]], | |
expected=['Hey you', 'Hey you', 'Hey, you']) | |
test(name='3.1', | |
queries=[["APPEND", "Hey you"], ["BACKSPACE"], ["BACKSPACE"], ["MOVE", "0"], ["BACKSPACE"]], | |
expected = ["Hey you", "Hey yo", "Hey y", "Hey y", "Hey y"]) | |
test(name='3.2', | |
queries=[["APPEND", "!"], ["BACKSPACE"], ["BACKSPACE"]], | |
expected = ['!', '', '']) | |
test(name='4', | |
queries=[["APPEND", "Hello cruel world!"], ["SELECT", "5", "11"], ["APPEND", ","], ["SELECT", "5", "12"], ["BACKSPACE"], ["SELECT", "4", "6"], ["MOVE", "1"]], | |
expected=["Hello cruel world!", "Hello cruel world!", "Hello, world!", "Hello, world!", "Hello!", "Hello!", "Hello!"]) | |
test(name='5-6', | |
queries=[["APPEND", "Hello, world!"], ["SELECT", "5", "12"], ["COPY"], ["MOVE", "12"], ["PASTE"], ["PASTE"]], | |
expected = ["Hello, world!", "Hello, world!", "Hello, world!", "Hello, world!", "Hello, world, world!", "Hello, world, world, world!"]) | |
test(name='7', | |
queries=[["APPEND", "Hello, world!"], ["SELECT", "7", "12"], ["BACKSPACE"], ["UNDO"], ["APPEND", "you"], ["MOVE", 6], ["BACKSPACE"], ["UNDO"]], | |
expected=["Hello, world!", "Hello, world!", "Hello, !", "Hello, world!", "Hello, you!", "Hello, you!", "Hello you!", "Hello, you!"]) | |
test(name='8', | |
queries=[["APPEND", "Hello, world!"], ["SELECT", "7", "12"], ["APPEND", "you"], ["SELECT", "7", "10"], ["BACKSPACE"], ["MOVE", "6"], ["UNDO"], ["UNDO"], ["UNDO"], ["REDO"], ["REDO"], ["REDO"]], | |
expected=["Hello, world!", "Hello, world!", "Hello, you!", "Hello, you!", "Hello, !", "Hello, !", "Hello, you!", "Hello, world!", "", "Hello, world!", "Hello, you!", "Hello, !"]) | |
test(name='9', | |
queries=[["UNDO"], ["APPEND", "Hello, world!"], ["UNDO"], ["UNDO"], ["REDO"], ["REDO"]], | |
expected=["", "Hello, world!", "", "", "Hello, world!", "Hello, world!"]) | |
test(name='10', | |
queries=[["APPEND", "Hello, world!"], ["SELECT", "5", "12"], ["COPY"], ["PASTE"], ["PASTE"], ["UNDO"], ["UNDO"], ["UNDO"], ["REDO"], ["REDO"], ["REDO"]], | |
expected=["Hello, world!", "Hello, world!", "Hello, world!", "Hello, world!", "Hello, world, world!", "Hello, world!", "Hello, world!", "", "Hello, world!", "Hello, world!", "Hello, world, world!"]) | |
test(name='11', | |
queries=[["APPEND", "Hello"], ["UNDO"], ["UNDO"], ["APPEND", "Hi"], ["UNDO"], ["REDO"], ["REDO"]], | |
expected=["Hello", "", "", "Hi", "", "Hi", "Hi"]) | |
print('All tests passed') |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment