From 6451afd2d62a87f6607b92074ee896d80bcaf72d Mon Sep 17 00:00:00 2001 From: Justin Date: Sun, 21 Jan 2024 13:26:36 -0700 Subject: [PATCH 01/38] remove unused parse_standard_exp_tr --- src/sciform/format_utils.py | 23 ----------------------- tests/test_invalid_options.py | 8 -------- 2 files changed, 31 deletions(-) diff --git a/src/sciform/format_utils.py b/src/sciform/format_utils.py index 5c9ee6e3..f0af5f52 100644 --- a/src/sciform/format_utils.py +++ b/src/sciform/format_utils.py @@ -238,29 +238,6 @@ def get_exp_str( # noqa: PLR0913 return get_standard_exp_str(base, exp_val, capitalize=capitalize) -def parse_standard_exp_str(exp_str: str) -> tuple[int, int]: - """Extract base and exponent value from standard exponent string.""" - match = re.match( - r""" - ^ - (?P[eEbB]) - (?P[+-]\d+) - $ - """, - exp_str, - re.VERBOSE, - ) - - exp_symbol = match.group("exp_symbol") - symbol_to_base_dict = {"e": 10, "b": 2} - base = symbol_to_base_dict[exp_symbol.lower()] - - exp_val_str = match.group("exp_val") - exp_val = int(exp_val_str) - - return base, exp_val - - def get_sign_str(num: Decimal, sign_mode: SignModeEnum) -> str: """Get the format sign string.""" if num < 0: diff --git a/tests/test_invalid_options.py b/tests/test_invalid_options.py index c58f3705..00f36444 100644 --- a/tests/test_invalid_options.py +++ b/tests/test_invalid_options.py @@ -9,7 +9,6 @@ get_sign_str, get_top_digit, get_top_digit_binary, - parse_standard_exp_str, ) from sciform.formatting import format_non_finite from sciform.user_options import UserOptions @@ -233,13 +232,6 @@ def test_get_prefix_dict_bad_format(self): extra_parts_per_forms={}, ) - def test_parse_standard_exp_str_binary(self): - """ - This is the only place that this is tested while binary - value/uncertainty is not implemented. - """ - self.assertEqual(parse_standard_exp_str("b+10"), (2, 10)) - def test_mode_str_to_enum_fail(self): self.assertRaises( ValueError, From be275372d9c0fd8913401ddb560561b19585b9bb Mon Sep 17 00:00:00 2001 From: Justin Date: Sun, 21 Jan 2024 13:26:55 -0700 Subject: [PATCH 02/38] latex output conversion --- src/sciform/output_conversion.py | 58 ++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 src/sciform/output_conversion.py diff --git a/src/sciform/output_conversion.py b/src/sciform/output_conversion.py new file mode 100644 index 00000000..1fb2e97d --- /dev/null +++ b/src/sciform/output_conversion.py @@ -0,0 +1,58 @@ +import re + +from sciform.format_utils import get_superscript_exp_str + +times_str = "×" + + +def standard_exp_str_to_superscript_exp_str(match: re.Match) -> str: + exp_symbol = match.group("exp_symbol") + symbol_to_base_dict = {"e": 10, "b": 2} + base = symbol_to_base_dict[exp_symbol.lower()] + + exp_val_str = match.group("exp_val") + exp_val = int(exp_val_str) + + superscript_exp_str = get_superscript_exp_str(base, exp_val) + return superscript_exp_str + + +def make_latex_superscript(match: re.Match) -> str: + sup_trans = str.maketrans("⁺⁻⁰¹²³⁴⁵⁶⁷⁸⁹", "+-0123456789") + exp_val_non_sup = match.group(0).translate(sup_trans) + return fr"^{{{exp_val_non_sup}}}" + + +def sciform_to_latex(formatted_str: str) -> str: + result_str = re.sub( + r"((?P[eEbB])(?P[+-]\d+))$", + standard_exp_str_to_superscript_exp_str, + formatted_str, + ) + + result_str = re.sub( + r"([a-zA-Zμ]+)", + r"\\text{\1}", + result_str, + ) + + replacements = ( + ("(", r"\left("), + (")", r"\right)"), + ("%", r"\%"), + ("_", r"\_"), + (" ", r"\:"), + ("±", r"\pm"), + ("×", r"\times"), + ("μ", r"\textmu") + ) + for old_chars, new_chars in replacements: + result_str = result_str.replace(old_chars, new_chars) + + result_str = re.sub( + r"([⁺⁻⁰¹²³⁴⁵⁶⁷⁸⁹]+)", + make_latex_superscript, + result_str, + ) + + return result_str From 4fed95dac6fc94e83987a153d25d2c4acba03fce Mon Sep 17 00:00:00 2001 From: Justin Date: Sun, 21 Jan 2024 13:58:28 -0700 Subject: [PATCH 03/38] tests --- src/sciform/__init__.py | 2 ++ tests/test_latex_conversion.py | 35 ++++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 tests/test_latex_conversion.py diff --git a/src/sciform/__init__.py b/src/sciform/__init__.py index 79bf702d..e575b684 100644 --- a/src/sciform/__init__.py +++ b/src/sciform/__init__.py @@ -8,6 +8,7 @@ set_global_defaults, ) from sciform.modes import AutoDigits, AutoExpVal +from sciform.output_conversion import sciform_to_latex from sciform.scinum import SciNum __all__ = [ @@ -18,5 +19,6 @@ "set_global_defaults", "AutoDigits", "AutoExpVal", + "sciform_to_latex", "SciNum", ] diff --git a/tests/test_latex_conversion.py b/tests/test_latex_conversion.py new file mode 100644 index 00000000..86af21d3 --- /dev/null +++ b/tests/test_latex_conversion.py @@ -0,0 +1,35 @@ +from __future__ import annotations + +import unittest + +from sciform import sciform_to_latex + + +class TestLatexConversion(unittest.TestCase): + def run_latex_conversion_cases(self, cases_list: list[tuple[str, str]]): + for input_str, expected_str in cases_list: + converted_str = sciform_to_latex(input_str) + with self.subTest( + input_str=input_str, + expected_str=expected_str, + actual_str=converted_str, + ): + self.assertEqual(converted_str, expected_str) + + def test_cases(self): + cases_list = [ + ("6.26070e-04", r"6.26070\times10^{-4}"), + ("(0.000000(1.234560))e+02", r"\left(0.000000\left(1.234560\right)\right)\times10^{2}"), + ("000_000_004_567_899.765_432_1", r"000\_000\_004\_567\_899.765\_432\_1"), + ("(nan)%", r"\left(\text{nan}\right)\%"), + ("123000 ppm", r"123000\:\text{ppm}"), + ("0b+00", r"0\times2^{0}"), + ("16.18033E+03", r"16.18033\times10^{3}"), + (" 1.20e+01", r"\:\:\:\:1.20\times10^{1}"), + ("(-INF)E+00", r"\left(-\text{INF}\right)\times10^{0}"), + ("(0.123456(789))e+03", r"\left(0.123456\left(789\right)\right)\times10^{3}"), + (" 123.46 ± 0.79", r"\:\:123.46\:\pm\:\:\:\:\:0.79"), + ("(7.8900 ± 0.0001)×10²", r"\left(7.8900\:\pm\:0.0001\right)\times10^{2}"), + ] + + self.run_latex_conversion_cases(cases_list) From fbe220640554c45800eab9a26819b41fdb825d31 Mon Sep 17 00:00:00 2001 From: Justin Date: Sun, 21 Jan 2024 14:00:05 -0700 Subject: [PATCH 04/38] ruff format --- src/sciform/output_conversion.py | 4 ++-- tests/test_latex_conversion.py | 10 ++++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/sciform/output_conversion.py b/src/sciform/output_conversion.py index 1fb2e97d..eaf82acf 100644 --- a/src/sciform/output_conversion.py +++ b/src/sciform/output_conversion.py @@ -20,7 +20,7 @@ def standard_exp_str_to_superscript_exp_str(match: re.Match) -> str: def make_latex_superscript(match: re.Match) -> str: sup_trans = str.maketrans("⁺⁻⁰¹²³⁴⁵⁶⁷⁸⁹", "+-0123456789") exp_val_non_sup = match.group(0).translate(sup_trans) - return fr"^{{{exp_val_non_sup}}}" + return rf"^{{{exp_val_non_sup}}}" def sciform_to_latex(formatted_str: str) -> str: @@ -44,7 +44,7 @@ def sciform_to_latex(formatted_str: str) -> str: (" ", r"\:"), ("±", r"\pm"), ("×", r"\times"), - ("μ", r"\textmu") + ("μ", r"\textmu"), ) for old_chars, new_chars in replacements: result_str = result_str.replace(old_chars, new_chars) diff --git a/tests/test_latex_conversion.py b/tests/test_latex_conversion.py index 86af21d3..1240911a 100644 --- a/tests/test_latex_conversion.py +++ b/tests/test_latex_conversion.py @@ -19,7 +19,10 @@ def run_latex_conversion_cases(self, cases_list: list[tuple[str, str]]): def test_cases(self): cases_list = [ ("6.26070e-04", r"6.26070\times10^{-4}"), - ("(0.000000(1.234560))e+02", r"\left(0.000000\left(1.234560\right)\right)\times10^{2}"), + ( + "(0.000000(1.234560))e+02", + r"\left(0.000000\left(1.234560\right)\right)\times10^{2}", + ), ("000_000_004_567_899.765_432_1", r"000\_000\_004\_567\_899.765\_432\_1"), ("(nan)%", r"\left(\text{nan}\right)\%"), ("123000 ppm", r"123000\:\text{ppm}"), @@ -27,7 +30,10 @@ def test_cases(self): ("16.18033E+03", r"16.18033\times10^{3}"), (" 1.20e+01", r"\:\:\:\:1.20\times10^{1}"), ("(-INF)E+00", r"\left(-\text{INF}\right)\times10^{0}"), - ("(0.123456(789))e+03", r"\left(0.123456\left(789\right)\right)\times10^{3}"), + ( + "(0.123456(789))e+03", + r"\left(0.123456\left(789\right)\right)\times10^{3}", + ), (" 123.46 ± 0.79", r"\:\:123.46\:\pm\:\:\:\:\:0.79"), ("(7.8900 ± 0.0001)×10²", r"\left(7.8900\:\pm\:0.0001\right)\times10^{2}"), ] From f7ad328a9fb2c7ebf3e06904abd0e9290caf6db3 Mon Sep 17 00:00:00 2001 From: Justin Date: Sun, 21 Jan 2024 20:53:43 -0700 Subject: [PATCH 05/38] no \left or \right --- src/sciform/output_conversion.py | 2 -- tests/test_latex_conversion.py | 11 ++++++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/sciform/output_conversion.py b/src/sciform/output_conversion.py index eaf82acf..12c7e604 100644 --- a/src/sciform/output_conversion.py +++ b/src/sciform/output_conversion.py @@ -37,8 +37,6 @@ def sciform_to_latex(formatted_str: str) -> str: ) replacements = ( - ("(", r"\left("), - (")", r"\right)"), ("%", r"\%"), ("_", r"\_"), (" ", r"\:"), diff --git a/tests/test_latex_conversion.py b/tests/test_latex_conversion.py index 1240911a..575b2756 100644 --- a/tests/test_latex_conversion.py +++ b/tests/test_latex_conversion.py @@ -21,21 +21,22 @@ def test_cases(self): ("6.26070e-04", r"6.26070\times10^{-4}"), ( "(0.000000(1.234560))e+02", - r"\left(0.000000\left(1.234560\right)\right)\times10^{2}", + r"(0.000000(1.234560))\times10^{2}", ), ("000_000_004_567_899.765_432_1", r"000\_000\_004\_567\_899.765\_432\_1"), - ("(nan)%", r"\left(\text{nan}\right)\%"), + ("(nan)%", r"(\text{nan})\%"), ("123000 ppm", r"123000\:\text{ppm}"), ("0b+00", r"0\times2^{0}"), ("16.18033E+03", r"16.18033\times10^{3}"), (" 1.20e+01", r"\:\:\:\:1.20\times10^{1}"), - ("(-INF)E+00", r"\left(-\text{INF}\right)\times10^{0}"), + ("(-INF)E+00", r"(-\text{INF})\times10^{0}"), ( "(0.123456(789))e+03", - r"\left(0.123456\left(789\right)\right)\times10^{3}", + r"(0.123456(789))\times10^{3}", ), (" 123.46 ± 0.79", r"\:\:123.46\:\pm\:\:\:\:\:0.79"), - ("(7.8900 ± 0.0001)×10²", r"\left(7.8900\:\pm\:0.0001\right)\times10^{2}"), + ("(7.8900 ± 0.0001)×10²", r"(7.8900\:\pm\:0.0001)\times10^{2}"), + ("(0.123456 ± 0.000789) k", r"(0.123456\:\pm\:0.000789)\:\text{k}"), ] self.run_latex_conversion_cases(cases_list) From 8d4cf02822479deb628ec2f5a50ddc50718b2936 Mon Sep 17 00:00:00 2001 From: Justin Date: Sun, 21 Jan 2024 21:26:05 -0700 Subject: [PATCH 06/38] Remove old latex functionality --- pyproject.toml | 1 + src/sciform/format_utils.py | 33 +------ src/sciform/formatter.py | 6 -- src/sciform/formatting.py | 19 +--- src/sciform/global_configuration.py | 4 - src/sciform/global_options.py | 1 - src/sciform/rendered_options.py | 1 - src/sciform/user_options.py | 1 - tests/test_float_formatter.py | 61 ------------ tests/test_latex_conversion.py | 142 +++++++++++++++++++++++++++- tests/test_print.py | 2 - tests/test_val_unc_formatter.py | 47 --------- 12 files changed, 141 insertions(+), 177 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 31c320bc..f3a35ee3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,6 +57,7 @@ ignore = [ 'TD002', 'TD003', 'FIX002', + 'RET504', ] [tool.ruff.per-file-ignores] diff --git a/src/sciform/format_utils.py b/src/sciform/format_utils.py index f0af5f52..2e3679d0 100644 --- a/src/sciform/format_utils.py +++ b/src/sciform/format_utils.py @@ -197,8 +197,6 @@ def get_exp_str( # noqa: PLR0913 extra_iec_prefixes: dict[int, str], extra_parts_per_forms: dict[int, str], capitalize: bool, - latex: bool, - latex_trim_whitespace: bool, superscript: bool, ) -> str: """Get formatting exponent string.""" @@ -224,14 +222,8 @@ def get_exp_str( # noqa: PLR0913 if exp_val in text_exp_dict and text_exp_dict[exp_val] is not None: exp_str = f" {text_exp_dict[exp_val]}" exp_str = exp_str.rstrip(" ") - if latex: - if latex_trim_whitespace: - exp_str = exp_str.lstrip(" ") - exp_str = rf"\text{{{exp_str}}}" return exp_str - if latex: - return rf"\times {base}^{{{exp_val:+}}}" if superscript: return get_superscript_exp_str(base, exp_val) @@ -341,25 +333,6 @@ def format_num_by_top_bottom_dig( pad_str = get_pad_str(left_pad_char, num_top_digit, target_top_digit) return f"{sign_str}{pad_str}{abs_mantissa_str}" - -def latex_translate(input_str: str) -> str: - """Translate elements of a string for Latex compatibility.""" - result_str = input_str - replacements = ( - ("(", r"\left("), - (")", r"\right)"), - ("%", r"\%"), - ("_", r"\_"), - ("nan", r"\text{nan}"), - ("NAN", r"\text{NAN}"), - ("inf", r"\text{inf}"), - ("INF", r"\text{INF}"), - ) - for old_chars, new_chars in replacements: - result_str = result_str.replace(old_chars, new_chars) - return result_str - - def round_val_unc( val: Decimal, unc: Decimal, @@ -464,13 +437,12 @@ def construct_val_unc_str( # noqa: PLR0913 decimal_separator: SeparatorEnum, *, paren_uncertainty: bool, - latex: bool, pm_whitespace: bool, paren_uncertainty_separators: bool, ) -> str: """Construct the value/uncertainty part of the formatted string.""" if not paren_uncertainty: - pm_symb = r"\pm" if latex else "±" + pm_symb = "±" if pm_whitespace: pm_symb = f" {pm_symb} " val_unc_str = f"{val_mantissa_str}{pm_symb}{unc_mantissa_str}" @@ -515,7 +487,6 @@ def construct_val_unc_exp_str( # noqa: PLR0913 extra_iec_prefixes: dict[int, str | None], extra_parts_per_forms: dict[int, str | None], capitalize: bool, - latex: bool, superscript: bool, paren_uncertainty: bool, ) -> str: @@ -525,8 +496,6 @@ def construct_val_unc_exp_str( # noqa: PLR0913 exp_mode=exp_mode, exp_format=exp_format, capitalize=capitalize, - latex=latex, - latex_trim_whitespace=True, superscript=superscript, extra_si_prefixes=extra_si_prefixes, extra_iec_prefixes=extra_iec_prefixes, diff --git a/src/sciform/formatter.py b/src/sciform/formatter.py index 8c6e1d7f..a7562043 100644 --- a/src/sciform/formatter.py +++ b/src/sciform/formatter.py @@ -57,7 +57,6 @@ def __init__( # noqa: PLR0913 extra_parts_per_forms: dict[int, str] | None = None, capitalize: bool | None = None, superscript: bool | None = None, - latex: bool | None = None, nan_inf_exp: bool | None = None, paren_uncertainty: bool | None = None, pdg_sig_figs: bool | None = None, @@ -152,10 +151,6 @@ def __init__( # noqa: PLR0913 should be converted into superscript notation. E.g. ``'1.23e+02'`` is converted to ``'1.23×10²'`` :type superscript: ``bool | None`` - :param latex: Flag indicating if the resulting string should be - converted into a latex parseable code, e.g. - ``'\\left(1.23 \\pm 0.01\\right)\\times 10^{2}'``. - :type latex: ``bool | None`` :param nan_inf_exp: Flag indicating whether non-finite numbers such as ``float('nan')`` or ``float('inf')`` should be formatted with exponent symbols when exponent modes including @@ -211,7 +206,6 @@ def __init__( # noqa: PLR0913 extra_parts_per_forms=extra_parts_per_forms, capitalize=capitalize, superscript=superscript, - latex=latex, nan_inf_exp=nan_inf_exp, paren_uncertainty=paren_uncertainty, pdg_sig_figs=pdg_sig_figs, diff --git a/src/sciform/formatting.py b/src/sciform/formatting.py index b3d47560..9b614c75 100644 --- a/src/sciform/formatting.py +++ b/src/sciform/formatting.py @@ -15,7 +15,6 @@ get_val_unc_exp, get_val_unc_mantissa_strs, get_val_unc_top_digit, - latex_translate, round_val_unc, ) from sciform.grouping import add_separators @@ -53,8 +52,6 @@ def format_non_finite(num: Decimal, options: RenderedOptions) -> str: exp_mode=exp_mode, exp_format=options.exp_format, capitalize=options.capitalize, - latex=options.latex, - latex_trim_whitespace=True, superscript=options.superscript, extra_si_prefixes=options.extra_si_prefixes, extra_iec_prefixes=options.extra_iec_prefixes, @@ -73,9 +70,6 @@ def format_non_finite(num: Decimal, options: RenderedOptions) -> str: else: result = result.lower() - if options.latex: - result = latex_translate(result) - return result @@ -140,8 +134,6 @@ def format_num(num: Decimal, options: RenderedOptions) -> str: exp_mode=exp_mode, exp_format=options.exp_format, capitalize=options.capitalize, - latex=options.latex, - latex_trim_whitespace=False, superscript=options.superscript, extra_si_prefixes=options.extra_si_prefixes, extra_iec_prefixes=options.extra_iec_prefixes, @@ -150,9 +142,6 @@ def format_num(num: Decimal, options: RenderedOptions) -> str: result = f"{mantissa_str}{exp_str}" - if options.latex: - result = latex_translate(result) - return result @@ -240,7 +229,7 @@ def format_val_unc(val: Decimal, unc: Decimal, options: RenderedOptions) -> str: * With the calculated shared exponent * Without percent mode (percent mode for val/unc pairs is handled below in the scope of this function) - * Without superscript, prefix, parts-per, or latex translations. + * Without superscript, prefix, or parts-per translations. The remaining steps rely on parsing an exponent string like 'e+03' or similar. Such translations are handled within the scope of this function. @@ -253,7 +242,6 @@ def format_val_unc(val: Decimal, unc: Decimal, options: RenderedOptions) -> str: exp_mode=exp_mode, exp_val=exp_val, superscript=False, - latex=False, exp_format=ExpFormatEnum.STANDARD, ) @@ -277,7 +265,6 @@ def format_val_unc(val: Decimal, unc: Decimal, options: RenderedOptions) -> str: unc_mantissa, decimal_separator=options.decimal_separator, paren_uncertainty=options.paren_uncertainty, - latex=options.latex, pm_whitespace=options.pm_whitespace, paren_uncertainty_separators=options.paren_uncertainty_separators, ) @@ -292,7 +279,6 @@ def format_val_unc(val: Decimal, unc: Decimal, options: RenderedOptions) -> str: extra_iec_prefixes=options.extra_iec_prefixes, extra_parts_per_forms=options.extra_parts_per_forms, capitalize=options.capitalize, - latex=options.latex, superscript=options.superscript, paren_uncertainty=options.paren_uncertainty, ) @@ -302,7 +288,4 @@ def format_val_unc(val: Decimal, unc: Decimal, options: RenderedOptions) -> str: if options.exp_mode is ExpModeEnum.PERCENT: val_unc_exp_str = f"({val_unc_exp_str})%" - if options.latex: - val_unc_exp_str = latex_translate(val_unc_exp_str) - return val_unc_exp_str diff --git a/src/sciform/global_configuration.py b/src/sciform/global_configuration.py index 2489d5bf..4eeaeaf8 100644 --- a/src/sciform/global_configuration.py +++ b/src/sciform/global_configuration.py @@ -36,7 +36,6 @@ def set_global_defaults( # noqa: PLR0913 extra_parts_per_forms: dict[int, str] | None = None, capitalize: bool | None = None, superscript: bool | None = None, - latex: bool | None = None, nan_inf_exp: bool | None = None, paren_uncertainty: bool | None = None, pdg_sig_figs: bool | None = None, @@ -69,7 +68,6 @@ def set_global_defaults( # noqa: PLR0913 extra_parts_per_forms=extra_parts_per_forms, capitalize=capitalize, superscript=superscript, - latex=latex, nan_inf_exp=nan_inf_exp, paren_uncertainty=paren_uncertainty, pdg_sig_figs=pdg_sig_figs, @@ -121,7 +119,6 @@ def __init__( # noqa: PLR0913 extra_parts_per_forms: dict[int, str] | None = None, capitalize: bool | None = None, superscript: bool | None = None, - latex: bool | None = None, nan_inf_exp: bool | None = None, paren_uncertainty: bool | None = None, pdg_sig_figs: bool | None = None, @@ -149,7 +146,6 @@ def __init__( # noqa: PLR0913 extra_parts_per_forms=extra_parts_per_forms, capitalize=capitalize, superscript=superscript, - latex=latex, nan_inf_exp=nan_inf_exp, paren_uncertainty=paren_uncertainty, pdg_sig_figs=pdg_sig_figs, diff --git a/src/sciform/global_options.py b/src/sciform/global_options.py index 898de6b2..c78517e0 100644 --- a/src/sciform/global_options.py +++ b/src/sciform/global_options.py @@ -20,7 +20,6 @@ extra_parts_per_forms={}, capitalize=False, superscript=False, - latex=False, nan_inf_exp=False, paren_uncertainty=False, pdg_sig_figs=False, diff --git a/src/sciform/rendered_options.py b/src/sciform/rendered_options.py index b50223d3..99f20ad7 100644 --- a/src/sciform/rendered_options.py +++ b/src/sciform/rendered_options.py @@ -36,7 +36,6 @@ class RenderedOptions: extra_parts_per_forms: dict[int, str] capitalize: bool superscript: bool - latex: bool nan_inf_exp: bool paren_uncertainty: bool pdg_sig_figs: bool diff --git a/src/sciform/user_options.py b/src/sciform/user_options.py index a754918c..014aedf1 100644 --- a/src/sciform/user_options.py +++ b/src/sciform/user_options.py @@ -30,7 +30,6 @@ class UserOptions: extra_parts_per_forms: dict[int, str] | None = None capitalize: bool | None = None superscript: bool | None = None - latex: bool | None = None nan_inf_exp: bool | None = None paren_uncertainty: bool | None = None pdg_sig_figs: bool | None = None diff --git a/tests/test_float_formatter.py b/tests/test_float_formatter.py index d6634e70..cfc64bdf 100644 --- a/tests/test_float_formatter.py +++ b/tests/test_float_formatter.py @@ -104,62 +104,6 @@ def test_left_pad_and_separators(self): self.run_float_formatter_cases(cases_list) - def test_latex(self): - cases_list = [ - ( - 789, - [ - ( - Formatter(exp_mode="scientific", latex=True), - r"7.89\times 10^{+2}", - ), - # Latex mode takes precedence over superscript - ( - Formatter( - exp_mode="scientific", - latex=True, - superscript=True, - ), - r"7.89\times 10^{+2}", - ), - ], - ), - ( - 12345, - [ - ( - Formatter( - exp_mode="scientific", - exp_val=-1, - upper_separator="_", - latex=True, - ), - r"123\_450\times 10^{-1}", - ), - ( - Formatter( - exp_mode="scientific", - exp_val=3, - exp_format="prefix", - latex=True, - ), - r"12.345\text{ k}", - ), - ], - ), - ( - 1024, - [ - ( - Formatter(exp_mode="binary", exp_val=8, latex=True), - r"4\times 2^{+8}", - ), - ], - ), - ] - - self.run_float_formatter_cases(cases_list) - def test_nan(self): cases_list = [ ( @@ -167,11 +111,6 @@ def test_nan(self): [ (Formatter(exp_mode="percent"), "nan"), (Formatter(exp_mode="percent", nan_inf_exp=True), "(nan)%"), - (Formatter(exp_mode="percent", latex=True), r"\text{nan}"), - ( - Formatter(exp_mode="percent", latex=True, nan_inf_exp=True), - r"\left(\text{nan}\right)\%", - ), ], ), ] diff --git a/tests/test_latex_conversion.py b/tests/test_latex_conversion.py index 575b2756..e4d21b37 100644 --- a/tests/test_latex_conversion.py +++ b/tests/test_latex_conversion.py @@ -2,11 +2,14 @@ import unittest -from sciform import sciform_to_latex +from sciform import Formatter, sciform_to_latex + +ValFormatterCases = list[tuple[float, list[tuple[Formatter, str]]]] +ValUncFormatterCases = list[tuple[tuple[float, float], list[tuple[Formatter, str]]]] class TestLatexConversion(unittest.TestCase): - def run_latex_conversion_cases(self, cases_list: list[tuple[str, str]]): + def run_direct_conversions(self, cases_list: list[tuple[str, str]]): for input_str, expected_str in cases_list: converted_str = sciform_to_latex(input_str) with self.subTest( @@ -16,7 +19,31 @@ def run_latex_conversion_cases(self, cases_list: list[tuple[str, str]]): ): self.assertEqual(converted_str, expected_str) - def test_cases(self): + def run_val_formatter_conversions(self, cases_list: ValFormatterCases): + for val, format_list in cases_list: + for formatter, expected_output in format_list: + actual_str_output = formatter(val) + actual_latex_output = sciform_to_latex(actual_str_output) + with self.subTest( + val=val, + expected_output=expected_output, + actual_output=actual_latex_output, + ): + self.assertEqual(actual_latex_output, expected_output) + + def run_val_unc_formatter_conversions(self, cases_list: ValUncFormatterCases): + for (val, unc), format_list in cases_list: + for formatter, expected_output in format_list: + actual_str_output = formatter(val, unc) + actual_latex_output = sciform_to_latex(actual_str_output) + with self.subTest( + val=val, + expected_output=expected_output, + actual_output=actual_latex_output, + ): + self.assertEqual(actual_latex_output, expected_output) + + def test_direct_cases(self): cases_list = [ ("6.26070e-04", r"6.26070\times10^{-4}"), ( @@ -39,4 +66,111 @@ def test_cases(self): ("(0.123456 ± 0.000789) k", r"(0.123456\:\pm\:0.000789)\:\text{k}"), ] - self.run_latex_conversion_cases(cases_list) + self.run_direct_conversions(cases_list) + + def test_val_formatter_cases(self): + cases_list = [ + ( + 789, + [ + ( + Formatter(exp_mode="scientific"), + r"7.89\times10^{2}", + ), + # Latex mode takes precedence over superscript + ( + Formatter( + exp_mode="scientific", + superscript=True, + ), + r"7.89\times10^{2}", + ), + ], + ), + ( + 12345, + [ + ( + Formatter( + exp_mode="scientific", + exp_val=-1, + upper_separator="_", + ), + r"123\_450\times10^{-1}", + ), + ( + Formatter( + exp_mode="scientific", + exp_val=3, + exp_format="prefix", + ), + r"12.345\:\text{k}", + ), + ], + ), + ( + 1024, + [ + ( + Formatter(exp_mode="binary", exp_val=8), + r"4\times2^{8}", + ), + ], + ), + ( + float("nan"), + [ + (Formatter(exp_mode="percent"), r"\text{nan}"), + ( + Formatter(exp_mode="percent", nan_inf_exp=True), + r"(\text{nan})\%", + ), + ], + ), + ] + + self.run_val_formatter_conversions(cases_list) + + def test_val_unc_formatter_cases(self): + cases_list = [ + ( + (12345, 0.2), + [ + ( + Formatter( + exp_mode="scientific", + exp_val=-1, + upper_separator="_", + ), + r"(123\_450\:\pm\:2)\times10^{-1}", + ), + ( + Formatter( + exp_mode="scientific", + exp_format="prefix", + exp_val=3, + ), + r"(12.3450\:\pm\:0.0002)\:\text{k}", + ), + ], + ), + ( + (0.123_456_78, 0.000_002_55), + [ + ( + Formatter(lower_separator="_", exp_mode="percent"), + r"(12.345\_678\:\pm\:0.000\_255)\%", + ), + ( + Formatter( + lower_separator="_", + exp_mode="percent", + paren_uncertainty=True, + ), + r"(12.345\_678(255))\%", + ), + ], + ), + ] + + self.run_val_unc_formatter_conversions(cases_list) diff --git a/tests/test_print.py b/tests/test_print.py index a919597e..931d89c1 100644 --- a/tests/test_print.py +++ b/tests/test_print.py @@ -27,7 +27,6 @@ def test_print_global_defaults(self): " 'extra_parts_per_forms': {},\n" " 'capitalize': False,\n" " 'superscript': False,\n" - " 'latex': False,\n" " 'nan_inf_exp': False,\n" " 'paren_uncertainty': False,\n" " 'pdg_sig_figs': False,\n" @@ -60,7 +59,6 @@ def test_unrendered_options_repr(self): " 'extra_parts_per_forms': {},\n" " 'capitalize': True,\n" " 'superscript': False,\n" - " 'latex': False,\n" " 'nan_inf_exp': False,\n" " 'paren_uncertainty': False,\n" " 'pdg_sig_figs': False,\n" diff --git a/tests/test_val_unc_formatter.py b/tests/test_val_unc_formatter.py index 760db7a8..c8a8e7b9 100644 --- a/tests/test_val_unc_formatter.py +++ b/tests/test_val_unc_formatter.py @@ -354,53 +354,6 @@ def test_superscript(self): self.run_val_unc_formatter_cases(cases_list) - def test_latex(self): - cases_list = [ - ( - (12345, 0.2), - [ - ( - Formatter( - exp_mode="scientific", - exp_val=-1, - upper_separator="_", - latex=True, - ), - r"\left(123\_450 \pm 2\right)\times 10^{-1}", - ), - ( - Formatter( - exp_mode="scientific", - exp_format="prefix", - exp_val=3, - latex=True, - ), - r"\left(12.3450 \pm 0.0002\right)\text{k}", - ), - ], - ), - ( - (0.123_456_78, 0.000_002_55), - [ - ( - Formatter(lower_separator="_", exp_mode="percent", latex=True), - r"\left(12.345\_678 \pm 0.000\_255\right)\%", - ), - ( - Formatter( - lower_separator="_", - exp_mode="percent", - paren_uncertainty=True, - latex=True, - ), - r"\left(12.345\_678\left(255\right)\right)\%", - ), - ], - ), - ] - - self.run_val_unc_formatter_cases(cases_list) - def test_pdg(self): cases_list = [ ( From 4c26d89891f166bc11ddea5acd55c00b89518fa7 Mon Sep 17 00:00:00 2001 From: Justin Date: Sun, 21 Jan 2024 23:05:48 -0700 Subject: [PATCH 07/38] docstrings and minor reorganization --- src/sciform/format_utils.py | 1 + src/sciform/output_conversion.py | 46 +++++++++++++++++++++++++++----- 2 files changed, 41 insertions(+), 6 deletions(-) diff --git a/src/sciform/format_utils.py b/src/sciform/format_utils.py index 2e3679d0..a3b63503 100644 --- a/src/sciform/format_utils.py +++ b/src/sciform/format_utils.py @@ -333,6 +333,7 @@ def format_num_by_top_bottom_dig( pad_str = get_pad_str(left_pad_char, num_top_digit, target_top_digit) return f"{sign_str}{pad_str}{abs_mantissa_str}" + def round_val_unc( val: Decimal, unc: Decimal, diff --git a/src/sciform/output_conversion.py b/src/sciform/output_conversion.py index 12c7e604..1d55e830 100644 --- a/src/sciform/output_conversion.py +++ b/src/sciform/output_conversion.py @@ -6,6 +6,7 @@ def standard_exp_str_to_superscript_exp_str(match: re.Match) -> str: + """Convert matched ascii exp_str to unicode superscript exp_str.""" exp_symbol = match.group("exp_symbol") symbol_to_base_dict = {"e": 10, "b": 2} base = symbol_to_base_dict[exp_symbol.lower()] @@ -18,18 +19,57 @@ def standard_exp_str_to_superscript_exp_str(match: re.Match) -> str: def make_latex_superscript(match: re.Match) -> str: + """Convert matched superscript exp_str to latex exp_str.""" sup_trans = str.maketrans("⁺⁻⁰¹²³⁴⁵⁶⁷⁸⁹", "+-0123456789") exp_val_non_sup = match.group(0).translate(sup_trans) return rf"^{{{exp_val_non_sup}}}" def sciform_to_latex(formatted_str: str) -> str: + r""" + Convert a sciform output string into a latex string. + + conversion proceeds by + + 1. If an exponent string is present and in ascii format + (e.g. "e+03") then convert it to superscript notation + (e.g. "×10³"). + 2. Bundle any unicode superscript substrings into latex + superscripts, e.g. "⁻²" -> r"^{-2}". + 3. Wrap any strings of alphabetic characters (plus μ) in latex text + environment, e.g. "nan" -> r"\text{nan}" or "k" -> r"\text{k}". + 4. Make the following character replacments: + + * "%" -> r"\%" + * "_" -> r"\_" + * " " -> r"\:" + * "±" -> r"\pm" + * "×" -> r"\times" + * "μ" -> r"\textmu" + + ("(7.8900 ± 0.0001)×10²", r"(7.8900\:\pm\:0.0001)\times10^{2}"), + ("(0.123456 ± 0.000789) k", r"(0.123456\:\pm\:0.000789)\:\text{k}"), + + Examples + -------- + >>> from sciform import sciform_to_latex + >>> print(sciform_to_latex("(7.8900 ± 0.0001)×10²") + (7.8900\:\pm\:0.0001)\times10^{2} + >>> print(sciform_to_latex("16.18033E+03")) + 16.18033\times10^{3} + """ result_str = re.sub( r"((?P[eEbB])(?P[+-]\d+))$", standard_exp_str_to_superscript_exp_str, formatted_str, ) + result_str = re.sub( + r"([⁺⁻⁰¹²³⁴⁵⁶⁷⁸⁹]+)", + make_latex_superscript, + result_str, + ) + result_str = re.sub( r"([a-zA-Zμ]+)", r"\\text{\1}", @@ -47,10 +87,4 @@ def sciform_to_latex(formatted_str: str) -> str: for old_chars, new_chars in replacements: result_str = result_str.replace(old_chars, new_chars) - result_str = re.sub( - r"([⁺⁻⁰¹²³⁴⁵⁶⁷⁸⁹]+)", - make_latex_superscript, - result_str, - ) - return result_str From 86790837332dd74fb1c8e4320c5e07be80f6dc4e Mon Sep 17 00:00:00 2001 From: Justin Date: Sun, 21 Jan 2024 23:09:18 -0700 Subject: [PATCH 08/38] allowed-confusables --- pyproject.toml | 3 +++ src/sciform/format_utils.py | 2 +- src/sciform/formatter.py | 2 +- src/sciform/output_conversion.py | 2 ++ 4 files changed, 7 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f3a35ee3..f5fc044a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,6 +45,9 @@ readme = {file = "README.rst"} [tool.setuptools_scm] +[tool.ruff] +allowed-confusables = ["×"] + [tool.ruff.lint] select = ['ALL'] ignore = [ diff --git a/src/sciform/format_utils.py b/src/sciform/format_utils.py index a3b63503..32f7284a 100644 --- a/src/sciform/format_utils.py +++ b/src/sciform/format_utils.py @@ -154,7 +154,7 @@ def get_standard_exp_str(base: int, exp_val: int, *, capitalize: bool = False) - def get_superscript_exp_str(base: int, exp_val: int) -> str: - """Get superscript (e.g. '×10⁺²') exponent string.""" # noqa: RUF002 + """Get superscript (e.g. '×10⁺²') exponent string.""" sup_trans = str.maketrans("+-0123456789", "⁺⁻⁰¹²³⁴⁵⁶⁷⁸⁹") exp_val_str = f"{exp_val}".translate(sup_trans) return f"×{base}{exp_val_str}" diff --git a/src/sciform/formatter.py b/src/sciform/formatter.py index a7562043..1fed370b 100644 --- a/src/sciform/formatter.py +++ b/src/sciform/formatter.py @@ -188,7 +188,7 @@ def __init__( # noqa: PLR0913 :param add_ppth_form: (default ``False``) if ``True``, adds ``{-3: 'ppth'}`` to ``extra_parts_per_forms``. :type add_ppth_form: ``bool`` - """ # noqa: RUF002 + """ self._user_options = UserOptions( exp_mode=exp_mode, exp_val=exp_val, diff --git a/src/sciform/output_conversion.py b/src/sciform/output_conversion.py index 1d55e830..e925aff2 100644 --- a/src/sciform/output_conversion.py +++ b/src/sciform/output_conversion.py @@ -1,3 +1,5 @@ +"""Convert sciform outputs into latex commands.""" + import re from sciform.format_utils import get_superscript_exp_str From b77e140ccc5c5d262791e6c53b660a19f3d55e2d Mon Sep 17 00:00:00 2001 From: Justin Date: Sun, 21 Jan 2024 23:17:54 -0700 Subject: [PATCH 09/38] typo --- src/sciform/output_conversion.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/sciform/output_conversion.py b/src/sciform/output_conversion.py index e925aff2..6fd46330 100644 --- a/src/sciform/output_conversion.py +++ b/src/sciform/output_conversion.py @@ -49,13 +49,10 @@ def sciform_to_latex(formatted_str: str) -> str: * "×" -> r"\times" * "μ" -> r"\textmu" - ("(7.8900 ± 0.0001)×10²", r"(7.8900\:\pm\:0.0001)\times10^{2}"), - ("(0.123456 ± 0.000789) k", r"(0.123456\:\pm\:0.000789)\:\text{k}"), - Examples -------- >>> from sciform import sciform_to_latex - >>> print(sciform_to_latex("(7.8900 ± 0.0001)×10²") + >>> print(sciform_to_latex("(7.8900 ± 0.0001)×10²")) (7.8900\:\pm\:0.0001)\times10^{2} >>> print(sciform_to_latex("16.18033E+03")) 16.18033\times10^{3} From e09b3661962162612e460a4618c73f324b5b8c2d Mon Sep 17 00:00:00 2001 From: Justin Date: Mon, 22 Jan 2024 14:54:08 -0700 Subject: [PATCH 10/38] documentation --- docs/source/api.rst | 5 +++ docs/source/options.rst | 54 ++++--------------------------- docs/source/usage.rst | 55 ++++++++++++++++++++++++++++++-- src/sciform/output_conversion.py | 25 +++++++-------- tests/test_latex_conversion.py | 13 ++++++++ 5 files changed, 90 insertions(+), 62 deletions(-) diff --git a/docs/source/api.rst b/docs/source/api.rst index 3309ef2e..6054ccf0 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -32,3 +32,8 @@ Global Configuration .. autofunction:: reset_global_defaults() .. autoclass:: GlobalDefaultsContext() + +Output Conversion +================= + +.. autofunction:: sciform_to_latex diff --git a/docs/source/options.rst b/docs/source/options.rst index 1455e798..36ffb1a4 100644 --- a/docs/source/options.rst +++ b/docs/source/options.rst @@ -72,13 +72,13 @@ mantissa ``m`` satisfies ``1 <= m < 10``. >>> print(sform(123.456, 0.001)) (1.23456 ± 0.00001)e+02 -Note that, for all exponent modes, the exponent integer is always -displayed with a sign symbol (+ or -) and is left padded with a zero so -that it is at least two digits wide. -There are no options to modify this behavior for standard exponent -display. -The :ref:`superscript` or :ref:`latex_format` options can be used as -alternatives. +By default the exponent is expressed using ASCII characters, e.g. +``e+02``. +The sign symbol is always included and the exponent value is left padded +so that it is at least two digits wide. +These behaviors for ASCII exponents cannot be modified. +However the :ref:`superscript` mode can be used to represent the +exponent using unicode characters. .. _engineering: @@ -567,46 +567,6 @@ superscript notation as opposed to e.g. ``e+02`` notation. >>> print(sform(789)) 7.89×10² -.. _latex_format: - -Latex Format -============ - -The ``latex`` option can be chosen to convert strings into latex -parseable codes. - ->>> sform = Formatter( -... exp_mode="scientific", -... exp_val=-1, -... upper_separator="_", -... latex=True, -... ) ->>> print(sform(12345)) -123\_450\times 10^{-1} ->>> sform = Formatter( -... exp_mode="percent", -... lower_separator="_", -... latex=True, -... ) ->>> print(sform(0.12345678, 0.00000255)) -\left(12.345\_678 \pm 0.000\_255\right)\% - -The latex format makes the following changes: - -* Convert standard exponent strings such as ``'e+02'`` into latex - superscript strings like ``'\times 10^{+2}`` -* Replace ``'('`` and ``')'`` by latex size-aware delimiters - ``'\left('`` and ``'\right)'``. -* Replace ``'±'`` by ``'\pm'`` -* Replace ``'_'`` by ``'\_'`` -* Replace ``'%'`` by ``'\%'`` -* Exponent replacements such as ``'M'``, ``'Ki'``, or ``'ppb'`` and - non-finite numbers such as ``'nan'``, ``'NAN'``, ``'inf'``, and - ``'INF'`` are wrapped in ``'\text{}'``. - -Note that use of ``latex`` renders the use of the ``superscript`` -option meaningless. - Include Exponent on nan and inf =============================== diff --git a/docs/source/usage.rst b/docs/source/usage.rst index a0cb952c..c29ae766 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -138,7 +138,6 @@ package default settings): 'extra_parts_per_forms': {}, 'capitalize': False, 'superscript': False, - 'latex': False, 'nan_inf_exp': False, 'paren_uncertainty': False, 'pdg_sig_figs': False, @@ -176,7 +175,6 @@ unchanged. 'extra_parts_per_forms': {}, 'capitalize': False, 'superscript': False, - 'latex': False, 'nan_inf_exp': False, 'paren_uncertainty': False, 'pdg_sig_figs': False, @@ -218,6 +216,59 @@ If the user wishes to configure these options, but also use the :ref:`FSML `, then they must do so by modifying the global default settings. +.. _latex_conversion: + +Latex Conversion +================ + +The :func:`sciform_to_latex` function can be used to convert ``sciform`` +output strings into latex commands. + +>>> from sciform import sciform_to_latex +>>> sform = Formatter( +... exp_mode="scientific", +... exp_val=-1, +... upper_separator="_", +... ) +>>> formatted_str = sform(12345) +>>> latex_str = sciform_to_latex(formatted_str) +>>> print(f"{formatted_str} -> {latex_str}") +123_450e-01 -> 123\_450\times10^{-1} + +>>> sform = Formatter( +... exp_mode="percent", +... lower_separator="_", +... ) +>>> formatted_str = sform(0.12345678, 0.00000255) +>>> latex_str= sciform_to_latex(formatted_str) +>>> print(f"{formatted_str} -> {latex_str}") +(12.345_678 ± 0.000_255)% -> (12.345\_678\:\pm\:0.000\_255)\% + +>>> sform = Formatter( +... exp_mode="engineering", +... exp_format="prefix", +... ndigits=4 +... ) +>>> formatted_str = sform(314.159e-6, 2.71828e-6) +>>> latex_str = sciform_to_latex(formatted_str) +>>> print(f"{formatted_str} -> {latex_str}") +(314.159 ± 2.718) μ -> (314.159\:\pm\:2.718)\:\text{\textmu} + +The latex format makes the following changes: + +* Convert all ASCII (``"e+02"``) and superscript (``"×10²"``) exponents + into latex superscript strings like ``"\times10^{2}"``. +* Wrap all text like ``"nan"``, ``"INF"``, ``"M"``, ``"μ"``, ``"Ki"``, + or ``"ppb"`` in the latex math mode text environment ``"\text{}"``. +* Make the following symbol replacements + + * ``"%"`` -> ``r"\%"`` + * ``"_"`` -> ``r"\_"`` + * ``" "`` -> ``r"\:"`` + * ``"±"`` -> ``r"\pm"`` + * ``"×"`` -> ```r"\times"`` + * ``"μ"`` -> ``r"\textmu"`` + .. _dec_and_float: Note on Decimals and Floats diff --git a/src/sciform/output_conversion.py b/src/sciform/output_conversion.py index 6fd46330..fb67af38 100644 --- a/src/sciform/output_conversion.py +++ b/src/sciform/output_conversion.py @@ -34,23 +34,22 @@ def sciform_to_latex(formatted_str: str) -> str: conversion proceeds by 1. If an exponent string is present and in ascii format - (e.g. "e+03") then convert it to superscript notation - (e.g. "×10³"). + (e.g. ``"e+03"``) then convert it to superscript notation + (e.g. ``"×10³"``). 2. Bundle any unicode superscript substrings into latex - superscripts, e.g. "⁻²" -> r"^{-2}". - 3. Wrap any strings of alphabetic characters (plus μ) in latex text - environment, e.g. "nan" -> r"\text{nan}" or "k" -> r"\text{k}". + superscripts, e.g. ``"⁻²"`` -> ``r"^{-2}"``. + 3. Wrap any strings of alphabetic characters (plus ``"μ"``) in latex + text environment, e.g. ``"nan"`` -> ``r"\text{nan}"`` or + ``"k"`` -> ``r"\text{k}"``. 4. Make the following character replacments: - * "%" -> r"\%" - * "_" -> r"\_" - * " " -> r"\:" - * "±" -> r"\pm" - * "×" -> r"\times" - * "μ" -> r"\textmu" + * ``"%"`` -> ``r"\%"`` + * ``"_"`` -> ``r"\_"`` + * ``" "`` -> ``r"\:"`` + * ``"±"`` -> ``r"\pm"`` + * ``"×"`` -> ```r"\times"`` + * ``"μ"`` -> ``r"\textmu"`` - Examples - -------- >>> from sciform import sciform_to_latex >>> print(sciform_to_latex("(7.8900 ± 0.0001)×10²")) (7.8900\:\pm\:0.0001)\times10^{2} diff --git a/tests/test_latex_conversion.py b/tests/test_latex_conversion.py index e4d21b37..a5c03ad8 100644 --- a/tests/test_latex_conversion.py +++ b/tests/test_latex_conversion.py @@ -171,6 +171,19 @@ def test_val_unc_formatter_cases(self): ), ], ), + ( + (314.159e-6, 2.71828e-6), + [ + ( + Formatter( + exp_mode="engineering", + exp_format="prefix", + ndigits=4 + ), + r"(314.159\:\pm\:2.718)\:\text{\textmu}", + ), + ], + ), ] self.run_val_unc_formatter_conversions(cases_list) From 9ca141a297fc736b19ed655f556ce6bb4cba7830 Mon Sep 17 00:00:00 2001 From: Justin Date: Tue, 23 Jan 2024 07:18:50 -0700 Subject: [PATCH 11/38] docstring ruff formatting --- pyproject.toml | 3 +++ tests/test_latex_conversion.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index f5fc044a..e93cb470 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,6 +63,9 @@ ignore = [ 'RET504', ] +[tool.ruff.format] +docstring-code-format = true + [tool.ruff.per-file-ignores] "tests/*.py" = ["D", "ANN", "PT"] "examples/*.py" = ["D"] diff --git a/tests/test_latex_conversion.py b/tests/test_latex_conversion.py index a5c03ad8..2dfbe1dc 100644 --- a/tests/test_latex_conversion.py +++ b/tests/test_latex_conversion.py @@ -178,7 +178,7 @@ def test_val_unc_formatter_cases(self): Formatter( exp_mode="engineering", exp_format="prefix", - ndigits=4 + ndigits=4, ), r"(314.159\:\pm\:2.718)\:\text{\textmu}", ), From 1ed8e3fa7db5a55af52edf3dba80d907cddd5a42 Mon Sep 17 00:00:00 2001 From: Justin Date: Tue, 23 Jan 2024 20:22:18 -0700 Subject: [PATCH 12/38] add sciform_to_html and FormattedNumber --- src/sciform/__init__.py | 3 +- src/sciform/formatter.py | 39 ++++++- src/sciform/output_conversion.py | 40 +++++++ tests/test_html_conversion.py | 188 +++++++++++++++++++++++++++++++ tests/test_latex_conversion.py | 17 ++- 5 files changed, 275 insertions(+), 12 deletions(-) create mode 100644 tests/test_html_conversion.py diff --git a/src/sciform/__init__.py b/src/sciform/__init__.py index e575b684..c9a056f1 100644 --- a/src/sciform/__init__.py +++ b/src/sciform/__init__.py @@ -8,7 +8,7 @@ set_global_defaults, ) from sciform.modes import AutoDigits, AutoExpVal -from sciform.output_conversion import sciform_to_latex +from sciform.output_conversion import sciform_to_html, sciform_to_latex from sciform.scinum import SciNum __all__ = [ @@ -19,6 +19,7 @@ "set_global_defaults", "AutoDigits", "AutoExpVal", + "sciform_to_html", "sciform_to_latex", "SciNum", ] diff --git a/src/sciform/formatter.py b/src/sciform/formatter.py index 1fed370b..42935baa 100644 --- a/src/sciform/formatter.py +++ b/src/sciform/formatter.py @@ -6,6 +6,7 @@ from typing import TYPE_CHECKING from sciform.formatting import format_num, format_val_unc +from sciform.output_conversion import sciform_to_html, sciform_to_latex from sciform.user_options import UserOptions if TYPE_CHECKING: # pragma: no cover @@ -222,7 +223,7 @@ def __call__( value: Number, uncertainty: Number | None = None, /, - ) -> str: + ) -> FormattedNumber: """ Format a value or value/uncertainty pair. @@ -240,4 +241,38 @@ def __call__( Decimal(str(uncertainty)), rendered_options, ) - return output + return FormattedNumber(output) + + +class FormattedNumber(str): + """ + Representation (typically string) of a formatted number. + + The ``FormattedNumber`` class is returned by ``sciform`` formatting + methods. In most cases it behaves like a regular python string, but + it provides the possibility for post-converting the string to + various other formats such as latex or html. This allows the + formatted number to be displayed in a range of contexts other than + e.g. text terminals. + + """ + + __slots__ = () + + def as_str(self: FormattedNumber) -> str: + """Return the string representation of the formatted number.""" + return self.__str__() + + def as_html(self: FormattedNumber) -> str: + """Return the html representation of the formatted number.""" + return self._repr_html_() + + def as_latex(self: FormattedNumber) -> str: + """Return the latex representation of the formatted number.""" + return self._repr_latex_() + + def _repr_html_(self: FormattedNumber) -> str: + return sciform_to_html(self) + + def _repr_latex_(self: FormattedNumber) -> str: + return sciform_to_latex(self) diff --git a/src/sciform/output_conversion.py b/src/sciform/output_conversion.py index fb67af38..e9d84a78 100644 --- a/src/sciform/output_conversion.py +++ b/src/sciform/output_conversion.py @@ -27,6 +27,13 @@ def make_latex_superscript(match: re.Match) -> str: return rf"^{{{exp_val_non_sup}}}" +def make_html_superscript(match: re.Match) -> str: + """Convert matched superscript exp_str to html exp_str.""" + sup_trans = str.maketrans("⁺⁻⁰¹²³⁴⁵⁶⁷⁸⁹", "+-0123456789") + exp_val_non_sup = match.group(0).translate(sup_trans) + return rf"{exp_val_non_sup}" + + def sciform_to_latex(formatted_str: str) -> str: r""" Convert a sciform output string into a latex string. @@ -86,3 +93,36 @@ def sciform_to_latex(formatted_str: str) -> str: result_str = result_str.replace(old_chars, new_chars) return result_str + + +def sciform_to_html(formatted_str: str) -> str: + r""" + Convert a sciform output string into a html string. + + conversion proceeds by + + 1. If an exponent string is present and in ascii format + (e.g. ``"e+03"``) then convert it to superscript notation + (e.g. ``"×10³"``). + 2. Bundle any unicode superscript substrings into latex + superscripts, e.g. ``"⁻²"`` -> ``r"-2"``. + + >>> from sciform import sciform_to_html + >>> print(sciform_to_latex("(7.8900 ± 0.0001)×10²")) + (7.8900 ± 0.0001)×102 + >>> print(sciform_to_latex("16.18033E+03")) + 16.18033×103 + """ + result_str = re.sub( + r"((?P[eEbB])(?P[+-]\d+))$", + standard_exp_str_to_superscript_exp_str, + formatted_str, + ) + + result_str = re.sub( + r"([⁺⁻⁰¹²³⁴⁵⁶⁷⁸⁹]+)", + make_html_superscript, + result_str, + ) + + return result_str diff --git a/tests/test_html_conversion.py b/tests/test_html_conversion.py new file mode 100644 index 00000000..1186c795 --- /dev/null +++ b/tests/test_html_conversion.py @@ -0,0 +1,188 @@ +from __future__ import annotations + +import unittest + +from sciform import Formatter, sciform_to_html + +ValFormatterCases = list[tuple[float, list[tuple[Formatter, str]]]] +ValUncFormatterCases = list[tuple[tuple[float, float], list[tuple[Formatter, str]]]] + + +class TestHTMLConversion(unittest.TestCase): + def run_direct_conversions(self, cases_list: list[tuple[str, str]]): + for input_str, expected_str in cases_list: + converted_str = sciform_to_html(input_str) + with self.subTest( + input_str=input_str, + expected_str=expected_str, + actual_str=converted_str, + ): + self.assertEqual(converted_str, expected_str) + + def run_val_formatter_conversions(self, cases_list: ValFormatterCases): + for val, format_list in cases_list: + for formatter, expected_output in format_list: + sciform_output = formatter(val) + html_output = sciform_output.as_html() + with self.subTest( + val=val, + expected_output=expected_output, + actual_output=html_output, + ): + self.assertEqual(html_output, expected_output) + + def run_val_unc_formatter_conversions(self, cases_list: ValUncFormatterCases): + for (val, unc), format_list in cases_list: + for formatter, expected_output in format_list: + sciform_output = formatter(val, unc) + html_output = sciform_output.as_html() + with self.subTest( + val=val, + expected_output=expected_output, + actual_output=html_output, + ): + self.assertEqual(html_output, expected_output) + + def test_direct_cases(self): + cases_list = [ + ("6.26070e-04", r"6.26070×10-4"), + ( + "(0.000000(1.234560))e+02", + r"(0.000000(1.234560))×102", + ), + ("000_000_004_567_899.765_432_1", r"000_000_004_567_899.765_432_1"), + ("(nan)%", r"(nan)%"), + ("123000 ppm", r"123000 ppm"), + ("0b+00", r"0×20"), + ("16.18033E+03", r"16.18033×103"), + (" 1.20e+01", r" 1.20×101"), + ("(-INF)E+00", r"(-INF)×100"), + ( + "(0.123456(789))e+03", + r"(0.123456(789))×103", + ), + (" 123.46 ± 0.79", r" 123.46 ± 0.79"), + ("(7.8900 ± 0.0001)×10²", r"(7.8900 ± 0.0001)×102"), + ("(0.123456 ± 0.000789) k", r"(0.123456 ± 0.000789) k"), + ] + + self.run_direct_conversions(cases_list) + + def test_val_formatter_cases(self): + cases_list = [ + ( + 789, + [ + ( + Formatter(exp_mode="scientific"), + r"7.89×102", + ), + ( + Formatter( + exp_mode="scientific", + superscript=True, + ), + r"7.89×102", + ), + ], + ), + ( + 12345, + [ + ( + Formatter( + exp_mode="scientific", + exp_val=-1, + upper_separator="_", + ), + r"123_450×10-1", + ), + ( + Formatter( + exp_mode="scientific", + exp_val=3, + exp_format="prefix", + ), + r"12.345 k", + ), + ], + ), + ( + 1024, + [ + ( + Formatter(exp_mode="binary", exp_val=8), + r"4×28", + ), + ], + ), + ( + float("nan"), + [ + (Formatter(exp_mode="percent"), r"nan"), + ( + Formatter(exp_mode="percent", nan_inf_exp=True), + r"(nan)%", + ), + ], + ), + ] + + self.run_val_formatter_conversions(cases_list) + + def test_val_unc_formatter_cases(self): + cases_list = [ + ( + (12345, 0.2), + [ + ( + Formatter( + exp_mode="scientific", + exp_val=-1, + upper_separator="_", + ), + r"(123_450 ± 2)×10-1", + ), + ( + Formatter( + exp_mode="scientific", + exp_format="prefix", + exp_val=3, + ), + r"(12.3450 ± 0.0002) k", + ), + ], + ), + ( + (0.123_456_78, 0.000_002_55), + [ + ( + Formatter(lower_separator="_", exp_mode="percent"), + r"(12.345_678 ± 0.000_255)%", + ), + ( + Formatter( + lower_separator="_", + exp_mode="percent", + paren_uncertainty=True, + ), + r"(12.345_678(255))%", + ), + ], + ), + ( + (314.159e-6, 2.71828e-6), + [ + ( + Formatter( + exp_mode="engineering", + exp_format="prefix", + ndigits=4, + ), + r"(314.159 ± 2.718) μ", + ), + ], + ), + ] + + self.run_val_unc_formatter_conversions(cases_list) diff --git a/tests/test_latex_conversion.py b/tests/test_latex_conversion.py index 2dfbe1dc..b985dc23 100644 --- a/tests/test_latex_conversion.py +++ b/tests/test_latex_conversion.py @@ -22,26 +22,26 @@ def run_direct_conversions(self, cases_list: list[tuple[str, str]]): def run_val_formatter_conversions(self, cases_list: ValFormatterCases): for val, format_list in cases_list: for formatter, expected_output in format_list: - actual_str_output = formatter(val) - actual_latex_output = sciform_to_latex(actual_str_output) + sciform_output = formatter(val) + latex_output = sciform_output.as_latex() with self.subTest( val=val, expected_output=expected_output, - actual_output=actual_latex_output, + actual_output=latex_output, ): - self.assertEqual(actual_latex_output, expected_output) + self.assertEqual(latex_output, expected_output) def run_val_unc_formatter_conversions(self, cases_list: ValUncFormatterCases): for (val, unc), format_list in cases_list: for formatter, expected_output in format_list: - actual_str_output = formatter(val, unc) - actual_latex_output = sciform_to_latex(actual_str_output) + sciform_output = formatter(val, unc) + latex_output = sciform_output.as_latex() with self.subTest( val=val, expected_output=expected_output, - actual_output=actual_latex_output, + actual_output=latex_output, ): - self.assertEqual(actual_latex_output, expected_output) + self.assertEqual(latex_output, expected_output) def test_direct_cases(self): cases_list = [ @@ -77,7 +77,6 @@ def test_val_formatter_cases(self): Formatter(exp_mode="scientific"), r"7.89\times10^{2}", ), - # Latex mode takes precedence over superscript ( Formatter( exp_mode="scientific", From f60a7103306a0b38058db325d4da8582df8aa126 Mon Sep 17 00:00:00 2001 From: Justin Date: Tue, 23 Jan 2024 21:57:53 -0700 Subject: [PATCH 13/38] surrounding $$ --- src/sciform/output_conversion.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/sciform/output_conversion.py b/src/sciform/output_conversion.py index e9d84a78..ae9d5f82 100644 --- a/src/sciform/output_conversion.py +++ b/src/sciform/output_conversion.py @@ -92,6 +92,8 @@ def sciform_to_latex(formatted_str: str) -> str: for old_chars, new_chars in replacements: result_str = result_str.replace(old_chars, new_chars) + result_str = rf"${result_str}$" + return result_str From b5a33891f554d6d44f7dc26aaa94a8d33c69a1d8 Mon Sep 17 00:00:00 2001 From: Justin Date: Tue, 23 Jan 2024 22:02:11 -0700 Subject: [PATCH 14/38] add strip_env_symbs flag --- src/sciform/formatter.py | 7 +++++-- tests/test_latex_conversion.py | 30 +++++++++++++++--------------- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/src/sciform/formatter.py b/src/sciform/formatter.py index 42935baa..e283092c 100644 --- a/src/sciform/formatter.py +++ b/src/sciform/formatter.py @@ -267,9 +267,12 @@ def as_html(self: FormattedNumber) -> str: """Return the html representation of the formatted number.""" return self._repr_html_() - def as_latex(self: FormattedNumber) -> str: + def as_latex(self: FormattedNumber, *, strip_env_symbs: bool = False) -> str: """Return the latex representation of the formatted number.""" - return self._repr_latex_() + latex_repr = self._repr_latex_() + if strip_env_symbs: + latex_repr = latex_repr.strip("$") + return latex_repr def _repr_html_(self: FormattedNumber) -> str: return sciform_to_html(self) diff --git a/tests/test_latex_conversion.py b/tests/test_latex_conversion.py index b985dc23..4dcda954 100644 --- a/tests/test_latex_conversion.py +++ b/tests/test_latex_conversion.py @@ -23,7 +23,7 @@ def run_val_formatter_conversions(self, cases_list: ValFormatterCases): for val, format_list in cases_list: for formatter, expected_output in format_list: sciform_output = formatter(val) - latex_output = sciform_output.as_latex() + latex_output = sciform_output.as_latex(strip_env_symbs=True) with self.subTest( val=val, expected_output=expected_output, @@ -35,7 +35,7 @@ def run_val_unc_formatter_conversions(self, cases_list: ValUncFormatterCases): for (val, unc), format_list in cases_list: for formatter, expected_output in format_list: sciform_output = formatter(val, unc) - latex_output = sciform_output.as_latex() + latex_output = sciform_output.as_latex(strip_env_symbs=True) with self.subTest( val=val, expected_output=expected_output, @@ -45,25 +45,25 @@ def run_val_unc_formatter_conversions(self, cases_list: ValUncFormatterCases): def test_direct_cases(self): cases_list = [ - ("6.26070e-04", r"6.26070\times10^{-4}"), + ("6.26070e-04", r"$6.26070\times10^{-4}$"), ( "(0.000000(1.234560))e+02", - r"(0.000000(1.234560))\times10^{2}", + r"$(0.000000(1.234560))\times10^{2}$", ), - ("000_000_004_567_899.765_432_1", r"000\_000\_004\_567\_899.765\_432\_1"), - ("(nan)%", r"(\text{nan})\%"), - ("123000 ppm", r"123000\:\text{ppm}"), - ("0b+00", r"0\times2^{0}"), - ("16.18033E+03", r"16.18033\times10^{3}"), - (" 1.20e+01", r"\:\:\:\:1.20\times10^{1}"), - ("(-INF)E+00", r"(-\text{INF})\times10^{0}"), + ("000_000_004_567_899.765_432_1", r"$000\_000\_004\_567\_899.765\_432\_1$"), + ("(nan)%", r"$(\text{nan})\%$"), + ("123000 ppm", r"$123000\:\text{ppm}$"), + ("0b+00", r"$0\times2^{0}$"), + ("16.18033E+03", r"$16.18033\times10^{3}$"), + (" 1.20e+01", r"$\:\:\:\:1.20\times10^{1}$"), + ("(-INF)E+00", r"$(-\text{INF})\times10^{0}$"), ( "(0.123456(789))e+03", - r"(0.123456(789))\times10^{3}", + r"$(0.123456(789))\times10^{3}$", ), - (" 123.46 ± 0.79", r"\:\:123.46\:\pm\:\:\:\:\:0.79"), - ("(7.8900 ± 0.0001)×10²", r"(7.8900\:\pm\:0.0001)\times10^{2}"), - ("(0.123456 ± 0.000789) k", r"(0.123456\:\pm\:0.000789)\:\text{k}"), + (" 123.46 ± 0.79", r"$\:\:123.46\:\pm\:\:\:\:\:0.79$"), + ("(7.8900 ± 0.0001)×10²", r"$(7.8900\:\pm\:0.0001)\times10^{2}$"), + ("(0.123456 ± 0.000789) k", r"$(0.123456\:\pm\:0.000789)\:\text{k}$"), ] self.run_direct_conversions(cases_list) From 330f9fb2b05d86a4997be8fe242dabf16282368d Mon Sep 17 00:00:00 2001 From: Justin Date: Wed, 24 Jan 2024 23:44:56 -0700 Subject: [PATCH 15/38] refactor conversion code and add ASCII conversion --- src/sciform/__init__.py | 3 - src/sciform/formatter.py | 13 +- src/sciform/output_conversion.py | 244 ++++++++++++++++++------------- tests/test_ascii_conversion.py | 189 ++++++++++++++++++++++++ tests/test_html_conversion.py | 5 +- tests/test_latex_conversion.py | 5 +- 6 files changed, 348 insertions(+), 111 deletions(-) create mode 100644 tests/test_ascii_conversion.py diff --git a/src/sciform/__init__.py b/src/sciform/__init__.py index c9a056f1..79bf702d 100644 --- a/src/sciform/__init__.py +++ b/src/sciform/__init__.py @@ -8,7 +8,6 @@ set_global_defaults, ) from sciform.modes import AutoDigits, AutoExpVal -from sciform.output_conversion import sciform_to_html, sciform_to_latex from sciform.scinum import SciNum __all__ = [ @@ -19,7 +18,5 @@ "set_global_defaults", "AutoDigits", "AutoExpVal", - "sciform_to_html", - "sciform_to_latex", "SciNum", ] diff --git a/src/sciform/formatter.py b/src/sciform/formatter.py index e283092c..0e3f91e6 100644 --- a/src/sciform/formatter.py +++ b/src/sciform/formatter.py @@ -6,7 +6,7 @@ from typing import TYPE_CHECKING from sciform.formatting import format_num, format_val_unc -from sciform.output_conversion import sciform_to_html, sciform_to_latex +from sciform.output_conversion import convert_sciform_format from sciform.user_options import UserOptions if TYPE_CHECKING: # pragma: no cover @@ -263,6 +263,10 @@ def as_str(self: FormattedNumber) -> str: """Return the string representation of the formatted number.""" return self.__str__() + def as_ascii(self: FormattedNumber) -> str: + """Return the ascii representation of the formatted number.""" + return self._repr_ascii_() + def as_html(self: FormattedNumber) -> str: """Return the html representation of the formatted number.""" return self._repr_html_() @@ -274,8 +278,11 @@ def as_latex(self: FormattedNumber, *, strip_env_symbs: bool = False) -> str: latex_repr = latex_repr.strip("$") return latex_repr + def _repr_ascii_(self: FormattedNumber) -> str: + return convert_sciform_format(self, "ascii") + def _repr_html_(self: FormattedNumber) -> str: - return sciform_to_html(self) + return convert_sciform_format(self, "html") def _repr_latex_(self: FormattedNumber) -> str: - return sciform_to_latex(self) + return convert_sciform_format(self, "latex") diff --git a/src/sciform/output_conversion.py b/src/sciform/output_conversion.py index ae9d5f82..0b77efa2 100644 --- a/src/sciform/output_conversion.py +++ b/src/sciform/output_conversion.py @@ -1,54 +1,81 @@ """Convert sciform outputs into latex commands.""" +from __future__ import annotations import re - -from sciform.format_utils import get_superscript_exp_str - -times_str = "×" - - -def standard_exp_str_to_superscript_exp_str(match: re.Match) -> str: - """Convert matched ascii exp_str to unicode superscript exp_str.""" - exp_symbol = match.group("exp_symbol") - symbol_to_base_dict = {"e": 10, "b": 2} - base = symbol_to_base_dict[exp_symbol.lower()] - - exp_val_str = match.group("exp_val") - exp_val = int(exp_val_str) - - superscript_exp_str = get_superscript_exp_str(base, exp_val) - return superscript_exp_str +from typing import Literal, get_args + +ascii_exp_pattern = re.compile( + r"^(?P.*)(?P[eEbB])(?P[+-]\d+)$" +) +ascii_base_dict = {"e": 10, "E": 10, "b": 2, "B": 2} + +unicode_exp_pattern = re.compile( + r"^(?P.*)×(?P10|2)(?P[⁺⁻⁰¹²³⁴⁵⁶⁷⁸⁹])$" +) +superscript_translation = str.maketrans("⁺⁻⁰¹²³⁴⁵⁶⁷⁸⁹", "+-0123456789") + +output_formats = Literal["latex", "html", "ascii"] + + +def _make_exp_str( + base: int, + exp: int, + output_format: output_formats, + *, + capitalize: bool = False +) -> str: + if output_format == "latex": + return rf"\times{base}^{{{exp}}}" + if output_format == "html": + return f"×{base}{exp}" + if output_format == "ascii": + if base == 10: + exp_str = f"e{exp:+03d}" + elif base == 2: + exp_str = f"b{exp:+03d}" + else: + msg = f"base must be 10 or 2, not {base}" + raise ValueError(msg) + if capitalize: + exp_str = exp_str.upper() + return exp_str + msg = ( + f"output_format must be in {get_args(output_formats)}, not {output_format}" + ) + raise ValueError(msg) -def make_latex_superscript(match: re.Match) -> str: - """Convert matched superscript exp_str to latex exp_str.""" - sup_trans = str.maketrans("⁺⁻⁰¹²³⁴⁵⁶⁷⁸⁹", "+-0123456789") - exp_val_non_sup = match.group(0).translate(sup_trans) - return rf"^{{{exp_val_non_sup}}}" +def _string_replacements(input_str: str, replacements: list[tuple[str, str]]) -> str: + result_str = input_str + for old_chars, new_chars in replacements: + result_str = result_str.replace(old_chars, new_chars) + return result_str -def make_html_superscript(match: re.Match) -> str: - """Convert matched superscript exp_str to html exp_str.""" - sup_trans = str.maketrans("⁺⁻⁰¹²³⁴⁵⁶⁷⁸⁹", "+-0123456789") - exp_val_non_sup = match.group(0).translate(sup_trans) - return rf"{exp_val_non_sup}" +def convert_sciform_format( + formatted_str: str, + output_format: output_formats, +) -> str: + r""" + Convert sciform output to new format for different output contexts. + convert_sciform_format() is used to convert a sciform output string + into different formats for presentation in different contexts. + Currently, LaTeX, HTML, and ASCII outputs are supported. -def sciform_to_latex(formatted_str: str) -> str: - r""" - Convert a sciform output string into a latex string. + LaTeX + ===== - conversion proceeds by + For LaTeX conversion the resulting string is a valid LaTeX command + bracketed in "$" symbols to indicate it is in LaTeX math + environment. The following transformations are applied. - 1. If an exponent string is present and in ascii format - (e.g. ``"e+03"``) then convert it to superscript notation - (e.g. ``"×10³"``). - 2. Bundle any unicode superscript substrings into latex - superscripts, e.g. ``"⁻²"`` -> ``r"^{-2}"``. - 3. Wrap any strings of alphabetic characters (plus ``"μ"``) in latex - text environment, e.g. ``"nan"`` -> ``r"\text{nan}"`` or - ``"k"`` -> ``r"\text{k}"``. - 4. Make the following character replacments: + * The exponent is displayed using the LaTeX math superscript + construction, e.g. "10^{-3}" + * Any strings of alphabetic characters (plus ``"μ"``) are wrapped in + the LaTeX math-mode text environment, e.g. + ``"nan"`` -> ``r"\text{nan}"`` or ``"k"`` -> ``r"\text{k}"``. + * The following character replacments are made: * ``"%"`` -> ``r"\%"`` * ``"_"`` -> ``r"\_"`` @@ -57,74 +84,89 @@ def sciform_to_latex(formatted_str: str) -> str: * ``"×"`` -> ```r"\times"`` * ``"μ"`` -> ``r"\textmu"`` - >>> from sciform import sciform_to_latex - >>> print(sciform_to_latex("(7.8900 ± 0.0001)×10²")) + >>> from sciform.output_conversion import convert_sciform_format + >>> print(convert_sciform_format("(7.8900 ± 0.0001)×10²", "latex")) (7.8900\:\pm\:0.0001)\times10^{2} - >>> print(sciform_to_latex("16.18033E+03")) + >>> print(convert_sciform_format("16.18033E+03", "latex")) 16.18033\times10^{3} - """ - result_str = re.sub( - r"((?P[eEbB])(?P[+-]\d+))$", - standard_exp_str_to_superscript_exp_str, - formatted_str, - ) - result_str = re.sub( - r"([⁺⁻⁰¹²³⁴⁵⁶⁷⁸⁹]+)", - make_latex_superscript, - result_str, - ) + HTML + ==== - result_str = re.sub( - r"([a-zA-Zμ]+)", - r"\\text{\1}", - result_str, - ) + In HTML mode superscripts are representing using e.g. + "-3". - replacements = ( - ("%", r"\%"), - ("_", r"\_"), - (" ", r"\:"), - ("±", r"\pm"), - ("×", r"\times"), - ("μ", r"\textmu"), - ) - for old_chars, new_chars in replacements: - result_str = result_str.replace(old_chars, new_chars) - - result_str = rf"${result_str}$" - - return result_str - - -def sciform_to_html(formatted_str: str) -> str: - r""" - Convert a sciform output string into a html string. + >>> from sciform.output_conversion import convert_sciform_format + >>> print(convert_sciform_format("(7.8900 ± 0.0001)×10²", "html")) + (7.8900 ± 0.0001)×102 + >>> print(convert_sciform_format("16.18033E+03", "html")) + 16.18033×103 - conversion proceeds by + ASCII + ===== - 1. If an exponent string is present and in ascii format - (e.g. ``"e+03"``) then convert it to superscript notation - (e.g. ``"×10³"``). - 2. Bundle any unicode superscript substrings into latex - superscripts, e.g. ``"⁻²"`` -> ``r"-2"``. + In the ASCII mode exponents are always represented as e.g. "e-03". + Also, "±" is replaced by "+/-" and "μ" is replaced by "u". - >>> from sciform import sciform_to_html - >>> print(sciform_to_latex("(7.8900 ± 0.0001)×10²")) - (7.8900 ± 0.0001)×102 - >>> print(sciform_to_latex("16.18033E+03")) - 16.18033×103 + >>> from sciform.output_conversion import convert_sciform_format + >>> print(convert_sciform_format("(7.8900 ± 0.0001)×10²", "ascii")) + (7.8900 +/- 0.0001)e+02 + >>> print(convert_sciform_format("16.18033E+03", "ascii")) + 16.18033E+03 """ - result_str = re.sub( - r"((?P[eEbB])(?P[+-]\d+))$", - standard_exp_str_to_superscript_exp_str, - formatted_str, + if match := re.match(ascii_exp_pattern, formatted_str): + mantissa = match.group("mantissa") + ascii_base = match.group("ascii_base") + base = ascii_base_dict[ascii_base] + exp = int(match.group("exp")) + exp_str = _make_exp_str( + base, + exp, + output_format, + capitalize=ascii_base.isupper(), + ) + main_str = mantissa + suffix_str = exp_str + elif match := re.match(unicode_exp_pattern, formatted_str): + mantissa = match.group("mantissa") + base = int(match.group("base")) + super_exp = match.group("super_exp") + exp = int(super_exp.translate(superscript_translation)) + exp_str = _make_exp_str(base, exp, output_format) + main_str = mantissa + suffix_str = exp_str + else: + main_str = formatted_str + suffix_str = "" + + if output_format == "latex": + main_str = re.sub( + r"([a-zA-Zμ]+)", + r"\\text{\1}", + main_str, + ) + + replacements = [ + ("%", r"\%"), + ("_", r"\_"), + (" ", r"\:"), + ("±", r"\pm"), + ("×", r"\times"), + ("μ", r"\textmu"), + ] + main_str = _string_replacements(main_str, replacements) + return f"${main_str}{suffix_str}$" + + if output_format == "html": + return f"{main_str}{suffix_str}" + if output_format == "ascii": + replacements = [ + ("±", "+/-"), + ("μ", "u"), + ] + main_str = _string_replacements(main_str, replacements) + return f"{main_str}{suffix_str}" + msg = ( + f"output_format must be in {get_args(output_formats)}, not {output_format}" ) - - result_str = re.sub( - r"([⁺⁻⁰¹²³⁴⁵⁶⁷⁸⁹]+)", - make_html_superscript, - result_str, - ) - - return result_str + raise ValueError(msg) diff --git a/tests/test_ascii_conversion.py b/tests/test_ascii_conversion.py new file mode 100644 index 00000000..22357fa8 --- /dev/null +++ b/tests/test_ascii_conversion.py @@ -0,0 +1,189 @@ +from __future__ import annotations + +import unittest + +from sciform import Formatter +from sciform.output_conversion import convert_sciform_format + +ValFormatterCases = list[tuple[float, list[tuple[Formatter, str]]]] +ValUncFormatterCases = list[tuple[tuple[float, float], list[tuple[Formatter, str]]]] + + +class TestASCIIConversion(unittest.TestCase): + def run_direct_conversions(self, cases_list: list[tuple[str, str]]): + for input_str, expected_str in cases_list: + converted_str = convert_sciform_format(input_str, "ascii") + with self.subTest( + input_str=input_str, + expected_str=expected_str, + actual_str=converted_str, + ): + self.assertEqual(converted_str, expected_str) + + def run_val_formatter_conversions(self, cases_list: ValFormatterCases): + for val, format_list in cases_list: + for formatter, expected_output in format_list: + sciform_output = formatter(val) + html_output = sciform_output.as_ascii() + with self.subTest( + val=val, + expected_output=expected_output, + actual_output=html_output, + ): + self.assertEqual(html_output, expected_output) + + def run_val_unc_formatter_conversions(self, cases_list: ValUncFormatterCases): + for (val, unc), format_list in cases_list: + for formatter, expected_output in format_list: + sciform_output = formatter(val, unc) + html_output = sciform_output.as_ascii() + with self.subTest( + val=val, + expected_output=expected_output, + actual_output=html_output, + ): + self.assertEqual(html_output, expected_output) + + def test_direct_cases(self): + cases_list = [ + ("6.26070e-04", "6.26070e-04"), + ( + "(0.000000(1.234560))e+02", + "(0.000000(1.234560))e+02", + ), + ("000_000_004_567_899.765_432_1", "000_000_004_567_899.765_432_1"), + ("(nan)%", "(nan)%"), + ("123000 ppm", "123000 ppm"), + ("0b+00", "0b+00"), + ("16.18033E+03", "16.18033E+03"), + (" 1.20e+01", " 1.20e+01"), + ("(-INF)E+00", "(-INF)E+00"), + ( + "(0.123456(789))e+03", + "(0.123456(789))e+03", + ), + (" 123.46 ± 0.79", " 123.46 +/- 0.79"), + ("(7.8900 ± 0.0001)×10²", "(7.8900 +/- 0.0001)e+02"), + ("(0.123456 ± 0.000789) k", "(0.123456 +/- 0.000789) k"), + ] + + self.run_direct_conversions(cases_list) + + def test_val_formatter_cases(self): + cases_list = [ + ( + 789, + [ + ( + Formatter(exp_mode="scientific"), + "7.89e+02", + ), + ( + Formatter( + exp_mode="scientific", + superscript=True, + ), + "7.89e+02", + ), + ], + ), + ( + 12345, + [ + ( + Formatter( + exp_mode="scientific", + exp_val=-1, + upper_separator="_", + ), + "123_450e-01", + ), + ( + Formatter( + exp_mode="scientific", + exp_val=3, + exp_format="prefix", + ), + "12.345 k", + ), + ], + ), + ( + 1024, + [ + ( + Formatter(exp_mode="binary", exp_val=8), + "4b+08", + ), + ], + ), + ( + float("nan"), + [ + (Formatter(exp_mode="percent"), "nan"), + ( + Formatter(exp_mode="percent", nan_inf_exp=True), + "(nan)%", + ), + ], + ), + ] + + self.run_val_formatter_conversions(cases_list) + + def test_val_unc_formatter_cases(self): + cases_list = [ + ( + (12345, 0.2), + [ + ( + Formatter( + exp_mode="scientific", + exp_val=-1, + upper_separator="_", + ), + "(123_450 +/- 2)e-01", + ), + ( + Formatter( + exp_mode="scientific", + exp_format="prefix", + exp_val=3, + ), + "(12.3450 +/- 0.0002) k", + ), + ], + ), + ( + (0.123_456_78, 0.000_002_55), + [ + ( + Formatter(lower_separator="_", exp_mode="percent"), + "(12.345_678 +/- 0.000_255)%", + ), + ( + Formatter( + lower_separator="_", + exp_mode="percent", + paren_uncertainty=True, + ), + "(12.345_678(255))%", + ), + ], + ), + ( + (314.159e-6, 2.71828e-6), + [ + ( + Formatter( + exp_mode="engineering", + exp_format="prefix", + ndigits=4, + ), + "(314.159 +/- 2.718) u", + ), + ], + ), + ] + + self.run_val_unc_formatter_conversions(cases_list) diff --git a/tests/test_html_conversion.py b/tests/test_html_conversion.py index 1186c795..54228b5f 100644 --- a/tests/test_html_conversion.py +++ b/tests/test_html_conversion.py @@ -2,7 +2,8 @@ import unittest -from sciform import Formatter, sciform_to_html +from sciform import Formatter +from sciform.output_conversion import convert_sciform_format ValFormatterCases = list[tuple[float, list[tuple[Formatter, str]]]] ValUncFormatterCases = list[tuple[tuple[float, float], list[tuple[Formatter, str]]]] @@ -11,7 +12,7 @@ class TestHTMLConversion(unittest.TestCase): def run_direct_conversions(self, cases_list: list[tuple[str, str]]): for input_str, expected_str in cases_list: - converted_str = sciform_to_html(input_str) + converted_str = convert_sciform_format(input_str, "html") with self.subTest( input_str=input_str, expected_str=expected_str, diff --git a/tests/test_latex_conversion.py b/tests/test_latex_conversion.py index 4dcda954..f50d6a08 100644 --- a/tests/test_latex_conversion.py +++ b/tests/test_latex_conversion.py @@ -2,7 +2,8 @@ import unittest -from sciform import Formatter, sciform_to_latex +from sciform import Formatter +from sciform.output_conversion import convert_sciform_format ValFormatterCases = list[tuple[float, list[tuple[Formatter, str]]]] ValUncFormatterCases = list[tuple[tuple[float, float], list[tuple[Formatter, str]]]] @@ -11,7 +12,7 @@ class TestLatexConversion(unittest.TestCase): def run_direct_conversions(self, cases_list: list[tuple[str, str]]): for input_str, expected_str in cases_list: - converted_str = sciform_to_latex(input_str) + converted_str = convert_sciform_format(input_str, "latex") with self.subTest( input_str=input_str, expected_str=expected_str, From 046e60608e771a69a82ef45524e784bc09960084 Mon Sep 17 00:00:00 2001 From: Justin Date: Wed, 24 Jan 2024 23:50:26 -0700 Subject: [PATCH 16/38] ruff --- src/sciform/output_conversion.py | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/src/sciform/output_conversion.py b/src/sciform/output_conversion.py index 0b77efa2..da2ed2c7 100644 --- a/src/sciform/output_conversion.py +++ b/src/sciform/output_conversion.py @@ -5,12 +5,12 @@ from typing import Literal, get_args ascii_exp_pattern = re.compile( - r"^(?P.*)(?P[eEbB])(?P[+-]\d+)$" + r"^(?P.*)(?P[eEbB])(?P[+-]\d+)$", ) ascii_base_dict = {"e": 10, "E": 10, "b": 2, "B": 2} unicode_exp_pattern = re.compile( - r"^(?P.*)×(?P10|2)(?P[⁺⁻⁰¹²³⁴⁵⁶⁷⁸⁹])$" + r"^(?P.*)×(?P10|2)(?P[⁺⁻⁰¹²³⁴⁵⁶⁷⁸⁹])$", ) superscript_translation = str.maketrans("⁺⁻⁰¹²³⁴⁵⁶⁷⁸⁹", "+-0123456789") @@ -18,11 +18,11 @@ def _make_exp_str( - base: int, - exp: int, - output_format: output_formats, - *, - capitalize: bool = False + base: int, + exp: int, + output_format: output_formats, + *, + capitalize: bool = False, ) -> str: if output_format == "latex": return rf"\times{base}^{{{exp}}}" @@ -39,9 +39,7 @@ def _make_exp_str( if capitalize: exp_str = exp_str.upper() return exp_str - msg = ( - f"output_format must be in {get_args(output_formats)}, not {output_format}" - ) + msg = f"output_format must be in {get_args(output_formats)}, not {output_format}" raise ValueError(msg) @@ -53,8 +51,8 @@ def _string_replacements(input_str: str, replacements: list[tuple[str, str]]) -> def convert_sciform_format( - formatted_str: str, - output_format: output_formats, + formatted_str: str, + output_format: output_formats, ) -> str: r""" Convert sciform output to new format for different output contexts. @@ -166,7 +164,5 @@ def convert_sciform_format( ] main_str = _string_replacements(main_str, replacements) return f"{main_str}{suffix_str}" - msg = ( - f"output_format must be in {get_args(output_formats)}, not {output_format}" - ) + msg = f"output_format must be in {get_args(output_formats)}, not {output_format}" raise ValueError(msg) From bd37a8cd2c2c8566be7aa759ed7666d40c1aa220 Mon Sep 17 00:00:00 2001 From: Justin Date: Thu, 25 Jan 2024 00:32:50 -0700 Subject: [PATCH 17/38] work on documentation --- docs/source/api.rst | 6 ++++- docs/source/usage.rst | 58 ++++++++++++++++++++++++++++------------ src/sciform/formatter.py | 5 +--- 3 files changed, 47 insertions(+), 22 deletions(-) diff --git a/docs/source/api.rst b/docs/source/api.rst index 6054ccf0..846e0ebf 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -36,4 +36,8 @@ Global Configuration Output Conversion ================= -.. autofunction:: sciform_to_latex +.. module:: sciform.formatter + :noindex: + +.. autoclass:: FormattedNumber + :members: diff --git a/docs/source/usage.rst b/docs/source/usage.rst index c29ae766..e26a28ea 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -216,33 +216,50 @@ If the user wishes to configure these options, but also use the :ref:`FSML `, then they must do so by modifying the global default settings. -.. _latex_conversion: - -Latex Conversion -================ +.. _output_conversion: + +Output Conversion +================= + +Typically the output of the :class:`Formatter` is used as a regular +python string. +However, the :class:`Formatter` actually returns a +:class:`FormattedNumber` instance. +The :class:`FormattedNumber` class subclasses ``str`` and in many cases +is used like a normal python string. +However, the :class:`FormattedNumber` class exposes methods to convert +the standard string representation into LaTex, HTML, or ASCII +representations. +The LaTeX and HTML representations may be useful when :mod:`sciform` +outputs are being used in contexts outside of e.g. text terminals such +as `Matplotlib `_ plots, +`Jupyter `_ notebooks, or +`Quarto `_ documents which support richer display +functionality than unicode text. +The ASCII representation may be useful if :mod:`sciform` outputs are +being used in contexts in which only ASCII, and not unicode, text is +supported or preferred. + +These conversions can be accessed via the :func:`as_latex()`, +:func:`as_html()`, :func:`as_ascii()` methods on the +:class:`FormattedNumber` instance. -The :func:`sciform_to_latex` function can be used to convert ``sciform`` -output strings into latex commands. - ->>> from sciform import sciform_to_latex >>> sform = Formatter( ... exp_mode="scientific", ... exp_val=-1, ... upper_separator="_", ... ) >>> formatted_str = sform(12345) ->>> latex_str = sciform_to_latex(formatted_str) ->>> print(f"{formatted_str} -> {latex_str}") -123_450e-01 -> 123\_450\times10^{-1} +>>> print(f"{formatted_str} -> {formatted_str.as_latex()}") +123_450e-01 -> $123\_450\times10^{-1}$ >>> sform = Formatter( ... exp_mode="percent", ... lower_separator="_", ... ) >>> formatted_str = sform(0.12345678, 0.00000255) ->>> latex_str= sciform_to_latex(formatted_str) ->>> print(f"{formatted_str} -> {latex_str}") -(12.345_678 ± 0.000_255)% -> (12.345\_678\:\pm\:0.000\_255)\% +>>> print(f"{formatted_str} -> {formatted_str.as_latex()}") +(12.345_678 ± 0.000_255)% -> $(12.345\_678\:\pm\:0.000\_255)\%$ >>> sform = Formatter( ... exp_mode="engineering", @@ -250,9 +267,16 @@ output strings into latex commands. ... ndigits=4 ... ) >>> formatted_str = sform(314.159e-6, 2.71828e-6) ->>> latex_str = sciform_to_latex(formatted_str) ->>> print(f"{formatted_str} -> {latex_str}") -(314.159 ± 2.718) μ -> (314.159\:\pm\:2.718)\:\text{\textmu} +>>> print(f"{formatted_str} -> {formatted_str.as_latex()}") +(314.159 ± 2.718) μ -> $(314.159\:\pm\:2.718)\:\text{\textmu}$ + +.. _latex_conversion: + +Latex Conversion +================ + +The :func:`sciform_to_latex` function can be used to convert ``sciform`` +output strings into latex commands. The latex format makes the following changes: diff --git a/src/sciform/formatter.py b/src/sciform/formatter.py index 0e3f91e6..9f9199d3 100644 --- a/src/sciform/formatter.py +++ b/src/sciform/formatter.py @@ -265,7 +265,7 @@ def as_str(self: FormattedNumber) -> str: def as_ascii(self: FormattedNumber) -> str: """Return the ascii representation of the formatted number.""" - return self._repr_ascii_() + return convert_sciform_format(self, "ascii") def as_html(self: FormattedNumber) -> str: """Return the html representation of the formatted number.""" @@ -278,9 +278,6 @@ def as_latex(self: FormattedNumber, *, strip_env_symbs: bool = False) -> str: latex_repr = latex_repr.strip("$") return latex_repr - def _repr_ascii_(self: FormattedNumber) -> str: - return convert_sciform_format(self, "ascii") - def _repr_html_(self: FormattedNumber) -> str: return convert_sciform_format(self, "html") From dd59969ef0e2062a8d3f8118ab3d7cd7705d1935 Mon Sep 17 00:00:00 2001 From: Justin Date: Thu, 25 Jan 2024 22:02:07 -0700 Subject: [PATCH 18/38] fix unicode exp pattern regex --- src/sciform/output_conversion.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sciform/output_conversion.py b/src/sciform/output_conversion.py index da2ed2c7..f85ce66f 100644 --- a/src/sciform/output_conversion.py +++ b/src/sciform/output_conversion.py @@ -10,7 +10,7 @@ ascii_base_dict = {"e": 10, "E": 10, "b": 2, "B": 2} unicode_exp_pattern = re.compile( - r"^(?P.*)×(?P10|2)(?P[⁺⁻⁰¹²³⁴⁵⁶⁷⁸⁹])$", + r"^(?P.*)×(?P10|2)(?P[⁺⁻]?[⁰¹²³⁴⁵⁶⁷⁸⁹]+)$", ) superscript_translation = str.maketrans("⁺⁻⁰¹²³⁴⁵⁶⁷⁸⁹", "+-0123456789") From aa06c9b3c3dc7dea27ba7608c914639f36ef1e4a Mon Sep 17 00:00:00 2001 From: Justin Date: Thu, 25 Jan 2024 22:02:47 -0700 Subject: [PATCH 19/38] update documentation --- docs/source/api.rst | 5 +--- docs/source/usage.rst | 59 +++++++++++++++++++------------------------ 2 files changed, 27 insertions(+), 37 deletions(-) diff --git a/docs/source/api.rst b/docs/source/api.rst index 846e0ebf..98cdc2de 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -36,8 +36,5 @@ Global Configuration Output Conversion ================= -.. module:: sciform.formatter - :noindex: - -.. autoclass:: FormattedNumber +.. autoclass:: sciform.formatter.FormattedNumber :members: diff --git a/docs/source/usage.rst b/docs/source/usage.rst index e26a28ea..2a681555 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -224,12 +224,13 @@ Output Conversion Typically the output of the :class:`Formatter` is used as a regular python string. However, the :class:`Formatter` actually returns a -:class:`FormattedNumber` instance. -The :class:`FormattedNumber` class subclasses ``str`` and in many cases -is used like a normal python string. -However, the :class:`FormattedNumber` class exposes methods to convert -the standard string representation into LaTex, HTML, or ASCII -representations. +:class:`FormattedNumber ` instance. +The :class:`FormattedNumber ` class +subclasses ``str`` and in many cases is used like a normal python +string. +However, the :class:`FormattedNumber ` class +exposes methods to convert the standard string representation into +LaTex, HTML, or ASCII representations. The LaTeX and HTML representations may be useful when :mod:`sciform` outputs are being used in contexts outside of e.g. text terminals such as `Matplotlib `_ plots, @@ -240,18 +241,25 @@ The ASCII representation may be useful if :mod:`sciform` outputs are being used in contexts in which only ASCII, and not unicode, text is supported or preferred. -These conversions can be accessed via the :func:`as_latex()`, -:func:`as_html()`, :func:`as_ascii()` methods on the -:class:`FormattedNumber` instance. +These conversions can be accessed via the +:meth:`as_latex() `, +:meth:`as_html() `, and +:meth:`as_ascii() ` methods on the +:class:`FormattedNumber ` class. >>> sform = Formatter( ... exp_mode="scientific", ... exp_val=-1, ... upper_separator="_", +... superscript=True, ... ) >>> formatted_str = sform(12345) >>> print(f"{formatted_str} -> {formatted_str.as_latex()}") -123_450e-01 -> $123\_450\times10^{-1}$ +123_450×10⁻¹ -> $123\_450\times10^{-1}$ +>>> print(f"{formatted_str} -> {formatted_str.as_html()}") +123_450×10⁻¹ -> 123_450×10-1 +>>> print(f"{formatted_str} -> {formatted_str.as_ascii()}") +123_450×10⁻¹ -> 123_450e-01 >>> sform = Formatter( ... exp_mode="percent", @@ -260,6 +268,10 @@ These conversions can be accessed via the :func:`as_latex()`, >>> formatted_str = sform(0.12345678, 0.00000255) >>> print(f"{formatted_str} -> {formatted_str.as_latex()}") (12.345_678 ± 0.000_255)% -> $(12.345\_678\:\pm\:0.000\_255)\%$ +>>> print(f"{formatted_str} -> {formatted_str.as_html()}") +(12.345_678 ± 0.000_255)% -> (12.345_678 ± 0.000_255)% +>>> print(f"{formatted_str} -> {formatted_str.as_ascii()}") +(12.345_678 ± 0.000_255)% -> (12.345_678 +/- 0.000_255)% >>> sform = Formatter( ... exp_mode="engineering", @@ -269,29 +281,10 @@ These conversions can be accessed via the :func:`as_latex()`, >>> formatted_str = sform(314.159e-6, 2.71828e-6) >>> print(f"{formatted_str} -> {formatted_str.as_latex()}") (314.159 ± 2.718) μ -> $(314.159\:\pm\:2.718)\:\text{\textmu}$ - -.. _latex_conversion: - -Latex Conversion -================ - -The :func:`sciform_to_latex` function can be used to convert ``sciform`` -output strings into latex commands. - -The latex format makes the following changes: - -* Convert all ASCII (``"e+02"``) and superscript (``"×10²"``) exponents - into latex superscript strings like ``"\times10^{2}"``. -* Wrap all text like ``"nan"``, ``"INF"``, ``"M"``, ``"μ"``, ``"Ki"``, - or ``"ppb"`` in the latex math mode text environment ``"\text{}"``. -* Make the following symbol replacements - - * ``"%"`` -> ``r"\%"`` - * ``"_"`` -> ``r"\_"`` - * ``" "`` -> ``r"\:"`` - * ``"±"`` -> ``r"\pm"`` - * ``"×"`` -> ```r"\times"`` - * ``"μ"`` -> ``r"\textmu"`` +>>> print(f"{formatted_str} -> {formatted_str.as_html()}") +(314.159 ± 2.718) μ -> (314.159 ± 2.718) μ +>>> print(f"{formatted_str} -> {formatted_str.as_ascii()}") +(314.159 ± 2.718) μ -> (314.159 +/- 2.718) u .. _dec_and_float: From 66298ddd410d5f2c59746d1e82e07cd6de77be04 Mon Sep 17 00:00:00 2001 From: Justin Date: Thu, 25 Jan 2024 22:14:36 -0700 Subject: [PATCH 20/38] _repr_html_ and _repr_latex_ to use as_html and as_latex (instead of the other way around) --- src/sciform/formatter.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/sciform/formatter.py b/src/sciform/formatter.py index 9f9199d3..d4d56773 100644 --- a/src/sciform/formatter.py +++ b/src/sciform/formatter.py @@ -269,17 +269,17 @@ def as_ascii(self: FormattedNumber) -> str: def as_html(self: FormattedNumber) -> str: """Return the html representation of the formatted number.""" - return self._repr_html_() + return convert_sciform_format(self, "html") def as_latex(self: FormattedNumber, *, strip_env_symbs: bool = False) -> str: """Return the latex representation of the formatted number.""" - latex_repr = self._repr_latex_() + latex_repr = convert_sciform_format(self, "latex") if strip_env_symbs: latex_repr = latex_repr.strip("$") return latex_repr def _repr_html_(self: FormattedNumber) -> str: - return convert_sciform_format(self, "html") + return self.as_html() def _repr_latex_(self: FormattedNumber) -> str: - return convert_sciform_format(self, "latex") + return self.as_latex() From a9e081c4c441fe78c6f0353e822e0564b12fdedb Mon Sep 17 00:00:00 2001 From: Justin Date: Fri, 26 Jan 2024 08:45:34 -0700 Subject: [PATCH 21/38] consolidate output conversion tests --- tests/test_latex_conversion.py | 323 +++++++++++++++++++++++++++++---- 1 file changed, 284 insertions(+), 39 deletions(-) diff --git a/tests/test_latex_conversion.py b/tests/test_latex_conversion.py index f50d6a08..67cc9627 100644 --- a/tests/test_latex_conversion.py +++ b/tests/test_latex_conversion.py @@ -5,66 +5,248 @@ from sciform import Formatter from sciform.output_conversion import convert_sciform_format -ValFormatterCases = list[tuple[float, list[tuple[Formatter, str]]]] -ValUncFormatterCases = list[tuple[tuple[float, float], list[tuple[Formatter, str]]]] +ValFormatterCases = list[ + tuple[float, list[tuple[Formatter, tuple[str, str, str, str]]]] +] +ValUncFormatterCases = list[ + tuple[tuple[float, float], list[tuple[Formatter, tuple[str, str, str, str]]]] +] class TestLatexConversion(unittest.TestCase): - def run_direct_conversions(self, cases_list: list[tuple[str, str]]): - for input_str, expected_str in cases_list: - converted_str = convert_sciform_format(input_str, "latex") + def run_direct_conversions( + self, + cases_list: list[tuple[str, tuple[str, str, str]]], + ): + for input_str, (expected_ascii, expected_html, expected_latex) in cases_list: + ascii_str = convert_sciform_format(input_str, "ascii") with self.subTest( input_str=input_str, - expected_str=expected_str, - actual_str=converted_str, + expected_str=expected_ascii, + actual_str=ascii_str, ): - self.assertEqual(converted_str, expected_str) + self.assertEqual(ascii_str, expected_ascii) + + html_str = convert_sciform_format(input_str, "html") + with self.subTest( + input_str=input_str, + expected_str=expected_html, + actual_str=html_str, + ): + self.assertEqual(html_str, expected_html) + + latex_str = convert_sciform_format(input_str, "latex") + with self.subTest( + input_str=input_str, + expected_str=expected_latex, + actual_str=latex_str, + ): + self.assertEqual(latex_str, expected_latex) def run_val_formatter_conversions(self, cases_list: ValFormatterCases): for val, format_list in cases_list: - for formatter, expected_output in format_list: + for formatter, expected_outputs in format_list: + ( + expected_str, + expected_ascii, + expected_html, + expected_latex, + ) = expected_outputs sciform_output = formatter(val) + + str_output = sciform_output.as_str() + with self.subTest( + val=val, + expected_output=expected_str, + actual_output=str_output, + ): + self.assertEqual(str_output, expected_str) + + ascii_output = sciform_output.as_ascii() + with self.subTest( + val=val, + expected_output=expected_ascii, + actual_output=ascii_output, + ): + self.assertEqual(ascii_output, expected_ascii) + + html_output = sciform_output.as_html() + with self.subTest( + val=val, + expected_output=expected_html, + actual_output=html_output, + ): + self.assertEqual(html_output, expected_html) + latex_output = sciform_output.as_latex(strip_env_symbs=True) with self.subTest( val=val, - expected_output=expected_output, + expected_output=expected_latex, actual_output=latex_output, ): - self.assertEqual(latex_output, expected_output) + self.assertEqual(latex_output, expected_latex) def run_val_unc_formatter_conversions(self, cases_list: ValUncFormatterCases): for (val, unc), format_list in cases_list: - for formatter, expected_output in format_list: + for formatter, expected_outputs in format_list: + ( + expected_str, + expected_ascii, + expected_html, + expected_latex, + ) = expected_outputs sciform_output = formatter(val, unc) + + str_output = sciform_output.as_str() + with self.subTest( + val=val, + unc=unc, + expected_output=expected_str, + actual_output=str_output, + ): + self.assertEqual(str_output, expected_str) + + ascii_output = sciform_output.as_ascii() + with self.subTest( + val=val, + unc=unc, + expected_output=expected_ascii, + actual_output=ascii_output, + ): + self.assertEqual(ascii_output, expected_ascii) + + html_output = sciform_output.as_html() + with self.subTest( + val=val, + unc=unc, + expected_output=expected_html, + actual_output=html_output, + ): + self.assertEqual(html_output, expected_html) + latex_output = sciform_output.as_latex(strip_env_symbs=True) with self.subTest( val=val, - expected_output=expected_output, + unc=unc, + expected_output=expected_latex, actual_output=latex_output, ): - self.assertEqual(latex_output, expected_output) + self.assertEqual(latex_output, expected_latex) def test_direct_cases(self): cases_list = [ - ("6.26070e-04", r"$6.26070\times10^{-4}$"), + ( + "6.26070e-04", + ( + "6.26070e-04", + "6.26070×10-4", + r"$6.26070\times10^{-4}$", + ), + ), ( "(0.000000(1.234560))e+02", - r"$(0.000000(1.234560))\times10^{2}$", + ( + "(0.000000(1.234560))e+02", + "(0.000000(1.234560))×102", + r"$(0.000000(1.234560))\times10^{2}$", + ), + ), + ( + "000_000_004_567_899.765_432_1", + ( + "000_000_004_567_899.765_432_1", + "000_000_004_567_899.765_432_1", + r"$000\_000\_004\_567\_899.765\_432\_1$", + ), + ), + ( + "(nan)%", + ( + "(nan)%", + "(nan)%", + r"$(\text{nan})\%$", + ), + ), + ( + "123000 ppm", + ( + "123000 ppm", + "123000 ppm", + r"$123000\:\text{ppm}$", + ), + ), + ( + "0b+00", + ( + "0b+00", + "0×20", + r"$0\times2^{0}$", + ), + ), + ( + "16.18033E+03", + ( + "16.18033E+03", + "16.18033×103", + r"$16.18033\times10^{3}$", + ), + ), + ( + " 1.20e+01", + ( + " 1.20e+01", + " 1.20×101", + r"$\:\:\:\:1.20\times10^{1}$", + ), + ), + ( + "(-INF)E+00", + ( + "(-INF)E+00", + "(-INF)×100", + r"$(-\text{INF})\times10^{0}$", + ), ), - ("000_000_004_567_899.765_432_1", r"$000\_000\_004\_567\_899.765\_432\_1$"), - ("(nan)%", r"$(\text{nan})\%$"), - ("123000 ppm", r"$123000\:\text{ppm}$"), - ("0b+00", r"$0\times2^{0}$"), - ("16.18033E+03", r"$16.18033\times10^{3}$"), - (" 1.20e+01", r"$\:\:\:\:1.20\times10^{1}$"), - ("(-INF)E+00", r"$(-\text{INF})\times10^{0}$"), ( "(0.123456(789))e+03", - r"$(0.123456(789))\times10^{3}$", + ( + "(0.123456(789))e+03", + "(0.123456(789))×103", + r"$(0.123456(789))\times10^{3}$", + ), + ), + ( + " 123.46 ± 0.79", + ( + " 123.46 +/- 0.79", + " 123.46 ± 0.79", + r"$\:\:123.46\:\pm\:\:\:\:\:0.79$", + ), + ), + ( + "(7.8900 ± 0.0001)×10²", + ( + "(7.8900 +/- 0.0001)e+02", + "(7.8900 ± 0.0001)×102", + r"$(7.8900\:\pm\:0.0001)\times10^{2}$", + ), + ), + ( + "(7.8900 ± 0.0001)×10⁻²", + ( + "(7.8900 +/- 0.0001)e-02", + "(7.8900 ± 0.0001)×10-2", + r"$(7.8900\:\pm\:0.0001)\times10^{-2}$", + ), + ), + ( + "(0.123456 ± 0.000789) k", + ( + "(0.123456 +/- 0.000789) k", + "(0.123456 ± 0.000789) k", + r"$(0.123456\:\pm\:0.000789)\:\text{k}$", + ), ), - (" 123.46 ± 0.79", r"$\:\:123.46\:\pm\:\:\:\:\:0.79$"), - ("(7.8900 ± 0.0001)×10²", r"$(7.8900\:\pm\:0.0001)\times10^{2}$"), - ("(0.123456 ± 0.000789) k", r"$(0.123456\:\pm\:0.000789)\:\text{k}$"), ] self.run_direct_conversions(cases_list) @@ -76,14 +258,24 @@ def test_val_formatter_cases(self): [ ( Formatter(exp_mode="scientific"), - r"7.89\times10^{2}", + ( + "7.89e+02", + "7.89e+02", + "7.89×102", + r"7.89\times10^{2}", + ), ), ( Formatter( exp_mode="scientific", superscript=True, ), - r"7.89\times10^{2}", + ( + "7.89×10²", + "7.89e+02", + "7.89×102", + r"7.89\times10^{2}", + ), ), ], ), @@ -96,7 +288,12 @@ def test_val_formatter_cases(self): exp_val=-1, upper_separator="_", ), - r"123\_450\times10^{-1}", + ( + "123_450e-01", + "123_450e-01", + "123_450×10-1", + r"123\_450\times10^{-1}", + ), ), ( Formatter( @@ -104,7 +301,12 @@ def test_val_formatter_cases(self): exp_val=3, exp_format="prefix", ), - r"12.345\:\text{k}", + ( + "12.345 k", + "12.345 k", + "12.345 k", + r"12.345\:\text{k}", + ), ), ], ), @@ -113,17 +315,35 @@ def test_val_formatter_cases(self): [ ( Formatter(exp_mode="binary", exp_val=8), - r"4\times2^{8}", + ( + "4b+08", + "4b+08", + "4×28", + r"4\times2^{8}", + ), ), ], ), ( float("nan"), [ - (Formatter(exp_mode="percent"), r"\text{nan}"), + ( + Formatter(exp_mode="percent"), + ( + "nan", + "nan", + "nan", + r"\text{nan}", + ), + ), ( Formatter(exp_mode="percent", nan_inf_exp=True), - r"(\text{nan})\%", + ( + "(nan)%", + "(nan)%", + "(nan)%", + r"(\text{nan})\%", + ), ), ], ), @@ -142,7 +362,12 @@ def test_val_unc_formatter_cases(self): exp_val=-1, upper_separator="_", ), - r"(123\_450\:\pm\:2)\times10^{-1}", + ( + "(123_450 ± 2)e-01", + "(123_450 +/- 2)e-01", + "(123_450 ± 2)×10-1", + r"(123\_450\:\pm\:2)\times10^{-1}", + ), ), ( Formatter( @@ -150,7 +375,12 @@ def test_val_unc_formatter_cases(self): exp_format="prefix", exp_val=3, ), - r"(12.3450\:\pm\:0.0002)\:\text{k}", + ( + "(12.3450 ± 0.0002) k", + "(12.3450 +/- 0.0002) k", + "(12.3450 ± 0.0002) k", + r"(12.3450\:\pm\:0.0002)\:\text{k}", + ), ), ], ), @@ -159,7 +389,12 @@ def test_val_unc_formatter_cases(self): [ ( Formatter(lower_separator="_", exp_mode="percent"), - r"(12.345\_678\:\pm\:0.000\_255)\%", + ( + "(12.345_678 ± 0.000_255)%", + "(12.345_678 +/- 0.000_255)%", + "(12.345_678 ± 0.000_255)%", + r"(12.345\_678\:\pm\:0.000\_255)\%", + ), ), ( Formatter( @@ -167,7 +402,12 @@ def test_val_unc_formatter_cases(self): exp_mode="percent", paren_uncertainty=True, ), - r"(12.345\_678(255))\%", + ( + "(12.345_678(255))%", + "(12.345_678(255))%", + "(12.345_678(255))%", + r"(12.345\_678(255))\%", + ), ), ], ), @@ -180,7 +420,12 @@ def test_val_unc_formatter_cases(self): exp_format="prefix", ndigits=4, ), - r"(314.159\:\pm\:2.718)\:\text{\textmu}", + ( + "(314.159 ± 2.718) μ", + "(314.159 +/- 2.718) u", + "(314.159 ± 2.718) μ", + r"(314.159\:\pm\:2.718)\:\text{\textmu}", + ), ), ], ), From 67c66c3bc1806acb353dbb84484b238f7ee49c21 Mon Sep 17 00:00:00 2001 From: Justin Date: Fri, 26 Jan 2024 08:46:21 -0700 Subject: [PATCH 22/38] rename output conversion --- tests/test_ascii_conversion.py | 189 ------------------ tests/test_html_conversion.py | 189 ------------------ ...onversion.py => test_output_conversion.py} | 0 3 files changed, 378 deletions(-) delete mode 100644 tests/test_ascii_conversion.py delete mode 100644 tests/test_html_conversion.py rename tests/{test_latex_conversion.py => test_output_conversion.py} (100%) diff --git a/tests/test_ascii_conversion.py b/tests/test_ascii_conversion.py deleted file mode 100644 index 22357fa8..00000000 --- a/tests/test_ascii_conversion.py +++ /dev/null @@ -1,189 +0,0 @@ -from __future__ import annotations - -import unittest - -from sciform import Formatter -from sciform.output_conversion import convert_sciform_format - -ValFormatterCases = list[tuple[float, list[tuple[Formatter, str]]]] -ValUncFormatterCases = list[tuple[tuple[float, float], list[tuple[Formatter, str]]]] - - -class TestASCIIConversion(unittest.TestCase): - def run_direct_conversions(self, cases_list: list[tuple[str, str]]): - for input_str, expected_str in cases_list: - converted_str = convert_sciform_format(input_str, "ascii") - with self.subTest( - input_str=input_str, - expected_str=expected_str, - actual_str=converted_str, - ): - self.assertEqual(converted_str, expected_str) - - def run_val_formatter_conversions(self, cases_list: ValFormatterCases): - for val, format_list in cases_list: - for formatter, expected_output in format_list: - sciform_output = formatter(val) - html_output = sciform_output.as_ascii() - with self.subTest( - val=val, - expected_output=expected_output, - actual_output=html_output, - ): - self.assertEqual(html_output, expected_output) - - def run_val_unc_formatter_conversions(self, cases_list: ValUncFormatterCases): - for (val, unc), format_list in cases_list: - for formatter, expected_output in format_list: - sciform_output = formatter(val, unc) - html_output = sciform_output.as_ascii() - with self.subTest( - val=val, - expected_output=expected_output, - actual_output=html_output, - ): - self.assertEqual(html_output, expected_output) - - def test_direct_cases(self): - cases_list = [ - ("6.26070e-04", "6.26070e-04"), - ( - "(0.000000(1.234560))e+02", - "(0.000000(1.234560))e+02", - ), - ("000_000_004_567_899.765_432_1", "000_000_004_567_899.765_432_1"), - ("(nan)%", "(nan)%"), - ("123000 ppm", "123000 ppm"), - ("0b+00", "0b+00"), - ("16.18033E+03", "16.18033E+03"), - (" 1.20e+01", " 1.20e+01"), - ("(-INF)E+00", "(-INF)E+00"), - ( - "(0.123456(789))e+03", - "(0.123456(789))e+03", - ), - (" 123.46 ± 0.79", " 123.46 +/- 0.79"), - ("(7.8900 ± 0.0001)×10²", "(7.8900 +/- 0.0001)e+02"), - ("(0.123456 ± 0.000789) k", "(0.123456 +/- 0.000789) k"), - ] - - self.run_direct_conversions(cases_list) - - def test_val_formatter_cases(self): - cases_list = [ - ( - 789, - [ - ( - Formatter(exp_mode="scientific"), - "7.89e+02", - ), - ( - Formatter( - exp_mode="scientific", - superscript=True, - ), - "7.89e+02", - ), - ], - ), - ( - 12345, - [ - ( - Formatter( - exp_mode="scientific", - exp_val=-1, - upper_separator="_", - ), - "123_450e-01", - ), - ( - Formatter( - exp_mode="scientific", - exp_val=3, - exp_format="prefix", - ), - "12.345 k", - ), - ], - ), - ( - 1024, - [ - ( - Formatter(exp_mode="binary", exp_val=8), - "4b+08", - ), - ], - ), - ( - float("nan"), - [ - (Formatter(exp_mode="percent"), "nan"), - ( - Formatter(exp_mode="percent", nan_inf_exp=True), - "(nan)%", - ), - ], - ), - ] - - self.run_val_formatter_conversions(cases_list) - - def test_val_unc_formatter_cases(self): - cases_list = [ - ( - (12345, 0.2), - [ - ( - Formatter( - exp_mode="scientific", - exp_val=-1, - upper_separator="_", - ), - "(123_450 +/- 2)e-01", - ), - ( - Formatter( - exp_mode="scientific", - exp_format="prefix", - exp_val=3, - ), - "(12.3450 +/- 0.0002) k", - ), - ], - ), - ( - (0.123_456_78, 0.000_002_55), - [ - ( - Formatter(lower_separator="_", exp_mode="percent"), - "(12.345_678 +/- 0.000_255)%", - ), - ( - Formatter( - lower_separator="_", - exp_mode="percent", - paren_uncertainty=True, - ), - "(12.345_678(255))%", - ), - ], - ), - ( - (314.159e-6, 2.71828e-6), - [ - ( - Formatter( - exp_mode="engineering", - exp_format="prefix", - ndigits=4, - ), - "(314.159 +/- 2.718) u", - ), - ], - ), - ] - - self.run_val_unc_formatter_conversions(cases_list) diff --git a/tests/test_html_conversion.py b/tests/test_html_conversion.py deleted file mode 100644 index 54228b5f..00000000 --- a/tests/test_html_conversion.py +++ /dev/null @@ -1,189 +0,0 @@ -from __future__ import annotations - -import unittest - -from sciform import Formatter -from sciform.output_conversion import convert_sciform_format - -ValFormatterCases = list[tuple[float, list[tuple[Formatter, str]]]] -ValUncFormatterCases = list[tuple[tuple[float, float], list[tuple[Formatter, str]]]] - - -class TestHTMLConversion(unittest.TestCase): - def run_direct_conversions(self, cases_list: list[tuple[str, str]]): - for input_str, expected_str in cases_list: - converted_str = convert_sciform_format(input_str, "html") - with self.subTest( - input_str=input_str, - expected_str=expected_str, - actual_str=converted_str, - ): - self.assertEqual(converted_str, expected_str) - - def run_val_formatter_conversions(self, cases_list: ValFormatterCases): - for val, format_list in cases_list: - for formatter, expected_output in format_list: - sciform_output = formatter(val) - html_output = sciform_output.as_html() - with self.subTest( - val=val, - expected_output=expected_output, - actual_output=html_output, - ): - self.assertEqual(html_output, expected_output) - - def run_val_unc_formatter_conversions(self, cases_list: ValUncFormatterCases): - for (val, unc), format_list in cases_list: - for formatter, expected_output in format_list: - sciform_output = formatter(val, unc) - html_output = sciform_output.as_html() - with self.subTest( - val=val, - expected_output=expected_output, - actual_output=html_output, - ): - self.assertEqual(html_output, expected_output) - - def test_direct_cases(self): - cases_list = [ - ("6.26070e-04", r"6.26070×10-4"), - ( - "(0.000000(1.234560))e+02", - r"(0.000000(1.234560))×102", - ), - ("000_000_004_567_899.765_432_1", r"000_000_004_567_899.765_432_1"), - ("(nan)%", r"(nan)%"), - ("123000 ppm", r"123000 ppm"), - ("0b+00", r"0×20"), - ("16.18033E+03", r"16.18033×103"), - (" 1.20e+01", r" 1.20×101"), - ("(-INF)E+00", r"(-INF)×100"), - ( - "(0.123456(789))e+03", - r"(0.123456(789))×103", - ), - (" 123.46 ± 0.79", r" 123.46 ± 0.79"), - ("(7.8900 ± 0.0001)×10²", r"(7.8900 ± 0.0001)×102"), - ("(0.123456 ± 0.000789) k", r"(0.123456 ± 0.000789) k"), - ] - - self.run_direct_conversions(cases_list) - - def test_val_formatter_cases(self): - cases_list = [ - ( - 789, - [ - ( - Formatter(exp_mode="scientific"), - r"7.89×102", - ), - ( - Formatter( - exp_mode="scientific", - superscript=True, - ), - r"7.89×102", - ), - ], - ), - ( - 12345, - [ - ( - Formatter( - exp_mode="scientific", - exp_val=-1, - upper_separator="_", - ), - r"123_450×10-1", - ), - ( - Formatter( - exp_mode="scientific", - exp_val=3, - exp_format="prefix", - ), - r"12.345 k", - ), - ], - ), - ( - 1024, - [ - ( - Formatter(exp_mode="binary", exp_val=8), - r"4×28", - ), - ], - ), - ( - float("nan"), - [ - (Formatter(exp_mode="percent"), r"nan"), - ( - Formatter(exp_mode="percent", nan_inf_exp=True), - r"(nan)%", - ), - ], - ), - ] - - self.run_val_formatter_conversions(cases_list) - - def test_val_unc_formatter_cases(self): - cases_list = [ - ( - (12345, 0.2), - [ - ( - Formatter( - exp_mode="scientific", - exp_val=-1, - upper_separator="_", - ), - r"(123_450 ± 2)×10-1", - ), - ( - Formatter( - exp_mode="scientific", - exp_format="prefix", - exp_val=3, - ), - r"(12.3450 ± 0.0002) k", - ), - ], - ), - ( - (0.123_456_78, 0.000_002_55), - [ - ( - Formatter(lower_separator="_", exp_mode="percent"), - r"(12.345_678 ± 0.000_255)%", - ), - ( - Formatter( - lower_separator="_", - exp_mode="percent", - paren_uncertainty=True, - ), - r"(12.345_678(255))%", - ), - ], - ), - ( - (314.159e-6, 2.71828e-6), - [ - ( - Formatter( - exp_mode="engineering", - exp_format="prefix", - ndigits=4, - ), - r"(314.159 ± 2.718) μ", - ), - ], - ), - ] - - self.run_val_unc_formatter_conversions(cases_list) diff --git a/tests/test_latex_conversion.py b/tests/test_output_conversion.py similarity index 100% rename from tests/test_latex_conversion.py rename to tests/test_output_conversion.py From bb8cca429d99f559022db837ee8bf483b63cbc0d Mon Sep 17 00:00:00 2001 From: Justin Date: Fri, 26 Jan 2024 09:03:04 -0700 Subject: [PATCH 23/38] test latex/html repr --- tests/test_output_conversion.py | 38 +++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/tests/test_output_conversion.py b/tests/test_output_conversion.py index 67cc9627..fd1f8c6c 100644 --- a/tests/test_output_conversion.py +++ b/tests/test_output_conversion.py @@ -3,6 +3,7 @@ import unittest from sciform import Formatter +from sciform.formatter import FormattedNumber from sciform.output_conversion import convert_sciform_format ValFormatterCases = list[ @@ -432,3 +433,40 @@ def test_val_unc_formatter_cases(self): ] self.run_val_unc_formatter_conversions(cases_list) + + def test_special_repr_output(self): + cases_list = [ + "6.26070e-04", + "(0.000000(1.234560))e+02", + "000_000_004_567_899.765_432_1", + "(nan)%", + "123000 ppm", + "0b+00", + "16.18033E+03", + " 1.20e+01", + "(-INF)E+00", + "(0.123456(789))e+03", + " 123.46 ± 0.79", + "(7.8900 ± 0.0001)×10²", + "(7.8900 ± 0.0001)×10⁻²", + "(0.123456 ± 0.000789) k", + ] + for case in cases_list: + formatted_number = FormattedNumber(case) + with self.subTest( + name="check__repr_html_", + input_str=case, + ): + self.assertEqual( + formatted_number._repr_html_(), # noqa: SLF001 + formatted_number.as_html(), + ) + + with self.subTest( + name="check__repr_latex_", + input_str=case, + ): + self.assertEqual( + formatted_number._repr_latex_(), # noqa: SLF001 + formatted_number.as_latex(strip_env_symbs=False), + ) From 695e08b6de336946c754d0b4abf1a1d2a108bcb5 Mon Sep 17 00:00:00 2001 From: Justin Date: Fri, 26 Jan 2024 09:07:27 -0700 Subject: [PATCH 24/38] test invalid cases --- tests/test_invalid_options.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/test_invalid_options.py b/tests/test_invalid_options.py index 00f36444..a0d77060 100644 --- a/tests/test_invalid_options.py +++ b/tests/test_invalid_options.py @@ -11,6 +11,7 @@ get_top_digit_binary, ) from sciform.formatting import format_non_finite +from sciform.output_conversion import _make_exp_str, convert_sciform_format from sciform.user_options import UserOptions @@ -239,3 +240,29 @@ def test_mode_str_to_enum_fail(self): "eng", modes.ExpModeEnum, ) + + def test_convert_sciform_format_invalid_output_format(self): + self.assertRaises( + ValueError, + convert_sciform_format, + "123", + "md", + ) + + def test_make_exp_str_invalid_output_format(self): + self.assertRaises( + ValueError, + _make_exp_str, + 10, + 0, + "rst", + ) + + def test_make_exp_str_invalid_base(self): + self.assertRaises( + ValueError, + _make_exp_str, + 16, + 0, + "ascii", + ) From 5671994d2032807d9d20b866c8c691ce91bcca65 Mon Sep 17 00:00:00 2001 From: Justin Date: Fri, 26 Jan 2024 09:24:08 -0700 Subject: [PATCH 25/38] strip_math_mode rename and documentation --- docs/source/usage.rst | 16 ++++++++++++++++ src/sciform/formatter.py | 4 ++-- tests/test_output_conversion.py | 6 +++--- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/docs/source/usage.rst b/docs/source/usage.rst index 2a681555..8b832fce 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -286,6 +286,22 @@ These conversions can be accessed via the >>> print(f"{formatted_str} -> {formatted_str.as_ascii()}") (314.159 ± 2.718) μ -> (314.159 +/- 2.718) u +The LaTeX enclosing ``"$"`` math environment symbols can be optionally +stripped: + +>>> sform = Formatter( +... exp_mode="engineering", +... exp_format="prefix", +... ndigits=4 +... ) +>>> formatted_str = sform(314.159e-6, 2.71828e-6) +>>> print(f"{formatted_str} -> {formatted_str.as_latex(strip_math_mode=False)}") +(314.159 ± 2.718) μ -> $(314.159\:\pm\:2.718)\:\text{\textmu}$ +>>> print(f"{formatted_str} -> {formatted_str.as_latex(strip_math_mode=True)}") +(314.159 ± 2.718) μ -> (314.159\:\pm\:2.718)\:\text{\textmu} + + + .. _dec_and_float: Note on Decimals and Floats diff --git a/src/sciform/formatter.py b/src/sciform/formatter.py index d4d56773..ac0353c3 100644 --- a/src/sciform/formatter.py +++ b/src/sciform/formatter.py @@ -271,10 +271,10 @@ def as_html(self: FormattedNumber) -> str: """Return the html representation of the formatted number.""" return convert_sciform_format(self, "html") - def as_latex(self: FormattedNumber, *, strip_env_symbs: bool = False) -> str: + def as_latex(self: FormattedNumber, *, strip_math_mode: bool = False) -> str: """Return the latex representation of the formatted number.""" latex_repr = convert_sciform_format(self, "latex") - if strip_env_symbs: + if strip_math_mode: latex_repr = latex_repr.strip("$") return latex_repr diff --git a/tests/test_output_conversion.py b/tests/test_output_conversion.py index fd1f8c6c..faf31d4a 100644 --- a/tests/test_output_conversion.py +++ b/tests/test_output_conversion.py @@ -79,7 +79,7 @@ def run_val_formatter_conversions(self, cases_list: ValFormatterCases): ): self.assertEqual(html_output, expected_html) - latex_output = sciform_output.as_latex(strip_env_symbs=True) + latex_output = sciform_output.as_latex(strip_math_mode=True) with self.subTest( val=val, expected_output=expected_latex, @@ -125,7 +125,7 @@ def run_val_unc_formatter_conversions(self, cases_list: ValUncFormatterCases): ): self.assertEqual(html_output, expected_html) - latex_output = sciform_output.as_latex(strip_env_symbs=True) + latex_output = sciform_output.as_latex(strip_math_mode=True) with self.subTest( val=val, unc=unc, @@ -468,5 +468,5 @@ def test_special_repr_output(self): ): self.assertEqual( formatted_number._repr_latex_(), # noqa: SLF001 - formatted_number.as_latex(strip_env_symbs=False), + formatted_number.as_latex(strip_math_mode=False), ) From 1e448b36de878dc907196cf87018a7df598a0f81 Mon Sep 17 00:00:00 2001 From: Justin Date: Fri, 26 Jan 2024 09:32:30 -0700 Subject: [PATCH 26/38] _repr_html_ and _repr_latex_ docs --- docs/source/api.rst | 1 + src/sciform/formatter.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/docs/source/api.rst b/docs/source/api.rst index 98cdc2de..1f5c6038 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -38,3 +38,4 @@ Output Conversion .. autoclass:: sciform.formatter.FormattedNumber :members: + :private-members: diff --git a/src/sciform/formatter.py b/src/sciform/formatter.py index ac0353c3..49f6986d 100644 --- a/src/sciform/formatter.py +++ b/src/sciform/formatter.py @@ -279,7 +279,9 @@ def as_latex(self: FormattedNumber, *, strip_math_mode: bool = False) -> str: return latex_repr def _repr_html_(self: FormattedNumber) -> str: + """Hook for HTML display.""" # noqa: D401 return self.as_html() def _repr_latex_(self: FormattedNumber) -> str: + """Hook for LaTeX display.""" # noqa: D401 return self.as_latex() From 770dc08e7369382406668739b82348054bb00a43 Mon Sep 17 00:00:00 2001 From: Justin Date: Fri, 26 Jan 2024 09:44:30 -0700 Subject: [PATCH 27/38] jupyter/IPython display example --- docs/source/usage.rst | 16 +++++++++++++++- examples/outputs/jupyter_output.png | Bin 0 -> 41556 bytes 2 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 examples/outputs/jupyter_output.png diff --git a/docs/source/usage.rst b/docs/source/usage.rst index 8b832fce..1a133c54 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -300,7 +300,21 @@ stripped: >>> print(f"{formatted_str} -> {formatted_str.as_latex(strip_math_mode=True)}") (314.159 ± 2.718) μ -> (314.159\:\pm\:2.718)\:\text{\textmu} - +In addition to exposing +:meth:`as_latex() ` and +:meth:`as_html() `, +the :class:`FormattedNumber ` class defines +the aliases +:meth:`_repr_latex_() ` and +:meth:`_repr_html_() `. +The +`IPython display functions `_ +look for these methods, and if available, will use them to display +prettier representations of the class than the unicode ``__repr__`` +representation. + +.. image:: ../../examples/outputs/jupyter_output.png + :width: 400 .. _dec_and_float: diff --git a/examples/outputs/jupyter_output.png b/examples/outputs/jupyter_output.png new file mode 100644 index 0000000000000000000000000000000000000000..728ca5dcc36b0528f2cde43a6950f38db569ac87 GIT binary patch literal 41556 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

CbSv$*$`@zMdnel6K?ZAYFE_ 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

vW{?*dA85~%z7 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 G?!N%zW$8iy literal 0 HcmV?d00001 From b34bdbc431f08cbeaae0494f2efd4bef93104e70 Mon Sep 17 00:00:00 2001 From: Justin Date: Fri, 26 Jan 2024 09:44:48 -0700 Subject: [PATCH 28/38] break out "examples" dependencies --- examples/jupyter_output_example.ipynb | 50 +++++++++++++++++++++++++++ examples/requirements.txt | 4 --- pyproject.toml | 4 +++ 3 files changed, 54 insertions(+), 4 deletions(-) create mode 100644 examples/jupyter_output_example.ipynb delete mode 100644 examples/requirements.txt diff --git a/examples/jupyter_output_example.ipynb b/examples/jupyter_output_example.ipynb new file mode 100644 index 00000000..46a2aa3e --- /dev/null +++ b/examples/jupyter_output_example.ipynb @@ -0,0 +1,50 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "ec4427f1-623b-4cd5-a077-63d5204cc7b0", + "metadata": {}, + "outputs": [], + "source": [ + "from sciform import Formatter\n", + "from IPython.display import display_latex, display_html\n", + "\n", + "sform = Formatter(exp_mode=\"scientific\", superscript=True)\n", + "val = sform(123456, 789)\n", + "\n", + "print('String:')\n", + "print(val)\n", + "print('\\nASCII:')\n", + "print(val.as_ascii())\n", + "print('\\nLatex:')\n", + "display_latex(val)\n", + "print('\\nHTML:')\n", + "display_html(val)\n", + "print('\\nDefault Display (HTML)')\n", + "display(val)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/requirements.txt b/examples/requirements.txt deleted file mode 100644 index 346d9b71..00000000 --- a/examples/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -numpy -scipy -matplotlib -tabulate diff --git a/pyproject.toml b/pyproject.toml index e93cb470..0b630612 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,9 +35,13 @@ dev = [ "sphinx-rtd-theme", "sphinx-toolbox", "numpy", +] +examples = [ + "numpy", "scipy", "matplotlib", "tabulate", + "jupyter", ] [tool.setuptools.dynamic] From d9b6870d75e94a1ee748cc5eaaf2c92bd12b4a8d Mon Sep 17 00:00:00 2001 From: Justin Date: Fri, 26 Jan 2024 13:01:25 -0700 Subject: [PATCH 29/38] sort autodoc members by source --- docs/source/conf.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/source/conf.py b/docs/source/conf.py index 7d1f0650..b0aa3944 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -40,6 +40,8 @@ todo_include_todos = True todo_emit_warnings = True +autodoc_member_order = "bysource" + # For wrapping of text in tables html_css_files = [ "css/custom.css", From d55674d77434ae8f572cf038de22c674390cf382 Mon Sep 17 00:00:00 2001 From: Justin Date: Fri, 26 Jan 2024 13:11:39 -0700 Subject: [PATCH 30/38] test left_pad_matching --- tests/test_val_unc_formatter.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_val_unc_formatter.py b/tests/test_val_unc_formatter.py index c8a8e7b9..ccd95b6e 100644 --- a/tests/test_val_unc_formatter.py +++ b/tests/test_val_unc_formatter.py @@ -494,3 +494,9 @@ def test_pdg_sig_figs(self): def test_dec_place_warn(self): sform = Formatter(round_mode="dec_place") self.assertWarns(Warning, sform, 42, 24) + + def test_left_pad_matching(self): + sform = Formatter(left_pad_matching=True) + result = sform(123, 0.123) + expected_result = "123.000 ± 0.123" + self.assertEqual(result, expected_result) From 18e916ccec47039cb337dd9773c6149fc6009546 Mon Sep 17 00:00:00 2001 From: Justin Date: Fri, 26 Jan 2024 14:23:11 -0700 Subject: [PATCH 31/38] minor docs --- docs/source/options.rst | 2 +- docs/source/usage.rst | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/source/options.rst b/docs/source/options.rst index 36ffb1a4..a6460044 100644 --- a/docs/source/options.rst +++ b/docs/source/options.rst @@ -78,7 +78,7 @@ The sign symbol is always included and the exponent value is left padded so that it is at least two digits wide. These behaviors for ASCII exponents cannot be modified. However the :ref:`superscript` mode can be used to represent the -exponent using unicode characters. +exponent using Unicode characters. .. _engineering: diff --git a/docs/source/usage.rst b/docs/source/usage.rst index 1a133c54..3f39f050 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -236,9 +236,9 @@ outputs are being used in contexts outside of e.g. text terminals such as `Matplotlib `_ plots, `Jupyter `_ notebooks, or `Quarto `_ documents which support richer display -functionality than unicode text. +functionality than Unicode text. The ASCII representation may be useful if :mod:`sciform` outputs are -being used in contexts in which only ASCII, and not unicode, text is +being used in contexts in which only ASCII, and not Unicode, text is supported or preferred. These conversions can be accessed via the @@ -309,8 +309,8 @@ the aliases :meth:`_repr_html_() `. The `IPython display functions `_ -look for these methods, and if available, will use them to display -prettier representations of the class than the unicode ``__repr__`` +looks for these methods, and, if available, will use them to display +prettier representations of the class than the Unicode ``__repr__`` representation. .. image:: ../../examples/outputs/jupyter_output.png From 49ed1c1927c80be2c9679e395166fe0c0712017a Mon Sep 17 00:00:00 2001 From: Justin Date: Fri, 26 Jan 2024 14:24:53 -0700 Subject: [PATCH 32/38] minor docs --- docs/source/options.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/options.rst b/docs/source/options.rst index a6460044..4267e6bf 100644 --- a/docs/source/options.rst +++ b/docs/source/options.rst @@ -78,7 +78,7 @@ The sign symbol is always included and the exponent value is left padded so that it is at least two digits wide. These behaviors for ASCII exponents cannot be modified. However the :ref:`superscript` mode can be used to represent the -exponent using Unicode characters. +exponent using Unicode superscript characters. .. _engineering: From 1797a61f2747ae7d830a5388a73783aa6968442a Mon Sep 17 00:00:00 2001 From: Justin Date: Fri, 26 Jan 2024 19:17:30 -0700 Subject: [PATCH 33/38] changelog --- CHANGELOG.rst | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f030bd08..dba939a0 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -11,12 +11,30 @@ Unreleased Added ^^^^^ +* Added the ``FormattedNumber`` class. + This class is a subclass of ``str`` and is now returned by the + ``Formatter`` instead of ``str``. + The ``FormattedNumber`` allows post-conversion to ASCII, HTML, and + LaTeX formats. + [`#114 `_] * Added separate flags for code coverage reports for each python version. Changed ^^^^^^^ +* In addition to removing the ``latex`` option from the ``Formatter`` in + favor of the introduction of the ``FormattedNumber`` class, the + LaTeX conversion algorithm has been slightly modified. + Left and right parentheses are no longer converted to ``"\left("`` + and ``"\right)"`` due to introducing strange spacing issues. + See `Spacing around \\left and \\right `_. + Previously spaces within the ``sciform`` output were handled + inconsistently and occasionally required confusing extra handling. + Now any spaces in the input string are directly and explicitly + converted into math mode medium spaces: ``"\:"``. + Finally, ``"μ"`` is now included in math mode ``\text{}`` environment + and converted to ``"\textmu"``. * **[BREAKING]** Renamed ``fill_char`` to ``left_pad_char``. [`#126 `_] @@ -28,6 +46,16 @@ Fixed parentheses, e.g. ``"(1.2 ± 0.1)"``. [`#130 `_] +Removed +^^^^^^^ + +* **[BREAKING]** Removed the ``latex`` option in favor of the + introduction of the ``FormattedNumber.as_latex()`` method. + This removal simplies the formatted algorithm by separating LaTeX + formatting from other tasks like exponent string resolution. + The ``latex`` option also introduced a potential confusion with the + ``superscript`` option, which had no effect when ``latex=True``. + ---- 0.32.3 (2024-01-11) From 22f6c44b2c846d8576cb0207214cdb5412442adc Mon Sep 17 00:00:00 2001 From: Justin Date: Fri, 26 Jan 2024 19:32:54 -0700 Subject: [PATCH 34/38] changelog --- CHANGELOG.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index dba939a0..3af60b5a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -37,6 +37,11 @@ Changed and converted to ``"\textmu"``. * **[BREAKING]** Renamed ``fill_char`` to ``left_pad_char``. [`#126 `_] +* Slimmed down ``[dev]`` optional dependencies and created + ``[examples]`` optional dependencies. + The former includes development tools, while the latter includes + the heavy-weight requirements needed to run all the examples, + including, e.g. ``jupyter``, ``scipy``, etc. Fixed ^^^^^ From c6ca0c58100274c92f2117433f937dbe8635a128 Mon Sep 17 00:00:00 2001 From: Justin Date: Fri, 26 Jan 2024 20:51:38 -0700 Subject: [PATCH 35/38] changelog --- CHANGELOG.rst | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3af60b5a..c3237d18 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -26,15 +26,18 @@ Changed * In addition to removing the ``latex`` option from the ``Formatter`` in favor of the introduction of the ``FormattedNumber`` class, the LaTeX conversion algorithm has been slightly modified. - Left and right parentheses are no longer converted to ``"\left("`` - and ``"\right)"`` due to introducing strange spacing issues. - See `Spacing around \\left and \\right `_. - Previously spaces within the ``sciform`` output were handled - inconsistently and occasionally required confusing extra handling. - Now any spaces in the input string are directly and explicitly - converted into math mode medium spaces: ``"\:"``. - Finally, ``"μ"`` is now included in math mode ``\text{}`` environment - and converted to ``"\textmu"``. + + * Left and right parentheses are no longer converted to ``"\left("`` + and ``"\right)"`` due to introducing strange spacing issues. + See + `Spacing around \\left and \\right `_. + * Previously spaces within the ``sciform`` output were handled + inconsistently and occasionally required confusing extra handling. + Now any spaces in the input string are directly and explicitly + converted into math mode medium spaces: ``"\:"``. + * ``"μ"`` is now included in math mode ``\text{}`` environment + and converted to ``"\textmu"``. + * **[BREAKING]** Renamed ``fill_char`` to ``left_pad_char``. [`#126 `_] * Slimmed down ``[dev]`` optional dependencies and created From 4f9747fb0e081379373eb290150f26ef2142d5dc Mon Sep 17 00:00:00 2001 From: Justin Date: Fri, 26 Jan 2024 20:52:26 -0700 Subject: [PATCH 36/38] minor --- CHANGELOG.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index c3237d18..9a74ccea 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -14,8 +14,8 @@ Added * Added the ``FormattedNumber`` class. This class is a subclass of ``str`` and is now returned by the ``Formatter`` instead of ``str``. - The ``FormattedNumber`` allows post-conversion to ASCII, HTML, and - LaTeX formats. + The ``FormattedNumber`` class allows post-conversion to ASCII, HTML, + and LaTeX formats. [`#114 `_] * Added separate flags for code coverage reports for each python version. From 0b9afda5f1cff31707a03997d7d0931918473a4b Mon Sep 17 00:00:00 2001 From: Justin Date: Sat, 27 Jan 2024 05:31:52 -0700 Subject: [PATCH 37/38] minor --- CHANGELOG.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9a74ccea..ef4ba633 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -35,7 +35,7 @@ Changed inconsistently and occasionally required confusing extra handling. Now any spaces in the input string are directly and explicitly converted into math mode medium spaces: ``"\:"``. - * ``"μ"`` is now included in math mode ``\text{}`` environment + * ``"μ"`` is now included in the math mode ``\text{}`` environment and converted to ``"\textmu"``. * **[BREAKING]** Renamed ``fill_char`` to ``left_pad_char``. From c155d5a629cefe14bfd2f246cfd4e8d1993f8d10 Mon Sep 17 00:00:00 2001 From: Justin Date: Sat, 27 Jan 2024 05:38:09 -0700 Subject: [PATCH 38/38] minor --- CHANGELOG.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index ef4ba633..64effc1e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -59,7 +59,7 @@ Removed * **[BREAKING]** Removed the ``latex`` option in favor of the introduction of the ``FormattedNumber.as_latex()`` method. - This removal simplies the formatted algorithm by separating LaTeX + This removal simplifies the formatting algorithm by separating LaTeX formatting from other tasks like exponent string resolution. The ``latex`` option also introduced a potential confusion with the ``superscript`` option, which had no effect when ``latex=True``.