Skip to content

Instantly share code, notes, and snippets.

@ibeauregard
Created March 15, 2022 19:16
Show Gist options
  • Save ibeauregard/2002a8bbb723e018f0934b3578f46f52 to your computer and use it in GitHub Desktop.
Save ibeauregard/2002a8bbb723e018f0934b3578f46f52 to your computer and use it in GitHub Desktop.
A simple text editor, along with tests
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