7
\$\begingroup\$

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. 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:
 if words[0] == "\n":
 # Forced newline when "\n" is in the text
 del words[0]
 break
 line = " ".join((line, words.pop(0)))
 if not words:
 break
 width = font.size(" ".join((line, words[0])))[0]
 if font.size(line)[0] > 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 lines

note: 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.

janos
113k15 gold badges154 silver badges396 bronze badges
asked Dec 22, 2016 at 11:26
\$\endgroup\$
0

1 Answer 1

5
\$\begingroup\$

Bug

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 lines

Some 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)
answered Dec 22, 2016 at 13:01
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.