Skip to content

Commit 8486103

Browse files
attack68yeshsurya
authored andcommitted
ENH: add decimal and thousands args to Styler.format() (pandas-dev#40596)
1 parent 26641d8 commit 8486103

File tree

3 files changed

+145
-17
lines changed

3 files changed

+145
-17
lines changed

pandas/io/formats/style.py

+20-1
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,16 @@ class Styler(StylerRenderer):
102102
103103
.. versionadded:: 1.2.0
104104
105+
decimal : str, default "."
106+
Character used as decimal separator for floats, complex and integers
107+
108+
.. versionadded:: 1.3.0
109+
110+
thousands : str, optional, default None
111+
Character used as thousands separator for floats, complex and integers
112+
113+
.. versionadded:: 1.3.0
114+
105115
escape : bool, default False
106116
Replace the characters ``&``, ``<``, ``>``, ``'``, and ``"`` in cell display
107117
strings with HTML-safe sequences.
@@ -160,6 +170,8 @@ def __init__(
160170
cell_ids: bool = True,
161171
na_rep: str | None = None,
162172
uuid_len: int = 5,
173+
decimal: str = ".",
174+
thousands: str | None = None,
163175
escape: bool = False,
164176
):
165177
super().__init__(
@@ -175,7 +187,14 @@ def __init__(
175187
# validate ordered args
176188
self.precision = precision # can be removed on set_precision depr cycle
177189
self.na_rep = na_rep # can be removed on set_na_rep depr cycle
178-
self.format(formatter=None, precision=precision, na_rep=na_rep, escape=escape)
190+
self.format(
191+
formatter=None,
192+
precision=precision,
193+
na_rep=na_rep,
194+
escape=escape,
195+
decimal=decimal,
196+
thousands=thousands,
197+
)
179198

180199
def _repr_html_(self) -> str:
181200
"""

pandas/io/formats/style_render.py

+85-16
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
import pandas.core.common as com
3939

4040
jinja2 = import_optional_dependency("jinja2", extra="DataFrame.style requires jinja2.")
41-
from markupsafe import escape as escape_func # markupsafe is jinja2 dependency
41+
from markupsafe import escape as escape_html # markupsafe is jinja2 dependency
4242

4343
BaseFormatter = Union[str, Callable]
4444
ExtFormatter = Union[BaseFormatter, Dict[Any, Optional[BaseFormatter]]]
@@ -366,6 +366,8 @@ def format(
366366
subset: slice | Sequence[Any] | None = None,
367367
na_rep: str | None = None,
368368
precision: int | None = None,
369+
decimal: str = ".",
370+
thousands: str | None = None,
369371
escape: bool = False,
370372
) -> StylerRenderer:
371373
"""
@@ -390,6 +392,16 @@ def format(
390392
391393
.. versionadded:: 1.3.0
392394
395+
decimal : str, default "."
396+
Character used as decimal separator for floats, complex and integers
397+
398+
.. versionadded:: 1.3.0
399+
400+
thousands : str, optional, default None
401+
Character used as thousands separator for floats, complex and integers
402+
403+
.. versionadded:: 1.3.0
404+
393405
escape : bool, default False
394406
Replace the characters ``&``, ``<``, ``>``, ``'``, and ``"`` in cell display
395407
string with HTML-safe sequences. Escaping is done before ``formatter``.
@@ -482,6 +494,8 @@ def format(
482494
formatter is None,
483495
subset is None,
484496
precision is None,
497+
decimal == ".",
498+
thousands is None,
485499
na_rep is None,
486500
escape is False,
487501
)
@@ -502,8 +516,14 @@ def format(
502516
format_func = formatter[col]
503517
except KeyError:
504518
format_func = None
519+
505520
format_func = _maybe_wrap_formatter(
506-
format_func, na_rep=na_rep, precision=precision, escape=escape
521+
format_func,
522+
na_rep=na_rep,
523+
precision=precision,
524+
decimal=decimal,
525+
thousands=thousands,
526+
escape=escape,
507527
)
508528

509529
for row, value in data[[col]].itertuples():
@@ -607,7 +627,7 @@ def _format_table_styles(styles: CSSStyles) -> CSSStyles:
607627
]
608628

609629

610-
def _default_formatter(x: Any, precision: int) -> Any:
630+
def _default_formatter(x: Any, precision: int, thousands: bool = False) -> Any:
611631
"""
612632
Format the display of a value
613633
@@ -617,51 +637,100 @@ def _default_formatter(x: Any, precision: int) -> Any:
617637
Input variable to be formatted
618638
precision : Int
619639
Floating point precision used if ``x`` is float or complex.
640+
thousands : bool, default False
641+
Whether to group digits with thousands separated with ",".
620642
621643
Returns
622644
-------
623645
value : Any
624-
Matches input type, or string if input is float or complex.
646+
Matches input type, or string if input is float or complex or int with sep.
625647
"""
626648
if isinstance(x, (float, complex)):
649+
if thousands:
650+
return f"{x:,.{precision}f}"
627651
return f"{x:.{precision}f}"
652+
elif isinstance(x, int) and thousands:
653+
return f"{x:,.0f}"
654+
return x
655+
656+
657+
def _wrap_decimal_thousands(
658+
formatter: Callable, decimal: str, thousands: str | None
659+
) -> Callable:
660+
"""
661+
Takes a string formatting function and wraps logic to deal with thousands and
662+
decimal parameters, in the case that they are non-standard and that the input
663+
is a (float, complex, int).
664+
"""
665+
666+
def wrapper(x):
667+
if isinstance(x, (float, complex, int)):
668+
if decimal != "." and thousands is not None and thousands != ",":
669+
return (
670+
formatter(x)
671+
.replace(",", "§_§-") # rare string to avoid "," <-> "." clash.
672+
.replace(".", decimal)
673+
.replace("§_§-", thousands)
674+
)
675+
elif decimal != "." and (thousands is None or thousands == ","):
676+
return formatter(x).replace(".", decimal)
677+
elif decimal == "." and thousands is not None and thousands != ",":
678+
return formatter(x).replace(",", thousands)
679+
return formatter(x)
680+
681+
return wrapper
682+
683+
684+
def _str_escape_html(x):
685+
"""if escaping html: only use on str, else return input"""
686+
if isinstance(x, str):
687+
return escape_html(x)
628688
return x
629689

630690

631691
def _maybe_wrap_formatter(
632692
formatter: BaseFormatter | None = None,
633693
na_rep: str | None = None,
634694
precision: int | None = None,
695+
decimal: str = ".",
696+
thousands: str | None = None,
635697
escape: bool = False,
636698
) -> Callable:
637699
"""
638700
Allows formatters to be expressed as str, callable or None, where None returns
639701
a default formatting function. wraps with na_rep, and precision where they are
640702
available.
641703
"""
704+
# Get initial func from input string, input callable, or from default factory
642705
if isinstance(formatter, str):
643-
formatter_func = lambda x: formatter.format(x)
706+
func_0 = lambda x: formatter.format(x)
644707
elif callable(formatter):
645-
formatter_func = formatter
708+
func_0 = formatter
646709
elif formatter is None:
647710
precision = get_option("display.precision") if precision is None else precision
648-
formatter_func = partial(_default_formatter, precision=precision)
711+
func_0 = partial(
712+
_default_formatter, precision=precision, thousands=(thousands is not None)
713+
)
649714
else:
650715
raise TypeError(f"'formatter' expected str or callable, got {type(formatter)}")
651716

652-
def _str_escape(x, escape: bool):
653-
"""if escaping: only use on str, else return input"""
654-
if escape and isinstance(x, str):
655-
return escape_func(x)
656-
else:
657-
return x
717+
# Replace HTML chars if escaping
718+
if escape:
719+
func_1 = lambda x: func_0(_str_escape_html(x))
720+
else:
721+
func_1 = func_0
658722

659-
display_func = lambda x: formatter_func(partial(_str_escape, escape=escape)(x))
723+
# Replace decimals and thousands if non-standard inputs detected
724+
if decimal != "." or (thousands is not None and thousands != ","):
725+
func_2 = _wrap_decimal_thousands(func_1, decimal=decimal, thousands=thousands)
726+
else:
727+
func_2 = func_1
660728

729+
# Replace missing values if na_rep
661730
if na_rep is None:
662-
return display_func
731+
return func_2
663732
else:
664-
return lambda x: na_rep if isna(x) else display_func(x)
733+
return lambda x: na_rep if isna(x) else func_2(x)
665734

666735

667736
def non_reducing_slice(slice_):

pandas/tests/io/formats/style/test_format.py

+40
Original file line numberDiff line numberDiff line change
@@ -197,3 +197,43 @@ def test_format_subset():
197197
assert ctx["body"][1][1]["display_value"] == "1.1"
198198
assert ctx["body"][0][2]["display_value"] == "0.123400"
199199
assert ctx["body"][1][2]["display_value"] == raw_11
200+
201+
202+
@pytest.mark.parametrize("formatter", [None, "{:,.1f}"])
203+
@pytest.mark.parametrize("decimal", [".", "*"])
204+
@pytest.mark.parametrize("precision", [None, 2])
205+
def test_format_thousands(formatter, decimal, precision):
206+
s = DataFrame([[1000000.123456789]]).style # test float
207+
result = s.format(
208+
thousands="_", formatter=formatter, decimal=decimal, precision=precision
209+
)._translate()
210+
assert "1_000_000" in result["body"][0][1]["display_value"]
211+
212+
s = DataFrame([[1000000]]).style # test int
213+
result = s.format(
214+
thousands="_", formatter=formatter, decimal=decimal, precision=precision
215+
)._translate()
216+
assert "1_000_000" in result["body"][0][1]["display_value"]
217+
218+
s = DataFrame([[1 + 1000000.123456789j]]).style # test complex
219+
result = s.format(
220+
thousands="_", formatter=formatter, decimal=decimal, precision=precision
221+
)._translate()
222+
assert "1_000_000" in result["body"][0][1]["display_value"]
223+
224+
225+
@pytest.mark.parametrize("formatter", [None, "{:,.4f}"])
226+
@pytest.mark.parametrize("thousands", [None, ",", "*"])
227+
@pytest.mark.parametrize("precision", [None, 4])
228+
def test_format_decimal(formatter, thousands, precision):
229+
s = DataFrame([[1000000.123456789]]).style # test float
230+
result = s.format(
231+
decimal="_", formatter=formatter, thousands=thousands, precision=precision
232+
)._translate()
233+
assert "000_123" in result["body"][0][1]["display_value"]
234+
235+
s = DataFrame([[1 + 1000000.123456789j]]).style # test complex
236+
result = s.format(
237+
decimal="_", formatter=formatter, thousands=thousands, precision=precision
238+
)._translate()
239+
assert "000_123" in result["body"][0][1]["display_value"]

0 commit comments

Comments
 (0)