patternpythonMinor
Wrap a long string to an array of short-enough strings
Viewed 0 times
shortarrayenoughlongwrapstringsstring
Problem
I have made a function that wraps text in pygame (i.e. it turns a long string into an array of smaller strings that are short enough so they fit within the given width when rendered in the given font).
My function is below.
note:
As you can see, I have the statements
My function is below.
text is the string we want to break up, font is a pygame font object, and max_width is the number of pixels wide we want the lines to be at maximum (an integer).def wrap_text(text, font, max_width):
lines = []
words = text.split(" ")
while words:
line = words.pop(0)
if words:
width = font.size(" ".join((line, words[0])))[0]
while width max_width:
# When there is only one word on the line and it is still
# too long to fit within the given maximum width.
raise ValueError("".join(("\"", line, "\"", " is too long to be wrapped.")))
lines.append(line)
return linesnote:
font.size(string) returns a tuple containing the width and height the string will be when rendered in the given font.As you can see, I have the statements
while words:, if words: and if not words: all within each other. I have been trying to refactor this by moving things around but I simply cannot think of a way to remove any of the 3 statements above. Any help is much appreciated :). Any comments about anything else in my code is welcome too.Solution
Bug
You have a small bug. For
You can greatly simplify the handling of embedded whitespace characters by using
Don't repeat yourself
This (or almost the same) piece of code appears in multiple places:
Look out for duplication like this and use helper functions to eliminate.
Overcomplicated string joins
Instead of
it would be a lot simpler to write
Simplify the logic
Consider this alternative algorithm:
Implementation:
Some doctests to verify it works:
You have a small bug. For
text="abc \\n def" and max_width=10 the output is incorrectly ['abc', 'def'] instead of ['abc def'].You can greatly simplify the handling of embedded whitespace characters by using
re.split with a pattern r'\s+'.Don't repeat yourself
This (or almost the same) piece of code appears in multiple places:
" ".join((line, words[0])).Look out for duplication like this and use helper functions to eliminate.
Overcomplicated string joins
Instead of
" ".join((line, words[0])),it would be a lot simpler to write
line + " " + words[0].Simplify the logic
Consider this alternative algorithm:
- Create a word generator: from some text as input, extract the words one by one
- For each word:
- If the word is too long, raise an error
- If the current line + word would be too long, then
- Append the current line to the list of lines
- Start a new line with the current word
- If the current line + word is not too long, then append the word to the line
- Append the current line to the list of lines
Implementation:
def wrap_text(text, font, max_width):
def gen_words(text):
yield from re.split(r'\s+', text)
# or in older versions of Python:
# for word in re.split(r'\s+', text):
# yield word
def raise_word_too_long_error(word):
raise ValueError("\"{}\" is too long to be wrapped.".format(word))
def too_long(line):
return font.size(line)[0] > max_width
words = gen_words(text)
line = next(words)
if too_long(line):
raise_word_too_long_error(line)
lines = []
for word in words:
if too_long(word):
raise_word_too_long_error(word)
if too_long(line + " " + word):
lines.append(line)
line = word
else:
line += " " + word
lines.append(line)
return linesSome doctests to verify it works:
def _wrap_text_tester(text, max_width):
"""
>>> _wrap_text_tester("hello there", 7)
['hello', 'there']
>>> _wrap_text_tester("I am legend", 7)
['I am', 'legend']
>>> _wrap_text_tester("abc \\n def", 10)
['abc def']
>>> _wrap_text_tester("toobigtofit", 7)
Traceback (most recent call last):
...
ValueError: "toobigtofit" is too long to be wrapped.
>>> _wrap_text_tester("in the middle toobigtofit", 7)
Traceback (most recent call last):
...
ValueError: "toobigtofit" is too long to be wrapped.
>>> _wrap_text_tester("", 7)
['']
"""
return wrap_text(text, font, max_width)Code Snippets
def wrap_text(text, font, max_width):
def gen_words(text):
yield from re.split(r'\s+', text)
# or in older versions of Python:
# for word in re.split(r'\s+', text):
# yield word
def raise_word_too_long_error(word):
raise ValueError("\"{}\" is too long to be wrapped.".format(word))
def too_long(line):
return font.size(line)[0] > max_width
words = gen_words(text)
line = next(words)
if too_long(line):
raise_word_too_long_error(line)
lines = []
for word in words:
if too_long(word):
raise_word_too_long_error(word)
if too_long(line + " " + word):
lines.append(line)
line = word
else:
line += " " + word
lines.append(line)
return linesdef _wrap_text_tester(text, max_width):
"""
>>> _wrap_text_tester("hello there", 7)
['hello', 'there']
>>> _wrap_text_tester("I am legend", 7)
['I am', 'legend']
>>> _wrap_text_tester("abc \\n def", 10)
['abc def']
>>> _wrap_text_tester("toobigtofit", 7)
Traceback (most recent call last):
...
ValueError: "toobigtofit" is too long to be wrapped.
>>> _wrap_text_tester("in the middle toobigtofit", 7)
Traceback (most recent call last):
...
ValueError: "toobigtofit" is too long to be wrapped.
>>> _wrap_text_tester("", 7)
['']
"""
return wrap_text(text, font, max_width)Context
StackExchange Code Review Q#150584, answer score: 5
Revisions (0)
No revisions yet.