diff --git a/.vscode/settings.json b/.vscode/settings.json index 6150244e4..2b73011a0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -17,5 +17,7 @@ }, "python.testing.pytestArgs": ["tests"], "python.testing.unittestEnabled": false, - "python.testing.pytestEnabled": true + "python.testing.pytestEnabled": true, + "ruff.format.args": ["--exclude", "great_tables/vals.py"], + "ruff.lint.args": ["--exclude", "great_tables/vals.py"] } diff --git a/docs/_quarto.yml b/docs/_quarto.yml index c834f1756..168677abb 100644 --- a/docs/_quarto.yml +++ b/docs/_quarto.yml @@ -139,6 +139,7 @@ quartodoc: - GT.fmt_integer - GT.fmt_percent - GT.fmt_scientific + - GT.fmt_engineering - GT.fmt_currency - GT.fmt_bytes - GT.fmt_roman diff --git a/great_tables/_formats.py b/great_tables/_formats.py index 5d6e8e858..f05f23205 100644 --- a/great_tables/_formats.py +++ b/great_tables/_formats.py @@ -863,6 +863,353 @@ def fmt_scientific_context( return x_formatted +def fmt_engineering( + self: GTSelf, + columns: SelectExpr = None, + rows: int | list[int] | None = None, + decimals: int = 2, + n_sigfig: int | None = None, + drop_trailing_zeros: bool = False, + drop_trailing_dec_mark: bool = True, + scale_by: float = 1, + exp_style: str = "x10n", + pattern: str = "{x}", + dec_mark: str = ".", + force_sign_m: bool = False, + force_sign_n: bool = False, + locale: str | None = None, +) -> GTSelf: + """ + Format values to engineering notation. + + With numeric values in a table, we can perform formatting so that the targeted values are + rendered in engineering notation, where numbers are written in the form of a mantissa (`m`) and + an exponent (`n`). When combined the construction is either of the form *m* x 10^*n* or *m*E*n*. + The mantissa is a number between `1` and `1000` and the exponent is a multiple of `3`. For + example, the number `0.0000345` can be written in engineering notation as `34.50 x 10^-6`. This + notation helps to simplify calculations and make it easier to compare numbers that are on very + different scales. + + Engineering notation is particularly useful as it aligns with SI prefixes (e.g., *milli-*, + *micro-*, *kilo-*, *mega-*). For instance, numbers in engineering notation with exponent `-3` + correspond to milli-units, while those with exponent `6` correspond to mega-units. + + We have fine control over the formatting task, with the following options: + + - decimals: choice of the number of decimal places, option to drop trailing zeros, and a choice + of the decimal symbol + - scaling: we can choose to scale targeted values by a multiplier value + - pattern: option to use a text pattern for decoration of the formatted values + - locale-based formatting: providing a locale ID will result in formatting specific to the + chosen locale + + Parameters + ---------- + columns + The columns to target. Can either be a single column name or a series of column names + provided in a list. + rows + In conjunction with `columns=`, we can specify which of their rows should undergo + formatting. The default is all rows, resulting in all rows in targeted columns being + formatted. Alternatively, we can supply a list of row indices. + decimals + The `decimals` values corresponds to the exact number of decimal places to use. A value such + as `2.34` can, for example, be formatted with `0` decimal places and it would result in + `"2"`. With `4` decimal places, the formatted value becomes `"2.3400"`. The trailing zeros + can be removed with `drop_trailing_zeros=True`. + n_sigfig + A option to format numbers to *n* significant figures. By default, this is `None` and thus + number values will be formatted according to the number of decimal places set via + `decimals`. If opting to format according to the rules of significant figures, `n_sigfig` + must be a number greater than or equal to `1`. Any values passed to the `decimals` and + `drop_trailing_zeros` arguments will be ignored. + drop_trailing_zeros + A boolean value that allows for removal of trailing zeros (those redundant zeros after the + decimal mark). + drop_trailing_dec_mark + 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. + 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 + value is supplied. + exp_style + Style of formatting to use for the engineering notation formatting. By default this is + `"x10n"` but other options include using a single letter (e.g., `"e"`, `"E"`, etc.), a + letter followed by a `"1"` to signal a minimum digit width of one, or `"low-ten"` for using + a stylized `"10"` marker. + 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 + characters will be interpreted as string literals. + dec_mark + The string to be used as the decimal mark. For example, using `dec_mark=","` with the value + `0.152` would result in a formatted value of `"0,152"`). This argument is ignored if a + `locale` is supplied (i.e., is not `None`). + force_sign_m + Should the plus sign be shown for positive values of the mantissa (first component)? This + would effectively show a sign for all values except zero on the first numeric component of + the notation. If so, use `True` (the default for this is `False`), where only negative + numbers will display a sign. + force_sign_n + Should the plus sign be shown for positive values of the exponent (second component)? This + would effectively show a sign for all values except zero on the second numeric component of + the notation. If so, use `True` (the default for this is `False`), where only negative + numbers will display a sign. + locale + An optional locale identifier that can be used for formatting values according the locale's + rules. Examples include `"en"` for English (United States) and `"fr"` for French (France). + + Returns + ------- + GT + The GT object is returned. This is the same object that the method is called on so that we + can facilitate method chaining. + + Adapting output to a specific `locale` + -------------------------------------- + This formatting method can adapt outputs according to a provided `locale` value. Examples + include `"en"` for English (United States) and `"fr"` for French (France). The use of a valid + locale ID here means decimal marks will be correct for the given locale. Should a value be + provided in `dec_mark` it will be overridden by the locale's preferred values. + + Note that a `locale` value provided here will override any global locale setting performed in + [`GT()`](`great_tables.GT`)'s own `locale` argument (it is settable there as a value received by + all other methods that have a `locale` argument). + + Examples + -------- + With numeric values in a table, we can perform formatting so that the targeted values are + rendered in engineering notation. For example, the number `0.0000345` can be written in + engineering notation as `34.50 x 10^-6`. + + ```{python} + import polars as pl + from great_tables import GT + + numbers_df = pl.DataFrame({ + "numbers": [0.0000345, 3450, 3450000] + }) + + GT(numbers_df).fmt_engineering() + ``` + + Notice that in each case, the exponent is a multiple of `3`. + + Let's define a DataFrame that contains two columns of values (one small and one large). After + creating a simple table with `GT()`, we'll call `fmt_engineering()` on both columns. + + ```{python} + small_large_df = pl.DataFrame({ + "small": [10**-i for i in range(12, 0, -1)], + "large": [10**i for i in range(1, 13)] + }) + + GT(small_large_df).fmt_engineering() + ``` + + Notice that within the form of *m* x 10^*n*, the *n* values move in steps of 3 (away from 0), + and *m* values can have 1-3 digits before the decimal. Further to this, any values where *n* is + 0 results in a display of only *m* (the first two values in the `large` column demonstrates + this). + + Engineering notation expresses values so that they align to certain SI prefixes. Here is a table + that compares select SI prefixes and their symbols to decimal and engineering-notation + representations of the key numbers. + + ```{python} + import polars as pl + from great_tables import GT + + prefixes_df = pl.DataFrame({ + "name": [ + "peta", "tera", "giga", "mega", "kilo", + None, + "milli", "micro", "nano", "pico", "femto" + ], + "symbol": [ + "P", "T", "G", "M", "k", + None, + "m", "μ", "n", "p", "f" + ], + "decimal": [float(10**i) for i in range(15, -18, -3)], + }) + + prefixes_df = prefixes_df.with_columns( + engineering=pl.col("decimal") + ) + + ( + GT(prefixes_df) + .fmt_number(columns="decimal", n_sigfig=1) + .fmt_engineering(columns="engineering") + .sub_missing() + ) + ``` + + See Also + -------- + The functional version of this method, + [`val_fmt_engineering()`](`great_tables._formats_vals.val_fmt_engineering`), allows you to + format a single numerical value (or a list of them). + """ + + locale = _resolve_locale(self, locale=locale) + + # Use a locale-based decimal mark if a locale ID is provided + dec_mark = _get_locale_dec_mark(default=dec_mark, locale=locale) + + pf_format = partial( + fmt_engineering_context, + data=self, + decimals=decimals, + n_sigfig=n_sigfig, + drop_trailing_zeros=drop_trailing_zeros, + drop_trailing_dec_mark=drop_trailing_dec_mark, + scale_by=scale_by, + exp_style=exp_style, + dec_mark=dec_mark, + force_sign_m=force_sign_m, + force_sign_n=force_sign_n, + pattern=pattern, + ) + + return fmt_by_context(self, pf_format=pf_format, columns=columns, rows=rows) + + +# Generate a function that will operate on single `x` values in the table body +def fmt_engineering_context( + x: float | None, + data: GTData, + decimals: int, + n_sigfig: int | None, + drop_trailing_zeros: bool, + drop_trailing_dec_mark: bool, + scale_by: float, + exp_style: str, + dec_mark: str, + force_sign_m: bool, + force_sign_n: bool, + pattern: str, + context: str, +) -> str: + if is_na(data._tbl_data, x): + return x + + # Scale `x` value by a defined `scale_by` value + x = x * scale_by + + # Determine whether the value is positive + is_positive = _has_positive_value(value=x) + + minus_mark = _context_minus_mark(context=context) + + # For engineering notation, we need to calculate the exponent that is a multiple of 3 + # and adjust the mantissa accordingly + if x == 0: + # Special case for zero + m_part = _value_to_decimal_notation( + value=0, + decimals=decimals, + n_sigfig=n_sigfig, + drop_trailing_zeros=drop_trailing_zeros, + drop_trailing_dec_mark=drop_trailing_dec_mark, + use_seps=False, + sep_mark=",", + dec_mark=dec_mark, + force_sign=False, + ) + n_part = "0" + power_3 = 0 + else: + # Calculate the power of 1000 (engineering notation uses multiples of 3) + power_3 = int(math.floor(math.log10(abs(x)) / 3) * 3) + + # Calculate the mantissa by dividing by 10^power_3 + mantissa = x / (10**power_3) + + # Format the mantissa + m_part = _value_to_decimal_notation( + value=mantissa, + decimals=decimals, + n_sigfig=n_sigfig, + drop_trailing_zeros=drop_trailing_zeros, + drop_trailing_dec_mark=drop_trailing_dec_mark, + use_seps=False, + sep_mark=",", + dec_mark=dec_mark, + force_sign=False, + ) + + n_part = str(power_3) + + # Force the positive sign to be present if the `force_sign_m` option is taken + if is_positive and force_sign_m: + m_part = "+" + m_part + + if exp_style == "x10n": + # Define the exponent string based on the `exp_style` that is the default + # ('x10n'); this is styled as 'x 10^n' instead of using a fixed symbol like 'E' + + # Determine which values don't require the (x 10^n) for engineering formatting + # since their exponent would be zero + small_pos = power_3 == 0 + + # Force the positive sign to be present if the `force_sign_n` option is taken + if force_sign_n and not _str_detect(n_part, "-"): + n_part = "+" + n_part + + # Implement minus sign replacement for `m_part` and `n_part` + m_part = _replace_minus(m_part, minus_mark=minus_mark) + n_part = _replace_minus(n_part, minus_mark=minus_mark) + + if small_pos: + # If the exponent is zero, then the formatted value is based on only the `m_part` + x_formatted = m_part + else: + # Get the set of exponent marks, which are used to decorate the `n_part` + exp_marks = _context_exp_marks(context=context) + + # Create the formatted string based on `exp_marks` and the two parts + x_formatted = m_part + exp_marks[0] + n_part + exp_marks[1] + + else: + # Define the exponent string based on the `exp_style` that's not the default + # value of 'x10n' + + exp_str = _context_exp_str(exp_style=exp_style) + + n_min_width = 1 if _str_detect(exp_style, r"^[a-zA-Z]1$") else 2 + + # The `n_part` will be extracted here and it must be padded to + # the defined minimum number of decimal places + if _str_detect(n_part, "-"): + n_part = _str_replace(n_part, "-", "") + n_part = n_part.rjust(n_min_width, "0") + n_part = "-" + n_part + else: + n_part = n_part.rjust(n_min_width, "0") + if force_sign_n: + n_part = "+" + n_part + + # Implement minus sign replacement for `m_part` and `n_part` + m_part = _replace_minus(m_part, minus_mark=minus_mark) + n_part = _replace_minus(n_part, minus_mark=minus_mark) + + x_formatted = m_part + exp_str + n_part + + # Use a supplied pattern specification to decorate the formatted value + if pattern != "{x}": + # Escape LaTeX special characters from literals in the pattern + if context == "latex": + pattern = escape_pattern_str_latex(pattern_str=pattern) + + x_formatted = pattern.replace("{x}", x_formatted) + + return x_formatted + + def fmt_percent( self: GTSelf, columns: SelectExpr = None, @@ -2929,29 +3276,6 @@ def _value_to_scientific_notation( return result -def _value_to_engineering_notation(value: int | float, n_sigfig: int, exp_style: str) -> str: - """ - Engineering notation. - - Returns a string value with the correct precision and an exponent that is divisible by three. - The `exp_style` text is placed between the decimal value and the exponent. - """ - - is_negative, sig_digits, dot_power, ten_power = _get_sci_parts(value, n_sigfig) - - eng_power = 3 * math.floor(ten_power / 3) - eng_dot = dot_power + ten_power - eng_power - - result = ( - ("-" if is_negative else "") - + _insert_decimal_mark(digits=sig_digits, power=eng_dot) - + exp_style - + str(eng_power) - ) - - return result - - def _format_number_n_sigfig( value: int | float, n_sigfig: int, diff --git a/great_tables/_formats_vals.py b/great_tables/_formats_vals.py index 48ad0023b..8abb1e371 100644 --- a/great_tables/_formats_vals.py +++ b/great_tables/_formats_vals.py @@ -1,20 +1,17 @@ from __future__ import annotations -from functools import wraps +from functools import partial, wraps from pathlib import Path from typing import TYPE_CHECKING, Any, Callable, overload -from typing_extensions import TypeAlias, Concatenate, ParamSpec - -from ._gt_data import GTData, FramelessData -from ._tbl_data import PlExpr, SeriesLike, to_frame -from .gt import GT, _get_column_of_values +from typing_extensions import Concatenate, ParamSpec, TypeAlias # TODO: these imports make it so that vals.fmt_integer does not require pandas # as part of broader work to remove the pandas dependency from val functions. from ._formats import _get_locale_sep_mark, _resolve_locale, fmt_integer_context -from functools import partial - +from ._gt_data import FramelessData, GTData +from ._tbl_data import PlExpr, SeriesLike, to_frame +from .gt import GT, _get_column_of_values if TYPE_CHECKING: from ._formats import DateStyle, TimeStyle @@ -457,6 +454,134 @@ def val_fmt_scientific( return vals_fmt +@expressive +def val_fmt_engineering( + x: X, + decimals: int = 2, + n_sigfig: int | None = None, + drop_trailing_zeros: bool = False, + drop_trailing_dec_mark: bool = True, + scale_by: float = 1, + exp_style: str = "x10n", + pattern: str = "{x}", + dec_mark: str = ".", + force_sign_m: bool = False, + force_sign_n: bool = False, + locale: str | None = None, +) -> list[str]: + """ + Format values to engineering notation. + + With numeric values in a list, we can perform formatting so that the input values are rendered + in engineering notation, where numbers are written in the form of a mantissa (`m`) and an + exponent (`n`). When combined the construction is either of the form *m* x 10^*n* or *m*E*n*. + The mantissa is a number between `1` and `1000` and the exponent is a multiple of `3`. For + example, the number `0.0000345` can be written in engineering notation as `34.50 x 10^-6`. This + notation helps to simplify calculations and make it easier to compare numbers that are on very + different scales. + + Engineering notation is particularly useful as it aligns with SI prefixes (e.g., *milli-*, + *micro-*, *kilo-*, *mega-*). For instance, numbers in engineering notation with exponent `-3` + correspond to milli-units, while those with exponent `6` correspond to mega-units. + + We have fine control over the formatting task, with the following options: + + - decimals: choice of the number of decimal places, option to drop trailing zeros, and a choice + of the decimal symbol + - scaling: we can choose to scale targeted values by a multiplier value + - pattern: option to use a text pattern for decoration of the formatted values + - locale-based formatting: providing a locale ID will result in formatting specific to the + chosen locale + + Parameters + ---------- + x + A list of values to be formatted. + decimals + The `decimals` values corresponds to the exact number of decimal places to use. A value such + as `2.34` can, for example, be formatted with `0` decimal places and it would result in + `"2"`. With `4` decimal places, the formatted value becomes `"2.3400"`. The trailing zeros + can be removed with `drop_trailing_zeros=True`. + n_sigfig + A option to format numbers to *n* significant figures. By default, this is `None` and thus + number values will be formatted according to the number of decimal places set via + `decimals`. If opting to format according to the rules of significant figures, `n_sigfig` + must be a number greater than or equal to `1`. Any values passed to the `decimals` and + `drop_trailing_zeros` arguments will be ignored. + drop_trailing_zeros + A boolean value that allows for removal of trailing zeros (those redundant zeros after the + decimal mark). + drop_trailing_dec_mark + 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. + 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 + value is supplied. + exp_style + Style of formatting to use for the engineering notation formatting. By default this is + `"x10n"` but other options include using a single letter (e.g., `"e"`, `"E"`, etc.), a + letter followed by a `"1"` to signal a minimum digit width of one, or `"low-ten"` for using + a stylized `"10"` marker. + 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 + characters will be interpreted as string literals. + dec_mark + The string to be used as the decimal mark. For example, using `dec_mark=","` with the value + `0.152` would result in a formatted value of `"0,152"`). This argument is ignored if a + `locale` is supplied (i.e., is not `None`). + force_sign_m + Should the plus sign be shown for positive values of the mantissa (first component)? This + would effectively show a sign for all values except zero on the first numeric component of + the notation. If so, use `True` (the default for this is `False`), where only negative + numbers will display a sign. + force_sign_n + Should the plus sign be shown for positive values of the exponent (second component)? This + would effectively show a sign for all values except zero on the second numeric component of + the notation. If so, use `True` (the default for this is `False`), where only negative + numbers will display a sign. + locale + An optional locale identifier that can be used for formatting values according the locale's + rules. Examples include `"en"` for English (United States) and `"fr"` for French (France). + + Returns + ------- + list[str] + A list of formatted values is returned. + + Examples + -------- + ```{python} + from great_tables import vals + + vals.fmt_engineering([123456789, 0.000000425639], decimals=2) + ``` + """ + + gt_obj: GTData = _make_one_col_table(vals=x) + + gt_obj_fmt = gt_obj.fmt_engineering( + columns="x", + decimals=decimals, + n_sigfig=n_sigfig, + drop_trailing_zeros=drop_trailing_zeros, + drop_trailing_dec_mark=drop_trailing_dec_mark, + scale_by=scale_by, + exp_style=exp_style, + pattern=pattern, + dec_mark=dec_mark, + force_sign_m=force_sign_m, + force_sign_n=force_sign_n, + locale=locale, + ) + + vals_fmt = _get_column_of_values(gt=gt_obj_fmt, column_name="x", context="html") + + return vals_fmt + + @expressive def val_fmt_percent( x: X, diff --git a/great_tables/gt.py b/great_tables/gt.py index db227822f..beff6ae71 100644 --- a/great_tables/gt.py +++ b/great_tables/gt.py @@ -15,6 +15,7 @@ fmt_currency, fmt_date, fmt_datetime, + fmt_engineering, fmt_flag, fmt_icon, fmt_image, @@ -227,6 +228,7 @@ def __init__( fmt_integer = fmt_integer fmt_percent = fmt_percent fmt_scientific = fmt_scientific + fmt_engineering = fmt_engineering fmt_currency = fmt_currency fmt_bytes = fmt_bytes fmt_roman = fmt_roman diff --git a/great_tables/vals.py b/great_tables/vals.py index 81f79329b..11e9edbbb 100644 --- a/great_tables/vals.py +++ b/great_tables/vals.py @@ -6,6 +6,7 @@ val_fmt_number as fmt_number, val_fmt_integer as fmt_integer, val_fmt_scientific as fmt_scientific, + val_fmt_engineering as fmt_engineering, val_fmt_percent as fmt_percent, val_fmt_currency as fmt_currency, val_fmt_roman as fmt_roman, diff --git a/pyproject.toml b/pyproject.toml index bb43b2c1f..1386a009e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -105,6 +105,10 @@ markers = [ line-length = 100 +[tool.ruff.format] +exclude = ["great_tables/vals.py"] + + [tool.ruff.lint] exclude = ["docs", ".venv", "tests/*"] diff --git a/tests/test_formats.py b/tests/test_formats.py index 3bfef4c32..19165e15b 100644 --- a/tests/test_formats.py +++ b/tests/test_formats.py @@ -1154,6 +1154,169 @@ def test_fmt_scientific_case( assert x == x_out +# ------------------------------------------------------------------------------ +# Tests of `fmt_engineering()` +# ------------------------------------------------------------------------------ + + +@pytest.mark.parametrize( + "fmt_engineering_kwargs,x_in,x_out", + [ + # Basic decimals=2: test key ranges (very large, large, medium, small, very small) + ( + dict(decimals=2), + [ + 829300232923103939802.4, # Very large (10^18) + 2323435.1, # Medium large (10^6) + 1000.001, # Boundary (10^3) + 10.00001, # No exponent needed + 0.12345, # Small (10^-3) + 0.0000123456, # Very small (10^-6) + ], + [ + "829.30 × 1018", + "2.32 × 106", + "1.00 × 103", + "10.00", + "123.45 × 10−3", + "12.35 × 10−6", + ], + ), + # Negative values + ( + dict(decimals=2), + [-50000.01, -10.00001, -0.12345], + [ + "−50.00 × 103", + "−10.00", + "−123.45 × 10−3", + ], + ), + # exp_style="E" format + ( + dict(decimals=2, exp_style="E"), + [-3.49e13, 0, 82794], + [ + "−34.90E12", + "0.00E00", + "82.79E03", + ], + ), + # exp_style="E1" (single digit exponent) + ( + dict(decimals=2, exp_style="E1"), + [-3453, 0, 0.00007534], + [ + "−3.45E3", + "0.00E0", + "75.34E−6", + ], + ), + # force_sign_m: positive/negative/zero + ( + dict(decimals=2, force_sign_m=True), + [-3453, 0, 82794], + [ + "−3.45 × 103", + "0.00", + "+82.79 × 103", + ], + ), + # force_sign_n: positive/negative exponents + ( + dict(decimals=2, force_sign_n=True), + [-0.000234, 82794], + [ + "−234.00 × 10−6", + "82.79 × 10+3", + ], + ), + # force_sign_m and force_sign_n combined + ( + dict(decimals=2, force_sign_m=True, force_sign_n=True), + [-3453, 0, 82794], + [ + "−3.45 × 10+3", + "0.00", + "+82.79 × 10+3", + ], + ), + # pattern + ( + dict(decimals=2, pattern="a {x} b"), + [1234.5, 0.000123], + [ + "a 1.23 × 103 b", + "a 123.00 × 10−6 b", + ], + ), + # scale_by + ( + dict(decimals=2, scale_by=1 / 1000), + [492032183020.5, 50000.01], + [ + "492.03 × 106", + "50.00", + ], + ), + # Extreme values with higher precision + ( + dict(decimals=5), + [-1.5e200, 2.5, 3.5e200], + [ + "−150.00000 × 10198", + "2.50000", + "350.00000 × 10198", + ], + ), + ], +) +def test_fmt_engineering_case( + fmt_engineering_kwargs: dict[str, Any], x_in: list[float], x_out: list[str] +): + df = pd.DataFrame({"x": x_in}) + gt = GT(df).fmt_engineering(columns="x", **fmt_engineering_kwargs) + x = _get_column_of_values(gt, column_name="x", context="html") + + assert x == x_out + + +def test_fmt_engineering_with_missing_values(): + df = pd.DataFrame({"x": [1234.5, None, float("nan"), 0.000123]}) + gt = GT(df).fmt_engineering(columns="x", decimals=2) + x = _get_column_of_values(gt, column_name="x", context="html") + + assert x[1] == "" + assert x[2] == "" + + +def test_fmt_engineering_exp_style_force_sign(): + df = pd.DataFrame({"x": [1e6, 1e3, 1, 1e-3, 1e-6]}) + gt = GT(df).fmt_engineering(columns="x", decimals=2, exp_style="E1", force_sign_n=True) + x = _get_column_of_values(gt, column_name="x", context="html") + + assert x == [ + "1.00E+6", + "1.00E+3", + "1.00E+0", + "1.00E−3", + "1.00E−6", + ] + + +def test_fmt_engineering_latex_output(): + df = pd.DataFrame({"x": [1234.5, 0.000123]}) + gt = GT(df).fmt_engineering(columns="x", decimals=2, pattern="Value: {x} units") + x = _get_column_of_values(gt, column_name="x", context="latex") + + assert "Value:" in x[0] + assert "units" in x[0] + assert "1.23" in x[0] + assert "Value:" in x[1] + assert "units" in x[1] + assert "123.00" in x[1] + + # ------------------------------------------------------------------------------ # Tests of `fmt_currency()` # ------------------------------------------------------------------------------ diff --git a/tests/test_formats_vals.py b/tests/test_formats_vals.py index 0c6679674..48e4fe009 100644 --- a/tests/test_formats_vals.py +++ b/tests/test_formats_vals.py @@ -32,6 +32,20 @@ def test_val_fmt_image_multiple(img_paths: Path): assert 'img src="data:image/svg+xml;base64' in img2 +def test_val_fmt_engineering_single(): + result = vals.fmt_engineering(1234.5, decimals=2) + assert result == ["1.23 × 103"] + + +def test_val_fmt_engineering_multiple(): + result = vals.fmt_engineering([1234.5, 0.000123, 1e6], decimals=2) + assert result == [ + "1.23 × 103", + "123.00 × 10−6", + "1.00 × 106", + ] + + def test_val_fmt_to_expression(): expr = vals.fmt_integer(pl.col("x")) assert isinstance(expr, pl.Expr)