diff --git a/great_tables/_formats.py b/great_tables/_formats.py index 887c46234..af3249d48 100644 --- a/great_tables/_formats.py +++ b/great_tables/_formats.py @@ -22,16 +22,15 @@ DataFrameLike, PlExpr, SelectExpr, + _get_column_dtype, is_na, is_series, to_list, - _get_column_dtype, ) from ._text import _md_html, escape_pattern_str_latex from ._utils import _str_detect, _str_replace from ._utils_nanoplots import _generate_nanoplot - if TYPE_CHECKING: from ._types import GTSelf @@ -153,6 +152,7 @@ def fmt_number( drop_trailing_zeros: bool = False, drop_trailing_dec_mark: bool = True, use_seps: bool = True, + accounting: bool = False, scale_by: float = 1, compact: bool = False, pattern: str = "{x}", @@ -211,6 +211,9 @@ def fmt_number( The `use_seps` option allows for the use of digit group separators. The type of digit group separator is set by `sep_mark` and overridden if a locale ID is provided to `locale`. This setting is `True` by default. + accounting + Whether to use accounting style, which wraps negative numbers in parentheses instead of + using a minus sign. scale_by All numeric values will be multiplied by the `scale_by` value before undergoing formatting. Since the `default` value is `1`, no values will be changed unless a different multiplier @@ -294,6 +297,7 @@ def fmt_number( drop_trailing_zeros=drop_trailing_zeros, drop_trailing_dec_mark=drop_trailing_dec_mark, use_seps=use_seps, + accounting=accounting, scale_by=scale_by, compact=compact, sep_mark=sep_mark, @@ -313,6 +317,7 @@ def fmt_number_context( drop_trailing_zeros: bool, drop_trailing_dec_mark: bool, use_seps: bool, + accounting: bool, scale_by: float, compact: bool, sep_mark: str, @@ -355,10 +360,14 @@ def fmt_number_context( force_sign=force_sign, ) - # Implement minus sign replacement for `x_formatted` + # Implement minus sign replacement for `x_formatted` or use accounting style if is_negative: - minus_mark = _context_minus_mark(context=context) - x_formatted = _replace_minus(x_formatted, minus_mark=minus_mark) + if accounting: + x_formatted = f"({_remove_minus(x_formatted)})" + + else: + minus_mark = _context_minus_mark(context=context) + x_formatted = _replace_minus(x_formatted, minus_mark=minus_mark) # Use a supplied pattern specification to decorate the formatted value if pattern != "{x}": @@ -377,6 +386,7 @@ def fmt_integer( rows: int | list[int] | None = None, use_seps: bool = True, scale_by: float = 1, + accounting: bool = False, compact: bool = False, pattern: str = "{x}", sep_mark: str = ",", @@ -417,6 +427,9 @@ def fmt_integer( All numeric values will be multiplied by the `scale_by` value before undergoing formatting. Since the `default` value is `1`, no values will be changed unless a different multiplier value is supplied. + accounting + Whether to use accounting style, which wraps negative numbers in parentheses instead of + using a minus sign. compact A boolean value that allows for compact formatting of numeric values. Values will be scaled and decorated with the appropriate suffixes (e.g., `1230` becomes `1K`, and `1230000` @@ -487,6 +500,7 @@ def fmt_integer( data=self, use_seps=use_seps, scale_by=scale_by, + accounting=accounting, compact=compact, sep_mark=sep_mark, force_sign=force_sign, @@ -501,6 +515,7 @@ def fmt_integer_context( data: GTData, use_seps: bool, scale_by: float, + accounting: bool, compact: bool, sep_mark: str, force_sign: bool, @@ -542,10 +557,14 @@ def fmt_integer_context( force_sign=force_sign, ) - # Implement minus sign replacement for `x_formatted` + # Implement minus sign replacement for `x_formatted` or use accounting style if is_negative: - minus_mark = _context_minus_mark(context=context) - x_formatted = _replace_minus(x_formatted, minus_mark=minus_mark) + if accounting: + x_formatted = f"({_remove_minus(x_formatted)})" + + else: + minus_mark = _context_minus_mark(context=context) + x_formatted = _replace_minus(x_formatted, minus_mark=minus_mark) # Use a supplied pattern specification to decorate the formatted value if pattern != "{x}": @@ -848,6 +867,7 @@ def fmt_percent( drop_trailing_dec_mark: bool = True, scale_values: bool = True, use_seps: bool = True, + accounting: bool = False, pattern: str = "{x}", sep_mark: str = ",", dec_mark: str = ".", @@ -907,6 +927,9 @@ def fmt_percent( The `use_seps` option allows for the use of digit group separators. The type of digit group separator is set by `sep_mark` and overridden if a locale ID is provided to `locale`. This setting is `True` by default. + accounting + Whether to use accounting style, which wraps negative numbers in parentheses instead of + using a minus sign. pattern A formatting pattern that allows for decoration of the formatted value. The formatted value is represented by the `{x}` (which can be used multiple times, if needed) and all other @@ -994,6 +1017,7 @@ def fmt_percent( drop_trailing_zeros=drop_trailing_zeros, drop_trailing_dec_mark=drop_trailing_dec_mark, use_seps=use_seps, + accounting=accounting, scale_by=scale_by, sep_mark=sep_mark, dec_mark=dec_mark, @@ -1013,6 +1037,7 @@ def fmt_percent_context( drop_trailing_zeros: bool, drop_trailing_dec_mark: bool, use_seps: bool, + accounting: bool, scale_by: float, sep_mark: str, dec_mark: str, @@ -1066,10 +1091,14 @@ def fmt_percent_context( else: x_formatted = percent_pattern.replace("{x}", x_formatted) - # Implement minus sign replacement for `x_formatted` + # Implement minus sign replacement for `x_formatted` or use accounting style if is_negative: - minus_mark = _context_minus_mark(context="html") - x_formatted = _replace_minus(x_formatted, minus_mark=minus_mark) + if accounting: + x_formatted = f"({_remove_minus(x_formatted)})" + + else: + minus_mark = _context_minus_mark(context=context) + x_formatted = _replace_minus(x_formatted, minus_mark=minus_mark) # Use a supplied pattern specification to decorate the formatted value if pattern != "{x}": @@ -1091,6 +1120,7 @@ def fmt_currency( decimals: int | None = None, drop_trailing_dec_mark: bool = True, use_seps: bool = True, + accounting: bool = False, scale_by: float = 1, pattern: str = "{x}", sep_mark: str = ",", @@ -1150,6 +1180,9 @@ def fmt_currency( The `use_seps` option allows for the use of digit group separators. The type of digit group separator is set by `sep_mark` and overridden if a locale ID is provided to `locale`. This setting is `True` by default. + accounting + Whether to use accounting style, which wraps negative numbers in parentheses instead of + using a minus sign. scale_by All numeric values will be multiplied by the `scale_by` value before undergoing formatting. Since the `default` value is `1`, no values will be changed unless a different multiplier @@ -1253,6 +1286,7 @@ def fmt_currency( decimals=decimals, drop_trailing_dec_mark=drop_trailing_dec_mark, use_seps=use_seps, + accounting=accounting, scale_by=scale_by, sep_mark=sep_mark, dec_mark=dec_mark, @@ -1272,6 +1306,7 @@ def fmt_currency_context( decimals: int, drop_trailing_dec_mark: bool, use_seps: bool, + accounting: bool, scale_by: float, sep_mark: str, dec_mark: str, @@ -1330,10 +1365,14 @@ def fmt_currency_context( else: x_formatted = currency_pattern.replace("{x}", x_formatted) - # Implement minus sign replacement for `x_formatted` + # Implement minus sign replacement for `x_formatted` or use accounting style if is_negative: - minus_mark = _context_minus_mark(context=context) - x_formatted = _replace_minus(x_formatted, minus_mark=minus_mark) + if accounting: + x_formatted = f"({_remove_minus(x_formatted)})" + + else: + minus_mark = _context_minus_mark(context=context) + x_formatted = _replace_minus(x_formatted, minus_mark=minus_mark) # Use a supplied pattern specification to decorate the formatted value if pattern != "{x}": @@ -2942,6 +2981,19 @@ def _replace_minus(string: str, minus_mark: str) -> str: return _str_replace(string, "-", minus_mark) +def _remove_minus(string: str) -> str: + """ + Removes all occurrences of the minus sign '-' in the given string. + + Args: + string (str): The input string. + + Returns: + str: The modified string with the minus sign removed. + """ + return _str_replace(string, "-", "") + + T_dict = TypeVar("T_dict", bound=TypedDict) @@ -3764,9 +3816,10 @@ def to_html(self, val: Any): return span def to_latex(self, val: Any): - from ._gt_data import FormatterSkipElement from warnings import warn + from ._gt_data import FormatterSkipElement + warn("fmt_image() is not currently implemented in LaTeX output.") return FormatterSkipElement() diff --git a/great_tables/_formats_vals.py b/great_tables/_formats_vals.py index 9c5670dd5..a90280d6e 100644 --- a/great_tables/_formats_vals.py +++ b/great_tables/_formats_vals.py @@ -55,6 +55,7 @@ def val_fmt_number( drop_trailing_zeros: bool = False, drop_trailing_dec_mark: bool = True, use_seps: bool = True, + accounting: bool = False, scale_by: float = 1, compact: bool = False, pattern: str = "{x}", @@ -108,6 +109,9 @@ def val_fmt_number( The `use_seps` option allows for the use of digit group separators. The type of digit group separator is set by `sep_mark` and overridden if a locale ID is provided to `locale`. This setting is `True` by default. + accounting + An option to use accounting style for values. Normally, negative values will be shown with a + minus sign but using accounting style will instead put any negative values in parentheses. scale_by All numeric values will be multiplied by the `scale_by` value before undergoing formatting. Since the `default` value is `1`, no values will be changed unless a different multiplier @@ -151,6 +155,7 @@ def val_fmt_number( drop_trailing_zeros=drop_trailing_zeros, drop_trailing_dec_mark=drop_trailing_dec_mark, use_seps=use_seps, + accounting=accounting, scale_by=scale_by, compact=compact, pattern=pattern, @@ -168,6 +173,7 @@ def val_fmt_number( def val_fmt_integer( x: X, use_seps: bool = True, + accounting: bool = False, scale_by: float = 1, compact: bool = False, pattern: str = "{x}", @@ -200,6 +206,9 @@ def val_fmt_integer( The `use_seps` option allows for the use of digit group separators. The type of digit group separator is set by `sep_mark` and overridden if a locale ID is provided to `locale`. This setting is `True` by default. + accounting + An option to use accounting style for values. Normally, negative values will be shown with a + minus sign but using accounting style will instead put any negative values in parentheses. scale_by All numeric values will be multiplied by the `scale_by` value before undergoing formatting. Since the `default` value is `1`, no values will be changed unless a different multiplier @@ -235,6 +244,7 @@ def val_fmt_integer( gt_obj_fmt = gt_obj.fmt_integer( columns="x", use_seps=use_seps, + accounting=accounting, scale_by=scale_by, compact=compact, pattern=pattern, @@ -376,6 +386,7 @@ def val_fmt_percent( drop_trailing_zeros: bool = False, drop_trailing_dec_mark: bool = True, scale_values: bool = True, + accounting: bool = False, use_seps: bool = True, pattern: str = "{x}", sep_mark: str = ",", @@ -427,6 +438,9 @@ def val_fmt_percent( performed since the expectation is that incoming values are usually proportional. Setting to `False` signifies that the values are already scaled and require only the percent sign when formatted. + accounting + An option to use accounting style for values. Normally, negative values will be shown with a + minus sign but using accounting style will instead put any negative values in parentheses. use_seps The `use_seps` option allows for the use of digit group separators. The type of digit group separator is set by `sep_mark` and overridden if a locale ID is provided to `locale`. This @@ -471,6 +485,7 @@ def val_fmt_percent( drop_trailing_zeros=drop_trailing_zeros, drop_trailing_dec_mark=drop_trailing_dec_mark, scale_values=scale_values, + accounting=accounting, use_seps=use_seps, pattern=pattern, sep_mark=sep_mark, @@ -492,6 +507,7 @@ def val_fmt_currency( use_subunits: bool = True, decimals: int | None = None, drop_trailing_dec_mark: bool = True, + accounting: bool = False, use_seps: bool = True, scale_by: float = 1, pattern: str = "{x}", @@ -543,6 +559,9 @@ def val_fmt_currency( A boolean value that determines whether decimal marks should always appear even if there are no decimal digits to display after formatting (e.g., `23` becomes `23.` if `False`). By default trailing decimal marks are not shown. + accounting + An option to use accounting style for values. Normally, negative values will be shown with a + minus sign but using accounting style will instead put any negative values in parentheses. use_seps The `use_seps` option allows for the use of digit group separators. The type of digit group separator is set by `sep_mark` and overridden if a locale ID is provided to `locale`. This @@ -591,6 +610,7 @@ def val_fmt_currency( use_subunits=use_subunits, decimals=decimals, drop_trailing_dec_mark=drop_trailing_dec_mark, + accounting=accounting, use_seps=use_seps, scale_by=scale_by, pattern=pattern, diff --git a/tests/__snapshots__/test_formats.ambr b/tests/__snapshots__/test_formats.ambr index 66fec6a7e..c33fe126f 100644 --- a/tests/__snapshots__/test_formats.ambr +++ b/tests/__snapshots__/test_formats.ambr @@ -9,6 +9,49 @@
4334
+ ''' +# --- +# name: test_format_accounting_snap + ''' + class="gt_table" data-quarto-disable-processing="false" data-quarto-bootstrap="false"> + + + + number + percent + integer + currency + number_acc + percent_acc + integer_acc + currency_acc + + + + + −1.20 + −5.23% + 2,323 + −$24,334.23 + (1.20) + (5.23%) + 2,323 + ($24,334.23) + + + 23.60 + 36.30% + −23,213 + $7,323.25 + 23.60 + 36.30% + (23,213) + $7,323.25 + + + + + ''' # --- # name: test_format_repr_snap diff --git a/tests/test_formats.py b/tests/test_formats.py index 16a32251a..586754199 100644 --- a/tests/test_formats.py +++ b/tests/test_formats.py @@ -68,6 +68,36 @@ def test_format_snap(snapshot): assert_rendered_body(snapshot, new_gt) +def test_format_accounting_snap(snapshot): + df = pl.DataFrame( + { + "number": [-1.2, 23.6], + "percent": [-0.0523, 0.363], + "integer": [2323, -23213], + "currency": [-24334.23, 7323.253], + } + ).with_columns( + number_acc=pl.col("number"), + percent_acc=pl.col("percent"), + integer_acc=pl.col("integer"), + currency_acc=pl.col("currency"), + ) + + new_gt = ( + GT(df) + .fmt_number(columns="number") + .fmt_percent(columns="percent") + .fmt_integer(columns="integer") + .fmt_currency(columns="currency") + .fmt_number(columns="number_acc", accounting=True) + .fmt_percent(columns="percent_acc", accounting=True) + .fmt_integer(columns="integer_acc", accounting=True) + .fmt_currency(columns="currency_acc", accounting=True) + ) + + assert_repr_html(snapshot, new_gt) + + def test_format_repr_snap(snapshot): new_gt = ( GT(exibble)