Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: accounting notation for fmt_number(), fmt_percent(), fmt_integer() and fmt_currency() #513

Merged
merged 10 commits into from
Dec 9, 2024
81 changes: 69 additions & 12 deletions great_tables/_formats.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,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}",
Expand Down Expand Up @@ -211,6 +212,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
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
Expand Down Expand Up @@ -294,6 +298,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,
Expand All @@ -313,6 +318,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,
Expand Down Expand Up @@ -355,10 +361,15 @@ 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}":
Expand All @@ -377,6 +388,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 = ",",
Expand Down Expand Up @@ -417,6 +429,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
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.
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`
Expand Down Expand Up @@ -487,6 +502,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,
Expand All @@ -501,6 +517,7 @@ def fmt_integer_context(
data: GTData,
use_seps: bool,
scale_by: float,
accounting: bool,
compact: bool,
sep_mark: str,
force_sign: bool,
Expand Down Expand Up @@ -542,10 +559,15 @@ 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}":
Expand Down Expand Up @@ -848,6 +870,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 = ".",
Expand Down Expand Up @@ -907,6 +930,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
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.
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
Expand Down Expand Up @@ -994,6 +1020,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,
Expand All @@ -1013,6 +1040,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,
Expand Down Expand Up @@ -1066,10 +1094,15 @@ 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}":
Expand All @@ -1091,6 +1124,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 = ",",
Expand Down Expand Up @@ -1150,6 +1184,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
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
Expand Down Expand Up @@ -1253,6 +1290,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,
Expand All @@ -1272,6 +1310,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,
Expand Down Expand Up @@ -1330,10 +1369,15 @@ 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}":
Expand Down Expand Up @@ -2942,6 +2986,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)


Expand Down
20 changes: 20 additions & 0 deletions great_tables/_formats_vals.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}",
Expand Down Expand Up @@ -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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WDYT of changing these docstrings to something like "Whether to use accounting style, which wraps negative numbers in parentheses instead of using a minus sign."

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good, now done!

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
Expand Down Expand Up @@ -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,
Expand All @@ -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}",
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 = ",",
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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}",
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
43 changes: 43 additions & 0 deletions tests/__snapshots__/test_formats.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,49 @@
<td class="gt_row gt_center"><div><svg role="img" viewBox="0 0 200 130" style="height: 2em; margin-left: auto; margin-right: auto; font-size: inherit; overflow: visible; vertical-align: middle; position:relative;"><defs><pattern id="area_pattern" width="8" height="8" patternUnits="userSpaceOnUse"><path class="pattern-line" d="M 0,8 l 8,-8 M -1,1 l 4,-4 M 6,10 l 4,-4" stroke="#FF0000" stroke-width="1.5" stroke-linecap="round" shape-rendering="geometricPrecision"></path></pattern></defs><style> text { font-family: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'DejaVu Sans Mono', monospace; stroke-width: 0.15em; paint-order: stroke; stroke-linejoin: round; cursor: default; } .vert-line:hover rect { fill: #911EB4; fill-opacity: 40%; stroke: #FFFFFF60; color: red; } .vert-line:hover text { stroke: white; fill: #212427; } .horizontal-line:hover text {stroke: white; fill: #212427; } .ref-line:hover rect { stroke: #FFFFFF60; } .ref-line:hover line { stroke: #FF0000; } .ref-line:hover text { stroke: white; fill: #212427; } .y-axis-line:hover rect { fill: #EDEDED; fill-opacity: 60%; stroke: #FFFFFF60; color: red; } .y-axis-line:hover text { stroke: white; stroke-width: 0.20em; fill: #1A1C1F; } </style><path class="area-closed" d="M 50.0,115.0 150.0,15.0 150.0,125 50.0,125 Z" stroke="transparent" stroke-width="2" fill="url(#area_pattern)" fill-opacity="0.7"></path><path d="M 50.0,115.0 C 75.0,115.0 125.0,15.0 150.0,15.0" stroke="#4682B4" stroke-width="8" fill="none"></path><circle cx="50.0" cy="115.0" r="10" stroke="#FFFFFF" stroke-width="4" fill="#FF0000"></circle><circle cx="150.0" cy="15.0" r="10" stroke="#FFFFFF" stroke-width="4" fill="#FF0000"></circle><g class="y-axis-line"><rect x="0" y="0" width="65" height="130" stroke="transparent" stroke-width="0" fill="transparent"></rect><text x="0" y="19.0" fill="transparent" stroke="transparent" font-size="25">4</text><text x="0" y="126.0" fill="transparent" stroke="transparent" font-size="25">3</text></g><g class="vert-line"><rect x="40.0" y="0" width="20" height="130" stroke="transparent" stroke-width="12" fill="transparent"></rect><text x="60.0" y="20" fill="transparent" stroke="transparent" font-size="30px">3</text></g><g class="vert-line"><rect x="140.0" y="0" width="20" height="130" stroke="transparent" stroke-width="12" fill="transparent"></rect><text x="160.0" y="20" fill="transparent" stroke="transparent" font-size="30px">4</text></g></svg></div></td>
</tr>
</tbody>
'''
# ---
# name: test_format_accounting_snap
'''
class="gt_table" data-quarto-disable-processing="false" data-quarto-bootstrap="false">
<thead>

<tr class="gt_col_headings">
<th class="gt_col_heading gt_columns_bottom_border gt_right" rowspan="1" colspan="1" scope="col" id="number">number</th>
<th class="gt_col_heading gt_columns_bottom_border gt_right" rowspan="1" colspan="1" scope="col" id="percent">percent</th>
<th class="gt_col_heading gt_columns_bottom_border gt_right" rowspan="1" colspan="1" scope="col" id="integer">integer</th>
<th class="gt_col_heading gt_columns_bottom_border gt_right" rowspan="1" colspan="1" scope="col" id="currency">currency</th>
<th class="gt_col_heading gt_columns_bottom_border gt_right" rowspan="1" colspan="1" scope="col" id="number_acc">number_acc</th>
<th class="gt_col_heading gt_columns_bottom_border gt_right" rowspan="1" colspan="1" scope="col" id="percent_acc">percent_acc</th>
<th class="gt_col_heading gt_columns_bottom_border gt_right" rowspan="1" colspan="1" scope="col" id="integer_acc">integer_acc</th>
<th class="gt_col_heading gt_columns_bottom_border gt_right" rowspan="1" colspan="1" scope="col" id="currency_acc">currency_acc</th>
</tr>
</thead>
<tbody class="gt_table_body">
<tr>
<td class="gt_row gt_right">−1.20</td>
<td class="gt_row gt_right">−5.23%</td>
<td class="gt_row gt_right">2,323</td>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like this is not testing that fmt_integer uses accounting format on negative numbers

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The first four columns of this table are formatted but with accounting=False (minus signs persist) , the last four have accounting=True so we do get the negative values in parens there.

<td class="gt_row gt_right">−$24,334.23</td>
<td class="gt_row gt_right">(1.20)</td>
<td class="gt_row gt_right">(5.23%)</td>
<td class="gt_row gt_right">2,323</td>
<td class="gt_row gt_right">($24,334.23)</td>
</tr>
<tr>
<td class="gt_row gt_right">23.60</td>
<td class="gt_row gt_right">36.30%</td>
<td class="gt_row gt_right">−23,213</td>
<td class="gt_row gt_right">$7,323.25</td>
<td class="gt_row gt_right">23.60</td>
<td class="gt_row gt_right">36.30%</td>
<td class="gt_row gt_right">(23,213)</td>
<td class="gt_row gt_right">$7,323.25</td>
</tr>
</tbody>



'''
# ---
# name: test_format_repr_snap
Expand Down
Loading
Loading