import re
from decimal import Decimal
from typing import Union, Any, Optional, Tuple, cast, Dict
from babel import Locale
from babel.numbers import NumberPattern, number_re, parse_grouping, LC_NUMERIC
ASTERISK_CLEAN_RE = re.compile(r"[*]")
QUESTION_MARK_RE = re.compile(r"\?")
UNDERSCORE_RE = re.compile(r"_.")
MINUS_CLEAN_RE = re.compile(r"[-]")
CLEAN_CURRENCY_RE = re.compile(r"\[\$(.+?)(:?-[\d]+|)\]")
COLOR_FORMAT = re.compile(r"\[([A-Z]+)\]", re.IGNORECASE)
[docs]class ColorNumberPattern(NumberPattern):
def __init__(self, *args, **kwargs): # type: ignore
super(ColorNumberPattern, self).__init__(*args, **kwargs)
self.pos_color = self.neg_color = None
try:
self.pos_color = COLOR_FORMAT.findall(self.prefix[0])[0]
except IndexError:
pass
try:
self.neg_color = COLOR_FORMAT.findall(self.prefix[1])[0]
except IndexError:
pass
self.prefix = [COLOR_FORMAT.sub("", p) for p in self.prefix]
[docs] def apply(self, value: Any, locale: Union[str, Locale], **kwargs) -> str: # type: ignore
formatted = super(ColorNumberPattern, self).apply(value, locale, **kwargs)
return self.apply_color(value, formatted)
[docs] def apply_color(self, value: Any, formatted: str) -> str:
if not isinstance(value, Decimal):
value = Decimal(str(value))
value = value.scaleb(self.scale)
is_negative = int(value.is_signed())
template = '<span style="color: {color}">{value}</span>'
if is_negative and self.neg_color:
return template.format(color=self.neg_color, value=formatted)
if not is_negative and self.pos_color:
return template.format(color=self.pos_color, value=formatted)
return formatted
[docs]class PatternParser:
def __init__(self, pattern: str):
self.general_pattern = None
self.by_sign_pattern = None
def _match_number(pattern: str) -> Dict:
rv = number_re.search(pattern)
if rv is None:
raise ValueError("Invalid number pattern %r" % pattern)
return rv.groups()
def parse_precision(p: str) -> Tuple[int, int]:
"""Calculate the min and max allowed digits"""
min = max = 0
for c in p:
if c in "@0":
min += 1
max += 1
elif c == "#":
max += 1
elif c in ",. ":
continue
else:
break
return min, max
def generate_color_number_pattern(
pattern: str, number: str, prefix: Tuple[str, str], suffix: Tuple[str, str]
) -> Union[str, ColorNumberPattern]:
(grouping, int_prec, frac_prec, exp_prec, exp_plus) = handle_number(number)
if number != "":
return ColorNumberPattern(
pattern, prefix, suffix, grouping, int_prec, frac_prec, exp_prec, exp_plus
) # types: ignore
else:
return pattern
def handle_number(
number: str
) -> Tuple[
Tuple[int, int],
Tuple[int, int],
Tuple[int, int],
Optional[Tuple[int, int]],
Optional[bool],
]:
exp: Optional[str] = None
if "E" in number:
number, exp = number.split("E", 1)
if "@" in number:
if "." in number and "0" in number:
raise ValueError("Significant digit patterns can not contain " '"@" or "0"')
if "." in number:
integer, fraction = number.rsplit(".", 1)
else:
integer = number
fraction = ""
int_prec = parse_precision(integer)
frac_prec = parse_precision(fraction)
exp_prec: Optional[Tuple[int, int]] = None
exp_plus: Optional[bool] = None
if exp:
frac_prec = parse_precision(integer + fraction)
exp_plus = exp.startswith("+")
exp = exp.lstrip("+")
exp_prec = parse_precision(exp)
grouping = cast(Tuple[int, int], parse_grouping(integer))
return (grouping, int_prec, frac_prec, exp_prec, exp_plus)
"""Parse number format patterns"""
if isinstance(pattern, NumberPattern):
self.general_pattern = pattern
return
pattern = CLEAN_CURRENCY_RE.sub("\\1", pattern.replace("\\", ""))
pattern = ASTERISK_CLEAN_RE.sub("", pattern).strip()
pattern = UNDERSCORE_RE.sub(" ", pattern)
pattern = QUESTION_MARK_RE.sub("", pattern)
if ";" in pattern:
pattern_parts = pattern.split(";")
pos_pattern = pattern_parts[0]
neg_pattern = pattern_parts[1]
zero_pattern = pattern_parts[2] if len(pattern_parts) > 2 else pos_pattern
# text_pattern = pattern_parts[3] if len(pattern_parts) > 3 else pos_pattern
by_sign_pattern_list = []
for _pattern in [pos_pattern, neg_pattern, zero_pattern]:
prefix, number, suffix = _match_number(_pattern)
color_number_pattern = generate_color_number_pattern(
_pattern, number, (prefix, prefix), (suffix, suffix)
)
by_sign_pattern_list.append(color_number_pattern)
self.by_sign_pattern = tuple(by_sign_pattern_list)
else:
pattern = MINUS_CLEAN_RE.sub("", pattern)
pos_prefix, number, pos_suffix = _match_number(pattern)
neg_prefix = "-" + pos_prefix
neg_suffix = pos_suffix
self.general_pattern = generate_color_number_pattern(
pattern, number, (pos_prefix, neg_prefix), (pos_suffix, neg_suffix)
)
[docs] def apply(self, number: int, locale: str) -> str:
if self.general_pattern:
pattern = self.general_pattern
elif self.by_sign_pattern:
pos_pattern, neg_pattern, zero_pattern = self.by_sign_pattern
pattern = zero_pattern
if number > 0:
pattern = pos_pattern
elif number < 0:
pattern = neg_pattern
# in case there are no number included - pattern will be string
if isinstance(pattern, str):
return pattern
else:
return pattern.apply(number, locale)