Skip to content

Instantly share code, notes, and snippets.

@skizzerz
Created December 23, 2015 22:15
Show Gist options
  • Save skizzerz/68711a48d2c25f75d487 to your computer and use it in GitHub Desktop.
Save skizzerz/68711a48d2c25f75d487 to your computer and use it in GitHub Desktop.
import re
import string
""" Custom formatter mimicing str.format() with additional capabilities.
A format string can now additionally take the form:
{literal|:format_spec}
This form will treat the text "literal" as the text to be formatted
instead of attempting to get the value from the passed-in arguments.
Furthermore, the built-in format_spec mini language has been expanded.
The following may now appear as the "type":
plural[(int)] - converts the value to plural form
num(int) - converts the value to plural form and prefixes with int
list[(sep=", ")] - converts value to list separated by sep
is - returns "is" if value is 1 or "are" otherwise (value must be an int)
was - returns "was" if value is 1 or "were" otherwise
an - prefixes value with "a" or "an"
If the # specifier is used with num or an, it will cause the prefix
to be bolded. If it is used with list, it will omit the "and" prior to
the last list item and will not special-case the two-item form.
"""
# Note: {0:is} is simply shorthand for {is|:plural({0})}, same with {0:was}
# https://docs.python.org/3/library/string.html#format-specification-mini-language
# (enhanced with the above additional types)
FORMAT = re.compile(
r"^(?:(?P<fill>.?)(?P<align>[<>^=]))?(?P<sign>[+\- ]?)(?P<alt>#?)(?P<zero>0?)"
r"(?P<width>[1-9]+|0+|0[oO][1-7]+|0[xX][0-9a-fA-F]+|0[bB][01]+)?(?P<comma>,?)"
r"(?P<precision>\.(?:[1-9]+|0+|0[oO][1-7]+|0[xX][0-9a-fA-F]+|0[bB][01]+))?"
r"(?P<type>[bcdeEfFgGnosxX%]|plural|num|list|is|was|an)(?:\((?P<arg>.*)\))?$"
)
class CustomFormatter(string.Formatter):
def get_value(self, key, args, kwargs):
if not isinstance(key, int) and key[-1] == "|":
return key[:-1]
return super(CustomFormatter, self).get_value(key, args, kwargs)
def format_field(self, value, format_spec):
if format_spec:
m = FORMAT.match(format_spec)
if m:
fill = m.group("fill") or ""
align = m.group("align") or ""
sign = m.group("sign") or ""
alt = m.group("alt") == "#" # we don't pass alt through
zero = m.group("zero") or ""
width = m.group("width") or ""
comma = m.group("comma") or ""
precision = m.group("precision") or ""
ty = m.group("type")
arg = m.group("arg") # may be None
if ty in ("plural", "num", "list", "is", "was", "an"):
format_spec = fill + align + sign + zero + width + comma + precision + "s"
if ty == "plural":
arg = int(arg, 0) if arg else 2
value = self.plural(value, arg)
elif ty == "num":
if arg is None:
raise ValueError("num() format requires an argument")
arg = int(arg, 0)
prefix = str(arg) if arg > 0 else "no"
if alt:
prefix = "\u0002" + prefix + "\u0002"
value = prefix + " " + self.plural(value, arg)
elif ty == "an":
prefix = "an" if value[0] in ("a", "e", "i", "o", "u") else "a"
if alt:
prefix = "\u0002" + prefix + "\u0002"
value = prefix + " " + value
elif ty == "is":
if not isinstance(value, int):
value = int(value, 0)
value = "is" if value == 1 else "are"
elif ty == "was":
if not isinstance(value, int):
value = int(value, 0)
value = "was" if value == 1 else "were"
elif ty == "list":
sep = arg or ", "
lv = len(value)
if alt:
value = sep.join(value)
elif lv == 0:
value = ""
elif lv == 1:
value = value[0]
elif lv == 2:
value = value[0] + " and " + value[1]
else:
value = sep.join(value[:-1]) + sep + "and " + value[-1]
return super(CustomFormatter, self).format_field(value, format_spec)
def plural(self, value, count=2):
if count == 1:
return value
bits = value.split()
if bits[-1][-2:] == "'s":
bits[-1] = self.plural(bits[-1][:-2], count)
bits[-1] += "'" if bits[-1][-1] == "s" else "'s"
else:
bits[-1] = {"person": "people",
"wolf": "wolves",
"has": "have",
"is": "are",
"was": "were",
"succubus": "succubi"}.get(bits[-1], bits[-1] + "s")
return " ".join(bits)
# vim: set expandtab:sw=4:ts=4:
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment