'Text Manipulator: String position movement
The task is to build to a text manipulator: A program that simulates a set of text manipulation commands.Given an input piece of text and a string of commands, output the mutated input text and cursor position.
Starting simple:
Commands
h: move cursor one character to the left
l: move cursor one character to the right
r<c>: replace character under cursor with <c>
Repeating commands
# All commands can be repeated N times by prefixing them with a number.
#
# [N]h: move cursor N characters to the left
# [N]l: move cursor N characters to the right
# [N]r<c>: replace N characters, starting from the cursor, with <c> and move the cursor
Examples
# We'll use Hello World as our input text for all cases:
#
# Input: hhlhllhlhhll
# Output: Hello World
# _
# 2
#
# Input: rhllllllrw
# Output: hello world
# _
# 6
#
# Input: rh6l9l4hrw
# Output: hello world
# _
# 6
#
# Input: 9lrL7h2rL
# Output: HeLLo WorLd
# _
# 3
#
# Input: 999999999999999999999999999lr0
# Output: Hello Worl0
# _
# 10
#
# Input: 999rsom
# Output: sssssssssss
# _
# 10
I have written the following piece of code, but getting an error:
class Editor():
def __init__(self, text):
self.text = text
self.pos = 0
def f(self, step):
self.pos += int(step)
def b(self, step):
self.pos -= int(step)
def r(self, char):
s = list(self.text)
s[self.pos] = char
self.text = ''.join(s)
def run(self, command):
command = list(command)
# if command not in ('F','B', 'R'):
#
while command:
operation = command.pop(0).lower()
if operation not in ('f','b','r'):
raise ValueError('command not recognized.')
method = getattr(self, operation)
arg = command.pop(0)
method(arg)
def __str__(self):
return self.text
# Normal run
text = 'abcdefghijklmn'
command = 'F2B1F5Rw'
ed = Editor(text)
ed.run(command)
print(ed)
I have used 'F' and 'B' in my code instead of 'h' and 'l', but the problem is I am missing a piece that allows me to define the optional 'N'. My code only works if there is a number defined after an operation. How can I fix the code above to meet all the requirements?
Solution 1:[1]
@paddy gave you a good advice, but looking at the string you need to parse, it seems to me that a regular expression would do the job quite easily. For the part after parsing, a Command pattern fits pretty well. Afterall you have a list of operations (commands) that must be executed over an initial string.
In your case using this pattern brings mainly 3 advantages in my opinion:
Each
Commandrepresents an operation applied to the initial string. This also means that, for example, in case you want to add a shortcut for a sequence of operations, the number of finalCommands stays the same and you only have to adjust the parsing step. Another benefit is that you can have a history of commands and in general the design is much more flexible.All
Commands share a common interface: a methodexecute()and, if necessary, a methodunexecute()to undo the changes applied by theexecute()method.Commands decouple the operations execution from the parsing problem.
As for the implementation, first you define Commands, that do not contain any business logic except for the call to the receiver method.
from __future__ import annotations
import functools
import re
import abc
from typing import Iterable
class ICommand(abc.ABC):
@abc.abstractmethod
def __init__(self, target: TextManipulator):
self._target = target
@abc.abstractmethod
def execute(self):
pass
class MoveCursorLeftCommand(ICommand):
def __init__(self, target: TextManipulator, counter):
super().__init__(target)
self._counter = counter
def execute(self):
self._target.move_cursor_left(self._counter)
class MoveCursorRightCommand(ICommand):
def __init__(self, target: TextManipulator, counter):
super().__init__(target)
self._counter = counter
def execute(self):
self._target.move_cursor_right(self._counter)
class ReplaceCommand(ICommand):
def __init__(self, target: TextManipulator, counter, replacement):
super().__init__(target)
self._replacement = replacement
self._counter = counter
def execute(self):
self._target.replace_char(self._counter, self._replacement)
Then you have the receiver of commands, which is the TextManipulator and contains methods to change the text and the cursor position.
class TextManipulator:
"""
>>> def apply_commands(s, commands_str):
... return TextManipulator(s).run_commands(CommandParser.parse(commands_str))
>>> apply_commands('Hello World', 'hhlhllhlhhll')
('Hello World', 2)
>>> apply_commands('Hello World', 'rhllllllrw')
('hello world', 6)
>>> apply_commands('Hello World', 'rh6l9l4hrw')
('hello world', 6)
>>> apply_commands('Hello World', '9lrL7h2rL')
('HeLLo WorLd', 3)
>>> apply_commands('Hello World', '999999999999999999999999999lr0')
('Hello Worl0', 10)
>>> apply_commands('Hello World', '999rsom')
Traceback (most recent call last):
ValueError: command 'o' not recognized.
>>> apply_commands('Hello World', '7l5r1')
('Hello W1111', 10)
>>> apply_commands('Hello World', '7l4r1')
('Hello W1111', 10)
>>> apply_commands('Hello World', '7l3r1')
('Hello W111d', 9)
"""
def __init__(self, text):
self._text = text
self._cursor_pos = 0
def replace_char(self, counter, replacement):
assert len(replacement) == 1
assert counter >= 0
self._text = self._text[0:self._cursor_pos] + \
replacement * min(counter, len(self._text) - self._cursor_pos) + \
self._text[self._cursor_pos + counter:]
self.move_cursor_right(counter - 1)
def move_cursor_left(self, counter):
assert counter >= 0
self._cursor_pos = max(0, self._cursor_pos - counter)
def move_cursor_right(self, counter):
assert counter >= 0
self._cursor_pos = min(len(self._text) - 1, self._cursor_pos + counter)
def run_commands(self, commands: Iterable[ICommand]):
for cmd in map(lambda partial_cmd: partial_cmd(target=self), commands):
cmd.execute()
return (self._text, self._cursor_pos)
Nothing really hard to explain about this code except for the run_commands method, that accepts an iterable of partial commands. These partial commands are commands that have been initiated without the receiver object, that should be of type TextManipulator. Why should you do that? It is a possible way to decouple parsing from commands execution. I decided to do it with functools.partial but you have other valid options.
Eventually, the parsing part:
class CommandParser:
@staticmethod
def parse(commands_str: str):
def invalid_command(match: re.Match):
raise ValueError(f"command '{match.group(2)}' not recognized.")
get_counter_from_match = lambda m: int(m.group(1) or 1)
commands_map = {
'h': lambda match: functools.partial(MoveCursorLeftCommand, \
counter=get_counter_from_match(match)),
'l': lambda match: functools.partial(MoveCursorRightCommand, \
counter=get_counter_from_match(match)),
'r': lambda match: functools.partial(ReplaceCommand, \
counter=get_counter_from_match(match), replacement=match.group(3))
}
parsed_commands_iter = re.finditer(r'(\d*)(h|l|r(\w)|.)', commands_str)
commands = map(lambda match: \
commands_map.get(match.group(2)[0], invalid_command)(match), parsed_commands_iter)
return commands
if __name__ == '__main__':
import doctest
doctest.testmod()
As I said at the beginning, parsing can be done with regex in your case and command creation is based on the first letter of the second capturing group of each match. The reason is that for the char replacement, the second capturing group includes the char to be replaced too. The commands_map is accessed with match.group(2)[0] as key and return a partial Command. If the operation is not found in the map, it throws a ValueError exception. The parameters of each Command are inferred from the re.Match object.
Just put all these code snippet together and you have a working solution (and some tests provided by the docstring executed by doctest).
This could be an over complicated design in some scenarios, so I am not saying that is the correct way to do it (it is probably not if you are writing a simple tool for example). You can avoid the Commands part and just take the parsing solution, but I have found this to be an interesting (alternative) application of the pattern.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | Marco Luzzara |
