From 0b8e6cd8856fff9c3fd96935141d6446b9bb71ce Mon Sep 17 00:00:00 2001 From: paul019 <39464035+paul019@users.noreply.github.com> Date: Thu, 14 Mar 2024 11:15:24 +0100 Subject: [PATCH 01/25] Add domain objects --- src/api/tables/table.py | 0 src/domain/tables/column.py | 14 ++++++++++++++ src/domain/tables/table.py | 17 +++++++++++++++++ 3 files changed, 31 insertions(+) create mode 100644 src/api/tables/table.py create mode 100644 src/domain/tables/column.py create mode 100644 src/domain/tables/table.py diff --git a/src/api/tables/table.py b/src/api/tables/table.py new file mode 100644 index 00000000..e69de29b diff --git a/src/domain/tables/column.py b/src/domain/tables/column.py new file mode 100644 index 00000000..51a61b28 --- /dev/null +++ b/src/domain/tables/column.py @@ -0,0 +1,14 @@ +from dataclasses import dataclass +from typing import Union, List + +from domain.result import _Result + + +@dataclass +class _Column: + """ + A table column. + """ + + title: str + cells: List[Union[_Result, str]] \ No newline at end of file diff --git a/src/domain/tables/table.py b/src/domain/tables/table.py new file mode 100644 index 00000000..f1098577 --- /dev/null +++ b/src/domain/tables/table.py @@ -0,0 +1,17 @@ +from dataclasses import dataclass +from typing import List + +from domain.tables.column import _Column + + +@dataclass +class _Table: + """ + A table. + """ + + name: str + columns: List[_Column] + caption: str + resize_to_fit_page_: bool + horizontal: bool \ No newline at end of file From 750ed96007227e76214f4fe2bf81b9069ba3f03b Mon Sep 17 00:00:00 2001 From: paul019 <39464035+paul019@users.noreply.github.com> Date: Thu, 14 Mar 2024 11:19:02 +0100 Subject: [PATCH 02/25] Squashed commit of the following: commit d1d7d11e7076e4dcfd40850fee41b4b4982259ff Author: paul019 <39464035+paul019@users.noreply.github.com> Date: Thu Mar 14 11:18:45 2024 +0100 Refactor res.py and move parsers to application --- src/api/res.py | 158 +++---------------------------------- src/application/parsers.py | 143 +++++++++++++++++++++++++++++++++ 2 files changed, 152 insertions(+), 149 deletions(-) create mode 100644 src/application/parsers.py diff --git a/src/api/res.py b/src/api/res.py index 38203ca2..809d6932 100644 --- a/src/api/res.py +++ b/src/api/res.py @@ -4,10 +4,8 @@ from api.printable_result import PrintableResult from application.cache import _res_cache from application.rounder import _Rounder -from application.helpers import _Helpers +import application.parsers as parsers from domain.result import _Result -from domain.value import _Value -from domain.uncertainty import _Uncertainty # TODO: import types from typing to ensure backwards compatibility down to Python 3.8 @@ -38,7 +36,7 @@ def res( str, Tuple[Union[float, str], str], List[Union[float, str, Tuple[Union[float, str], str]]], - None + None, ] = None, sigfigs: Union[int, None] = None, decimal_places: Union[int, None] = None, @@ -78,7 +76,7 @@ def res( str, Tuple[Union[float, str], str], List[Union[float, str, Tuple[Union[float, str], str]]], - None + None, ] = None, unit: str = "", sigfigs: Union[int, None] = None, @@ -88,12 +86,12 @@ def res( uncert = [] # Parse user input - name_res = _parse_name(name) - value_res = _parse_value(value) - uncertainties_res = _parse_uncertainties(uncert) - unit_res = _parse_unit(unit) - sigfigs_res = _parse_sigfigs(sigfigs) - decimal_places_res = _parse_decimal_places(decimal_places) + name_res = parsers.parse_name(name) + value_res = parsers.parse_value(value) + uncertainties_res = parsers.parse_uncertainties(uncert) + unit_res = parsers.parse_unit(unit) + sigfigs_res = parsers.parse_sigfigs(sigfigs) + decimal_places_res = parsers.parse_decimal_places(decimal_places) # Assemble the result result = _Result( @@ -113,141 +111,3 @@ def res(*args, **kwargs) -> object: # This method only scans for all `overload`-decorated methods # and properly adds them as Plum methods. pass - - -def _check_if_number_string(value: str) -> None: - """Raises a ValueError if the string is not a valid number.""" - try: - float(value) - except ValueError as exc: - raise ValueError(f"String value must be a valid number, not {value}") from exc - - -def _parse_name(name: str) -> str: - """Parses the name.""" - if not isinstance(name, str): - raise TypeError(f"`name` must be a string, not {type(name)}") - - name = ( - name.replace("ä", "ae") - .replace("ö", "oe") - .replace("ü", "ue") - .replace("Ä", "Ae") - .replace("Ö", "Oe") - .replace("Ü", "Ue") - .replace("ß", "ss") - ) - - parsed_name = "" - next_chat_upper = False - - for char in name: - if char.isalpha(): - if next_chat_upper: - parsed_name += char.upper() - next_chat_upper = False - else: - parsed_name += char - elif char.isdigit(): - digit = _Helpers.number_to_word(int(char)) - if parsed_name == "": - parsed_name += digit - else: - parsed_name += digit[0].upper() + digit[1:] - next_chat_upper = True - elif char in [" ", "_", "-"]: - next_chat_upper = True - - return parsed_name - - -def _parse_unit(unit: str) -> str: - """Parses the unit.""" - if not isinstance(unit, str): - raise TypeError(f"`unit` must be a string, not {type(unit)}") - - # TODO: maybe add some basic checks to catch siunitx errors, e.g. - # unsupported symbols etc. But maybe leave this to LaTeX and just return - # the LaTeX later on. But catching it here would be more user-friendly, - # as the user would get immediate feedback and not only once they try to - # export the results. - return unit - - -def _parse_sigfigs(sigfigs: Union[int, None]) -> Union[int, None]: - """Parses the number of sigfigs.""" - if sigfigs is None: - return None - - if not isinstance(sigfigs, int): - raise TypeError(f"`sigfigs` must be an int, not {type(sigfigs)}") - - if sigfigs < 1: - raise ValueError("`sigfigs` must be positive") - - return sigfigs - - -def _parse_decimal_places(decimal_places: Union[int, None]) -> Union[int, None]: - """Parses the number of sigfigs.""" - if decimal_places is None: - return None - - if not isinstance(decimal_places, int): - raise TypeError(f"`decimal_places` must be an int, not {type(decimal_places)}") - - if decimal_places < 0: - raise ValueError("`decimal_places` must be non-negative") - - return decimal_places - - -def _parse_value(value: Union[float, str]) -> _Value: - """Converts the value to a _Value object.""" - if not isinstance(value, (float, str)): - raise TypeError(f"`value` must be a float or string, not {type(value)}") - - if isinstance(value, str): - _check_if_number_string(value) - - return _Value(value) - - -def _parse_uncertainties( - uncertainties: Union[ - float, - str, - Tuple[Union[float, str], str], - List[Union[float, str, Tuple[Union[float, str], str]]], - ] -) -> List[_Uncertainty]: - """Converts the uncertainties to a list of _Uncertainty objects.""" - uncertainties_res = [] - - # no list, but a single value was given - if isinstance(uncertainties, (float, str, Tuple)): - uncertainties = [uncertainties] - - assert isinstance(uncertainties, List) - - for uncert in uncertainties: - if isinstance(uncert, (float, str)): - if isinstance(uncert, str): - _check_if_number_string(uncert) - if float(uncert) <= 0: - raise ValueError("Uncertainty must be positive.") - uncertainties_res.append(_Uncertainty(uncert)) - - elif isinstance(uncert, Tuple): - if not isinstance(uncert[0], (float, str)): - raise TypeError( - f"First argument of uncertainty-tuple must be a float or a string, not {type(uncert[0])}" - ) - if isinstance(uncert[0], str): - _check_if_number_string(uncert[0]) - uncertainties_res.append(_Uncertainty(uncert[0], _parse_name(uncert[1]))) - - else: - raise TypeError(f"Each uncertainty must be a tuple or a float/str, not {type(uncert)}") - - return uncertainties_res diff --git a/src/application/parsers.py b/src/application/parsers.py new file mode 100644 index 00000000..be221b66 --- /dev/null +++ b/src/application/parsers.py @@ -0,0 +1,143 @@ +from typing import Union, List, Tuple + +from application.helpers import _Helpers +from domain.value import _Value +from domain.uncertainty import _Uncertainty + +def check_if_number_string(value: str) -> None: + """Raises a ValueError if the string is not a valid number.""" + try: + float(value) + except ValueError as exc: + raise ValueError(f"String value must be a valid number, not {value}") from exc + + + +def parse_name(name: str) -> str: + """Parses the name.""" + if not isinstance(name, str): + raise TypeError(f"`name` must be a string, not {type(name)}") + + name = ( + name.replace("ä", "ae") + .replace("ö", "oe") + .replace("ü", "ue") + .replace("Ä", "Ae") + .replace("Ö", "Oe") + .replace("Ü", "Ue") + .replace("ß", "ss") + ) + + parsed_name = "" + next_chat_upper = False + + for char in name: + if char.isalpha(): + if next_chat_upper: + parsed_name += char.upper() + next_chat_upper = False + else: + parsed_name += char + elif char.isdigit(): + digit = _Helpers.number_to_word(int(char)) + if parsed_name == "": + parsed_name += digit + else: + parsed_name += digit[0].upper() + digit[1:] + next_chat_upper = True + elif char in [" ", "_", "-"]: + next_chat_upper = True + + return parsed_name + + +def parse_unit(unit: str) -> str: + """Parses the unit.""" + if not isinstance(unit, str): + raise TypeError(f"`unit` must be a string, not {type(unit)}") + + # TODO: maybe add some basic checks to catch siunitx errors, e.g. + # unsupported symbols etc. But maybe leave this to LaTeX and just return + # the LaTeX later on. But catching it here would be more user-friendly, + # as the user would get immediate feedback and not only once they try to + # export the results. + return unit + + +def parse_sigfigs(sigfigs: Union[int, None]) -> Union[int, None]: + """Parses the number of sigfigs.""" + if sigfigs is None: + return None + + if not isinstance(sigfigs, int): + raise TypeError(f"`sigfigs` must be an int, not {type(sigfigs)}") + + if sigfigs < 1: + raise ValueError("`sigfigs` must be positive") + + return sigfigs + + +def parse_decimal_places(decimal_places: Union[int, None]) -> Union[int, None]: + """Parses the number of sigfigs.""" + if decimal_places is None: + return None + + if not isinstance(decimal_places, int): + raise TypeError(f"`decimal_places` must be an int, not {type(decimal_places)}") + + if decimal_places < 0: + raise ValueError("`decimal_places` must be non-negative") + + return decimal_places + + +def parse_value(value: Union[float, str]) -> _Value: + """Converts the value to a _Value object.""" + if not isinstance(value, (float, str)): + raise TypeError(f"`value` must be a float or string, not {type(value)}") + + if isinstance(value, str): + check_if_number_string(value) + + return _Value(value) + + +def parse_uncertainties( + uncertainties: Union[ + float, + str, + Tuple[Union[float, str], str], + List[Union[float, str, Tuple[Union[float, str], str]]], + ] +) -> List[_Uncertainty]: + """Converts the uncertainties to a list of _Uncertainty objects.""" + uncertainties_res = [] + + # no list, but a single value was given + if isinstance(uncertainties, (float, str, Tuple)): + uncertainties = [uncertainties] + + assert isinstance(uncertainties, List) + + for uncert in uncertainties: + if isinstance(uncert, (float, str)): + if isinstance(uncert, str): + check_if_number_string(uncert) + if float(uncert) <= 0: + raise ValueError("Uncertainty must be positive.") + uncertainties_res.append(_Uncertainty(uncert)) + + elif isinstance(uncert, Tuple): + if not isinstance(uncert[0], (float, str)): + raise TypeError( + f"First argument of uncertainty-tuple must be a float or a string, not {type(uncert[0])}" + ) + if isinstance(uncert[0], str): + check_if_number_string(uncert[0]) + uncertainties_res.append(_Uncertainty(uncert[0], parse_name(uncert[1]))) + + else: + raise TypeError(f"Each uncertainty must be a tuple or a float/str, not {type(uncert)}") + + return uncertainties_res From dc8eeea41b2fcce5d6d102bf8f3b23c704d78edf Mon Sep 17 00:00:00 2001 From: paul019 <39464035+paul019@users.noreply.github.com> Date: Thu, 14 Mar 2024 11:37:38 +0100 Subject: [PATCH 03/25] Add api for tables --- src/api/res.py | 2 +- src/api/tables/column.py | 11 ++++ src/api/tables/table.py | 26 +++++++++ src/api/tables/table_res.py | 110 ++++++++++++++++++++++++++++++++++++ src/application/cache.py | 16 ++++-- src/domain/tables/column.py | 2 +- src/domain/tables/table.py | 3 +- 7 files changed, 163 insertions(+), 7 deletions(-) create mode 100644 src/api/tables/column.py create mode 100644 src/api/tables/table_res.py diff --git a/src/api/res.py b/src/api/res.py index 809d6932..5aded65f 100644 --- a/src/api/res.py +++ b/src/api/res.py @@ -98,7 +98,7 @@ def res( name_res, value_res, unit_res, uncertainties_res, sigfigs_res, decimal_places_res ) _Rounder.round_result(result) - _res_cache.add(name, result) + _res_cache.add_res(name, result) return PrintableResult(result) diff --git a/src/api/tables/column.py b/src/api/tables/column.py new file mode 100644 index 00000000..e79153a8 --- /dev/null +++ b/src/api/tables/column.py @@ -0,0 +1,11 @@ +from typing import List, Union + +from domain.tables.column import _Column +from domain.result import _Result + + +def column( + title: str, + cells: List[Union[_Result, str]], +) -> _Column: + return _Column(title, cells) \ No newline at end of file diff --git a/src/api/tables/table.py b/src/api/tables/table.py index e69de29b..a17135f9 100644 --- a/src/api/tables/table.py +++ b/src/api/tables/table.py @@ -0,0 +1,26 @@ +from typing import List + +from application.cache import _res_cache +import application.parsers as parsers +from domain.tables.column import _Column +from domain.tables.table import _Table + + +def table( + name: str, + columns: List[_Column], + caption: str, + resize_to_fit_page_: bool = False, + horizontal: bool = False, + concentrate_units_if_possible: bool = True, +): + # Parse user input + name_res = parsers.parse_name(name) + + # Assemble the table + _table = _Table( + name_res, columns, caption, resize_to_fit_page_, horizontal, concentrate_units_if_possible + ) + _res_cache.add_table(name, _table) + + return diff --git a/src/api/tables/table_res.py b/src/api/tables/table_res.py new file mode 100644 index 00000000..e7528237 --- /dev/null +++ b/src/api/tables/table_res.py @@ -0,0 +1,110 @@ +from typing import Union, List, Tuple +from plum import dispatch, overload + +from application.rounder import _Rounder +import application.parsers as parsers +from domain.result import _Result + + +# TODO: import types from typing to ensure backwards compatibility down to Python 3.8 + +# TODO: use pydantic instead of manual and ugly type checking +# see: https://docs.pydantic.dev/latest/ +# This way we can code as if the happy path is the only path, and let pydantic +# handle the error checking and reporting. + + +@overload +def table_res( + name: str, + value: Union[float, str], + unit: str = "", + sigfigs: Union[int, None] = None, + decimal_places: Union[int, None] = None, +) -> _Result: + return table_res(name, value, [], unit, sigfigs, decimal_places) + + +@overload +def table_res( + name: str, + value: Union[float, str], + uncert: Union[ + float, + str, + Tuple[Union[float, str], str], + List[Union[float, str, Tuple[Union[float, str], str]]], + None, + ] = None, + sigfigs: Union[int, None] = None, + decimal_places: Union[int, None] = None, +) -> _Result: + return table_res(name, value, uncert, "", sigfigs, decimal_places) + + +@overload +def table_res( + name: str, + value: Union[float, str], + sigfigs: Union[int, None] = None, + decimal_places: Union[int, None] = None, +) -> _Result: + return table_res(name, value, [], "", sigfigs, decimal_places) + + +@overload +def table_res( + name: str, + value: Union[float, str], + sys: float, + stat: float, + unit: str = "", + sigfigs: Union[int, None] = None, + decimal_places: Union[int, None] = None, +) -> _Result: + return table_res(name, value, [(sys, "sys"), (stat, "stat")], unit, sigfigs, decimal_places) + + +@overload +def table_res( + name: str, + value: Union[float, str], + uncert: Union[ + float, + str, + Tuple[Union[float, str], str], + List[Union[float, str, Tuple[Union[float, str], str]]], + None, + ] = None, + unit: str = "", + sigfigs: Union[int, None] = None, + decimal_places: Union[int, None] = None, +) -> _Result: + if uncert is None: + uncert = [] + + # Parse user input + name_res = parsers.parse_name(name) + value_res = parsers.parse_value(value) + uncertainties_res = parsers.parse_uncertainties(uncert) + unit_res = parsers.parse_unit(unit) + sigfigs_res = parsers.parse_sigfigs(sigfigs) + decimal_places_res = parsers.parse_decimal_places(decimal_places) + + # Assemble the result + result = _Result( + name_res, value_res, unit_res, uncertainties_res, sigfigs_res, decimal_places_res + ) + _Rounder.round_result(result) + + return result + + +# Hack for method "overloading" in Python +# see https://beartype.github.io/plum/integration.html +# This is a good writeup: https://stackoverflow.com/a/29091980/ +@dispatch +def table_res(*args, **kwargs) -> object: + # This method only scans for all `overload`-decorated methods + # and properly adds them as Plum methods. + pass \ No newline at end of file diff --git a/src/application/cache.py b/src/application/cache.py index e25f537c..8acca268 100644 --- a/src/application/cache.py +++ b/src/application/cache.py @@ -1,4 +1,5 @@ from domain.result import _Result +from domain.tables.table import _Table class _ResultsCache: @@ -10,13 +11,20 @@ class _ResultsCache: """ def __init__(self): - self.cache: dict[str, _Result] = {} + self.res_cache: dict[str, _Result] = {} + self.table_cache: dict[str, _Table] = {} - def add(self, name, result: _Result): - self.cache[name] = result + def add_res(self, name: str, result: _Result): + self.res_cache[name] = result + + def add_table(self, name: str, table: _Table): + self.table_cache[name] = table def get_all_results(self) -> list[_Result]: - return list(self.cache.values()) + return list(self.res_cache.values()) + + def get_all_tables(self) -> list[_Table]: + return list(self.table_cache.values()) _res_cache = _ResultsCache() diff --git a/src/domain/tables/column.py b/src/domain/tables/column.py index 51a61b28..b7686c4f 100644 --- a/src/domain/tables/column.py +++ b/src/domain/tables/column.py @@ -11,4 +11,4 @@ class _Column: """ title: str - cells: List[Union[_Result, str]] \ No newline at end of file + cells: List[Union[_Result, str]] diff --git a/src/domain/tables/table.py b/src/domain/tables/table.py index f1098577..c6ae199e 100644 --- a/src/domain/tables/table.py +++ b/src/domain/tables/table.py @@ -14,4 +14,5 @@ class _Table: columns: List[_Column] caption: str resize_to_fit_page_: bool - horizontal: bool \ No newline at end of file + horizontal: bool + concentrate_units_if_possible: bool \ No newline at end of file From 85204bb906f116e4d6dd0864bb3787e41fefce2e Mon Sep 17 00:00:00 2001 From: paul019 <39464035+paul019@users.noreply.github.com> Date: Thu, 14 Mar 2024 11:43:18 +0100 Subject: [PATCH 04/25] Export tables --- src/api/export.py | 5 +++++ src/application/tables/latexer.py | 25 +++++++++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 src/application/tables/latexer.py diff --git a/src/api/export.py b/src/api/export.py index 5227be22..5ed711c7 100644 --- a/src/api/export.py +++ b/src/api/export.py @@ -1,5 +1,6 @@ from application.cache import _res_cache from application.latexer import _LaTeXer +from application.tables.latexer import _TableLaTeXer def export(filepath: str): @@ -8,6 +9,7 @@ def export(filepath: str): to a .tex file at the given filepath. """ results = _res_cache.get_all_results() + tables = _res_cache.get_all_tables() print(f"Processing {len(results)} result(s)") # Round and convert to LaTeX commands @@ -25,6 +27,9 @@ def export(filepath: str): for result in results: result_str = _LaTeXer.result_to_latex_cmd(result) cmds.append(result_str) + for table in tables: + table_str = _TableLaTeXer.table_to_latex_cmd(table) + cmds.append(table_str) # Write to file with open(filepath, "w", encoding="utf-8") as f: diff --git a/src/application/tables/latexer.py b/src/application/tables/latexer.py new file mode 100644 index 00000000..181f0ea6 --- /dev/null +++ b/src/application/tables/latexer.py @@ -0,0 +1,25 @@ +import textwrap +from domain.tables.table import _Table + + +# Config values: +min_exponent_for_non_scientific_notation = -2 +max_exponent_for_non_scientific_notation = 3 +table_identifier = "table" + + +class _TableLaTeXer: + @classmethod + def table_to_latex_cmd(cls, table: _Table) -> str: + """ + Returns the table as LaTeX command to be used in a .tex file. + """ + + cmd_name = table_identifier + table.name[0].upper() + table.name[1:] + + latex_str = rf""" + \newcommand*{{\{cmd_name}}}[1][]{{ + """ + latex_str = textwrap.dedent(latex_str).strip() + + raise NotImplementedError From 613f2f5cffb124751e676a19e8510a7088e40e7d Mon Sep 17 00:00:00 2001 From: paul019 <39464035+paul019@users.noreply.github.com> Date: Thu, 14 Mar 2024 11:45:24 +0100 Subject: [PATCH 05/25] Squashed commit of the following: commit d4f9b778784b88bf2ea35fa68ceec27f163b53e6 Author: paul019 <39464035+paul019@users.noreply.github.com> Date: Thu Mar 14 11:45:12 2024 +0100 Remove print statements commit d1d7d11e7076e4dcfd40850fee41b4b4982259ff Author: paul019 <39464035+paul019@users.noreply.github.com> Date: Thu Mar 14 11:18:45 2024 +0100 Refactor res.py and move parsers to application --- src/application/rounder.py | 2 -- tests/rounder_test.py | 2 -- 2 files changed, 4 deletions(-) diff --git a/src/application/rounder.py b/src/application/rounder.py index b9a63a5b..d866bd46 100644 --- a/src/application/rounder.py +++ b/src/application/rounder.py @@ -61,8 +61,6 @@ def round_result(cls, result: _Result) -> None: -_Helpers.get_exponent(u.uncertainty.get()) ) - print(normalized_value) - if round(normalized_value, 1) >= 3.0: u.uncertainty.set_sigfigs(1) else: diff --git a/tests/rounder_test.py b/tests/rounder_test.py index db5d47b6..dbe48abd 100644 --- a/tests/rounder_test.py +++ b/tests/rounder_test.py @@ -62,13 +62,11 @@ def test_hierarchy_4(self): res = _Result("", _Value(1.0), "", [_Uncertainty(0.295001)], None, None) _Rounder.round_result(res) - print(res.uncertainties[0].uncertainty.get_min_exponent()) assert res.value.get_min_exponent() == -1 assert res.uncertainties[0].uncertainty.get_min_exponent() == -1 res = _Result("", _Value(1.0), "", [_Uncertainty(0.4), _Uncertainty(0.04)], None, None) _Rounder.round_result(res) - print(res.uncertainties[0].uncertainty.get_min_exponent()) assert res.value.get_min_exponent() == -2 assert res.uncertainties[0].uncertainty.get_min_exponent() == -1 assert res.uncertainties[1].uncertainty.get_min_exponent() == -2 From 1c9d2147d12c81be8144788047ed93ef9c00184c Mon Sep 17 00:00:00 2001 From: paul019 <39464035+paul019@users.noreply.github.com> Date: Thu, 14 Mar 2024 12:01:37 +0100 Subject: [PATCH 06/25] Concentrate units and check for errors --- .vscode/settings.json | 2 ++ src/api/tables/table.py | 19 +++++++++++++++++++ src/domain/tables/column.py | 28 ++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+) diff --git a/.vscode/settings.json b/.vscode/settings.json index 2f0ed452..aa635aeb 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -67,11 +67,13 @@ "pydantic", "pylint", "pytest", + "resizebox", "sigfigs", "siunitx", "Stringifier", "textbf", "texttt", + "textwidth", "uncert", "usepackage" ], diff --git a/src/api/tables/table.py b/src/api/tables/table.py index a17135f9..f3f3c50c 100644 --- a/src/api/tables/table.py +++ b/src/api/tables/table.py @@ -17,6 +17,25 @@ def table( # Parse user input name_res = parsers.parse_name(name) + # Check if columns are valid: + if len(columns) == 0: + raise ValueError("A table must have at least one column.") + + length = None + for column in columns: + if length is None: + length = len(column.cells) + elif length != len(column.cells): + raise ValueError("All columns must have the same number of cells.") + + if length == 0: + raise ValueError("All columns must have at least one cell.") + + # Concentrate units: + if concentrate_units_if_possible: + for column in columns: + column.concentrate_units() + # Assemble the table _table = _Table( name_res, columns, caption, resize_to_fit_page_, horizontal, concentrate_units_if_possible diff --git a/src/domain/tables/column.py b/src/domain/tables/column.py index b7686c4f..b9b86cfe 100644 --- a/src/domain/tables/column.py +++ b/src/domain/tables/column.py @@ -12,3 +12,31 @@ class _Column: title: str cells: List[Union[_Result, str]] + unit: str + + def __init__(self, title: str, cells: List[Union[_Result, str]]): + self.title = title + self.cells = cells + self.unit = "" + + def concentrate_units(self): + """ + Concentrates the units of the cells in this column if possible. + """ + + unit = None + should_concentrate_units = True + + for cell in self.cells: + if isinstance(cell, _Result): + if unit is None: + unit = cell.unit + elif unit != cell.unit: + should_concentrate_units = False + break + else: + should_concentrate_units = False + break + + if should_concentrate_units and unit is not None: + self.unit = unit From eda79b8b3fe27a8bf9ed64a389ad2bc907a37cf1 Mon Sep 17 00:00:00 2001 From: paul019 <39464035+paul019@users.noreply.github.com> Date: Thu, 14 Mar 2024 12:13:05 +0100 Subject: [PATCH 07/25] Add table latexer --- src/application/latexer.py | 10 ++--- src/application/tables/latexer.py | 75 ++++++++++++++++++++++++++++--- src/domain/tables/table.py | 2 +- 3 files changed, 75 insertions(+), 12 deletions(-) diff --git a/src/application/latexer.py b/src/application/latexer.py index 5c19fb13..7a6714de 100644 --- a/src/application/latexer.py +++ b/src/application/latexer.py @@ -53,7 +53,7 @@ def result_to_latex_cmd(cls, result: _Result) -> str: else: uncertainty_name = _Helpers.number_to_word(i + 1) uncertainty_name = "error" + uncertainty_name[0].upper() + uncertainty_name[1:] - error_latex_str = cls._create_latex_str(u.uncertainty, [], result.unit) + error_latex_str = cls.create_latex_str(u.uncertainty, [], result.unit) latex_str += "\n" latex_str += rf" }}{{\ifthenelse{{\equal{{#1}}{{{uncertainty_name}}}}}{{" @@ -68,7 +68,7 @@ def result_to_latex_cmd(cls, result: _Result) -> str: short_result = result.get_short_result() _Rounder.round_result(short_result) - error_latex_str = cls._create_latex_str( + error_latex_str = cls.create_latex_str( short_result.uncertainties[0].uncertainty, [], result.unit ) @@ -123,17 +123,17 @@ def result_to_latex_str(cls, result: _Result) -> str: """ Returns the result as LaTeX string making use of the siunitx package. """ - return cls._create_latex_str(result.value, result.uncertainties, result.unit) + return cls.create_latex_str(result.value, result.uncertainties, result.unit) @classmethod def result_to_latex_str_value_only(cls, result: _Result) -> str: """ Returns only the value as LaTeX string making use of the siunitx package. """ - return cls._create_latex_str(result.value, [], result.unit) + return cls.create_latex_str(result.value, [], result.unit) @classmethod - def _create_latex_str(cls, value: _Value, uncertainties: List[_Uncertainty], unit: str) -> str: + def create_latex_str(cls, value: _Value, uncertainties: List[_Uncertainty], unit: str) -> str: """ Returns the result as LaTeX string making use of the siunitx package. diff --git a/src/application/tables/latexer.py b/src/application/tables/latexer.py index 181f0ea6..dea6db9d 100644 --- a/src/application/tables/latexer.py +++ b/src/application/tables/latexer.py @@ -1,5 +1,6 @@ -import textwrap from domain.tables.table import _Table +from domain.result import _Result +from application.latexer import _LaTeXer # Config values: @@ -17,9 +18,71 @@ def table_to_latex_cmd(cls, table: _Table) -> str: cmd_name = table_identifier + table.name[0].upper() + table.name[1:] - latex_str = rf""" - \newcommand*{{\{cmd_name}}}[1][]{{ - """ - latex_str = textwrap.dedent(latex_str).strip() + # New command: + latex_str = rf"\newcommand*{{\{cmd_name}}}[1]{{" + "\n" + + # Table header: + latex_str += r"\begin{table}[#1]" + "\n" + latex_str += r"\begin{center}" + "\n" + if table.resize_to_fit_page: + latex_str += r"\resizebox{\textwidth}{!}{" + latex_str += r"\begin{tabular}{|" + "\n" + for _ in range(len(table.columns)): + latex_str += r"c|" + latex_str += "}\n" + latex_str += r"\hline" + "\n" + + # Header row: + is_first_column = True + for column in table.columns: + if not is_first_column: + latex_str += "&" + is_first_column = False + + latex_str += rf"\textbf{{{column.title}}}" + + # Unit row: + exist_units = False + for column in table.columns: + if column.unit != "": + exist_units = True + if exist_units: + latex_str += "\\\\\n" + is_first_column = True + for column in table.columns: + if not is_first_column: + latex_str += "&" + is_first_column = False + + if column.unit != "": + latex_str += rf"$[\unit{{{column.unit}}}]$" + latex_str += "\\\\ \\hline \n" + + # Value rows: + for i, _ in enumerate(table.columns[0].cells): + is_first_column = True + for column in table.columns: + if not is_first_column: + latex_str += "&" + is_first_column = False + + cell = column.cells[i] + + if isinstance(cell, _Result): + value_str = _LaTeXer.create_latex_str(cell.value, cell.uncertainties, "") + latex_str += f"${value_str}$" + + latex_str += "\\\\\n" + + # Table footer: + latex_str += "\\hline\n" + latex_str += "\\end{tabular}\n" + if table.resize_to_fit_page: + latex_str += "}" + if table.caption != "": + latex_str += rf"\caption{{{table.caption}}}" + "\n" + latex_str += rf"\label{{{table.name}}}" + "\n" + latex_str += "\\end{center}\n" + latex_str += "\\end{table}\n" - raise NotImplementedError + return latex_str diff --git a/src/domain/tables/table.py b/src/domain/tables/table.py index c6ae199e..ccd1d792 100644 --- a/src/domain/tables/table.py +++ b/src/domain/tables/table.py @@ -13,6 +13,6 @@ class _Table: name: str columns: List[_Column] caption: str - resize_to_fit_page_: bool + resize_to_fit_page: bool horizontal: bool concentrate_units_if_possible: bool \ No newline at end of file From 0ba323160be4a4d156aaaa1b67f03f886075b32e Mon Sep 17 00:00:00 2001 From: paul019 <39464035+paul019@users.noreply.github.com> Date: Thu, 14 Mar 2024 12:14:03 +0100 Subject: [PATCH 08/25] Update __init__.py --- src/valuewizard/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/valuewizard/__init__.py b/src/valuewizard/__init__.py index 8b46b34a..c2f340b7 100644 --- a/src/valuewizard/__init__.py +++ b/src/valuewizard/__init__.py @@ -1,4 +1,7 @@ from api.res import res +from api.tables.table import table +from api.tables.column import column +from api.tables.table_res import table_res from api.export import export from application.cache import _ResultsCache From 115982e5eae96dcbe7a01e6d19dab067e93ee0ad Mon Sep 17 00:00:00 2001 From: paul019 <39464035+paul019@users.noreply.github.com> Date: Thu, 14 Mar 2024 12:17:38 +0100 Subject: [PATCH 09/25] Small fix and add to playground --- src/application/tables/latexer.py | 5 ++++- tests/playground.py | 15 +++++---------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/application/tables/latexer.py b/src/application/tables/latexer.py index dea6db9d..e6fdd798 100644 --- a/src/application/tables/latexer.py +++ b/src/application/tables/latexer.py @@ -26,7 +26,7 @@ def table_to_latex_cmd(cls, table: _Table) -> str: latex_str += r"\begin{center}" + "\n" if table.resize_to_fit_page: latex_str += r"\resizebox{\textwidth}{!}{" - latex_str += r"\begin{tabular}{|" + "\n" + latex_str += r"\begin{tabular}{|" for _ in range(len(table.columns)): latex_str += r"c|" latex_str += "}\n" @@ -71,6 +71,8 @@ def table_to_latex_cmd(cls, table: _Table) -> str: if isinstance(cell, _Result): value_str = _LaTeXer.create_latex_str(cell.value, cell.uncertainties, "") latex_str += f"${value_str}$" + else: + latex_str += str(cell) latex_str += "\\\\\n" @@ -84,5 +86,6 @@ def table_to_latex_cmd(cls, table: _Table) -> str: latex_str += rf"\label{{{table.name}}}" + "\n" latex_str += "\\end{center}\n" latex_str += "\\end{table}\n" + latex_str += "}" return latex_str diff --git a/tests/playground.py b/tests/playground.py index e1cb9318..8126314e 100644 --- a/tests/playground.py +++ b/tests/playground.py @@ -59,16 +59,11 @@ # after keyword arguments (here: uncert) # wiz.res("d", 1.0, uncert=[(0.01, "systematic"), (0.02, "stat")], r"\mm").print() -# wiz.table( -# "name", -# { -# "Header 1": ["Test", "Test2", ...], -# "Header 2": [wiz.cell_res(...), wiz.cell_res(...), ...], -# "Header 3": [wiz.cell_res(values[i], errors[i], r"\mm") for i in range(10)], -# }, -# "description", -# horizontal = True, -# ) +wiz.table( + "name", + [wiz.column("Test", ["Test"])], + "description", +) ############################# From 1b8cd05a574f7e959e8d678eb6ebb6d35c9567d9 Mon Sep 17 00:00:00 2001 From: paul019 <39464035+paul019@users.noreply.github.com> Date: Thu, 14 Mar 2024 19:10:08 +0100 Subject: [PATCH 10/25] Squashed commit of the following: commit 8e99d5aa65e41303a89ae55a2d73c1aa3fa60cc7 Author: paul019 <39464035+paul019@users.noreply.github.com> Date: Thu Mar 14 19:08:37 2024 +0100 Move parsers to api commit 2065616cd6a3ea391f515ae6d292496aa77a7187 Author: Splines Date: Thu Mar 14 14:35:09 2024 +0100 Get rid of unnecessary `_res_cache` variable `_res_cache` is already initialized in `application.cache`. commit 1d045a7a0cef05b41a369a33b5bbaa6cc11eb9e3 Author: Splines Date: Thu Mar 14 14:34:24 2024 +0100 Rename project to `ResultWizard` commit 6e000d5b8cf4c9bae80d865ee80876619cf57eb3 Author: Splines Date: Thu Mar 14 13:56:05 2024 +0100 Invoke pytest correctly in Actions commit ab951b139c4b1c576f5f91eda76dd01be0f8d815 Author: Splines Date: Thu Mar 14 13:49:53 2024 +0100 Fix tests not running with GitHub actions commit 554cca79952b18a1b9a8a1a346d7a3e2db63f57f Author: Splines Date: Thu Mar 14 13:44:10 2024 +0100 Move python testing settings to better section commit d4f9b778784b88bf2ea35fa68ceec27f163b53e6 Author: paul019 <39464035+paul019@users.noreply.github.com> Date: Thu Mar 14 11:45:12 2024 +0100 Remove print statements commit d1d7d11e7076e4dcfd40850fee41b4b4982259ff Author: paul019 <39464035+paul019@users.noreply.github.com> Date: Thu Mar 14 11:18:45 2024 +0100 Refactor res.py and move parsers to application --- .github/workflows/tests.yml | 7 +- .vscode/settings.json | 14 +- CHANGELOG.md | 2 +- DEVELOPMENT.md | 2 +- pyproject.toml | 17 ++- src/README.md | 2 +- src/api/parsers.py | 143 ++++++++++++++++++ src/api/res.py | 2 +- src/{valuewizard => resultwizard}/__init__.py | 2 - tests/playground.py | 3 +- 10 files changed, 171 insertions(+), 23 deletions(-) create mode 100644 src/api/parsers.py rename src/{valuewizard => resultwizard}/__init__.py (87%) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4d48587c..d6da7942 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -29,9 +29,12 @@ jobs: - name: Install dependencies (including dev dependencies) run: pipenv install --dev + - name: Install ResultWizard itself (as editable) + run: pipenv run pip3 install --editable . + - name: Run Pytest - run: pipenv run pytest + run: pipenv run pytest tests/ -# [1] https://github.com/orgs/community/discussions/26366 \ No newline at end of file +# [1] https://github.com/orgs/community/discussions/26366 diff --git a/.vscode/settings.json b/.vscode/settings.json index aa635aeb..1bf2f3ad 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -19,6 +19,11 @@ "pythonTestExplorer.testFramework": "pytest", "testExplorer.codeLens": true, "testExplorer.errorDecorationHover": true, + "python.testing.pytestArgs": [ + "." + ], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true, ////////////////////////////////////// // Files ////////////////////////////////////// @@ -67,7 +72,7 @@ "pydantic", "pylint", "pytest", - "resizebox", + "resultwizard", "sigfigs", "siunitx", "Stringifier", @@ -76,10 +81,5 @@ "textwidth", "uncert", "usepackage" - ], - "python.testing.pytestArgs": [ - "." - ], - "python.testing.unittestEnabled": false, - "python.testing.pytestEnabled": true + ] } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 142531cf..b90e5bfa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,3 @@ -# Changelog of ValueWizard +# Changelog of ResultWizard TODO diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 9a220b05..0ef362b0 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -9,7 +9,7 @@ Getting ready: - [ ] Recommended VSCode extensions installed (especially the formatter. It should automatically format on every save!) - [ ] on branch `value-wizard` with latest commit pulled - [ ] Work through the `Setup` section below (especially to install the necessary dependencies) -- [ ] Read the [`README.md`](https://github.com/paul019/ValueWizard/tree/value-wizard/src#code-structure) in the `src` folder (to get to know the code structure) & see our [feature list](https://github.com/paul019/ValueWizard/issues/16) +- [ ] Read the [`README.md`](https://github.com/paul019/ResultWizard/tree/value-wizard/src#code-structure) in the `src` folder (to get to know the code structure) & see our [feature list](https://github.com/paul019/ResultWizard/issues/16) Verify that everything worked: - [ ] try to run the tests, see the instructions in [`tests/playground.py`](./tests/playground.py) diff --git a/pyproject.toml b/pyproject.toml index fe027305..3c4096af 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,11 +7,11 @@ build-backend = "setuptools.build_meta" [tool.setuptools.packages.find] where = ["src"] -include = ["valuewizard"] +include = ["resultwizard"] namespaces = false [project] -name = "valuewizard" +name = "resultwizard" version = "0.1" authors = [ { name = "Paul Obernolte (paul019)", email = "todo@todo.de" }, @@ -47,10 +47,15 @@ classifiers = [ ] [project.urls] -Homepage = "https://github.com/paul019/ValueWizard" -Repository = "https://github.com/paul019/ValueWizard" -Issues = "https://github.com/paul019/ValueWizard/issues" -Changelog = "https://github.com/paul019/ValueWizard/blob/main/CHANGELOG.md" +Homepage = "https://github.com/paul019/ResultWizard" +Repository = "https://github.com/paul019/ResultWizard" +Issues = "https://github.com/paul019/ResultWizard/issues" +Changelog = "https://github.com/paul019/ResultWizard/blob/main/CHANGELOG.md" + +[tool.pytest.ini_options] +addopts = [ + "--import-mode=importlib", +] # TODO: Add these checks back [tool.pylint."messages control"] diff --git a/src/README.md b/src/README.md index bad7bfd6..d62045b4 100644 --- a/src/README.md +++ b/src/README.md @@ -4,7 +4,7 @@ We use the clean architecture paradigm. Modules in outer layers are allowed to i From outer layer to inner layer: -- `valuewizard/`: Entrypoint for the PIP package; mainly only import statements +- `resultwizard/`: Entrypoint for the PIP package; mainly only import statements - `api/`: User-facing API, e.g. `res()` and `export()` method - `application/`: Application code that uses the domain logic to solve specific problems - `domain/`: Domain logic, e.g. definition of what a `value`, how things are represented internally diff --git a/src/api/parsers.py b/src/api/parsers.py new file mode 100644 index 00000000..be221b66 --- /dev/null +++ b/src/api/parsers.py @@ -0,0 +1,143 @@ +from typing import Union, List, Tuple + +from application.helpers import _Helpers +from domain.value import _Value +from domain.uncertainty import _Uncertainty + +def check_if_number_string(value: str) -> None: + """Raises a ValueError if the string is not a valid number.""" + try: + float(value) + except ValueError as exc: + raise ValueError(f"String value must be a valid number, not {value}") from exc + + + +def parse_name(name: str) -> str: + """Parses the name.""" + if not isinstance(name, str): + raise TypeError(f"`name` must be a string, not {type(name)}") + + name = ( + name.replace("ä", "ae") + .replace("ö", "oe") + .replace("ü", "ue") + .replace("Ä", "Ae") + .replace("Ö", "Oe") + .replace("Ü", "Ue") + .replace("ß", "ss") + ) + + parsed_name = "" + next_chat_upper = False + + for char in name: + if char.isalpha(): + if next_chat_upper: + parsed_name += char.upper() + next_chat_upper = False + else: + parsed_name += char + elif char.isdigit(): + digit = _Helpers.number_to_word(int(char)) + if parsed_name == "": + parsed_name += digit + else: + parsed_name += digit[0].upper() + digit[1:] + next_chat_upper = True + elif char in [" ", "_", "-"]: + next_chat_upper = True + + return parsed_name + + +def parse_unit(unit: str) -> str: + """Parses the unit.""" + if not isinstance(unit, str): + raise TypeError(f"`unit` must be a string, not {type(unit)}") + + # TODO: maybe add some basic checks to catch siunitx errors, e.g. + # unsupported symbols etc. But maybe leave this to LaTeX and just return + # the LaTeX later on. But catching it here would be more user-friendly, + # as the user would get immediate feedback and not only once they try to + # export the results. + return unit + + +def parse_sigfigs(sigfigs: Union[int, None]) -> Union[int, None]: + """Parses the number of sigfigs.""" + if sigfigs is None: + return None + + if not isinstance(sigfigs, int): + raise TypeError(f"`sigfigs` must be an int, not {type(sigfigs)}") + + if sigfigs < 1: + raise ValueError("`sigfigs` must be positive") + + return sigfigs + + +def parse_decimal_places(decimal_places: Union[int, None]) -> Union[int, None]: + """Parses the number of sigfigs.""" + if decimal_places is None: + return None + + if not isinstance(decimal_places, int): + raise TypeError(f"`decimal_places` must be an int, not {type(decimal_places)}") + + if decimal_places < 0: + raise ValueError("`decimal_places` must be non-negative") + + return decimal_places + + +def parse_value(value: Union[float, str]) -> _Value: + """Converts the value to a _Value object.""" + if not isinstance(value, (float, str)): + raise TypeError(f"`value` must be a float or string, not {type(value)}") + + if isinstance(value, str): + check_if_number_string(value) + + return _Value(value) + + +def parse_uncertainties( + uncertainties: Union[ + float, + str, + Tuple[Union[float, str], str], + List[Union[float, str, Tuple[Union[float, str], str]]], + ] +) -> List[_Uncertainty]: + """Converts the uncertainties to a list of _Uncertainty objects.""" + uncertainties_res = [] + + # no list, but a single value was given + if isinstance(uncertainties, (float, str, Tuple)): + uncertainties = [uncertainties] + + assert isinstance(uncertainties, List) + + for uncert in uncertainties: + if isinstance(uncert, (float, str)): + if isinstance(uncert, str): + check_if_number_string(uncert) + if float(uncert) <= 0: + raise ValueError("Uncertainty must be positive.") + uncertainties_res.append(_Uncertainty(uncert)) + + elif isinstance(uncert, Tuple): + if not isinstance(uncert[0], (float, str)): + raise TypeError( + f"First argument of uncertainty-tuple must be a float or a string, not {type(uncert[0])}" + ) + if isinstance(uncert[0], str): + check_if_number_string(uncert[0]) + uncertainties_res.append(_Uncertainty(uncert[0], parse_name(uncert[1]))) + + else: + raise TypeError(f"Each uncertainty must be a tuple or a float/str, not {type(uncert)}") + + return uncertainties_res diff --git a/src/api/res.py b/src/api/res.py index 5aded65f..8c5ad148 100644 --- a/src/api/res.py +++ b/src/api/res.py @@ -4,7 +4,7 @@ from api.printable_result import PrintableResult from application.cache import _res_cache from application.rounder import _Rounder -import application.parsers as parsers +import api.parsers as parsers from domain.result import _Result diff --git a/src/valuewizard/__init__.py b/src/resultwizard/__init__.py similarity index 87% rename from src/valuewizard/__init__.py rename to src/resultwizard/__init__.py index c2f340b7..cb258914 100644 --- a/src/valuewizard/__init__.py +++ b/src/resultwizard/__init__.py @@ -4,5 +4,3 @@ from api.tables.table_res import table_res from api.export import export from application.cache import _ResultsCache - -_res_cache = _ResultsCache() diff --git a/tests/playground.py b/tests/playground.py index 8126314e..82dfdc8c 100644 --- a/tests/playground.py +++ b/tests/playground.py @@ -7,8 +7,7 @@ # From: https://setuptools.pypa.io/en/latest/userguide/quickstart.html#development-mode -# TODO rename to ResultWizard -import valuewizard as wiz +import resultwizard as wiz print("#############################") print("### Playground") From e66d692270ca11cac66b37fe6b4ecea9581a27d4 Mon Sep 17 00:00:00 2001 From: paul019 <39464035+paul019@users.noreply.github.com> Date: Thu, 14 Mar 2024 19:10:23 +0100 Subject: [PATCH 11/25] Fix imports --- src/api/tables/table.py | 2 +- src/api/tables/table_res.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/api/tables/table.py b/src/api/tables/table.py index f3f3c50c..a00b6d3a 100644 --- a/src/api/tables/table.py +++ b/src/api/tables/table.py @@ -1,7 +1,7 @@ from typing import List from application.cache import _res_cache -import application.parsers as parsers +import api.parsers as parsers from domain.tables.column import _Column from domain.tables.table import _Table diff --git a/src/api/tables/table_res.py b/src/api/tables/table_res.py index e7528237..24ba0b2b 100644 --- a/src/api/tables/table_res.py +++ b/src/api/tables/table_res.py @@ -2,7 +2,7 @@ from plum import dispatch, overload from application.rounder import _Rounder -import application.parsers as parsers +import api.parsers as parsers from domain.result import _Result From 85ebbcc9edf928074e1be3ece17a892f18f062e6 Mon Sep 17 00:00:00 2001 From: paul019 <39464035+paul019@users.noreply.github.com> Date: Thu, 14 Mar 2024 19:13:32 +0100 Subject: [PATCH 12/25] Remove property "name" from table_res --- src/api/tables/table_res.py | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/src/api/tables/table_res.py b/src/api/tables/table_res.py index 24ba0b2b..1cbb10bd 100644 --- a/src/api/tables/table_res.py +++ b/src/api/tables/table_res.py @@ -16,18 +16,16 @@ @overload def table_res( - name: str, value: Union[float, str], unit: str = "", sigfigs: Union[int, None] = None, decimal_places: Union[int, None] = None, ) -> _Result: - return table_res(name, value, [], unit, sigfigs, decimal_places) + return table_res(value, [], unit, sigfigs, decimal_places) @overload def table_res( - name: str, value: Union[float, str], uncert: Union[ float, @@ -39,22 +37,20 @@ def table_res( sigfigs: Union[int, None] = None, decimal_places: Union[int, None] = None, ) -> _Result: - return table_res(name, value, uncert, "", sigfigs, decimal_places) + return table_res(value, uncert, "", sigfigs, decimal_places) @overload def table_res( - name: str, value: Union[float, str], sigfigs: Union[int, None] = None, decimal_places: Union[int, None] = None, ) -> _Result: - return table_res(name, value, [], "", sigfigs, decimal_places) + return table_res(value, [], "", sigfigs, decimal_places) @overload def table_res( - name: str, value: Union[float, str], sys: float, stat: float, @@ -62,12 +58,11 @@ def table_res( sigfigs: Union[int, None] = None, decimal_places: Union[int, None] = None, ) -> _Result: - return table_res(name, value, [(sys, "sys"), (stat, "stat")], unit, sigfigs, decimal_places) + return table_res(value, [(sys, "sys"), (stat, "stat")], unit, sigfigs, decimal_places) @overload def table_res( - name: str, value: Union[float, str], uncert: Union[ float, @@ -84,7 +79,6 @@ def table_res( uncert = [] # Parse user input - name_res = parsers.parse_name(name) value_res = parsers.parse_value(value) uncertainties_res = parsers.parse_uncertainties(uncert) unit_res = parsers.parse_unit(unit) @@ -92,9 +86,7 @@ def table_res( decimal_places_res = parsers.parse_decimal_places(decimal_places) # Assemble the result - result = _Result( - name_res, value_res, unit_res, uncertainties_res, sigfigs_res, decimal_places_res - ) + result = _Result("", value_res, unit_res, uncertainties_res, sigfigs_res, decimal_places_res) _Rounder.round_result(result) return result @@ -107,4 +99,4 @@ def table_res( def table_res(*args, **kwargs) -> object: # This method only scans for all `overload`-decorated methods # and properly adds them as Plum methods. - pass \ No newline at end of file + pass From 427df27c505229d8c3d4c13b1cf251b394de4ac3 Mon Sep 17 00:00:00 2001 From: paul019 <39464035+paul019@users.noreply.github.com> Date: Thu, 14 Mar 2024 19:20:29 +0100 Subject: [PATCH 13/25] Fix display of units in tables --- src/application/tables/latexer.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/application/tables/latexer.py b/src/application/tables/latexer.py index e6fdd798..9bf0870c 100644 --- a/src/application/tables/latexer.py +++ b/src/application/tables/latexer.py @@ -69,7 +69,9 @@ def table_to_latex_cmd(cls, table: _Table) -> str: cell = column.cells[i] if isinstance(cell, _Result): - value_str = _LaTeXer.create_latex_str(cell.value, cell.uncertainties, "") + value_str = _LaTeXer.create_latex_str( + cell.value, cell.uncertainties, cell.unit if column.unit == "" else "" + ) latex_str += f"${value_str}$" else: latex_str += str(cell) From 90051985a7f5d4e819463c6dcf9b6163c6ceb7a9 Mon Sep 17 00:00:00 2001 From: paul019 <39464035+paul019@users.noreply.github.com> Date: Thu, 14 Mar 2024 21:13:03 +0100 Subject: [PATCH 14/25] Update playground.py --- tests/playground.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/tests/playground.py b/tests/playground.py index 82dfdc8c..64d2c14f 100644 --- a/tests/playground.py +++ b/tests/playground.py @@ -8,6 +8,7 @@ import resultwizard as wiz +from random import random print("#############################") print("### Playground") @@ -60,7 +61,25 @@ wiz.table( "name", - [wiz.column("Test", ["Test"])], + [ + wiz.column("Num.", [f"{i+1}" for i in range(10)]), + wiz.column( + "Random 1", [wiz.table_res(random(), random() * 0.1, r"\mm") for i in range(10)] + ), + wiz.column( + "Random 2", + [wiz.table_res(random(), random() * 0.1, r"\electronvolt") for i in range(10)], + ), + wiz.column( + "Random 3", + [ + wiz.table_res( + random(), random() * 0.1, r"\electronvolt" if random() > 0.5 else r"\mm" + ) + for i in range(10) + ], + ), + ], "description", ) From 7f880b6e0d1f19e4bfadf28d79808faa06d8dcb2 Mon Sep 17 00:00:00 2001 From: paul019 <39464035+paul019@users.noreply.github.com> Date: Thu, 14 Mar 2024 21:28:59 +0100 Subject: [PATCH 15/25] Make float mode of tables an optional argument in LaTeX command --- src/application/tables/latexer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/application/tables/latexer.py b/src/application/tables/latexer.py index 9bf0870c..73166196 100644 --- a/src/application/tables/latexer.py +++ b/src/application/tables/latexer.py @@ -19,7 +19,7 @@ def table_to_latex_cmd(cls, table: _Table) -> str: cmd_name = table_identifier + table.name[0].upper() + table.name[1:] # New command: - latex_str = rf"\newcommand*{{\{cmd_name}}}[1]{{" + "\n" + latex_str = rf"\newcommand*{{\{cmd_name}}}[1][]{{" + "\n" # Table header: latex_str += r"\begin{table}[#1]" + "\n" From 5c550a85b3502a9740e9dfa7e365799406ded9fb Mon Sep 17 00:00:00 2001 From: paul019 <39464035+paul019@users.noreply.github.com> Date: Thu, 14 Mar 2024 21:29:24 +0100 Subject: [PATCH 16/25] Make it possible for single columns to overwrite table settings for unit concentration --- src/api/tables/column.py | 3 ++- src/api/tables/table.py | 5 ++--- src/domain/tables/column.py | 29 ++++++++++++++++++++++++----- tests/playground.py | 1 + 4 files changed, 29 insertions(+), 9 deletions(-) diff --git a/src/api/tables/column.py b/src/api/tables/column.py index e79153a8..d95bb74d 100644 --- a/src/api/tables/column.py +++ b/src/api/tables/column.py @@ -7,5 +7,6 @@ def column( title: str, cells: List[Union[_Result, str]], + concentrate_units_if_possible: Union[bool, None] = None, ) -> _Column: - return _Column(title, cells) \ No newline at end of file + return _Column(title, cells, concentrate_units_if_possible) diff --git a/src/api/tables/table.py b/src/api/tables/table.py index a00b6d3a..b51f4544 100644 --- a/src/api/tables/table.py +++ b/src/api/tables/table.py @@ -32,9 +32,8 @@ def table( raise ValueError("All columns must have at least one cell.") # Concentrate units: - if concentrate_units_if_possible: - for column in columns: - column.concentrate_units() + for column in columns: + column.concentrate_units(concentrate_units_if_possible) # Assemble the table _table = _Table( diff --git a/src/domain/tables/column.py b/src/domain/tables/column.py index b9b86cfe..26dbaaca 100644 --- a/src/domain/tables/column.py +++ b/src/domain/tables/column.py @@ -13,20 +13,38 @@ class _Column: title: str cells: List[Union[_Result, str]] unit: str + concentrate_units_if_possible: Union[bool, None] + + def __init__( + self, + title: str, + cells: List[Union[_Result, str]], + concentrate_units_if_possible: Union[bool, None] = None, + ): + """ + Init method. - def __init__(self, title: str, cells: List[Union[_Result, str]]): + The parameter `concentrate_units_if_possible` is `None` by default and only overwrites the + master setting from the table object if it is manually set to `True` or `False`. + """ self.title = title self.cells = cells self.unit = "" + self.concentrate_units_if_possible = concentrate_units_if_possible - def concentrate_units(self): + def concentrate_units(self, concentrate_units_if_possible_master: bool): """ - Concentrates the units of the cells in this column if possible. + Concentrates the units of the cells in this column if possible and if desired. """ - unit = None - should_concentrate_units = True + # Check if concentration of units is desired: + if self.concentrate_units_if_possible is not None: + should_concentrate_units = self.concentrate_units_if_possible + else: + should_concentrate_units = concentrate_units_if_possible_master + # Check if concentration of units is possible given the cell values: + unit = None for cell in self.cells: if isinstance(cell, _Result): if unit is None: @@ -38,5 +56,6 @@ def concentrate_units(self): should_concentrate_units = False break + if should_concentrate_units and unit is not None: self.unit = unit diff --git a/tests/playground.py b/tests/playground.py index 64d2c14f..4f1692a4 100644 --- a/tests/playground.py +++ b/tests/playground.py @@ -69,6 +69,7 @@ wiz.column( "Random 2", [wiz.table_res(random(), random() * 0.1, r"\electronvolt") for i in range(10)], + concentrate_units_if_possible=False, ), wiz.column( "Random 3", From 4b2b467fe905b4d72180ad22154c252bc57de771 Mon Sep 17 00:00:00 2001 From: paul019 <39464035+paul019@users.noreply.github.com> Date: Thu, 14 Mar 2024 21:34:52 +0100 Subject: [PATCH 17/25] Squashed commit of the following: commit b700b331d3ba57851e0ac47c0c6373a20442d14d Author: paul019 <39464035+paul019@users.noreply.github.com> Date: Thu Mar 14 21:32:49 2024 +0100 Make small fix regarding parenthesis in output strings and test it in playground commit 8e99d5aa65e41303a89ae55a2d73c1aa3fa60cc7 Author: paul019 <39464035+paul019@users.noreply.github.com> Date: Thu Mar 14 19:08:37 2024 +0100 Move parsers to api commit 2065616cd6a3ea391f515ae6d292496aa77a7187 Author: Splines Date: Thu Mar 14 14:35:09 2024 +0100 Get rid of unnecessary `_res_cache` variable `_res_cache` is already initialized in `application.cache`. commit 1d045a7a0cef05b41a369a33b5bbaa6cc11eb9e3 Author: Splines Date: Thu Mar 14 14:34:24 2024 +0100 Rename project to `ResultWizard` commit 6e000d5b8cf4c9bae80d865ee80876619cf57eb3 Author: Splines Date: Thu Mar 14 13:56:05 2024 +0100 Invoke pytest correctly in Actions commit ab951b139c4b1c576f5f91eda76dd01be0f8d815 Author: Splines Date: Thu Mar 14 13:49:53 2024 +0100 Fix tests not running with GitHub actions commit 554cca79952b18a1b9a8a1a346d7a3e2db63f57f Author: Splines Date: Thu Mar 14 13:44:10 2024 +0100 Move python testing settings to better section commit d4f9b778784b88bf2ea35fa68ceec27f163b53e6 Author: paul019 <39464035+paul019@users.noreply.github.com> Date: Thu Mar 14 11:45:12 2024 +0100 Remove print statements commit d1d7d11e7076e4dcfd40850fee41b4b4982259ff Author: paul019 <39464035+paul019@users.noreply.github.com> Date: Thu Mar 14 11:18:45 2024 +0100 Refactor res.py and move parsers to application --- src/api/stringifier.py | 4 ++-- src/application/latexer.py | 4 ++-- tests/playground.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/api/stringifier.py b/src/api/stringifier.py index 884e0c75..5b1d72b3 100644 --- a/src/api/stringifier.py +++ b/src/api/stringifier.py @@ -67,7 +67,7 @@ def result_to_str(cls, result: _Result): string += rf"e{exponent}" else: - if len(uncertainties) > 0: + if len(uncertainties) > 0 and unit != "": string += "(" value_normalized = value.get_abs() @@ -83,7 +83,7 @@ def result_to_str(cls, result: _Result): if len(uncertainties) > 1: string += rf" ({u.name})" - if len(uncertainties) > 0: + if len(uncertainties) > 0 and unit != "": string += ")" if has_unit: diff --git a/src/application/latexer.py b/src/application/latexer.py index 7a6714de..e8014f48 100644 --- a/src/application/latexer.py +++ b/src/application/latexer.py @@ -189,7 +189,7 @@ def create_latex_str(cls, value: _Value, uncertainties: List[_Uncertainty], unit latex_str += rf" \cdot 10^{{{exponent}}}" else: - if len(uncertainties) > 0: + if len(uncertainties) > 0 and unit != "": latex_str += "(" value_normalized = value.get_abs() @@ -202,7 +202,7 @@ def create_latex_str(cls, value: _Value, uncertainties: List[_Uncertainty], unit if len(uncertainties) > 1: latex_str += rf"_{{\text{{{u.name}}}}}" - if len(uncertainties) > 0: + if len(uncertainties) > 0 and unit != "": latex_str += ")" if has_unit: diff --git a/tests/playground.py b/tests/playground.py index 4f1692a4..0a5fc2d1 100644 --- a/tests/playground.py +++ b/tests/playground.py @@ -49,7 +49,7 @@ # wiz.standard_sigfigs(3) -wiz.res("f", "1.0e4").print() +wiz.res("f", "1.0e1", 25e-1).print() # f: 1.0 # wiz.res("g", 1.0, sys=0.01, stat=0.02, unit=r"\mm").print() From 72e222a22b4907c51fa8b1de4929f84b1050498e Mon Sep 17 00:00:00 2001 From: paul019 <39464035+paul019@users.noreply.github.com> Date: Thu, 14 Mar 2024 21:40:30 +0100 Subject: [PATCH 18/25] Allow for customization of table label --- src/api/tables/table.py | 11 +++++++++-- src/application/tables/latexer.py | 2 +- src/domain/tables/table.py | 1 + tests/playground.py | 3 ++- 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/api/tables/table.py b/src/api/tables/table.py index b51f4544..eaaa061e 100644 --- a/src/api/tables/table.py +++ b/src/api/tables/table.py @@ -1,4 +1,4 @@ -from typing import List +from typing import List, Union from application.cache import _res_cache import api.parsers as parsers @@ -10,6 +10,7 @@ def table( name: str, columns: List[_Column], caption: str, + label: Union[str, None] = None, resize_to_fit_page_: bool = False, horizontal: bool = False, concentrate_units_if_possible: bool = True, @@ -37,7 +38,13 @@ def table( # Assemble the table _table = _Table( - name_res, columns, caption, resize_to_fit_page_, horizontal, concentrate_units_if_possible + name_res, + columns, + caption, + label if label is not None else name_res, + resize_to_fit_page_, + horizontal, + concentrate_units_if_possible, ) _res_cache.add_table(name, _table) diff --git a/src/application/tables/latexer.py b/src/application/tables/latexer.py index 73166196..44c0d2c1 100644 --- a/src/application/tables/latexer.py +++ b/src/application/tables/latexer.py @@ -85,7 +85,7 @@ def table_to_latex_cmd(cls, table: _Table) -> str: latex_str += "}" if table.caption != "": latex_str += rf"\caption{{{table.caption}}}" + "\n" - latex_str += rf"\label{{{table.name}}}" + "\n" + latex_str += rf"\label{{{table.label}}}" + "\n" latex_str += "\\end{center}\n" latex_str += "\\end{table}\n" latex_str += "}" diff --git a/src/domain/tables/table.py b/src/domain/tables/table.py index ccd1d792..7b0935be 100644 --- a/src/domain/tables/table.py +++ b/src/domain/tables/table.py @@ -13,6 +13,7 @@ class _Table: name: str columns: List[_Column] caption: str + label: str resize_to_fit_page: bool horizontal: bool concentrate_units_if_possible: bool \ No newline at end of file diff --git a/tests/playground.py b/tests/playground.py index 0a5fc2d1..484172af 100644 --- a/tests/playground.py +++ b/tests/playground.py @@ -7,8 +7,8 @@ # From: https://setuptools.pypa.io/en/latest/userguide/quickstart.html#development-mode -import resultwizard as wiz from random import random +import resultwizard as wiz print("#############################") print("### Playground") @@ -82,6 +82,7 @@ ), ], "description", + resize_to_fit_page_=True ) From 5478160814b4b6c5ef42f2e06900c4ceb466fddc Mon Sep 17 00:00:00 2001 From: paul019 <39464035+paul019@users.noreply.github.com> Date: Thu, 14 Mar 2024 22:00:53 +0100 Subject: [PATCH 19/25] Add support for horizontal table layouts --- src/application/tables/latexer.py | 77 +++++++++++++++++++++++++------ tests/playground.py | 28 +++++++++++ 2 files changed, 90 insertions(+), 15 deletions(-) diff --git a/src/application/tables/latexer.py b/src/application/tables/latexer.py index 44c0d2c1..075179c8 100644 --- a/src/application/tables/latexer.py +++ b/src/application/tables/latexer.py @@ -26,7 +26,27 @@ def table_to_latex_cmd(cls, table: _Table) -> str: latex_str += r"\begin{center}" + "\n" if table.resize_to_fit_page: latex_str += r"\resizebox{\textwidth}{!}{" - latex_str += r"\begin{tabular}{|" + + if table.horizontal: + latex_str += cls._table_to_latex_tabular_horizontal(table) + else: + latex_str += cls._table_to_latex_tabular_vertical(table) + + # Table footer: + if table.resize_to_fit_page: + latex_str += "}" + if table.caption != "": + latex_str += rf"\caption{{{table.caption}}}" + "\n" + latex_str += rf"\label{{{table.label}}}" + "\n" + latex_str += "\\end{center}\n" + latex_str += "\\end{table}\n" + latex_str += "}" + + return latex_str + + @classmethod + def _table_to_latex_tabular_vertical(cls, table: _Table) -> str: + latex_str = r"\begin{tabular}{|" for _ in range(len(table.columns)): latex_str += r"c|" latex_str += "}\n" @@ -42,11 +62,7 @@ def table_to_latex_cmd(cls, table: _Table) -> str: latex_str += rf"\textbf{{{column.title}}}" # Unit row: - exist_units = False - for column in table.columns: - if column.unit != "": - exist_units = True - if exist_units: + if cls._exist_units(table): latex_str += "\\\\\n" is_first_column = True for column in table.columns: @@ -78,16 +94,47 @@ def table_to_latex_cmd(cls, table: _Table) -> str: latex_str += "\\\\\n" - # Table footer: latex_str += "\\hline\n" latex_str += "\\end{tabular}\n" - if table.resize_to_fit_page: - latex_str += "}" - if table.caption != "": - latex_str += rf"\caption{{{table.caption}}}" + "\n" - latex_str += rf"\label{{{table.label}}}" + "\n" - latex_str += "\\end{center}\n" - latex_str += "\\end{table}\n" - latex_str += "}" return latex_str + + @classmethod + def _table_to_latex_tabular_horizontal(cls, table: _Table) -> str: + latex_str = r"\begin{tabular}{|l" + if cls._exist_units(table): + latex_str += r"c||" + else: + latex_str += r"||" + for _ in range(len(table.columns[0].cells)): + latex_str += r"c|" + latex_str += "}\n" + latex_str += r"\hline" + "\n" + + # Iterate through columns (that are rows now): + for row in table.columns: + latex_str += rf"\textbf{{{row.title}}}" + if row.unit != "": + latex_str += rf" & $[\unit{{{row.unit}}}]$" + else: + latex_str += " & " + for cell in row.cells: + if isinstance(cell, _Result): + value_str = _LaTeXer.create_latex_str( + cell.value, cell.uncertainties, cell.unit if row.unit == "" else "" + ) + latex_str += f" & ${value_str}$" + else: + latex_str += f" & {cell}" + latex_str += "\\\\ \\hline \n" + + latex_str += "\\end{tabular}\n" + + return latex_str + + @classmethod + def _exist_units(cls, table: _Table) -> bool: + for column in table.columns: + if column.unit != "": + return True + return False diff --git a/tests/playground.py b/tests/playground.py index 484172af..6ed2a308 100644 --- a/tests/playground.py +++ b/tests/playground.py @@ -85,6 +85,34 @@ resize_to_fit_page_=True ) +wiz.table( + "name horizontal", + [ + wiz.column("Num.", [f"{i+1}" for i in range(4)]), + wiz.column( + "Random 1", [wiz.table_res(random(), random() * 0.1, r"\mm") for i in range(4)] + ), + wiz.column( + "Random 2", + [wiz.table_res(random(), random() * 0.1, r"\electronvolt") for i in range(4)], + concentrate_units_if_possible=False, + ), + wiz.column( + "Random 3", + [ + wiz.table_res( + random(), random() * 0.1, r"\electronvolt" if random() > 0.5 else r"\mm" + ) + for i in range(4) + ], + ), + ], + "description", + horizontal=True, + resize_to_fit_page_=True, + label="tab:horizontal", +) + ############################# # Export From 1e97f774b5afa8f2e725623aa4706d660c1ce553 Mon Sep 17 00:00:00 2001 From: paul019 <39464035+paul019@users.noreply.github.com> Date: Thu, 14 Mar 2024 22:03:06 +0100 Subject: [PATCH 20/25] Change behavior if user does not specify table label --- src/api/tables/table.py | 2 +- src/application/tables/latexer.py | 4 ++-- src/domain/tables/table.py | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/api/tables/table.py b/src/api/tables/table.py index eaaa061e..85d53e77 100644 --- a/src/api/tables/table.py +++ b/src/api/tables/table.py @@ -41,7 +41,7 @@ def table( name_res, columns, caption, - label if label is not None else name_res, + label, resize_to_fit_page_, horizontal, concentrate_units_if_possible, diff --git a/src/application/tables/latexer.py b/src/application/tables/latexer.py index 075179c8..8bff4001 100644 --- a/src/application/tables/latexer.py +++ b/src/application/tables/latexer.py @@ -37,7 +37,7 @@ def table_to_latex_cmd(cls, table: _Table) -> str: latex_str += "}" if table.caption != "": latex_str += rf"\caption{{{table.caption}}}" + "\n" - latex_str += rf"\label{{{table.label}}}" + "\n" + latex_str += rf"\label{{{table.label if table.label is not None else cmd_name}}}" + "\n" latex_str += "\\end{center}\n" latex_str += "\\end{table}\n" latex_str += "}" @@ -127,7 +127,7 @@ def _table_to_latex_tabular_horizontal(cls, table: _Table) -> str: else: latex_str += f" & {cell}" latex_str += "\\\\ \\hline \n" - + latex_str += "\\end{tabular}\n" return latex_str diff --git a/src/domain/tables/table.py b/src/domain/tables/table.py index 7b0935be..208947a5 100644 --- a/src/domain/tables/table.py +++ b/src/domain/tables/table.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from typing import List +from typing import List, Union from domain.tables.column import _Column @@ -13,7 +13,7 @@ class _Table: name: str columns: List[_Column] caption: str - label: str + label: Union[str, None] resize_to_fit_page: bool horizontal: bool - concentrate_units_if_possible: bool \ No newline at end of file + concentrate_units_if_possible: bool From da1127a0acebc9de72d8a25d18ca343bcd1b7176 Mon Sep 17 00:00:00 2001 From: paul019 <39464035+paul019@users.noreply.github.com> Date: Thu, 14 Mar 2024 22:12:46 +0100 Subject: [PATCH 21/25] Fix issue regarding the formatting of horizontal tables --- src/application/tables/latexer.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/application/tables/latexer.py b/src/application/tables/latexer.py index 8bff4001..2b65fa6f 100644 --- a/src/application/tables/latexer.py +++ b/src/application/tables/latexer.py @@ -113,11 +113,17 @@ def _table_to_latex_tabular_horizontal(cls, table: _Table) -> str: # Iterate through columns (that are rows now): for row in table.columns: + # Header column: latex_str += rf"\textbf{{{row.title}}}" - if row.unit != "": - latex_str += rf" & $[\unit{{{row.unit}}}]$" - else: - latex_str += " & " + + # Unit column: + if cls._exist_units(table): + if row.unit != "": + latex_str += rf" & $[\unit{{{row.unit}}}]$" + else: + latex_str += " & " + + # Value columns: for cell in row.cells: if isinstance(cell, _Result): value_str = _LaTeXer.create_latex_str( From e6851e8f2a276629611d1cbbee6799f0c69a0ff9 Mon Sep 17 00:00:00 2001 From: paul019 <39464035+paul019@users.noreply.github.com> Date: Mon, 9 Sep 2024 11:28:22 +0200 Subject: [PATCH 22/25] Squashed commit of the following: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit commit e8d3467f984b4fc8454618a2072a7003a03a6b56 Author: Splines <37160523+Splines@users.noreply.github.com> Date: Fri Sep 6 19:02:36 2024 +0200 Implement new `wiz.res()` API (#41) * Remove plum as dependency * Implement new `wiz.res()` API (see #33) * Fix errors in error messages * Use consistent comment format in error messages * Add docstrings to `wiz.res()` * Add missing `int` type for uncertainties * Add PrintableResult to res() method signature * Rename `uncert` to `uncerts` (plural) commit cc6bf5b20b239ac662c42acdb262f686f142c161 Author: paul019 <39464035+paul019@users.noreply.github.com> Date: Sat Apr 27 20:25:54 2024 +0200 Improve wording: use `uncertainty` instead of `error` (#43) * Remove spaces in units * Detect percent as unit in console stringifier * Replace error by uncert * Undo unwanted changes * Replace error by uncertainty in various places commit 62c34a571fa8250e0a09027c9f548be19a3a7186 Author: Splines <37160523+Splines@users.noreply.github.com> Date: Sat Apr 27 19:45:34 2024 +0200 Add missing `site.baseurl` to relative links (#52) commit f5e0007ef39d3b1e0ea185a17cda60f5cef96de5 Author: Splines <37160523+Splines@users.noreply.github.com> Date: Sat Apr 27 19:45:19 2024 +0200 Truncate Readme & refer to documentation (#51) * Truncate Readme & refer to documentation * Remove GitHub-flavored markdown Otherwise, it won't be rendered correctly on PyPI. * Make Readme image clickable (points to docs) commit 7af242c4f74feafc99e83d220bb88df5e2e04dc9 Author: Splines <37160523+Splines@users.noreply.github.com> Date: Wed Apr 24 17:56:11 2024 +0200 Add first version of docs (#37) * Init _Just the docs_ basic structure * Add Jupyter-related items to TODO * Add GitHub pages docs workflow * Add 404 page * Add landing page of documentation * Add LaTeX rendered output * Add quickstart & backbone for other pages * Add `export()` & siunitx troubleshooting * Add docs for `wiz.res()` * Add `wiz.res()` tip for multiple occurrences * Add config options & siunitx tips * Improve Jupyter section & add "Is this really for me?" * Add about page * Add table of contents (TOC) back to about page * Add docs build workflow for PRs * Specify correct cwd for docs workflow * Fix remaining docs workflow errors * Add tip for how to suppress output in JupyterNotebook. This fixes #44. commit 63f2a6b617eb6b787bb9a2b67dd61986e47c90f4 Author: Splines <37160523+Splines@users.noreply.github.com> Date: Mon Apr 22 23:31:50 2024 +0200 Update GitHub URLs to point to new repo location (#47) commit 6a84259c7d9bfadfe570f4107246049ecfd55283 Merge: 08cc319 e070e84 Author: Splines <37160523+Splines@users.noreply.github.com> Date: Sun Mar 24 10:13:19 2024 +0100 Continuous Release 1.0.0-alpha.2 Merge pull request #23 from resultwizard/alpha-2 commit e070e8472b6994725efdcb22f8d38cb091923e07 Author: Splines Date: Sat Mar 23 19:41:46 2024 +0100 Add more todo notes (one related to plum resolver) commit f02f58035b5687218fe02d081baff0d8130cf919 Author: Splines Date: Sat Mar 23 19:25:47 2024 +0100 Add raw strings to todo notes for documentation commit e99ca361db66fc6ea8c4f406d2fb303ebb66ca30 Author: Splines Date: Sat Mar 23 19:19:34 2024 +0100 Add config option to ignore shadowed result warning commit 6ecf6d11612107406095d4fa18e234f0c204cc57 Author: Splines Date: Wed Mar 20 01:03:35 2024 +0100 Improve Readme commit 64c22667289831df52e96a937596488b9bea83ed Author: Splines Date: Wed Mar 20 00:48:39 2024 +0100 Assert `n>=0` when rounding to decimal places commit 22d5dc802ac53fca1c8d970d0ad49ab2e7abc555 Author: Splines Date: Wed Mar 20 00:46:07 2024 +0100 Fix "consider-using-from-import" to import error messages commit 5525348f4f0330cd7198b47c1a499ab33de0112f Author: paul019 <39464035+paul019@users.noreply.github.com> Date: Tue Mar 19 12:41:30 2024 +0100 Fix potential vulnerability by replacing float with Decimal commit 398df1c33c2b3d16c0b1714e2d82ee481d223c3a Author: paul019 <39464035+paul019@users.noreply.github.com> Date: Tue Mar 19 12:39:15 2024 +0100 Restructure rounder test and test all rounding hierarchies commit 2f9ea35d62f758dad0f8f599518af103557097f0 Author: paul019 <39464035+paul019@users.noreply.github.com> Date: Tue Mar 19 12:14:32 2024 +0100 Add example to playground commit 35ef3fa776fb97772ea87cd622fbef7f7577fd84 Author: paul019 <39464035+paul019@users.noreply.github.com> Date: Tue Mar 19 12:13:21 2024 +0100 Print an error when number of decimal places is too small commit 6229921767082dad66aab01bde1b9868b487a51b Author: paul019 <39464035+paul019@users.noreply.github.com> Date: Tue Mar 19 11:31:13 2024 +0100 Centralize error messages commit 0d58dc542b55912bbe6646bf29b81249f38a8762 Author: Splines Date: Tue Mar 19 11:17:04 2024 +0100 Add instructions for how to release to PyPI commit 78aa5de684fb35b2848e5c1f356d5eb651062a91 Author: paul019 <39464035+paul019@users.noreply.github.com> Date: Tue Mar 19 10:54:51 2024 +0100 Remove code duplication in config commit 4377ed8b58fdfdb175b68d798083ccb6406fb758 Author: paul019 <39464035+paul019@users.noreply.github.com> Date: Tue Mar 19 10:47:40 2024 +0100 Fix behavior when the specified number of decimals is not high enough to display the value commit 24cb5015bbd632a4f490b42e0d4345eb2e9fabaf Author: Splines Date: Tue Mar 19 02:09:28 2024 +0100 Split up line that is too long commit 7c3c8ccdd854581536d4b9c84b70deeab5c1642d Author: Splines Date: Tue Mar 19 02:07:46 2024 +0100 Remove license field, instead use license classifier See the following link for the reasons: https://packaging.python.org/en/latest/guides/writing-pyproject-toml/#license commit 85bf53d1ccb03fb1a067ec578c98d5717581445b Author: Splines Date: Tue Mar 19 01:48:33 2024 +0100 Allow `Decimal`s for `sys` and `stat` uncertainties commit 93ddb303d61c257005cd0c7f2da450e058ea4957 Author: Splines Date: Tue Mar 19 01:33:48 2024 +0100 Let users pass in Decimals & add precision config key commit 0951c3e3822a329aef1e932d1f78de7f34622c77 Author: Splines Date: Tue Mar 19 00:04:19 2024 +0100 Use decimal module internally Also changed the test cases accordingly. commit 59ffc715406b63cdadc47dc5688da0c16ab4f9d8 Author: Splines Date: Mon Mar 18 17:43:53 2024 +0100 Improve unit string in console output This fixes #24. commit dfb865b19036875bc264a2c2aff4297bb42a1d21 Author: Splines Date: Mon Mar 18 17:37:07 2024 +0100 Only print "processing .. results" if needed commit c61d5af5d33d5142402bc1f44f8ef1cfd5113952 Author: Splines Date: Mon Mar 18 12:27:14 2024 +0100 Implement `export_auto_to` config option If not empty, each `res()` call will automatically export all results to the file the user specifies with this keyword. They can still use the `export()` method to export results to other files as well. This option might be particularly useful in Jupyter notebooks. commit 843abea40e76079f370816c8dec7d4386b48968f Author: Splines Date: Mon Mar 18 02:08:12 2024 +0100 Shorten LaTeX error message We can't use line breaks here, so we should aim for a short message such that the user can ideally view all of it (without the message being pruned) commit bb4a2790430c16d194226b2792ae240655fa71fd Author: Splines Date: Mon Mar 18 01:50:26 2024 +0100 Add correct version specifier for next alpha commit 8daa7db0913911ec79cf6e2a684e326ef2cf297f Author: Splines Date: Mon Mar 18 01:49:04 2024 +0100 Change default identifier to "result" See issue #25 for the reasons. commit c71ce30637647c546b661fdc43fc68c996c859ea Author: Splines Date: Mon Mar 18 01:35:59 2024 +0100 Disable false duplicate code warning (pylint) commit 124cddf2f487be21da7f137e8ca788d61961017a Author: Splines Date: Mon Mar 18 01:30:39 2024 +0100 Split latex stringifier into commandifier This is essentially a refactoring of the previous commit a4d33be. commit a4d33be0ddfb5641f3b37ed4f4f6521556f215a1 Author: Splines Date: Mon Mar 18 00:32:40 2024 +0100 Make use of siunitx \qty and \num (new stringify logic) We also added a fallback option to the config in order to switch back to the old behavior. It's called `siunitx_fallback` (bool). Note that this commit contains some duplicated code, which will be cleaned up in subsequent commits. commit cf3ef0f1f1539b4e5b93aea4cb4b07ef1c6a43e8 Author: Splines Date: Sun Mar 17 01:08:38 2024 +0100 Add some TODO notes commit 78b5857d97220cdb0467329694fbb14599b75e07 Author: Splines Date: Sun Mar 17 01:08:30 2024 +0100 Add Python 3.8 classifier to project setup commit 08cc319332976a9c215b391b03dfa6925b1a08cf Merge: b7739e6 e2a4693 Author: Splines <37160523+Splines@users.noreply.github.com> Date: Sat Mar 16 23:33:43 2024 +0100 Continuous Release 1.0.0-alpha.1 Merge pull request #19 from paul019/dev commit e2a4693ebbc07a78997a9e8c8add4b34d47db83e Merge: 97c0ece b7739e6 Author: Splines Date: Sat Mar 16 23:24:33 2024 +0100 Merge branch 'main' into dev commit 97c0ecebef6b5e351bec675c442825e7c5d55859 Merge: 14e3f0d cb5f173 Author: Splines <37160523+Splines@users.noreply.github.com> Date: Sat Mar 16 23:15:47 2024 +0100 Merge pull request #18 from paul019/value-wizard ResultWizard project commit cb5f173be334787d448dbab04afb5a42070efbff Merge: b56af58 21f1eec Author: Splines Date: Sat Mar 16 23:07:05 2024 +0100 Merge remote-tracking branch 'origin/value-wizard' into value-wizard commit b56af58768535b47512ff83faabcb6e2957a4bfc Author: Splines Date: Sat Mar 16 23:06:46 2024 +0100 Add disclaimer to troubleshooting section commit 21f1eecd3c98fb262a3f75f23742985978edb285 Author: paul019 <39464035+paul019@users.noreply.github.com> Date: Sat Mar 16 22:59:14 2024 +0100 Fix shadowing warning message commit 61ba5619e9ac298ed184ba13d37bad17c9bdcca1 Author: Splines Date: Sat Mar 16 20:23:11 2024 +0100 Add troubleshooting for old`siunitx` versions commit d51872873000ca270c9d4101e2318cdbbdecda5b Author: Splines Date: Sat Mar 16 16:13:34 2024 +0100 Add latex option to call result "withoutUnit" i.e. value and errors, but not the unit In contrast, "value" only outputs the value itself without any errors. commit 56b94e00d32a7c96883d8df3bf48e168319130ad Author: Splines Date: Sat Mar 16 16:06:37 2024 +0100 Improve runtime error messages commit 6cfcf9482cfec4f19e2dc4c8c5429bff8c592188 Author: paul019 <39464035+paul019@users.noreply.github.com> Date: Sat Mar 16 14:40:37 2024 +0100 Disable too-many-locals in stringifier commit d11658f5759ceceeb08f5d9ff3bd994379a01986 Author: paul019 <39464035+paul019@users.noreply.github.com> Date: Sat Mar 16 14:34:22 2024 +0100 Disallow int for uncertainties in certain cases to prevent ambiguities for overloaded methods commit 66fb17339e3b169e20e6703897dfe214ef74d7ab Author: paul019 <39464035+paul019@users.noreply.github.com> Date: Sat Mar 16 14:29:51 2024 +0100 Remove todo in value.py commit 89e6c17c13522ee3488fa4e08c8d8740458412f6 Author: paul019 <39464035+paul019@users.noreply.github.com> Date: Sat Mar 16 14:28:54 2024 +0100 Add error message to runtime error in rounder commit c201917b3ceb80104aab9fd4d9e5d5dc03f44392 Author: paul019 <39464035+paul019@users.noreply.github.com> Date: Sat Mar 16 14:25:21 2024 +0100 Add error message when user gives exact value AND specifies sigfigs/decimal places commit 7fb8e1ad7be30948d248ac5ee4e232b030c9e96f Author: paul019 <39464035+paul019@users.noreply.github.com> Date: Sat Mar 16 14:21:17 2024 +0100 Add a warning when shadowing values in cache. commit f894c15f64c9ca23c330c8f06a9e4b6539acc0c9 Author: paul019 <39464035+paul019@users.noreply.github.com> Date: Sat Mar 16 14:17:19 2024 +0100 Always print uncertainty name when user specifies it commit c6d680b5ed3dc27219b955252d23965eb543f871 Author: paul019 <39464035+paul019@users.noreply.github.com> Date: Sat Mar 16 14:12:36 2024 +0100 Replace _modify_value by value_prefix and value_suffix in stringifier commit 5a6a07dd4c07939c89f52bd430d175efc6218681 Author: Splines Date: Sat Mar 16 14:04:16 2024 +0100 Add `siunitx` import in resulting tex file & add to docs commit 34f797f2c0606286612d2a393ed893776b09384f Author: Splines Date: Sat Mar 16 13:54:04 2024 +0100 Rename `latex_str` to `string` in `stringifier.py` commit fcf0e4440ab5c11ecf71573018affe9c1b533f92 Author: Splines Date: Sat Mar 16 13:53:33 2024 +0100 Use \num{} for latex values commit 0075a508da7ff9f49ff5a10a3c0746726bea57dd Author: Splines Date: Sat Mar 16 13:28:26 2024 +0100 Use "SS" instead of "Ss" as replacement for "ẞ" commit 1cd31aa9d52ed56cf4cb2aaf9e3b185b6c1fa9c8 Author: Splines Date: Sat Mar 16 13:26:23 2024 +0100 Add first test for value parser commit 2be5700e91a2a90717f2acf1019417dceb8d226f Author: Splines Date: Sat Mar 16 12:54:21 2024 +0100 Add tests for special char parsing commit 57e8737b0dee3a0f65472e06286343d2b03d53f3 Author: Splines Date: Sat Mar 16 12:44:09 2024 +0100 Add some name parser tests & extract method commit 3ee6f8cbbb41fb6a08a0e793380e290947e8aa52 Author: Splines Date: Sat Mar 16 12:27:08 2024 +0100 Restore old greedy digit counting commit c3a815131604d127b5743c21ecd448917e16c132 Author: Splines Date: Sat Mar 16 01:09:36 2024 +0100 Improve pyproject description commit 26c0fe9fd29b4514083ce05c8eedceef02753be0 Author: Splines Date: Sat Mar 16 00:50:24 2024 +0100 Reorganize import in test commit d5ef148b7dda1a9fc93c2f38a961f71bb275b650 Author: Splines Date: Sat Mar 16 00:50:01 2024 +0100 Fix pylint errors in tests commit e457ce18406872a8a400fcd6e39f65b5686bed88 Author: Splines Date: Sat Mar 16 00:47:03 2024 +0100 Add "number to word" tests & forbid too small/big numbers commit 6ae7c553144e5bfe8016351fc8694a7275bf285f Author: Splines Date: Sat Mar 16 00:27:45 2024 +0100 Refactor name parser commit 9952a1ada042c02a1425c591ed1f14b2750ad896 Author: Splines Date: Fri Mar 15 23:50:57 2024 +0100 Remove unwanted comments in domain/Value commit 94209daec4a6d77fd9606bfc7fd00168411e4a30 Author: Splines Date: Fri Mar 15 23:50:21 2024 +0100 Get rid of unnecessary "_ClassName" convention Instead, just don't import the classes in the public api, then the user will also never see them. Note that we still use the convention for attributes to mark them as internal to a class. commit d7345cf149007d0c4fe28085001887a98ba6782a Author: Splines Date: Fri Mar 15 23:44:59 2024 +0100 Use more meaningful names for stringifiers commit dca2e750715a4243b522950bd64b024343851e08 Author: Splines Date: Fri Mar 15 23:34:00 2024 +0100 Use Protocol for MasterStringifier commit 557d8606682c289c6592f0916d10318fa298f67a Author: paul019 <39464035+paul019@users.noreply.github.com> Date: Fri Mar 15 20:27:02 2024 +0100 Implement master stringifier that is customized by latexer and stringifier commit b988fd724248e1eee11f42bbec92a49cec795e9c Author: Splines Date: Fri Mar 15 20:04:24 2024 +0100 Add very basic install & usage instructions commit ef1ee87569ffec77e8b00dca74d1f635e0194a5f Author: Splines Date: Fri Mar 15 19:53:36 2024 +0100 Create release workflow for PyPI commit c6b1c6b0ce92985042c8caabc502903c79cbf9b4 Author: paul019 <39464035+paul019@users.noreply.github.com> Date: Fri Mar 15 17:21:46 2024 +0100 Add support for parsing of numbers up to 999 in names commit d6adf88b8fdbbf42afe7559868e41587e3630758 Author: paul019 <39464035+paul019@users.noreply.github.com> Date: Fri Mar 15 17:01:05 2024 +0100 Remove unnecessary imports commit 68fd9d06c3fa8c5580605cfa89d888f4c037083b Author: paul019 <39464035+paul019@users.noreply.github.com> Date: Fri Mar 15 17:00:36 2024 +0100 Refactor stringifier commit d3395a1848ab8a72859a2d1763d99045741e7a25 Author: Splines Date: Fri Mar 15 16:35:33 2024 +0100 Fix too-few-public-methods & other small pylint warnings commit 3fb7ec0bdc8de7d0aee553d4491f3274692afbd1 Author: Splines Date: Fri Mar 15 16:26:13 2024 +0100 Fix more pylint warnings commit 321958348edad977e052178c8ca653d1527559fe Author: Splines Date: Fri Mar 15 16:16:55 2024 +0100 Add docstring & disable some pylint warnings commit a66ffff9be56844e852c363e634e5efde92fec60 Author: Splines Date: Fri Mar 15 16:02:11 2024 +0100 Fix misunderstood "requires" key See https://docs.pipenv.org/en/latest/advanced.html#specifying-versions-of-python commit 5977adda75bdaf6393682a421f708750f7749229 Author: Splines Date: Fri Mar 15 15:48:52 2024 +0100 Remove outdated todo notes commit 1a735c8612722b0a121b5c780fdde5101f879258 Author: Splines Date: Fri Mar 15 15:38:33 2024 +0100 Rename to `to_latex_str` & add docstrings commit 06a3d2179b54b965d8c9bdbee83bc35fcd8ddfef Author: Splines Date: Fri Mar 15 15:28:35 2024 +0100 Simplify creation of latex string commit 0a4c4389d8b21ee76d535409d811e8b4a83eed53 Author: Splines Date: Fri Mar 15 14:59:19 2024 +0100 Reduce spacing between value and unit commit d07f1441a1592d53b2a826c56cbf83661ee7f3de Author: Splines Date: Fri Mar 15 14:58:18 2024 +0100 Fix latexer else branch commit da5d0ec20d92cf4821733dc8b48dd8c98e611f71 Author: Splines Date: Fri Mar 15 14:47:12 2024 +0100 Only include non-empty keywords to error list commit ce5a7f169f8a4c115d7da66821ec52d06e1870cd Merge: 21b7ba1 f429ec1 Author: Splines Date: Fri Mar 15 14:40:21 2024 +0100 Merge remote-tracking branch 'origin/value-wizard' into value-wizard commit 21b7ba1ef7428eca2e997664c5589ed3adef6668 Author: Splines Date: Fri Mar 15 14:40:05 2024 +0100 Refactor result to latex cmd via builder pattern commit f429ec19895912cea68c67afffa98b14ff5ddb36 Author: paul019 <39464035+paul019@users.noreply.github.com> Date: Fri Mar 15 14:05:52 2024 +0100 Raise error when user specifies sigfigs and decimal places commit 71cfa44f6217868a21c2969d360530d4321e9fe5 Author: paul019 <39464035+paul019@users.noreply.github.com> Date: Fri Mar 15 14:01:29 2024 +0100 Update rounder_test.py commit 45504e38374d061385c11fc8e32961340e51f8af Author: paul019 <39464035+paul019@users.noreply.github.com> Date: Fri Mar 15 13:56:11 2024 +0100 Return errors when making wrong settings in config commit 3a274d010bb53f7b7312f1cafaca1943acc99fbb Author: paul019 <39464035+paul019@users.noreply.github.com> Date: Fri Mar 15 13:44:42 2024 +0100 Change rounding hierarchy (prioritize settings in single results over default settings) commit 9ad1256407c2f7e8c6b9b9f7c2753a84ded70b55 Author: paul019 <39464035+paul019@users.noreply.github.com> Date: Fri Mar 15 13:43:19 2024 +0100 Add more rounding options to config commit ef2bc83bb6bf41248b3a0ded9e1ad9c69b20a55b Author: paul019 <39464035+paul019@users.noreply.github.com> Date: Fri Mar 15 13:26:23 2024 +0100 Remove unnecessary imports commit 9b0f11f41864e258a1d1f2079afc3847b9bc0b8b Author: paul019 <39464035+paul019@users.noreply.github.com> Date: Fri Mar 15 13:24:18 2024 +0100 Move the parsing of exact values from domain to api commit bc3c7adaaa07b7102a92e751740fedb476b3d6f3 Author: paul019 <39464035+paul019@users.noreply.github.com> Date: Fri Mar 15 13:03:42 2024 +0100 Add option "without error" to latexer commit 9757fbc524f67ff325ef7f1c9271d8f8f6f652e4 Author: paul019 <39464035+paul019@users.noreply.github.com> Date: Fri Mar 15 12:39:38 2024 +0100 Add support for int values commit bccdef2e50f1ed52998d9a5f542b7445839f6039 Author: paul019 <39464035+paul019@users.noreply.github.com> Date: Fri Mar 15 12:21:40 2024 +0100 Improve parsing of names commit ee01e73617fa08be78d80fcbc066e4a77fbe551d Author: paul019 <39464035+paul019@users.noreply.github.com> Date: Fri Mar 15 12:12:43 2024 +0100 Fix capital ß commit 29f6c05b0e4426566c10dbcdfc652d28be23a9ee Author: paul019 <39464035+paul019@users.noreply.github.com> Date: Fri Mar 15 12:11:28 2024 +0100 Fix error message for short results commit 9590ffae34ac4d29576349be4e1f1b51505edf17 Author: Splines Date: Fri Mar 15 12:00:14 2024 +0100 Allow python versions down to `3.8` commit 42c404430fd320287bf77f80d2a8478041db75b5 Author: Splines Date: Fri Mar 15 11:54:06 2024 +0100 Regenerate lockfile & fix missing dependency commit f6a8fc3e9f6b29f68079553c8d50d19dc759920a Author: Splines Date: Fri Mar 15 11:40:54 2024 +0100 Remove dummy mail from project settings commit 09c1fc0e058e9288e6694fcc9a690f5587c13d6b Author: Splines Date: Fri Mar 15 03:07:32 2024 +0100 Let all negative values signify "no decimal places specified" commit 78babce048e33650a2425beaf001005c1819849a Author: Splines Date: Fri Mar 15 03:06:47 2024 +0100 Fix missing copy of python objects commit a2ee1d7a818f8bf4a03ea7a6413d27c72f66a868 Author: Splines Date: Fri Mar 15 03:06:30 2024 +0100 Fix wrong caller syntax in tests commit 8d00de8210cf226c81cd50c333650bda92cf44a0 Author: Splines Date: Fri Mar 15 02:42:46 2024 +0100 Always add a "valueOnly" option & do not include unit commit 75a3db266ebdf94b44b99f7f10f0ac00e5c89fe3 Author: Splines Date: Fri Mar 15 02:42:10 2024 +0100 Add TODO to support int values commit b102601913113366190874dc5d4a36bfcd5ec5dd Author: Splines Date: Fri Mar 15 02:38:32 2024 +0100 Remove unnecessary JSON printouts in config commit 3d0432dc13d13fa1820b5752fa77dca878adbabe Author: Splines Date: Fri Mar 15 02:22:26 2024 +0100 Fix package discovery Also see https://setuptools.pypa.io/en/latest/userguide/package_discovery.html#finding-simple-packages commit ca331cc81e8d0ac4828e29845dc4c1ea65ccbc96 Author: Splines Date: Fri Mar 15 02:13:59 2024 +0100 Remove unnecessary import mode config commit 8ee4d0b3ef3f85a914cd4f449bd4c1712c2a3d50 Author: Splines Date: Fri Mar 15 02:08:03 2024 +0100 Use configuration in stringifier commit 99549cc16672923c573ab231f139c9eea331811f Author: Splines Date: Fri Mar 15 02:05:34 2024 +0100 Implement "print auto" & fix broken config share commit 215c75694f9b72293520190d0a0077c481031e57 Author: Splines Date: Fri Mar 15 01:33:32 2024 +0100 Raise errors when user passes in empty name commit 9a0299dc53c0ea996857337597a6bee277ca7f57 Author: Splines Date: Fri Mar 15 01:31:46 2024 +0100 Remove unnecessary else branch Also added a question as TODO commit a5842063aa2eafd90a3a370042e18908cefe772e Author: Splines Date: Fri Mar 15 01:30:07 2024 +0100 Add support for decimal places in Rounder commit 39ff7c49c6a2c3cf8aad08cd3466fafa46718af4 Author: Splines Date: Fri Mar 15 00:54:11 2024 +0100 Also replace capitalized "ß" (ẞ) in name parser commit f4df2be653951b167ef08914226b68f2789bc57e Author: Splines Date: Fri Mar 15 00:43:35 2024 +0100 Make Rounder configurable We also had to move some things around as the latexer should not be responsible to call the Rounder for a short result. This should be the job of the rounder itself. commit 07146a235e393ad059c5c86eeb938932b4a21093 Author: Splines Date: Fri Mar 15 00:02:09 2024 +0100 Use configuration in LaTeXer commit 9a0d27708ce685b88788706420a40a0a24950093 Author: Splines Date: Thu Mar 14 23:46:37 2024 +0100 Move `_res_cache` instantiation to API commit 767c89e62cfefa6af82336634ae4ffe43bb42e23 Author: Splines Date: Thu Mar 14 23:45:05 2024 +0100 Add link to possible pluml fix commit e1af3b66f32ce694bfc3b4bba897e75c98216725 Author: Splines Date: Thu Mar 14 23:37:10 2024 +0100 Redesign config such that defaults are visible in API commit b83316febb47f6c21fc8a77928fac548e065783a Author: Splines Date: Thu Mar 14 23:32:26 2024 +0100 Add first draft for configuration commit a364a42d7644872df2c1f2717a5005e4db5ceec3 Author: paul019 <39464035+paul019@users.noreply.github.com> Date: Thu Mar 14 22:04:06 2024 +0100 Add TODO commit b700b331d3ba57851e0ac47c0c6373a20442d14d Author: paul019 <39464035+paul019@users.noreply.github.com> Date: Thu Mar 14 21:32:49 2024 +0100 Make small fix regarding parenthesis in output strings and test it in playground commit 8e99d5aa65e41303a89ae55a2d73c1aa3fa60cc7 Author: paul019 <39464035+paul019@users.noreply.github.com> Date: Thu Mar 14 19:08:37 2024 +0100 Move parsers to api commit 2065616cd6a3ea391f515ae6d292496aa77a7187 Author: Splines Date: Thu Mar 14 14:35:09 2024 +0100 Get rid of unnecessary `_res_cache` variable `_res_cache` is already initialized in `application.cache`. commit 1d045a7a0cef05b41a369a33b5bbaa6cc11eb9e3 Author: Splines Date: Thu Mar 14 14:34:24 2024 +0100 Rename project to `ResultWizard` commit 6e000d5b8cf4c9bae80d865ee80876619cf57eb3 Author: Splines Date: Thu Mar 14 13:56:05 2024 +0100 Invoke pytest correctly in Actions commit ab951b139c4b1c576f5f91eda76dd01be0f8d815 Author: Splines Date: Thu Mar 14 13:49:53 2024 +0100 Fix tests not running with GitHub actions commit 554cca79952b18a1b9a8a1a346d7a3e2db63f57f Author: Splines Date: Thu Mar 14 13:44:10 2024 +0100 Move python testing settings to better section commit d4f9b778784b88bf2ea35fa68ceec27f163b53e6 Author: paul019 <39464035+paul019@users.noreply.github.com> Date: Thu Mar 14 11:45:12 2024 +0100 Remove print statements commit d1d7d11e7076e4dcfd40850fee41b4b4982259ff Author: paul019 <39464035+paul019@users.noreply.github.com> Date: Thu Mar 14 11:18:45 2024 +0100 Refactor res.py and move parsers to application commit b7739e6a3851d7adf3c542faf0be3d49ba0fd154 Author: paul019 <39464035+paul019@users.noreply.github.com> Date: Sun Dec 24 00:25:27 2023 +0100 Add MIT license --- .github/workflows/docs-verify.yml | 32 ++ .github/workflows/docs.yml | 69 +++++ .github/workflows/release.yml | 41 +++ .vscode/settings.json | 12 +- CHANGELOG.md | 4 +- DEVELOPMENT.md | 14 +- Pipfile | 1 - Pipfile.lock | 54 +--- README.md | 18 +- TODO.md | 33 ++ docs/.gitignore | 12 + docs/404.html | 26 ++ docs/Gemfile | 4 + docs/Gemfile.lock | 82 +++++ docs/README.md | 13 + docs/_api/config.md | 54 ++++ docs/_api/export.md | 39 +++ docs/_api/res.md | 123 ++++++++ docs/_config.yml | 41 +++ docs/_includes/nav_footer_custom.html | 4 + docs/_includes/title.html | 4 + .../color_schemes/resultwizard-colors.scss | 6 + docs/_sass/custom/setup.scss | 4 + docs/_tips/jupyter.md | 54 ++++ docs/_tips/siunitx.md | 58 ++++ docs/favicon.ico | Bin 0 -> 9584 bytes docs/index.md | 86 ++++++ docs/pages/about.md | 38 +++ docs/pages/quickstart.md | 192 ++++++++++++ docs/pages/trouble.md | 94 ++++++ pyproject.toml | 33 +- src/api/config.py | 169 +++++++++++ src/api/console_stringifier.py | 48 +++ src/api/export.py | 70 ++++- src/api/latexer.py | 18 ++ src/api/parsers.py | 169 ++++++++--- src/api/printable_result.py | 24 +- src/api/res.py | 136 ++++----- src/api/stringifier.py | 100 ------ src/application/cache.py | 29 +- src/application/error_messages.py | 67 +++++ src/application/helpers.py | 47 ++- .../latex_better_siunitx_stringifier.py | 59 ++++ src/application/latex_commandifier.py | 95 ++++++ src/application/latex_ifelse.py | 33 ++ src/application/latex_stringifier.py | 30 ++ src/application/latexer.py | 211 ------------- src/application/rounder.py | 164 +++++++--- src/application/stringifier.py | 171 +++++++++++ src/domain/result.py | 41 ++- src/domain/uncertainty.py | 13 +- src/domain/value.py | 67 ++--- src/resultwizard/__init__.py | 5 +- tests/number_word_test.py | 31 ++ tests/parsers_test.py | 95 ++++++ tests/playground.py | 75 ++--- tests/rounder_test.py | 284 +++++++++++++----- 57 files changed, 2721 insertions(+), 775 deletions(-) create mode 100644 .github/workflows/docs-verify.yml create mode 100644 .github/workflows/docs.yml create mode 100644 .github/workflows/release.yml create mode 100644 TODO.md create mode 100644 docs/.gitignore create mode 100644 docs/404.html create mode 100644 docs/Gemfile create mode 100644 docs/Gemfile.lock create mode 100644 docs/README.md create mode 100644 docs/_api/config.md create mode 100644 docs/_api/export.md create mode 100644 docs/_api/res.md create mode 100644 docs/_config.yml create mode 100644 docs/_includes/nav_footer_custom.html create mode 100644 docs/_includes/title.html create mode 100644 docs/_sass/color_schemes/resultwizard-colors.scss create mode 100644 docs/_sass/custom/setup.scss create mode 100644 docs/_tips/jupyter.md create mode 100644 docs/_tips/siunitx.md create mode 100644 docs/favicon.ico create mode 100644 docs/index.md create mode 100644 docs/pages/about.md create mode 100644 docs/pages/quickstart.md create mode 100644 docs/pages/trouble.md create mode 100644 src/api/config.py create mode 100644 src/api/console_stringifier.py create mode 100644 src/api/latexer.py delete mode 100644 src/api/stringifier.py create mode 100644 src/application/error_messages.py create mode 100644 src/application/latex_better_siunitx_stringifier.py create mode 100644 src/application/latex_commandifier.py create mode 100644 src/application/latex_ifelse.py create mode 100644 src/application/latex_stringifier.py delete mode 100644 src/application/latexer.py create mode 100644 src/application/stringifier.py create mode 100644 tests/number_word_test.py create mode 100644 tests/parsers_test.py diff --git a/.github/workflows/docs-verify.yml b/.github/workflows/docs-verify.yml new file mode 100644 index 00000000..18c603e2 --- /dev/null +++ b/.github/workflows/docs-verify.yml @@ -0,0 +1,32 @@ +name: Documentation + +on: + pull_request: + types: [opened, reopened, synchronize, ready_for_review] + paths: + - "docs/**" + +jobs: + # Just check that the build works and doesn't throw any errors + # The actual build and deployment is done on the main branch + # with another GitHub Actions workflow. + build: + name: Build + runs-on: ubuntu-latest + defaults: + run: + working-directory: docs + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.1' + bundler-cache: true # runs 'bundle install' and caches installed gems automatically + cache-version: 0 # Increment this number if you need to re-download cached gems + working-directory: '${{ github.workspace }}/docs' + + - name: Build with Jekyll + run: bundle exec jekyll build diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 00000000..ee97c592 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,69 @@ +# See the template here: https://github.com/just-the-docs/just-the-docs-template + +name: Documentation + +on: + push: + branches: + - "main" + paths: + - "docs/**" + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write # to deploy to Pages + id-token: write # to verify the deployment originates from an appropriate source + +# Allow one concurrent deployment +concurrency: + group: "pages" + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + defaults: + run: + working-directory: docs + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.1' + bundler-cache: true # runs 'bundle install' and caches installed gems automatically + cache-version: 0 # Increment this number if you need to re-download cached gems + working-directory: '${{ github.workspace }}/docs' + + - name: Setup Pages + id: pages + uses: actions/configure-pages@v5 + + - name: Build with Jekyll + # Outputs to the './_site' directory by default + run: bundle exec jekyll build --baseurl "${{ steps.pages.outputs.base_path }}" + env: + JEKYLL_ENV: production + + # Automatically creates an github-pages artifact used by the deployment job + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: "docs/_site/" + + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + needs: build + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..d9c0044e --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,41 @@ +name: Release package + +on: + release: + types: [published] + +jobs: + releasepypi: + # see https://docs.pypi.org/trusted-publishers/using-a-publisher/ + # and https://packaging.python.org/en/latest/tutorials/packaging-projects/#uploading-the-distribution-archives + name: Release to PyPI + runs-on: "ubuntu-latest" + environment: release + permissions: + id-token: write + + steps: + - name: Checkout source + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: 3.11 + cache: 'pipenv' + + - name: Run tests (one last time before release) + run: | + pip install pipenv + pipenv install --dev + pipenv run pip3 install --editable . + pipenv run pytest tests/ + + - name: Install build tooling + run: python3 -m pip install --upgrade build + + - name: Build distributions + run: python3 -m build + + - name: Upload to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.vscode/settings.json b/.vscode/settings.json index 1bf2f3ad..bd636e55 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -30,7 +30,8 @@ "files.exclude": { "**/__pycache__/": true, "**/*.egg-info/": true, - ".pytest_cache/": true + "**/.pytest_cache/": true, + "**/.jekyll-cache/": true }, ////////////////////////////////////// // Editor @@ -61,6 +62,8 @@ // Spell Checker ////////////////////////////////////// "cSpell.words": [ + "Commandifier", + "getcontext", "github", "ifthen", "ifthenelse", @@ -69,17 +72,22 @@ "newcommand", "normalsize", "pipenv", + "prec", "pydantic", "pylint", "pytest", "resultwizard", + "scriptsize", + "scriptstyle", + "setcontext", "sigfigs", "siunitx", "Stringifier", "textbf", "texttt", - "textwidth", + "TLDR", "uncert", + "uncerts", "usepackage" ] } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index b90e5bfa..4f7e8678 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,3 @@ -# Changelog of ResultWizard +# Changelog -TODO +👀 Nothing here yet diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 0ef362b0..01e04685 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -9,7 +9,7 @@ Getting ready: - [ ] Recommended VSCode extensions installed (especially the formatter. It should automatically format on every save!) - [ ] on branch `value-wizard` with latest commit pulled - [ ] Work through the `Setup` section below (especially to install the necessary dependencies) -- [ ] Read the [`README.md`](https://github.com/paul019/ResultWizard/tree/value-wizard/src#code-structure) in the `src` folder (to get to know the code structure) & see our [feature list](https://github.com/paul019/ResultWizard/issues/16) +- [ ] Read the [`README.md`](https://github.com/resultwizard/ResultWizard/tree/main/src#code-structure) in the `src` folder (to get to know the code structure) & see our [feature list](https://github.com/resultwizard/ResultWizard/issues/16) Verify that everything worked: - [ ] try to run the tests, see the instructions in [`tests/playground.py`](./tests/playground.py) @@ -66,3 +66,15 @@ Also try adding `import pytest` as first line of a test file. Does it give you a Note that tests are also run on every commit via a GitHub action. In order to learn how to write the tests with pytest, start with the [`Get Started` guide](https://docs.pytest.org/en/8.0.x/getting-started.html#create-your-first-test). Probably also relevant: ["How to use fixtures"](https://docs.pytest.org/en/8.0.x/how-to/fixtures.html). There are lots of [How-to guides](https://docs.pytest.org/en/8.0.x/how-to/index.html) available. + + +## Release to PyPI + +To release a new version to [PyPI](https://pypi.org/project/resultwizard/), do the following: + +- Create a PR that is going to get merged into `main`. Name it "Continuous Release ". +- Make sure all tests pass and review the PR. Merge it into `main` via a *Merge commit* +
(from `dev` to `main` always via *Merge commit*, not *Rebase* or *Squash and merge*). +- On `main`, create a new release on GitHub. In this process (via the GitHub UI), create a new tag named "v", e.g. "v1.0.0-alpha.42". +- The tag creation will trigger a GitHub action that builds and uploads the package to PyPI. As this action uses a special "release" environment, code owners have to approve this step. +- Make sure the new version is available on PyPI [here](https://pypi.org/project/resultwizard/). diff --git a/Pipfile b/Pipfile index f1651b4f..70624017 100644 --- a/Pipfile +++ b/Pipfile @@ -4,7 +4,6 @@ verify_ssl = true name = "pypi" [packages] -plum-dispatch = "*" [dev-packages] pylint = "~=3.0" diff --git a/Pipfile.lock b/Pipfile.lock index ea9b0240..94768b86 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "e32904fba65ee53f95fbe90b1633c0bd9f71823a326e0e4ed6942d78b8270d34" + "sha256": "a5f83ecca60c4365a35a8eb10b1051286f4099f88ff14dab578be8367888ba0e" }, "pipfile-spec": 6, "requires": { @@ -15,57 +15,7 @@ } ] }, - "default": { - "beartype": { - "hashes": [ - "sha256:c22b21e1f785cfcf5c4d3d13070f532b6243a3ad67e68d2298ff08d539847dce", - "sha256:e911e1ae7de4bccd15745f7643609d8732f64de5c2fb844e89cbbed1c5a8d495" - ], - "markers": "python_full_version >= '3.8.0'", - "version": "==0.17.2" - }, - "markdown-it-py": { - "hashes": [ - "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", - "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb" - ], - "markers": "python_version >= '3.8'", - "version": "==3.0.0" - }, - "mdurl": { - "hashes": [ - "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", - "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba" - ], - "markers": "python_version >= '3.7'", - "version": "==0.1.2" - }, - "plum-dispatch": { - "hashes": [ - "sha256:96f519d416accf9a009117682f689114eb23e867bb6f977eed74ef85ef7fef9d", - "sha256:f49f00dfdf7ab0f16c9b85cc27cc5241ffb59aee02218bac671ec7c1ac65e139" - ], - "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==2.3.2" - }, - "pygments": { - "hashes": [ - "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c", - "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367" - ], - "markers": "python_version >= '3.7'", - "version": "==2.17.2" - }, - "rich": { - "hashes": [ - "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222", - "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432" - ], - "markers": "python_full_version >= '3.7.0'", - "version": "==13.7.1" - } - }, + "default": {}, "develop": { "astroid": { "hashes": [ diff --git a/README.md b/README.md index b999a173..a594386d 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,17 @@
- + + +

ResultWizard

-

Intelligent interface between Python-computed values and your LaTeX work

+

Intelligent interface between Python-computed values and your LaTeX work

+Annoyed of having to copy around values from Python code to your LaTeX work? Think of `ResultWizard` as an interface between the two. Export any variables from Python including possible uncertainties and their units and directly reference them in your LaTeX document. -## `ResultWizard` is ... - -- a -- b - - -## Usage +> **Warning ⚠** +> ResultWizard is still in its *alpha* stage. We're happy to receive your feedback, e.g. report any bugs. But note that the API might still change before we hit the first stable release 1.0.0. +> **📄** +> **For installation/usage/API, refer to our [documentation](https://resultwizard.github.io/ResultWizard/).** diff --git a/TODO.md b/TODO.md new file mode 100644 index 00000000..15c58d03 --- /dev/null +++ b/TODO.md @@ -0,0 +1,33 @@ +## Documentation + +**Things we should include in the documentation:** + +- an overview of what the goal of the project is with a small illustration where we sketch python code, the `results.tex` file and the LaTeX document and explain how they are connected +- most common ways to use the library, but then also a comprehensive list of all possible ways to call `res` (preferable on a separate page of the documentation). How to guides for most common cases +- ways to use the variable in LaTeX code, e.g. list all keys, tell users they should enter \resMyVariable[test] and then see the error message to find out what the possible keys are +- passing in as string means exact value +- how to specify the name of a variable. Numbers only allowed from 0 up to 1000. Special characters are stripped. Explain camel case etc. +-> Explain that this has the great potential for loops: users can specify variables in a loop and use format strings, e.g. `wiz.res(f"my_variable_{i}", ...)` +- how to pass in uncertainties. How to pass in one? What about systematic and statistical ones. What if I want to add my own name for the uncertainty? How can I control that output. +- a list of all possible keys for `config_init` including their default values, e.g. `identifier`. In-depth explanation especially for sigfigs and decimal places and how they differ from respective fallback options +- a hint that the output is completely customizable and that the user can change it with the `\sisetup{...}`, e.g. `\cdot` vs. `\times` for exponent, `separate-uncertainty=true` (!) +- how to use the unit string. explain that strings from `siunitx` can be passed in, e.g. `\cm \per \N` etc. Explain how python raw strings can help, e.g. `r"\cm \per \N"` instead of having to do `\\cm` etc. all the time. However, `r'\\tesla'` will fail as the double backslash is treated a raw string and not as an escape character. Use `r'\tesla'` instead. +- possible ways to print a result. Recommended: activate `print_auto`. Other way: call `print()` on result object. Users can also call `resVariable.to_latex_str()` to retrieve the LaTeX representation. This can be useful to plot the result in a matplotlib figure, e.g. the fit parameter of a curve fit. +- Suggest some good initial configuration for Jupyter notebook, e.g. `print_auto=True` and `ignore_result_overwrite=True`. +- Naming: we call it "uncertainty". Give a hint that others might also call it "error" interchangeably. +- Jupyter Notebook tip to avoid + +``` + +``` +as output. Instead append a `;` to the `wiz.res(...)` call and the output will be suppressed. + +- Use fuzzy search in IntelliSense to search for result names. + + + +## Other + +- Setup issue template and contribution guide. Clean up `DEVELOPMENT.md`. +- Long-term: Ask real users what they really need in the scientific day-to-day life, see [here](https://github.com/resultwizard/ResultWizard/issues/9). +- If user enters an uncertainty of `0.0`, don't just issue warning "Uncertainty must be positive", but also give a hint that the user might want to use a different caller syntax for `res` which does not even have the uncertainty as argument. diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 00000000..93b45e81 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,12 @@ +# These are directly copied from Jekyll's first-party docs on `.gitignore` files: +# https://jekyllrb.com/tutorials/using-jekyll-with-bundler/#commit-to-source-control + +# Ignore the default location of the built site, and caches and metadata generated by Jekyll +_site/ +.sass-cache/ +.jekyll-cache/ +.jekyll-metadata + +# Ignore folders generated by Bundler +.bundle/ +vendor/ diff --git a/docs/404.html b/docs/404.html new file mode 100644 index 00000000..f75bc878 --- /dev/null +++ b/docs/404.html @@ -0,0 +1,26 @@ +--- +permalink: /404.html +layout: default +--- + + + +
+

404

+ +

Page not found 😥

+

The requested page could not be found.

+
diff --git a/docs/Gemfile b/docs/Gemfile new file mode 100644 index 00000000..25440b6f --- /dev/null +++ b/docs/Gemfile @@ -0,0 +1,4 @@ +source 'https://rubygems.org' + +gem "jekyll", "~> 4.3.3" # installed by `gem jekyll` +gem "just-the-docs", "0.8.1" # pinned to the current release diff --git a/docs/Gemfile.lock b/docs/Gemfile.lock new file mode 100644 index 00000000..017aea1b --- /dev/null +++ b/docs/Gemfile.lock @@ -0,0 +1,82 @@ +GEM + remote: https://rubygems.org/ + specs: + addressable (2.8.6) + public_suffix (>= 2.0.2, < 6.0) + colorator (1.1.0) + concurrent-ruby (1.2.3) + em-websocket (0.5.3) + eventmachine (>= 0.12.9) + http_parser.rb (~> 0) + eventmachine (1.2.7) + ffi (1.16.3) + forwardable-extended (2.6.0) + google-protobuf (4.26.1-x86_64-linux) + rake (>= 13) + http_parser.rb (0.8.0) + i18n (1.14.4) + concurrent-ruby (~> 1.0) + jekyll (4.3.3) + addressable (~> 2.4) + colorator (~> 1.0) + em-websocket (~> 0.5) + i18n (~> 1.0) + jekyll-sass-converter (>= 2.0, < 4.0) + jekyll-watch (~> 2.0) + kramdown (~> 2.3, >= 2.3.1) + kramdown-parser-gfm (~> 1.0) + liquid (~> 4.0) + mercenary (>= 0.3.6, < 0.5) + pathutil (~> 0.9) + rouge (>= 3.0, < 5.0) + safe_yaml (~> 1.0) + terminal-table (>= 1.8, < 4.0) + webrick (~> 1.7) + jekyll-include-cache (0.2.1) + jekyll (>= 3.7, < 5.0) + jekyll-sass-converter (3.0.0) + sass-embedded (~> 1.54) + jekyll-seo-tag (2.8.0) + jekyll (>= 3.8, < 5.0) + jekyll-watch (2.2.1) + listen (~> 3.0) + just-the-docs (0.8.1) + jekyll (>= 3.8.5) + jekyll-include-cache + jekyll-seo-tag (>= 2.0) + rake (>= 12.3.1) + kramdown (2.4.0) + rexml + kramdown-parser-gfm (1.1.0) + kramdown (~> 2.0) + liquid (4.0.4) + listen (3.9.0) + rb-fsevent (~> 0.10, >= 0.10.3) + rb-inotify (~> 0.9, >= 0.9.10) + mercenary (0.4.0) + pathutil (0.16.2) + forwardable-extended (~> 2.6) + public_suffix (5.0.5) + rake (13.2.1) + rb-fsevent (0.11.2) + rb-inotify (0.10.1) + ffi (~> 1.0) + rexml (3.2.6) + rouge (4.2.1) + safe_yaml (1.0.5) + sass-embedded (1.74.1-x86_64-linux-gnu) + google-protobuf (>= 3.25, < 5.0) + terminal-table (3.0.2) + unicode-display_width (>= 1.1.1, < 3) + unicode-display_width (2.5.0) + webrick (1.8.1) + +PLATFORMS + x86_64-linux + +DEPENDENCIES + jekyll (~> 4.3.3) + just-the-docs (= 0.8.1) + +BUNDLED WITH + 2.4.22 diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..eccaa7d3 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,13 @@ +# ResultWizard documentation + +🧾 Find the documentation [here](https://resultwizard.github.io/ResultWizard/). + +To build and preview the docs locally, see [this](https://github.com/just-the-docs/just-the-docs-template?tab=readme-ov-file#building-and-previewing-your-site-locally). In summary, have [Bundler](https://bundler.io/) installed, then run: + +```bash +cd docs/ +bundle install +bundle exec jekyll serve +``` + +Preview the docs at [localhost:4000](http://localhost:4000). Files are stored in the `_site` directory locally. diff --git a/docs/_api/config.md b/docs/_api/config.md new file mode 100644 index 00000000..cdadfbb8 --- /dev/null +++ b/docs/_api/config.md @@ -0,0 +1,54 @@ +--- +layout: default +title: wiz.config_init() & wiz.config() +nav_order: 1 +--- + +# `wiz.config_init` & `wiz.config()` +{: .no_toc } + +
+ + Content + + {: .text-delta } + +- TOC +{:toc} + +
+ + +## Purpose + +The methods `wiz.config_init()` and `wiz.config()` allow you to configure `ResultWizard` to your needs. Note that this mainly affects the rounding mechanism as well as convenience features. How the results are formatted in the LaTeX document is mainly controlled by the `siunitx` package and how you set it up in your LaTeX preamble. If this is what you want to configure, then you should take a look [here]({{site.baseurl}}/tips/siunitx). + +## Usage + +With `config_init()` you set the initial configuration for `ResultWizard`. With later calls to `config()`, you can update individual settings without having to reconfigure every parameter. + +{: .warning } +Some options are only available in `config_init()` and cannot be changed later with `config()`. +
TODO: Do we really want that? + +Here is the list of available options: + +{: .warning } +TODO: sort options alphabetically? Make clearer what the difference between `sigfigs` and `sigfigs_fallback` is. Maybe even rename/unify these options? Same for `decimal_places` and `decimal_places_fallback`. We also need better explanations for `min_exponent_...` and `max_exponent_...`. + +| Option | Default | Available in
`config_init()` | Available in
`config()` | Description | +|:---|:---:|:---:|:---:|:---| +| `sigfigs` (int) | `-1` | ✔ | ✔ | The number of significant figures to round to.
TODO: explain what a sigfig is. | +| `decimal_places` (int) | `-1` | ✔ | ✔ | The number of decimal places to round to. | +| `sigfigs_fallback` (int) | `2` | ✔ | ✔ | The number of significant figures to use as a fallback if other rounding rules don't apply. | +| `decimal_places_fallback` (int) | `-1` | ✔ | ✔ | The number of decimal places to use as a fallback if other rounding rules don't apply. | +| `identifier` (str) | `"result"` | ✔ | | The identifier that will be used in the LaTeX document to reference the result. | +| `print_auto` (bool) | `False` | ✔ | ✔ | If `True`, every call to `wiz.res()` will automatically print the result to the console, such that you don't have to use `.print()` on every single result. | +| `export_auto_to` (str) | `""` | ✔ | | If set to a path, every call to `wiz.res()` will automatically export the result to the specified file. This is especially useful for Jupyter notebooks where every execution of a cell that contains a call to `wiz.res()` will automatically export to the file. | +| `siunitx_fallback` (bool) | `False` | ✔ | | If `True`, `ResultWizard` will use a fallback for the `siunitx` package if you have an old version installed. See [here]({{site.baseurl}}/trouble#package-siunitx-invalid-number) for more information. We don't recommend to use this option and instead upgrade your `siunitx` version to exploit the full power of `ResultWizard`. | +`precision` (int) | `100` | ✔ | | The precision `ResultWizard` uses internally to handle the floating point numbers. You may have to increase this number if you encounter the error "Your precision is set too low". | +| `ignore_result_overwrite` (bool) | `False` | ✔ | | If `True`, `ResultWizard` will not raise a warning if you overwrite a result with the same identifier. This is especially useful for Jupyter notebooks where cells are oftentimes run multiple times. | +| `min_exponent_for_`
`non_scientific_notation` (int) | `-2` | ✔ | | The minimum exponent for which `ResultWizard` will use non-scientific notation. If the exponent is smaller than this value, scientific notation will be used. TODO: explain better. | +| `max_exponent_for_`
`non_scientific_notation` (int) | `3` | ✔ | | The maximum exponent for which `ResultWizard` will use non-scientific notation. If the exponent is larger than this value, scientific notation will be used. TODO: explain better. | + +If you're using a Jupyter Notebook, you might find [this configuration]({{site.baseurl}}/tips/jupyter) useful. diff --git a/docs/_api/export.md b/docs/_api/export.md new file mode 100644 index 00000000..a476f087 --- /dev/null +++ b/docs/_api/export.md @@ -0,0 +1,39 @@ +--- +layout: default +title: wiz.export() +nav_order: 3 +--- + +# `wiz.export()` +{: .no_toc } + +
+ + Content + + {: .text-delta } + +- TOC +{:toc} + +
+ + +## Purpose + +Call `wiz.export()` after you have defined your results with `wiz.res()`. `wiz.export()` will generate a LaTeX file containing all your results. This file can be included in your LaTeX document with `\input{./path/to/results.tex}` in the LaTeX preamble (see [here]({{site.baseurl}}/quickstart#2-include-results-in-latex)). + +## Usage + +```py +wiz.export(filepath: str) +``` + +- `filepath` (str): The (relative or absolute) path to the LaTeX file to be generated, e.g. `./results.tex`. + + +## Tips + +- The `filepath` should end with `.tex` to be recognized as a LaTeX file by your IDE / LaTeX editor. +- For a convenient setup, have Python code reside next to your LaTeX document. This way, you can easily reference the generated LaTeX file. For example, you could have two folders `latex/` & `code/` in your project. Then export the results to `../latex/results.tex` from your python code residing in the `code` folder. In LaTeX, you can then include the file with `\input{./results.tex}`. +- Especially for Jupyter Notebooks, we recommend to use the [`export_auto_to` config option]({{site.baseurl}}/api/config#export_auto_to). This way, you can automatically export the results to a file after each call to `wiz.res()`. See [this page]({{site.baseurl}}/tips/jupyter) for a suitable configuration of `ResultWizard` in Jupyter Notebooks. diff --git a/docs/_api/res.md b/docs/_api/res.md new file mode 100644 index 00000000..0f689375 --- /dev/null +++ b/docs/_api/res.md @@ -0,0 +1,123 @@ +--- +layout: default +title: wiz.res() +nav_order: 2 +--- + +# `wiz.res()` +{: .no_toc } + +
+ + Content + + {: .text-delta } + +- TOC +{:toc} + +
+ +{: .warning} +The API for `wiz.res()` is not yet finalized as of `v1.0.0a2` and might change before the stable release `1.0.0`. This is due to some issues we are currently experiencing with the multiple dispatch mechanism in [`plum`]. + + +## Purpose + +`wiz.res()` is at the heart of `ResultWizard`. With this method, you define your results, i.e. numerical values with uncertaintie(s) (optional) and a unit (optional). See the [basic usage]({{site.baseurl}}/quickstart#-basic-usage) for a first example. + + +When we talk about a **"measurement result"**, we usually refer to these components: + +- _Value_: The numerical value of your measurement, i.e. the value you have measured and that you are interested in. +- _Uncertainties_: They denote the precision of your measurement since you can never measure a value exactly in the real world. Another term commonly used for this is "error", but we will use "uncertainty" throughout. +- _Unit_: The SI unit of your measurement, e.g. meters, seconds, kilograms etc. + + +## Usage + + +### Define a result + +`wiz.res()` is overloaded[^1], i.e. you can call it with different argument types and even different number of arguments. This allows you to define your results in a way that suits you best, e.g. sometimes you might only have the value without any uncertainties, or you don't need a unit etc. + +In the following, we use these abbreviations. Refer to the [python docs](https://docs.python.org/3/library/decimal.html) if you're unsure about how `Decimal` works. We recommend using `Decimal` for all numerical values to avoid floating point errors. Also see the [precision page](TODO). +```py +numlike := float | int | str | Decimal +numlike_without_int := float | str | Decimal +uncertainties := Tuple[numlike, str] | List[numlike | Tuple[numlike, str]] +``` + +These are the possible ways to call `wiz.res()`. Note you can use IntelliSense (`Ctrl + Space`) in your IDE to see all possible signatures and get error messages if you use the arguments incorrectly. +```py +wiz.res(name: str, value: numlike) +wiz.res(name: str, value: numlike, unit: str = "") +wiz.res(name: str, value: numlike, uncert: numlike | uncertainties) +wiz.res(name: str, value: numlike, sys: float | Decimal, stat: float | Decimal, unit: str = "") +wiz.res(name: str, value: numlike, uncert: numlike_without_int | uncertainties | None, unit: str = "") +``` + +{: .warning } +Some signatures of `wiz.res()` don't allow for an `int` to be passed in. This is currently due to a technical limitation that we are trying to work around before the stable release. + +Note that `uncert` stands for "uncertainties" and can be a single value (for symmetric uncertainties) or a list (for asymmetric uncertainties). When you specify a tuple, the first element is the numerical value of the uncertainty, the second element is a string that describes the type of uncertainty, e.g. "systematic", "statistical" etc. +```py +wiz.res("i did it my way", 42.0, [0.22, (0.25, "systematic"), (0.314, "yet another one")]) + +# These two lines are equivalent (the last line is just a convenient shorthand) +# Note however with the last line, you cannot pass in "0.1" or "0.2" as strings. +wiz.res("atom diameter", 42.0, [(0.1, "sys"), (0.2, "stat")]) +wiz.res("atom diameter", 42.0, 0.1, 0.2) +``` + + +### Override the rounding mechanism + +Sometimes, you don't want a result to be rounded at all. You can tell `ResultWizard` to not round a numerical value by passing this value as string instead: +```py +calculated_uncert = 0.063 +wiz.res("abc", "1.2345", str(calculated_uncert)).print() +# will print: abc = 1.2345 ± 0.063 +``` + +You might also use the following keyword arguments with any signature of `wiz.res()`. They will override whatever you have configured via [`config_init()` or `config()`]({{site.baseurl}}/api/config), but just for the specific result. +```py +wiz.res(name, ..., sigfigs: int = None, decimal_places: int = None) +``` + + +### Return type + +`wiz.res()` returns a `PrintableResult`. On this object, you can call: + +```py +my_res = wiz.res("abc", 1.2345, 0.063) +my_res.print() # will print: abc = 1.23 ± 0.06 +my_latex_str = my_res.to_latex_str() +print(my_latex_str) # will print: \num{1.23 \pm 0.06} +``` + +- `print()` will print the result to the console. If you find yourself using this a lot, consider setting the [`print_auto` config option]({{site.baseurl}}/api/config#print_auto) to `True`, which will automatically print the result after each call to `wiz.res()`. +- `to_latex_str()` converts the result to a LaTeX string. This might be useful if you want to show the result as label in a `matplotlib` plot. For this to work, you have to tell `matplotlib` that you're using `siunitx` by defining the preamble in your Python script: +```py +import matplotlib.pyplot as plt +plt.rc('text.latex', preamble=r""" + \usepackage{siunitx} + \sisetup{locale=US, group-separator={,}, group-digits=integer, + per-mode=symbol, separate-uncertainty=true}""") +``` + + + +## Tips + +You might need a variable in your LaTeX document multiple times: in one place _with_ a unit and in another one _without_ a unit (or uncertainty etc.). Don't define the result twice in this case. +Instead, call `wiz.res()` once and pass in everything you know about your result, e.g. value, unit, uncertainties. Then use `$$\resultMyVariableName[withoutUnit]$$` or `$$\resultMyVariableName[unit]$$` etc. in the LaTeX document to only use a specific part of the result. See the [quickstart]({{site.baseurl}}/quickstart#latex-subset-syntax) for more information. + + +--- + +[^1]: For the technically interested: we use [`plum`] to achieve this "multiple dispatch" in Python. Natively, Python does not allow for method overloading, a concept you might know from other programming languages like Java. + + +[`plum`]: https://github.com/beartype/plum \ No newline at end of file diff --git a/docs/_config.yml b/docs/_config.yml new file mode 100644 index 00000000..c1be61f1 --- /dev/null +++ b/docs/_config.yml @@ -0,0 +1,41 @@ +title: ResultWizard +description: Documentation for the ResultWizard Python library. +theme: just-the-docs +include: ["pages"] +logo: "https://github.com/resultwizard/ResultWizard/assets/37160523/e3ce32b9-2e41-4ddc-88e3-c1adadd305e9" + +url: https://resultwizard.github.io/ResultWizard/ + +aux_links: + GitHub: https://github.com/resultwizard/ResultWizard + Report a bug: https://github.com/resultwizard/ResultWizard/issues + PyPI: https://pypi.org/project/resultwizard/ + +collections: + api: + permalink: "/:collection/:path/" + output: true + tips: + permalink: "/:collection/:path/" + output: true +just_the_docs: + collections: + api: + name: API Reference + nav_fold: false + tips: + name: Tips/Guides + nav_fold: false + +enable_copy_code_button: true + +color_scheme: resultwizard-colors +callouts: + warning: + title: Warning + color: resultwizard-warning-purple + opacity: 0.1 + tldr: + title: TL;DR + color: resultwizard-tldr + opacity: 0.15 diff --git a/docs/_includes/nav_footer_custom.html b/docs/_includes/nav_footer_custom.html new file mode 100644 index 00000000..3f94c423 --- /dev/null +++ b/docs/_includes/nav_footer_custom.html @@ -0,0 +1,4 @@ + diff --git a/docs/_includes/title.html b/docs/_includes/title.html new file mode 100644 index 00000000..6b6eb0a9 --- /dev/null +++ b/docs/_includes/title.html @@ -0,0 +1,4 @@ +{% if site.logo %} + +{% endif %} +{{ site.title }} \ No newline at end of file diff --git a/docs/_sass/color_schemes/resultwizard-colors.scss b/docs/_sass/color_schemes/resultwizard-colors.scss new file mode 100644 index 00000000..783d1c34 --- /dev/null +++ b/docs/_sass/color_schemes/resultwizard-colors.scss @@ -0,0 +1,6 @@ +$resultwizard-blue: #1C6CB3; +$resultwizard-purple-light: #A03F70; +$resultwizard-purple-dark: #773377; + +$link-color: $resultwizard-purple-dark; +$btn-primary-color: $resultwizard-purple-dark; diff --git a/docs/_sass/custom/setup.scss b/docs/_sass/custom/setup.scss new file mode 100644 index 00000000..a1fbd3bd --- /dev/null +++ b/docs/_sass/custom/setup.scss @@ -0,0 +1,4 @@ +$resultwizard-warning-purple-000: #A03F70; +$resultwizard-warning-purple-300: #773377; +$resultwizard-tldr-000: #4699CD; +$resultwizard-tldr-300: #1C6CB3; diff --git a/docs/_tips/jupyter.md b/docs/_tips/jupyter.md new file mode 100644 index 00000000..110d8f18 --- /dev/null +++ b/docs/_tips/jupyter.md @@ -0,0 +1,54 @@ +--- +layout: default +title: Jupyter Notebook +nav_order: 1 +--- + +# Jupyter Notebook +{: .no_toc } + +
+ + Content + + {: .text-delta } + +- TOC +{:toc} + +
+ +{: .warning } +Note that using a Jupyter Notebook **in a browser** won't make much sense in conjunction with `ResultWizard` as you won't be able to directly include the results in your LaTeX document. However, you can still use `ResultWizard` to export the `results.tex` file and copy the contents manually into your LaTeX document. But this is not the originally intended workflow and might be tedious +
Note that VSCode offers great support for Jupyter Notebook natively, see [this guide](https://code.visualstudio.com/docs/datascience/jupyter-notebooks). + +## Useful configuration + +In the context of a [*Python Jupyter Notebook*](https://jupyter.org/), you might find this `ResultWizard` configuration useful: +```py +wiz.config_init(print_auto=True, export_auto_to="./results.tex", ignore_result_overwrite=True) +``` +- With the `print_auto` option set to `True`, you will see the results printed to the console automatically without having to call `.print()` on them. +- The `export_auto_to` option will automatically export the results to a file each time you call `.res()`. That is, you don't have to scroll down to the end of your notebook to call `wiz.export()`. Instead, just execute the cell where you call `.res()` and the results will be exported to the file you specified automatically. +- With the `ignore_result_overwrite` option you won't see a warning if you overwrite a result with the same name. This is useful in Jupyter notebooks since cells are often executed multiple times. + + +## Cell execution order & cache + +Watch out if you use [`wiz.config()`]({{site.baseurl}}/api/config) in a Jupyter Notebook. The order of the cell execution is what matters, not where they appear in the document. E.g. you might call `wiz.config()` somewhere at the end of your notebook. Then go back to the top and execute a cell with `wiz.res()`. The configuration will be applied to this cell as well. This is an inherent limitation/feature of Jupyter Notebooks, just be aware of it. + +It might be useful to reset the kernel and clear all outputs from time to time. This way, you can also check if your notebook still runs as expected from top to bottom and exports the results correctly. It can also help get rid of any clutter in `results.tex`, e.g. if you have exported a variable that you deleted later on in the code. This variable will still be in `results.tex` as deleting the `wiz.res()` line in the code doesn't remove the variable from the cache. + + +## Omit output from last line + +In interactive python environments like Jupyter Notebooks, the last line of a cell is automatically printed to the console, so you might see something like the following under a cell: + +``` + +``` + +If you don't want this, you can add a semicolon `;` at the end of the last line in the cell (also see [this StackOverflow answer](https://stackoverflow.com/a/45519070/)). This will suppress the output. For example, write this: +```py +wiz.res("jupyter notebook output", 5.0, 0.1, unit="\m\per\s^2"); # <-- note the semicolon here +``` \ No newline at end of file diff --git a/docs/_tips/siunitx.md b/docs/_tips/siunitx.md new file mode 100644 index 00000000..7f25663c --- /dev/null +++ b/docs/_tips/siunitx.md @@ -0,0 +1,58 @@ +--- +layout: default +title: Siunitx Configuration +nav_order: 2 +--- + +# `siunitx` Configuration +{: .no_toc } + +
+ + Content + + {: .text-delta } + +- TOC +{:toc} + +
+ + +## Purpose + +The [`siunitx`] package offers a wide range of options to configure the formatting of numbers and units in LaTeX. In the exported `results.tex` file, we make use of `siunitx` syntax, e.g. we might transform a `wiz.res()` call into something like `\qty{1.23 \pm 0.05}{\m\per\s^2}` that you also could have written manually. This means, you have full control over how the numbers and units are displayed in your LaTeX document by configuring `siunitx` itself. + +If you want to configure `ResultWizard` itself instead, see the [`config_init()` & `config()`]({{site.baseurl}}/api/config) methods. + + +## Important configuration options + +All options are specified as `key=value` pairs in `\sisetup{}` inside your LaTeX preamble (before `\begin{document}`), e.g.: +```latex +\usepackage{siunitx} +\sisetup{ + locale=US, + group-separator={,}, + group-digits=integer, + per-mode=symbol, + uncertainty-mode=separate, +} +``` + +Here, we present just a small (admittedly random) subset of the options of [`siunitx`]. See the **[`siunitx` documentation](https://texdoc.org/serve/siunitx/0).** for more details and all available options. + +[Siunitx Documentation](https://texdoc.org/serve/siunitx/0){: .btn .btn-primary .fs-5 .mb-4 .mb-md-0 .mr-2 } + +- `locale=UK|US|DE|PL|FR|SI|ZA`. This option sets the locale for the output of numbers and units, e.g. in English speaking countries, the decimal separator is a dot `1.234`, while in Germany it's a comma `1,234`. +- `group-separator={,}`. This option sets the group separator, e.g. `1,234,567`. +- `group-digits=integer=none|decimal|integer`. This option affects how the grouping into blocks of three is taking blace. +- `per-mode=power|fraction|symbol`. This option allows to specify how `\per` is handled (e.g. in the case of this unit: `\joule\per\mole\per\kelvin`). `fraction` uses the `\frac{}` command, while `symbol` uses a `/` symbol per default (can be changed with `per-symbol`). +- `uncertainty-mode=full|compact|compact-marker|separate`. When a single uncertainty is given, it can be printed in parentheses, e.g. `1.234(5)`, or with a `±` sign, e.g. `1.234 ± 0.005` (use `separate` as option to achieve this). In older versions of `siunitx`, there existed the following flag instead: `separate-uncertainty=true|false` (it might still work in newer versions). +- `exponent-product=\times`. This option allows to specify the product symbol between mantissa and exponent, e.g. `1.23 \times 10^3` or `1.23 \cdot 10^3`. The latter is more common in European countries. This is also affected by the `locale` option. + + + + + +[`siunitx`]: https://ctan.org/pkg/siunitx diff --git a/docs/favicon.ico b/docs/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..65a75d9462c96b617c968d58ecbb827297812b3e GIT binary patch literal 9584 zcmcI~by$=^wD(K6w4`*2G*VL1As~$cBCvFafRr@5fG8az4K7GacP%B|-2&2xz|y%a zcfH>~_xt|)o_p`S&v|C%dCxoZ&Y78We&@^p00#Q`moNZkz=#b1DADDJS6VNL@M-bU zMIu!dMV)_d{}C=GI-4Sq75rCob=T6v82j#>4ghQvs*3V@K8pug-Uhl0&G$R^{s{3) zv7Jo+z2RR%J7QXrLb3A~)kkESnnbJ%Nv zoVoZ>6Mt8-7XS{$li!|fO;0DJvLxoC2@IGdl(}Tv?gH2ov`iOOIYiL1p+$O&w>=lBJ_H0C#-U` zLoP2)FO%u&Q$)W*)Hq;Rn1C=?=hu)f8IO33iEazzY+iRvX$5lmq*Wl$4mT2jD$U(( z#C_Q5zX1hDxNRu}8}zjMFEDhhVg@=(k z`k z>BQ)dPzz7#A1G{)3t7&)nOYT2U22Ikfx!^#t%6hgc(LaqqmL2NOPTu7SXhZz0CfB8 z?%6^fXa5o5%z_X&fkgXxGaNcq!@tjZ znB#S)jg5PIP{m&{RF1V;%bL%7lj)|$P=k6=3e12U7Q8)7Z20d^N#0Lv!c^6~G0mr> z1U+6&bz}+FH_U3RAP@k=kdm%_9H+f7R((&%SL~dHd&5SS=0nL7U_IFLs;Msu^zur8 z0R-5qUGkHtYBf);$|^r=PEn;#=@0a?w=b=(W#dgJ-O6G-BqKkhrU}eYxYXdx->grm zQ92ngf4bUpukUZ^vN5&G^5i9BR!%>@9MGZg0D9!!hU#2QLjXSqkWzI=PGN! zy>1M!7mMC;0sePaVpLjzL5tRDVOzJoJF)`sg(z>i51ufkiaOA$r>n>^^jMn|v!kEO zygrwdY&aLyZWB2*@qK1I)F_=O53hU646q%*pIrqb7F>j< z=R+`F=}dw3*b-H`yK;35*zf^+BZX*-c(bLuDy@F~*6ih%0mMuva9b*}KN$%yV^iJv z4qNxV)4$y0>h5r|0o%9$QNsuCL^%+(k&D^jzvMaT;NLaajcawRcrM+iX>IR7n83=_ z^LTJ5M&*HG{H!UlnAjr0XhmfIk%?Di^^k;PuFQ}$^%!=yhq?m$QG92$IAETGK>~MZ zxSc8#H!yliVI>5lTF-|nss-SEN_@?YBNw+ts(>V&`2GGo6etLN8PR3cJHipY)HZB} zH6-=q2j=?U;xt|wHu%Y>&RIUR4tWGhduRC`tyu{$pdr-llD!UwvK+fIy3O&!E?o1Rul%z2z^S=s7+k_u1*Ksu=7sRHEB ze20pemA{9Uk|8H=7n{_@aN_$79E)D!+%egav+$DKw}i)_uQ=F^k^r<95O}@!S~NoE zk$v;LZLN1v%t8m@m|82jy!Zy4#b5Fr5+jn#fijmjrOjA`0HBE>ckyc@sRX9@M(?q2 znBFyiRG6hHLxV84>_V7A8-+?MxhBxw9){Ele&4;sOTqz;B+APZ^66yI}`3O3r`6oNK?=4}ZCUpNxJ< zXQZ^NY)V`eVrJJ@)#&OHW<8>nz-MvaFt%EU#0(G*{c+qk9(;lBa9xCe0GJk1@BR1( zhc|bQK6a&us!kPF-2;V>e1vzgxX1Bc|MuM*o3kW3WX<|8^q!FOB(@}i$DZGb{tx3T zMlp8B>_l}rDbWgnXZ$_9-!532Bw7RBh07iv+W;MtX93qSu)aX?9UdI>&)q2!(#1Y< zKi(5?UE=b15b*et=;ySBT zeldx;KzM{eg37Z$ z>K5}F0?A)Q3nA#xKT>BiBAta*fd$oUS-wNM zo4pV6_|P;qm{`Lds`d0LZAG&O^EH2A4PUG9t*Yhl8=XzV_Efr@raZ#wRUR2c_co6^ zA<1K#i7=(dh9brJNK^v42eA6mt(!NgdZXrYDVf$A!It83D^H)5dh=Hx^OB(PBRX(-&<3u^96?uo9*O#VU{-1ZDd;T z4()~k(Qbc)m zP~+&Ne`wesuH1fvj^}I|vPWhQWHP8MePSEh&)d7UNVz8I{PQv^Mk*l5Z(Tp64BUHQ z&dwE+s2ld3D($4eSZzf%;jwv3<+Sxpfz3^x9-B}f=sHnG5@iit0>eya<#_&b^kSf= zD*T6Zt4V0|{vy2ghZ@MvQ!!fymjxvv)$i)CTgvO~SdP#m2b>DA045BOdz}u$=fA>ngNVgrTopfw9mA>C?tRs>N>*)%>m-lWqd`&p2%7douDiK`ON)y%qJ?g8$O`{U*1#sg zSkVP~9}e?9bS=n%zK!Bd;wjv2cq^=FV_wwSAP$=`ofJs0eJTN3F_Ks@GnxHDJ2b3O zzPm?F&GdE|PU(fT*Wer|<{2q*b>574ME9soK}*UE80M~d9YANmw^jr-F?b2dRBR=s z+)?1wG>=`&r7>my0ue@r5`8#(!}09LI)0N)rfShf#dqQCvZDHu=CtrWH2TuBQPtI< z2>oWTGAAu1vU>2Bk$c3IGrNX6asjLLNg@tTlUNiRlC%@^fqA-Lk%rqsOqf^kH`S`q zX|xKk%i8n9S=R`5fZI>KSM)R}&YuLIL0t>zfORcJpgm7c_E= zw|=2>chbX@T^H^YfMuDyfte0k)fFMxrZ2s#(*5@qtw}H)jC*S8fAC6_GDjFqs-N8@ zI<1Io@usP6aq@19UTS1KBD{#HH2lR4!pZM(&g45b{s|Q$?D$GXk}{J_E;n|fS*B)- ze|%CtIEcH}Z=lN7%R_;vHt5y#lfbTgK_y>|+QF;!i2ero*jx=&^}0Q`Y-A2@S`35u?hZ3gBBx&s z&`B8iC#+K0%x1T>e-^u9AId&m=2B7ovKT$MK3+Qbaor+7{v$q?71on7B`;&;W&6tx z_31VM`rz^Ax@uLvymk%sgBWfaUO&@Rlh4&5`?cKDBPBkv5IuvDcZ2E`+`~lFW%e&rOC%4Pte3NY_pw>xhTxe2Cy)8|Ov%?!%1+$u6Sqa44DJp5zmw`On>hsKglFH| znMu~j%RE4X`mdJ_9=CUa)ged^KmSi}B(8p(%&O>^vea1{RDqO(yy=`Ickyg$ud34{ z_1D$G6$WPpsQ{;>a7BF?d(+1(L-9pP;q%kTzK>BsW6M3_n2HHBjB%4`Y$IbYAjqn+ ze6f(PFY&m3uX$JuM(yyVX%6Tq`vs&8vL3Hv%c(A(|MD|3^pGJ4Lv3uRyZDDIJpk7J zZrPL(ALc86q~htRn|}i1&r_yI_+~q!=k|z$+>%nbSDB)qC&lX!qS9?GGj1yrPo z4l+yCTvS`*ucY;9fb0YY*$YmRaI|?wtZLiTw7lV#HLRsI)0U8~Gh$;ir>x@YydDr7 zMztjz`z|vm>H^Hv_5*gu+!@^=`R79PQ8>WQIRXN4tp+kKiURi$93DU6b066P9Gp#} zp{Q@&Zu&}KA9I&?)^1GScvW`1TOnLM|4M=OT;RFn4yX-Ax%&zjLY!=5~P zDrr32r;Q$B5pSk)e%2^s_6yfs~vL)f8U%upJ!2y za=(Nn_cnWQBq*2oYIYK%8!rN1!mCI473V}fc_jV3$HMQjHkU3ZKOZBiRU&waLD$5; z>@S%LE-%_vikZ;-uXK@96C&@tL=Mx{7Zs6D5OXyr^OQTeKZ|X!A9Z*B!hA?eUL?4+ zQQ^Bzj%J^xxVL^Ja&JkqMrZV$;@-T%?`9_>t1$VkH|J*wQrT8+Czr3iLH6^SP%+Pm$DP-tCY%8WtFHe7L3IMmi6-y! z?=02VYA?v|Sp!=0s!jURk^L1=Trc+CNX*y-1~xrL3baI^y&Ql^U~b=7PwFB^?E0-g zH-5H-+Z!B>pO%x3cxJ56%vWE1Y9nfbF5m-DVMgF*=#XPeo7ltl?PL)dUng*3pE%C& zlR-xxd^3uu{1lTZBL438l4e%d|70@F`m<}jhPHi*-~`u-n*#_4o}ZUFPN|uIue^T1 zowfXgW3(@`Es3Rr97r?b`zLkNiqF!IQ!1t(Crs^yKf%UuhUYMqa$8Z>U1ot+EnI9i}XP?IV^-eCKEv@8Sd zBZsBlcLH&HjMTcZAB;oNn2;1Cs!kpBub(AQX9lIVDsVJb{vcOi9{>43PP7XFky&l9 zU58z<%E>~?45w$Os=AP>i9kvq{;Di}s^0wfUkk9tq&5Yy+Q?-B07}UT;M*l>;RJi@ zh51j)Ag54v4s6C!8ed4`Zu!^(%bE1&LEqWvn=7W>Sp^V9x`r{DMeJXhCnygd~@6xMG0#&WXUV30dtK}=!RKs=Ja#ef8BNV^O=XU-a#0Y1x zwJ^E#h4fx+4j8Tbjwzmn;7J$%KKo)%BxLye>l2JW=l!Nf$=4!h5GCsx*N@481yrK` z0Lur1ZdY+qk83wMQ#!6PE$6qlk)Mn*(pRTL5sln-0N`U7Q8{mWGNwP|bgkpNil8$& zZqn+nev(HV7oW^)AgOWl+n_(CkVTY%IFk*qSlq77`ZV`&<*ILDJT4ZS58Mya8jz8S zd}^P|C0)bgMEYO?bWO8w|F|-1O)LK z*7|bV&Oi1F`F0ckrmE$Y((7JUdWr_tq{4D_i?ZqJ+a>g-+<bDPNWZXM6 zo!?6Gf7p!QIXt-hLS99L(`^a--pXYrgzkOsg{8nGs}A?pAWPS)vFw8(yMp6UAIuY{ zBCv=jtmvEGCqt%YzMbUGIxh!adR-uYh)ykSRbOWB7-Og`*zC7m{#=tL_MGu4K0Wol zT$XLT);Ch%0*)L#imq@;PQS(i0t@Tr&F8lMnFJ`CE}~EbV%OEna?bs= z0V_#(cPgu{M)V2`3GRe04gn-9=HadhN5iUq292|aL?dF2$^)mvW6qG_parz#T72qg zwoK@~B7Sq=W%Lva@M#*qzy4?`)-ZO6L&fm~Q6k}Tv6 ze)mV0j$v4IsRZG;(>2`id^gbJu@LPNjCz?HFn3w0puhL5KYPDm(w$IBt*i>FnwKAB zS~FYrUkaFcP~$qfdNuKelVW{^KdZhB4+!VlSn6X;5AdD6SsK!J;xF!tkAj&7qUP(D z%&6mcu;or%$eEn!rbJT8X1t0|ZA4d11d;omqHIin4-JCp58WTc?pxN*LA~EK`}3U% zoe56h184ordgO5LHOJck!H?GLz)?H2l{D*7Te^z90vGi>4UAeI))856tk}FY|jc z;L6ExKG4PAEi7iS{DbNKbp3by-Z|P2D@Ho2$IF~Wj-1lzJ;uB11 z&dI`_0IL2Ps^H;@K!=U&a`i0!mZrgr_0P&Zqzgbt=8J4@qx{h1){)Q2ojWSIcey`m zEXiA1a$d{b?9VTQhcr!Q=b1M|+$TsAggDyffOFG-@fW*I$|$jcO{dYrT$moJa`3r-$E~8n~>7UhUxY zkX@I2aieK_st}zdzrlai7-X?qH86Wl5W4p<#1KWr^)Q$ZM%P%;5L7#=BYklMF5z$* z9(6FwAO}csq00rNi?+nGM$Lgv#q}jwR4+#U-ZaVkQUFIfp-@HEG~8duU(NG!3y)mJ z^T^)=gCV-V+NG{NqeeDnl=YtMH0L259VvO({r#dw%OeL%)w#nPe9wH65)ka_Fe1u5 zK}1Y4#%h}BHEPe!1Dn~Rfy?G|+MFK&EQZptWzkn~bwr=hEbB+?ASSS#!znx~rf6jM z)z-Hnx6!b;=1L> z1%;bcC956FL)~Pb+vh*DNqJOVv^-lQILN1da!I*~;GMBekmTL)PF-1s9MtGqmu+}p zn5Q@(U4+i4I33}OZ^b=#YrzVI=pion{`TjTz&L2-TB^%zw{GFikta z1y7}j)L!TCTdQHhU}*p=x{7d0c{H$n8)fI;nD@@oDaQ2>XJX(Gm- z>0jm_M@HtIlRP#Rs;-^oSD8g;*vN7$EOOFt&EQ!kGuLEG-}PE7w)i9D8Ufk4Xm-+} zJe`q6dL^uS>=jKJyRIh-xl_KAL|&`yZfBuKkBG z|7Q|5x*6twO`!ogfIdqFagFx+w|F` zY!wIOmW#$t2><=%NHXE+y<4zB^M>dC-v2pOVmgsRb=~Qr!<{az4tL9MTHfsN2YB7| zs)2VzMC00OE*7kn79CYrY&5U63ZC0nQsmZ3VT( zmA3SY@jtQ;ZP=8SW}c|v93(S15{Y1CzsUSTcBQ?mO?jTn$40?e_>6`!tb zf4e`dtBqs$gIFrOKXBh$tw4Ut@j&@X2c}^iVBsu2tK?D^CUE?2D9Y}5LF_ttZv#VZ zEf24(39>MZKgV8~Y&?Xi1kh-`@&8!E*^6)%b5 ziaFc&b^JC4^UnxQDmp%Z+5o^S&!uWDga@7vKNf$Wuh({w|6O@l6|~D>(hM zy&LkgxX<(l@$Ht0w8L6G9EqW$=+37EZ^mTjx^WlU1>pps`f^0!u`O;=R1s$&P<2 zPZUQRT&Rnc5CpAt4R6Pcq5Jgu-XjFQqYsi`NlYVE%l$qul1v@|1o zg9)(r643)zV@TP(kM|I(Jz1jM4n0DVAz187`dbk&Mn`l_{4QmG-iutTl2KN;e`JVD zw+x2_kX=s-VEz6KK9l{c^nK}~%2xw?8IK7qKG2R{?8Y@ed~<~ZnA-xVBJ!L|=C`CH z+RmDm&)Jlr-#RBAiMkehPB?ZnaD%7Qu=<0pMd-ZV?taT*EDD)=f~d8{2j?r0 z4leZ`AgNuMumD?Z;QV1P<9v`J|1d_L4+Xc=J0>~T>lD&YF{CIym5>>o2S0D!!K>@C z^aS_g!nAcVb=fqn&$vl)QVK*i?{cjaJV)z(>j1wW0NK6bceT_`!-9_;Ytt6V`HNMj z1{)!>F>(z+>(Jv;jaVWen-Y*Bb9~hTvHnztGK7GR*Ikp#?_(d% z2QSgwKUWP}#r+jX`(otel6Ic)c=!!AFi?-*(lOPt9_*Sym z!>F}sSt{iU`UGNlJz!7vpC^eJ3f>TatriUeIbUW zVWM0sT2qBnQ8w}%eMbTQ0TeDC>^7d!LQH#bw?W&+)2*oR^sxqJfoX=rAp_u}i-n0R zif#1$|52m=w^IGDH2}(vHhs)!V~4&K^FN!7)6L#vctEv?&lMiBqb&}gs-&e@u3#SW EAMGcZHUIzs literal 0 HcmV?d00001 diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000..da53d172 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,86 @@ +--- +layout: home +title: Home +nav_order: 1 +--- +
+ ResultWizard Header Image +
+ +# ResultWizard +{: .fs-9 } + +Think of ResultWizard as the glue
+between Python code & your LaTeX work. + +{: .fs-6 .fw-300 } + +{% capture intro_link %}{% link pages/quickstart.md %}{% endcapture %} +[Quickstart]({{intro_link}}){: .btn .btn-primary .fs-5 .mb-4 .mb-md-0 .mr-2 } +[Source Code (GitHub)](https://github.com/resultwizard/ResultWizard){: .btn .fs-5 .mb-4 .mb-md-0 } +[PyPI](https://pypi.org/project/resultwizard/){: .btn .fs-5 .mb-4 .mb-md-0 } + + +{: .warning } +ResultWizard is currently fully functional but still in its **alpha stage**, i.e. the API might change. We're happy to receive your feedback until the first stable release. +
Please report any issues on [GitHub](https://github.com/resultwizard/ResultWizard/issues). To get the latest alpha version, you have to install it via +
`pip install resultwizard==1.0.0a2` (otherwise you end up using the older version `0.1`). + +--- + + +## 💥 The problem ResultWizard tries to solve + +Various scientific disciplines deal with huge amounts of experimental data on a daily basis. Oftentimes, this data is processed in Python to calculate important quantities. In the end, these quantities will get published in scientific papers written in LaTeX. You may have manually copied values from the Python console or a Jupyter notebook to a LaTeX document. This **lack of a programmatic interface between Python code & LaTeX** is what `ResultWizard` is addressing. + +## 💡 How does ResultWizard help? + +In a nutshell, export any variables from Python including possible uncertainties and their units. + +```py +# Your Python code +import resultwizard as wiz +wiz.res("Tour Eiffel Height", 330.362019, 0.5, "\m") +wiz.res("g", 9.81, 0.78, "\m/\s^2") +wiz.export("./results.tex") +``` + +This will generate a LaTeX file `results.tex`. Add this file to your LaTeX preamble: + +```latex +% Your LaTeX preamble +\input{./results.tex} +\begin{document} +... +``` + +Then, you can reference the variables in your LaTeX document: + +```latex +% Your LaTeX document +This allowed us to calculate the acceleration due to gravity $g$ as +\begin{align} + g &= \resultG +\end{align} +Therefore, the height of the Eiffel Tower is given by $h = \resultTourEiffelHeight$. +``` + +It will render as follows: + +![result rendered in LaTeX](https://github.com/resultwizard/ResultWizard/assets/37160523/d2b5fcce-fa99-4b6f-b32a-26125e5c6d9b) + + +That's the gist of `ResultWizard`. Of course, there are many more features and customizations available. + + +## Why shouldn't I continue to manually copy-paste values? + +Here's a few reasons why you should consider using `ResultWizard` as programmatic interface instead: + +- _Sync_: By manually copying values, you risk getting your LaTeX document out of sync with your Python code. The latter might have to be adjusted even during the writing process of your paper. And one line of code might change multiple variables that you have to update in your LaTeX document. With `ResultWizard`, you can simply re-export the variables and you're good to go: +- _Significant figures_: `ResultWizard` takes care of significant figures rounding for you, e.g. a result `9.81 ± 0.78` will be rendered as `9.8 ± 0.8` in LaTeX (customizable). + +`ResultWizard` allows you to have your variables in one place: your Python code where you calculated them anyways. **Focus on your research, not on copying values around**. + +[Get started now]({{intro_link}}){: .btn .btn-primary .fs-5 .mb-4 .mb-md-0 .mr-2 } diff --git a/docs/pages/about.md b/docs/pages/about.md new file mode 100644 index 00000000..dcc3db18 --- /dev/null +++ b/docs/pages/about.md @@ -0,0 +1,38 @@ +--- +layout: default +title: About +permalink: about +nav_order: 4 +--- + +# About +{: .no_toc } + +
+ + Content + + {: .text-delta } + +- TOC +{:toc} + +
+ + +## Little bit of history +This project started out at the end of 2023 under the name `python2latex` when [*Paul Obernolte*](https://github.com/paul019) was working on reports for the practical parts of the physics curriculum at the University of Heidelberg. In these reports, experimental data was processed in Python and many results had to be included in LaTeX. The manual copy-paste process was error-prone and took too much time, so he started this project. Originally, it just consisted of one single file, that got the job done. + +In 2024, the project was renamed to `ResultWizard` as [*Dominic Plein*](https://github.com/splines) joined the team. Together, we completely redesigned the project, added new features and improved the code quality with linting, tests, CI/CD and a modular clean architecture approach. We set up a branding, published first alpha releases to PyPI and started this documentation. We learned many things along the way and are proud to have published our first Python package. + +We hope that `ResultWizard` will help you save time and make dealing with data in Python and LaTeX more enjoyable. If you have any feedback, please let us know on [GitHub](https://github.com/resultwizard/ResultWizard/issues). If you like the project, consider starring it on GitHub as we're always happy to see that people find our work (we're doing in our free-time) useful. + + +## The future (plans for 2024) + +As of mid-April 2024, we're still in the alpha stage. But the first stable release is not that far away. We want to make sure we ship a solid product you can rely on, so expect a few more months of alpha testing. We're happy to receive your feedback until the first stable release. After this release, we plan to maintain the project (especially with regards to security fixes). But we will probably add new features only sparingly (in an effort to keep the API stable and in the light of our limited free-time resources). + + +## Acknowledgements + +We like to thank the many great people in our surroundings who have helped make this project see the light of the day. We're grateful for the support of our friends, family and colleagues, both mentally and in the form of very concrete feedback as alpha testers. We also want to thank the open-source community for providing us with so many great tools and libraries, among others: [`siunitx`](https://github.com/josephwright/siunitx) by Joseph Wright, [`plum`](https://github.com/beartype/plum) by Wessel Bruinsma and other contributors as well as [`pylint`](https://github.com/pylint-dev/pylint/) by Pierre Sassoulas and others. We also want to thank the whole `Python` and `LaTeX` communities for providing us with such powerful tools to work with in the first place. And the [`just-the-docs`](https://github.com/just-the-docs/just-the-docs) contributors for such an amazing Docs theme. Thank you all! 🙏 diff --git a/docs/pages/quickstart.md b/docs/pages/quickstart.md new file mode 100644 index 00000000..3dde4770 --- /dev/null +++ b/docs/pages/quickstart.md @@ -0,0 +1,192 @@ +--- +layout: default +title: Quickstart +permalink: quickstart +nav_order: 2 +--- + +# Quickstart +{: .no_toc } + +
+ + Content + + {: .text-delta } + +- TOC +{:toc} + +
+ + + +## ❓ Is this for me? + +**Before you start and to avoid frustration**, let's make sure that `ResultWizard` is the right tool for you: + +- It's not primarily for you, if you're writing your LaTeX code in a web-based editor like Overleaf. `ResultWizard` is a Python package that will export a `.tex` file in the end. You have to include this file in your LaTeX project and the closer your Python code is to your LaTeX document, the easier it is to reference it without having to do anything manually in-between. You could still use `ResultWizard` in your Python code and manually copy the contents of the generated `results.tex` file into Overleaf, but this is not the originally intended workflow. +- The same philosophy applies to Jupyter Notebooks that run in a browser. Instead, you should use a local Jupyter Notebook setup and have your LaTeX project reside next to your Python code. Using VSCode as editor is one way to do this. It has built-in support [for Jupyter Notebooks](https://code.visualstudio.com/docs/datascience/jupyter-notebooks) and with the [LaTeX Workshop extension](https://marketplace.visualstudio.com/items?itemName=James-Yu.latex-workshop) you can easily compile your LaTeX document and see the changes immediately. For a possible setup within WSL, see [this guide](https://github.com/Splines/vscode-latex-wsl-setup). + +Ideally, you'd have a folder structure where `code` (or `python` or `src` or whatever) and `thesis` (or `latex` or `document` or whatever) are folders on the same level. See also the [wiz.export() API]({{site.baseurl}}/api/export). + +Ideally, you also have used the [`siunitx`] LaTeX package beforehand to know how to format units, e.g. `\m\per\s^2`. But don't worry if you haven't, you can still use `ResultWizard` and learn about `siunitx` along the way. You might also want to check out the [siunitx configuration]({{site.baseurl}}/tips/siunitx) page. + + + +## 💻 Installation & prerequisites + +{: .tldr } +> Have a LaTeX toolchain including [`siunitx`] set up and Python `>=3.8` installed.
Then install the `ResultWizard` package via [`pip`]: +> ``` +pip install resultwizard # use `pip install resultwizard==1.0.0a2` in the current alpha stage +``` +> Move on to [Basic usage](#-basic-usage). + + + +### Python package + +You can install `ResultWizard` via [`pip`]: + +``` +pip install resultwizard +``` + +{: .warning } +ResultWizard is currently fully functional but still in its **alpha stage**, i.e. the API might change. We're happy to receive your feedback until the first stable release. +
Please report any issues on [GitHub](https://github.com/resultwizard/ResultWizard/issues). To get the latest alpha version, you have to install it via +
`pip install resultwizard==1.0.0a2` (otherwise you end up using the older version `0.1`). + +Verify you're using the version you intended to install: + +``` +pip show resultwizard | grep Version +``` + +### LaTeX toolchain + +To compile the LaTeX document, you need a working LaTeX toolchain. If you don't have one yet, there are many guides available online for different OS, e.g. on the [LaTeX project website](https://www.latex-project.org/get/). +- For MacOS, you might want to use [MacTex](https://www.tug.org/mactex/). +- For Windows, we recommend [MikTex](https://miktex.org/). +- For Linux (Ubuntu, e.g. also in WSL), we recommend [Tex Live](https://www.tug.org/texlive/)[^1]: +``` +sudo apt install texlive texlive-latex-extra texlive-science +``` + + +No matter what LaTeX distribution you're using, you will have to install the [`siunitx`] LaTeX package. This package is used to format numbers and units in LaTeX, e.g. for units you can use strings like `\kg\per\cm`. In the Tex Live distribution, this package is already included if you have installed `sudo apt texlive-science`. + + +### Checklist + +- [x] Python `>=3.8` installed & `ResultWizard` installed via [`pip`] +- [x] LaTeX toolchain set up, e.g. TeX Live +- [x] [`siunitx`] LaTeX package installed + + + + + + + + +## 🌟 Basic usage + +{: .tldr } +> 1. Import the library in your Python code, declare your results and export them: +>```py +# In your Python code +import resultwizard as wiz +wiz.res("Tour Eiffel Height", 330.362019, 0.5, "\m") # units must be `siunitx` compatible +wiz.export("./results.tex") +``` +> 2. Include the results in your LaTeX document: +>```latex +% In your LaTeX document +\input{./results.tex} +\begin{document} + The height of the Eiffel Tower is given by $h = \resultTourEiffelHeight$. + % also try out: $h = \resultTourEiffelHeight[value]$ +\end{document} +``` + + +### 1. Declare & export variables in Python + +In your Python code, import `ResultWizard` and use the `wiz.res` function to declare your results. Then, export them to a LaTeX file by calling `wiz.export`. For the unit, you must use a [`siunitx`] compatible string, e.g. `\m` for meters or `\kg\per\s^2`. See the [siunitx docs](https://texdoc.org/serve/siunitx/0#page=42) for more information. + +```py +import resultwizard as wiz + +# your complex calculations +# ... +value = 330.362019 # decimal places are made up +uncertainty = 0.5 +wiz.res("Tour Eiffel Height", value, uncertainty, "\m").print() +# will print: TourEiffelHeight = (330.4 ± 0.5) m + +wiz.export("./results.tex") +``` +There are many [more ways to call `wiz.res()`](TODO), try to use IntelliSense (`Ctrl + Space`) in your IDE to see all possibilities. If you want to omit any rounding, pass in values as string, e.g.: +```py +wiz.res("pi", "3.1415").print() # congrats, you found an exact value for pi! +# will print: pi = 3.1415 +wiz.res("Tour Eiffel Height", str(value), str(uncertainty), "\m").print() +# will print: TourEiffelHeight = (330.362019 ± 0.5) m +``` + +You can also use the `wiz.config_init` function to set some defaults for the whole program. See many more [configuration options here](config). +```py +wiz.config_init(sigfigs_fallback=3, identifier="customResult") +# default to 2 and "result" respectively +``` + +If you're working in a *Jupyter Notebook*, please see [this page]({{site.baseurl}}/tips/jupyter) for a suitable configuration of `ResultWizard` that doesn't annoy you with warnings and prints/exports the results automatically. + + +### 2. Include results in LaTeX + +You have either called `wiz.export(.results.tex)` or set up automatic exporting with `wiz.config_init(export_auto_to="./results.tex")`. In any case, you end up with a LaTeX file `./results.tex`. Import it in your LaTeX preamble (you only have to do this once for every new document you create): + +```latex +% Your LaTeX preamble +\input{./results.tex} +\begin{document} +... +``` + +Then, you can reference the variables in your LaTeX document: + +```latex +% Your LaTeX document +This allowed us to calculate the acceleration due to gravity $g$ as +\begin{align} + g &= \resultG +\end{align} +Therefore, the height of the Eiffel Tower is given by $h = \resultTourEiffelHeight$. +``` + +It will render as follows (given respective values for `g` and `Tour Eiffel Height` are exported): + +![result rendered in LaTeX](https://github.com/resultwizard/ResultWizard/assets/37160523/d2b5fcce-fa99-4b6f-b32a-26125e5c6d9b) + +If you set up your IDE or your LaTeX editor properly, you can use IntelliSense (`Ctrl + Space`) here as well to see all available results, e.g. type `\resultTo`. Changing the value in Python and re-exporting will automatically update the value in your LaTeX document[^2]. + +

+Also try out the following syntax: + +```latex +% Your LaTeX document +The unit of $h$ is $\resultTourEiffelHeight[unit]$ and its value is $\resultTourEiffelHeight[value]$. +``` + +Use `\resultTourEiffelHeight[x]` to get a message printed in your LaTeX document informing you about the possible strings you can use instead of `x` (e.g. `withoutUnit`, `value` etc.). + +--- + +[^1]: For differences between texlive packages, see [this post](https://tex.stackexchange.com/a/504566/). For Ubuntu users, there's also an installation guide available [here](https://wiki.ubuntuusers.de/TeX_Live/#Installation). If you're interested to see how Tex Live can be configured in VSCode inside WSL, see [this post](https://github.com/Splines/vscode-latex-wsl-setup). +[^2]: You have to recompile your LaTeX document, of course. But note that you can set up your LaTeX editor / IDE to recompile the PDF automatically for you as soon as any files, like `results.tex` change. For VSCode, you can use the [LaTeX Workshop extension](https://marketplace.visualstudio.com/items?itemName=James-Yu.latex-workshop) for this purpose, see a guide [here](https://github.com/Splines/vscode-latex-wsl-setup). In the best case, you only have to update a variable in your Python code (and run that code) and see the change in your PDF document immediately. + +[`siunitx`]: https://ctan.org/pkg/siunitx +[`pip`]: https://pypi.org/project/resultwizard diff --git a/docs/pages/trouble.md b/docs/pages/trouble.md new file mode 100644 index 00000000..ff1951dc --- /dev/null +++ b/docs/pages/trouble.md @@ -0,0 +1,94 @@ +--- +layout: default +title: Troubleshooting +permalink: trouble +nav_order: 3 +--- + +# Troubleshooting +{: .no_toc } + +

+ + Content + + {: .text-delta } + +- TOC +{:toc} + +
+ +There might be several reasons for your LaTeX document not building. **Try to find the root cause** by looking at the **log file** of your LaTeX compiler (sometimes you have to scroll way up to find the error responsible for the failing build). Also open the [exported]({{site.baseurl}}/api/export) `results.tex` file to see if your editor/IDE shows any errors there. You might encounter one of the following problems. Please make sure to try the solutions provided here before opening an [issue on GitHub](https://github.com/resultwizard/ResultWizard/issues). + + + +## Package siunitx: Invalid number + +{: .tldr} +You have probably specified **multiple uncertainties** in `wiz.res()`, right? If this is the case and you get this error, you have an **old version of `siunitx`** installed. Please update it (recommended) or use the `siunitx_fallback` option in the [`config_init`]({{site.baseurl}}/api/config) method. + +In version [`v3.1.0 (2022-04-25)`](https://github.com/josephwright/siunitx/blob/main/CHANGELOG.md#v310---2022-04-25), `siunitx` introduced "support for multiple uncertainty values in both short and long form input". We make use of this feature in `ResultWizard` when you specify multiple uncertainties for a result. + +Unfortunately, it may be the case that you're using an older version of `siunitx` that doesn't ship with this feature yet. Especially if you've installed LaTeX via a package manager (e.g. you installed `siunitx` via `sudo apt install texlive-science`). To determine your `siunitx` version, include the following line in your LaTeX document: + +```latex +\listfiles % add this before \begin{document}, i.e. in your LaTeX preamble +``` + +Then, compile your document and check the log for the version of `siunitx`. +
If it's **older than `v3.1.0 (2022-04-25)`**, don't despair. We have two solutions for you: + +### Solution 1: Don't update `siunitx` and stick with your old version + +Sure, fine, we won't force you to update `siunitx` (although we'd recommend it). To keep using your old version, specify the following key in the `config_init` method: + +```python +wiz.config_init(siunitx_fallback=True) +``` + +Note that with this "solution", you won't be able to fully customize the output of the result in your LaTeX document. For example, we will use a `±` between the value and the uncertainty, e.g. `3.14 ± 0.02`. You won't be able to change this in your `sisetup` by specifying + +```latex +\sisetup{separate-uncertainty=false} +``` + +to get another format like `3.14(2)`. There are also some other `siunitx` options that won't probably work with `ResultWizard`, e.g. [`exponent-product`](https://texdoc.org/serve/siunitx/0#page=29). If you're fine with this, go ahead and use the `siunitx_fallback` option. If not, consider updating `siunitx` to at least version `v3.1.0`. + +### Solution 2: Update `siunitx` (recommended, but more effort) + +How the update process works depends on your LaTeX distribution and how you installed it. E.g. you might be using `TeX Live` on `Ubuntu` and installed packages via `apt`, e.g. `sudo apt install texlive-science` (which includes the LaTeX `siunitx`). These pre-built packages are often outdated, e.g. for Ubuntu 22.04 LTS (jammy), the `siunitx` version that comes with the `texlive-science` package is `3.0.4`. Therefore, you might have to update `siunitx` manually. See an overview on how to install individual LaTeX packages on Linux [here](https://tex.stackexchange.com/a/73017/). + +A quick solution might be to simply install a new version of `siunitx` manually to your system. There's a great and short Ubuntu guide on how to install LaTeX packages manually [here](https://help.ubuntu.com/community/LaTeX#Installing_packages_manually). The following commands are based on this guide. We will download the version `3.1.11 (2022-12-05)` from GitHub (this is the last version before `3.2` where things might get more complicated to install) and install it locally. Don't be scared, do it one step at a time and use the power of GPTs and search engines in case you're stuck. Execute the following commands in your terminal: + +```sh +# Install "unzip", a tool to extract zip files +sudo apt install unzip + +# Download v3.1.11 of siunitx from GitHub +curl -L https://github.com/josephwright/siunitx/releases/download/v3.1.11/siunitx-ctan.zip > siunitx-ctan-3.1.11.zip + +# Unzip the file +unzip ./siunitx-ctan-3.1.11.zip +cd siunitx/ + +# Run LaTeX on the .ins file to generate a usable .sty file +# (LaTeX needs the .sty file to know what to do with the \usepackage{siunitx} +# command in your LaTeX preamble.) +latex siunitx.ins + +# Create a new directory in your home directory +# to store the new package .sty file +mkdir -p ~/texmf/tex/latex/siunitx # use any location you want, but this one is common +cp siunitx.sty ~/texmf/tex/latex/siunitx/ + +# Make LaTeX recognize the new package by pointing it to the new directory +texhash ~/texmf/ +``` + +🙌 Done. Try to recompile your LaTeX document again. You should see version `v3.1.11` of `siunitx` in the log file. And it should build. Don't forget to remove the `\listfiles` from your LaTeX document to avoid cluttering your log file (which is ironic for LaTeX, we know). + +In case you don't wan't the new siunitx version anymore, just run the following command to remove the `.sty` file. LaTeX will then use the version of siunitx it finds somewhere else in your system (which is probably the outdated one you had before). +```sh +rm ~/texmf/tex/latex/siunitx/siunitx.sty +``` diff --git a/pyproject.toml b/pyproject.toml index 3c4096af..7113c443 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,17 +7,17 @@ build-backend = "setuptools.build_meta" [tool.setuptools.packages.find] where = ["src"] -include = ["resultwizard"] -namespaces = false +include = ["api", "application", "domain", "resultwizard"] +namespaces = true [project] name = "resultwizard" -version = "0.1" +version = "1.0.0-alpha.2" authors = [ - { name = "Paul Obernolte (paul019)", email = "todo@todo.de" }, - { name = "Dominic Plein (Splines)", email = "todo@todo.de" }, + { name = "Paul Obernolte (paul019)" }, + { name = "Dominic Plein (Splines)" }, ] -description = "Python to LaTeX variable conversion" +description = "Intelligent interface between Python-computed values and your LaTeX work" keywords = [ "latex", "variable", @@ -29,16 +29,16 @@ keywords = [ "jupyter", ] readme = "README.md" -requires-python = ">=3.9" -license = { file = "LICENSE" } +requires-python = ">=3.8" # https://packaging.python.org/en/latest/guides/writing-pyproject-toml/#classifiers classifiers = [ - "Development Status :: 1 - Planning", + "Development Status :: 3 - Alpha", "Topic :: Scientific/Engineering :: Physics", "Topic :: Software Development :: Build Tools", "Topic :: Text Processing :: Markup :: LaTeX", "Intended Audience :: Science/Research", "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", @@ -47,15 +47,10 @@ classifiers = [ ] [project.urls] -Homepage = "https://github.com/paul019/ResultWizard" -Repository = "https://github.com/paul019/ResultWizard" -Issues = "https://github.com/paul019/ResultWizard/issues" -Changelog = "https://github.com/paul019/ResultWizard/blob/main/CHANGELOG.md" - -[tool.pytest.ini_options] -addopts = [ - "--import-mode=importlib", -] +Homepage = "https://resultwizard.github.io/ResultWizard/" +Repository = "https://github.com/resultwizard/ResultWizard" +Issues = "https://github.com/resultwizard/ResultWizard/issues" +Changelog = "https://github.com/resultwizard/ResultWizard/blob/main/CHANGELOG.md" # TODO: Add these checks back [tool.pylint."messages control"] @@ -63,6 +58,8 @@ disable = [ "missing-module-docstring", "missing-function-docstring", "missing-class-docstring", + "fixme", + "too-few-public-methods", ] [tool.pylint.format] diff --git a/src/api/config.py b/src/api/config.py new file mode 100644 index 00000000..2df28270 --- /dev/null +++ b/src/api/config.py @@ -0,0 +1,169 @@ +import decimal +from typing import Union, cast +from dataclasses import dataclass + +from api.res import _res_cache +from application.stringifier import StringifierConfig +from application.rounder import RoundingConfig +from application import error_messages + + +@dataclass +# pylint: disable-next=too-many-instance-attributes +class Config: + """Configuration settings for the application. + + Args: + sigfigs (int): The number of significant figures to round to. + decimal_places (int): The number of decimal places to round to. + print_auto (bool): Whether to print each result directly to the console. + export_auto_to (str): If not empty, each `res()` call will automatically + export all results to the file you specify with this keyword. You can + still use the `export()` method to export results to other files as well. + This option might be particularly useful when working in a Jupyter + notebook. This way, you don't have to call `export()` manually each time + you want to export results. + min_exponent_for_non_scientific_notation (int): The minimum exponent + for non-scientific notation. + max_exponent_for_non_scientific_notation (int): The maximum exponent + for non-scientific notation. + identifier (str): The identifier for the result variable in LaTeX. This + identifier will be prefix each result variable name. + sigfigs_fallback (int): The number of significant figures to use as a + fallback if other rounding rules don't apply. + decimal_places_fallback (int): The number of decimal places to use as + a fallback if other rounding rules don't apply. + siunitx_fallback (bool): Whether to use a fallback logic such that LaTeX + commands still work with an older version of siunitx. See + the docs for more information: TODO. + precision (int): The precision to use for the decimal module. Defaults to + 40 in ResultsWizard. You may have to increase this if you get the error + "Your precision is set too low". + ignore_result_overwrite (bool): If True, you won't get any warnings if you + overwrite a result with the same name. Defaults to False. This option + might be useful for Jupyter notebooks when you want to re-run cells + without getting any warnings that a result with the same name already + exists. + """ + + sigfigs: int + decimal_places: int + print_auto: bool + export_auto_to: str + min_exponent_for_non_scientific_notation: int + max_exponent_for_non_scientific_notation: int + identifier: str + sigfigs_fallback: int + decimal_places_fallback: int + siunitx_fallback: bool + precision: int + ignore_result_overwrite: bool + + def to_stringifier_config(self) -> StringifierConfig: + return StringifierConfig( + self.min_exponent_for_non_scientific_notation, + self.max_exponent_for_non_scientific_notation, + self.identifier, + ) + + def to_rounding_config(self) -> RoundingConfig: + return RoundingConfig( + self.sigfigs, + self.decimal_places, + self.sigfigs_fallback, + self.decimal_places_fallback, + ) + + +def _check_config() -> None: + if configuration.sigfigs > -1 and configuration.decimal_places > -1: + raise ValueError(error_messages.SIGFIGS_AND_DECIMAL_PLACES_AT_SAME_TIME) + + if configuration.sigfigs_fallback > -1 and configuration.decimal_places_fallback > -1: + raise ValueError(error_messages.SIGFIGS_FALLBACK_AND_DECIMAL_PLACES_FALLBACK_AT_SAME_TIME) + + if configuration.sigfigs_fallback <= -1 and configuration.decimal_places_fallback <= -1: + raise ValueError( + error_messages.ONE_OF_SIGFIGS_FALLBACK_AND_DECIMAL_PLACES_FALLBACK_MUST_BE_SET + ) + + if configuration.sigfigs == 0: + raise ValueError(error_messages.CONFIG_SIGFIGS_VALID_RANGE) + + if configuration.sigfigs_fallback == 0: + raise ValueError(error_messages.CONFIG_SIGFIGS_FALLBACK_VALID_RANGE) + + +# pylint: disable-next=too-many-arguments +def config_init( + sigfigs: int = -1, # -1: "per default use rounding rules instead" + decimal_places: int = -1, # -1: "per default use rounding rules instead" + print_auto: bool = False, + export_auto_to: str = "", + min_exponent_for_non_scientific_notation: int = -2, + max_exponent_for_non_scientific_notation: int = 3, + identifier: str = "result", + sigfigs_fallback: int = 2, + decimal_places_fallback: int = -1, # -1: "per default use sigfigs as fallback instead" + siunitx_fallback: bool = False, + precision: int = 100, + ignore_result_overwrite: bool = False, +) -> None: + global configuration # pylint: disable=global-statement + + decimal.DefaultContext.prec = precision + decimal.setcontext(decimal.DefaultContext) + + configuration = Config( + sigfigs, + decimal_places, + print_auto, + export_auto_to, + min_exponent_for_non_scientific_notation, + max_exponent_for_non_scientific_notation, + identifier, + sigfigs_fallback, + decimal_places_fallback, + siunitx_fallback, + precision, + ignore_result_overwrite, + ) + + _res_cache.configure(not ignore_result_overwrite) + + _check_config() + + +configuration = cast(Config, None) # pylint: disable=invalid-name +config_init() + + +def config( + sigfigs: Union[int, None] = None, + decimal_places: Union[int, None] = None, + print_auto: Union[bool, None] = None, + sigfigs_fallback: Union[int, None] = None, + decimal_places_fallback: Union[int, None] = None, +): + if sigfigs is not None: + configuration.sigfigs = sigfigs + if sigfigs > -1 and decimal_places is None: + configuration.decimal_places = -1 + if decimal_places is not None: + configuration.decimal_places = decimal_places + if decimal_places > -1 and sigfigs is None: + configuration.sigfigs = -1 + + if print_auto is not None: + configuration.print_auto = print_auto + + if sigfigs_fallback is not None: + configuration.sigfigs_fallback = sigfigs_fallback + if sigfigs_fallback > -1 and decimal_places_fallback is None: + configuration.decimal_places_fallback = -1 + if decimal_places_fallback is not None: + configuration.decimal_places_fallback = decimal_places_fallback + if decimal_places_fallback > -1 and sigfigs_fallback is None: + configuration.sigfigs_fallback = -1 + + _check_config() diff --git a/src/api/console_stringifier.py b/src/api/console_stringifier.py new file mode 100644 index 00000000..e3a77dbc --- /dev/null +++ b/src/api/console_stringifier.py @@ -0,0 +1,48 @@ +from domain.result import Result +from application.stringifier import Stringifier + + +class ConsoleStringifier(Stringifier): + plus_minus = "±" + negative_sign = "-" + positive_sign = "" + + left_parenthesis = "(" + right_parenthesis = ")" + + value_prefix = "" + value_suffix = "" + + uncertainty_name_prefix = " (" + uncertainty_name_suffix = ")" + + scientific_notation_prefix = "e" + scientific_notation_suffix = "" + + unit_prefix = " " + unit_suffix = "" + + def result_to_str(self, result: Result): + """ + Returns the result as human-readable string. + """ + + return f"{result.name} = {self.create_str(result.value, result.uncertainties, result.unit)}" + + def _modify_unit(self, unit: str) -> str: + """ + Returns the modified unit. + """ + unit = ( + unit.replace(r"\squared", "^2") + .replace(r"\cubed", "^3") + .replace("\\per\\", "/") + .replace(r"\per", "/") + .replace("\\", " ") + .strip() + ) + + if unit[0] == "/": + unit = f"1{unit}" + + return unit diff --git a/src/api/export.py b/src/api/export.py index 5ed711c7..d21f9cd5 100644 --- a/src/api/export.py +++ b/src/api/export.py @@ -1,6 +1,8 @@ -from application.cache import _res_cache -from application.latexer import _LaTeXer -from application.tables.latexer import _TableLaTeXer +from typing import Set +from api.latexer import get_latexer +from api.res import _res_cache +import api.config as c +from application.helpers import Helpers def export(filepath: str): @@ -8,30 +10,70 @@ def export(filepath: str): Rounds all results according to the significant figures and writes them to a .tex file at the given filepath. """ + return _export(filepath, print_completed=True) + + +def _export(filepath: str, print_completed: bool): results = _res_cache.get_all_results() - tables = _res_cache.get_all_tables() - print(f"Processing {len(results)} result(s)") + + if print_completed: + print(f"Processing {len(results)} result(s)") # Round and convert to LaTeX commands - cmds = [ + lines = [ r"%", r"% In your `main.tex` file, put this line directly before `\begin{document}`:", r"% \input{" + filepath.split("/")[-1].split(".")[0] + r"}", r"%", r"", r"% Import required package:", + r"\usepackage{siunitx}", r"\usepackage{ifthen}", r"", - r"% Define commands to print the results:", ] + + latexer = get_latexer() + + uncertainty_names = set() + result_lines = [] for result in results: - result_str = _LaTeXer.result_to_latex_cmd(result) - cmds.append(result_str) - for table in tables: - table_str = _TableLaTeXer.table_to_latex_cmd(table) - cmds.append(table_str) + uncertainty_names.update(u.name for u in result.uncertainties if u.name != "") + result_str = latexer.result_to_latex_cmd(result) + result_lines.append(result_str) + + if not c.configuration.siunitx_fallback: + siunitx_setup = _uncertainty_names_to_siunitx_setup(uncertainty_names) + if siunitx_setup != "": + lines.append("% Commands to correctly print the uncertainties in siunitx:") + lines.append(siunitx_setup) + lines.append("") + + lines.append("% Commands to print the results. Use them in your document.") + lines.extend(result_lines) # Write to file with open(filepath, "w", encoding="utf-8") as f: - f.write("\n".join(cmds)) - print(f'Exported to "{filepath}"') + f.write("\n".join(lines)) + if print_completed: + print(f'Exported to "{filepath}"') + + +def _uncertainty_names_to_siunitx_setup(uncert_names: Set[str]) -> str: + """ + Returns the preamble for the LaTeX document to use the siunitx package. + """ + if len(uncert_names) == 0: + return "" + + cmd_names = [] + cmds = [] + for name in uncert_names: + cmd_name = f"\\Uncert{Helpers.capitalize(name)}" + cmd_names.append(cmd_name) + cmds.append(rf"\NewDocumentCommand{{{cmd_name}}}{{}}{{_{{\text{{{name}}}}}}}") + + string = "\n".join(cmds) + string += "\n" + string += rf"\sisetup{{input-digits=0123456789{''.join(cmd_names)}}}" + + return string diff --git a/src/api/latexer.py b/src/api/latexer.py new file mode 100644 index 00000000..8bf3cadc --- /dev/null +++ b/src/api/latexer.py @@ -0,0 +1,18 @@ +import api.config as c +from application.latex_better_siunitx_stringifier import LatexBetterSiunitxStringifier +from application.latex_commandifier import LatexCommandifier +from application.latex_stringifier import LatexStringifier +from application.stringifier import Stringifier + + +def get_latexer() -> LatexCommandifier: + return LatexCommandifier(_choose_latex_stringifier()) + + +def _choose_latex_stringifier() -> Stringifier: + use_fallback = c.configuration.siunitx_fallback + stringifier_config = c.configuration.to_stringifier_config() + + if use_fallback: + return LatexStringifier(stringifier_config) + return LatexBetterSiunitxStringifier(stringifier_config) diff --git a/src/api/parsers.py b/src/api/parsers.py index be221b66..c54f6511 100644 --- a/src/api/parsers.py +++ b/src/api/parsers.py @@ -1,60 +1,94 @@ from typing import Union, List, Tuple +from decimal import Decimal + +from application.helpers import Helpers +from application import error_messages +from domain.value import Value +from domain.uncertainty import Uncertainty -from application.helpers import _Helpers -from domain.value import _Value -from domain.uncertainty import _Uncertainty def check_if_number_string(value: str) -> None: """Raises a ValueError if the string is not a valid number.""" try: float(value) except ValueError as exc: - raise ValueError(f"String value must be a valid number, not {value}") from exc - + raise ValueError(error_messages.STRING_MUST_BE_NUMBER.format(value=value)) from exc def parse_name(name: str) -> str: """Parses the name.""" if not isinstance(name, str): - raise TypeError(f"`name` must be a string, not {type(name)}") + raise TypeError(error_messages.FIELD_MUST_BE_STRING.format(field="`name`", type=type(name))) + + if name == "": + raise ValueError(error_messages.FIELD_MUST_NOT_BE_EMPTY.format(field="`name`")) name = ( name.replace("ä", "ae") - .replace("ö", "oe") - .replace("ü", "ue") .replace("Ä", "Ae") + .replace("ö", "oe") .replace("Ö", "Oe") + .replace("ü", "ue") .replace("Ü", "Ue") .replace("ß", "ss") + # we use "SS" instead of "Ss" as replacement for "ẞ" + # since "ẞ" is only allowed in uppercase names in German + .replace("ẞ", "SS") ) parsed_name = "" - next_chat_upper = False + next_char_upper = False + ignored_chars = set() + + while name != "": + char = name[0] - for char in name: if char.isalpha(): - if next_chat_upper: + if next_char_upper: parsed_name += char.upper() - next_chat_upper = False + next_char_upper = False else: parsed_name += char elif char.isdigit(): - digit = _Helpers.number_to_word(int(char)) - if parsed_name == "": - parsed_name += digit - else: - parsed_name += digit[0].upper() + digit[1:] - next_chat_upper = True + num_digits = _greedily_count_digits_at_start_of_str(name) + word = Helpers.number_to_word(int(name[:num_digits])) + if parsed_name != "": + word = Helpers.capitalize(word) + parsed_name += word + next_char_upper = True + name = name[num_digits:] # Skip the parsed digits + continue elif char in [" ", "_", "-"]: - next_chat_upper = True + next_char_upper = True + else: + ignored_chars.add(char) + + name = name[1:] + + if len(ignored_chars) > 0: + print(error_messages.INVALID_CHARS_IGNORED.format(chars=", ".join(ignored_chars))) + + if parsed_name == "": + raise ValueError(error_messages.STRING_EMPTY_AFTER_IGNORING_INVALID_CHARS) return parsed_name +def _greedily_count_digits_at_start_of_str(word: str) -> int: + """Counts the number of digits at the start of the string.""" + num_digits = 0 + for c in word: + if c.isdigit(): + num_digits += 1 + else: + break + return num_digits + + def parse_unit(unit: str) -> str: """Parses the unit.""" if not isinstance(unit, str): - raise TypeError(f"`unit` must be a string, not {type(unit)}") + raise TypeError(error_messages.FIELD_MUST_BE_STRING.format(field="`unit`", type=type(unit))) # TODO: maybe add some basic checks to catch siunitx errors, e.g. # unsupported symbols etc. But maybe leave this to LaTeX and just return @@ -70,10 +104,12 @@ def parse_sigfigs(sigfigs: Union[int, None]) -> Union[int, None]: return None if not isinstance(sigfigs, int): - raise TypeError(f"`sigfigs` must be an int, not {type(sigfigs)}") + raise TypeError( + error_messages.FIELD_MUST_BE_INT.format(field="`sigfigs`", type=type(sigfigs)) + ) if sigfigs < 1: - raise ValueError("`sigfigs` must be positive") + raise ValueError(error_messages.FIELD_MUST_BE_POSITIVE.format(field="`sigfigs`")) return sigfigs @@ -84,60 +120,101 @@ def parse_decimal_places(decimal_places: Union[int, None]) -> Union[int, None]: return None if not isinstance(decimal_places, int): - raise TypeError(f"`decimal_places` must be an int, not {type(decimal_places)}") + raise TypeError( + error_messages.FIELD_MUST_BE_INT.format( + field="`decimal_places`", type=type(decimal_places) + ) + ) if decimal_places < 0: - raise ValueError("`decimal_places` must be non-negative") + raise ValueError(error_messages.FIELD_MUST_BE_NON_NEGATIVE.format(field="`decimal_places`")) return decimal_places -def parse_value(value: Union[float, str]) -> _Value: +def parse_value(value: Union[float, int, str, Decimal]) -> Value: """Converts the value to a _Value object.""" - if not isinstance(value, (float, str)): - raise TypeError(f"`value` must be a float or string, not {type(value)}") + if not isinstance(value, (float, int, str, Decimal)): + raise TypeError(error_messages.VALUE_TYPE.format(field="`value`", type=type(value))) if isinstance(value, str): check_if_number_string(value) + return parse_exact_value(value) + + return Value(Decimal(value)) + - return _Value(value) +def parse_exact_value(value: str) -> Value: + # Determine min exponent: + exponent_offset = 0 + value_str = value + if "e" in value_str: + exponent_offset = int(value_str[value_str.index("e") + 1 :]) + value_str = value_str[0 : value_str.index("e")] + if "." in value: + decimal_places = len(value_str) - value_str.index(".") - 1 + min_exponent = -decimal_places + exponent_offset + else: + min_exponent = exponent_offset + + return Value(Decimal(value), min_exponent) def parse_uncertainties( uncertainties: Union[ float, + int, str, - Tuple[Union[float, str], str], - List[Union[float, str, Tuple[Union[float, str], str]]], + Decimal, + Tuple[Union[float, int, str, Decimal], str], + List[Union[float, int, str, Decimal, Tuple[Union[float, int, str, Decimal], str]]], ] -) -> List[_Uncertainty]: +) -> List[Uncertainty]: """Converts the uncertainties to a list of _Uncertainty objects.""" uncertainties_res = [] # no list, but a single value was given - if isinstance(uncertainties, (float, str, Tuple)): + if isinstance(uncertainties, (float, int, str, Decimal, Tuple)): uncertainties = [uncertainties] - assert isinstance(uncertainties, List) - for uncert in uncertainties: - if isinstance(uncert, (float, str)): - if isinstance(uncert, str): - check_if_number_string(uncert) - if float(uncert) <= 0: - raise ValueError("Uncertainty must be positive.") - uncertainties_res.append(_Uncertainty(uncert)) + if isinstance(uncert, (float, int, str, Decimal)): + uncertainties_res.append(Uncertainty(_parse_uncertainty_value(uncert))) elif isinstance(uncert, Tuple): - if not isinstance(uncert[0], (float, str)): + if not isinstance(uncert[0], (float, int, str, Decimal)): raise TypeError( - f"First argument of uncertainty-tuple must be a float or a string, not {type(uncert[0])}" + error_messages.VALUE_TYPE.format( + field="First argument of uncertainty-tuple", type=type(uncert[0]) + ) ) - if isinstance(uncert[0], str): - check_if_number_string(uncert[0]) - uncertainties_res.append(_Uncertainty(uncert[0], parse_name(uncert[1]))) + uncertainties_res.append( + Uncertainty(_parse_uncertainty_value(uncert[0]), parse_name(uncert[1])) + ) else: - raise TypeError(f"Each uncertainty must be a tuple or a float/str, not {type(uncert)}") + raise TypeError( + error_messages.UNCERTAINTIES_MUST_BE_TUPLES_OR.format(type=type(uncert)) + ) return uncertainties_res + + +def _parse_uncertainty_value(value: Union[float, int, str, Decimal]) -> Value: + """Parses the value of an uncertainty.""" + + if isinstance(value, str): + try: + check_if_number_string(value) + except Exception as exc: + msg = error_messages.STRING_MUST_BE_NUMBER.format(value=value) + msg += ". " + error_messages.UNIT_NOT_PASSED_AS_KEYWORD_ARGUMENT + raise ValueError(msg) from exc + return_value = parse_exact_value(value) + else: + return_value = Value(Decimal(value)) + + if return_value.get() <= 0: + raise ValueError(error_messages.FIELD_MUST_BE_POSITIVE.format(field="Uncertainty")) + + return return_value diff --git a/src/api/printable_result.py b/src/api/printable_result.py index 663b6826..87189598 100644 --- a/src/api/printable_result.py +++ b/src/api/printable_result.py @@ -1,16 +1,24 @@ -from domain.result import _Result -from api.stringifier import Stringifier -from application.latexer import _LaTeXer +from api.console_stringifier import ConsoleStringifier +import api.config as c +from api.latexer import get_latexer +from domain.result import Result class PrintableResult: - def __init__(self, result: _Result): + def __init__(self, result: Result): self._result = result def print(self): """Prints the result to the console.""" - print(Stringifier.result_to_str(self._result)) + stringifier = ConsoleStringifier(c.configuration.to_stringifier_config()) + print(stringifier.result_to_str(self._result)) - def get_latex_str(self) -> str: - """Returns LaTeX string.""" - return _LaTeXer.result_to_latex_str(self._result) + def to_latex_str(self) -> str: + """Converts the result to a string that can be used in LaTeX documents. + + Note that you might also want to use the `export` method to export + all your results to a file, which can then be included in your LaTeX + document. + """ + latexer = get_latexer() + return latexer.result_to_latex_str(self._result) diff --git a/src/api/res.py b/src/api/res.py index 8c5ad148..ba06ff7a 100644 --- a/src/api/res.py +++ b/src/api/res.py @@ -1,113 +1,95 @@ +from decimal import Decimal from typing import Union, List, Tuple -from plum import dispatch, overload from api.printable_result import PrintableResult -from application.cache import _res_cache -from application.rounder import _Rounder -import api.parsers as parsers -from domain.result import _Result +from api import parsers +from application.cache import ResultsCache +from application.rounder import Rounder +from application import error_messages +from domain.result import Result +_res_cache = ResultsCache() -# TODO: import types from typing to ensure backwards compatibility down to Python 3.8 +# "Wrong" import position to avoid circular imports +from api.export import _export # pylint: disable=wrong-import-position,ungrouped-imports +import api.config as c # pylint: disable=wrong-import-position,ungrouped-imports -# TODO: use pydantic instead of manual and ugly type checking -# see: https://docs.pydantic.dev/latest/ -# This way we can code as if the happy path is the only path, and let pydantic -# handle the error checking and reporting. - -@overload -def res( - name: str, - value: Union[float, str], - unit: str = "", - sigfigs: Union[int, None] = None, - decimal_places: Union[int, None] = None, -) -> PrintableResult: - return res(name, value, [], unit, sigfigs, decimal_places) - - -@overload +# pylint: disable-next=too-many-arguments, too-many-locals def res( name: str, - value: Union[float, str], - uncert: Union[ + value: Union[float, int, str, Decimal], + uncerts: Union[ float, + int, str, - Tuple[Union[float, str], str], - List[Union[float, str, Tuple[Union[float, str], str]]], + Decimal, + Tuple[Union[float, int, str, Decimal], str], + List[Union[float, int, str, Decimal, Tuple[Union[float, int, str, Decimal], str]]], None, ] = None, + unit: str = "", + sys: Union[float, int, str, Decimal, None] = None, + stat: Union[float, int, str, Decimal, None] = None, sigfigs: Union[int, None] = None, decimal_places: Union[int, None] = None, ) -> PrintableResult: - return res(name, value, uncert, "", sigfigs, decimal_places) + """ + Declares your result. Give it a name and a value. You may also optionally provide + uncertainties (via `uncert` or `sys`/`stat`) and a unit in `siunitx` format. + You may additionally specify the number of significant figures or decimal places + to round this specific result to, irrespective of your global configuration. -@overload -def res( - name: str, - value: Union[float, str], - sigfigs: Union[int, None] = None, - decimal_places: Union[int, None] = None, -) -> PrintableResult: - return res(name, value, [], "", sigfigs, decimal_places) + TODO: provide a link to the docs for more information and examples. + """ + # Verify user input + if sigfigs is not None and decimal_places is not None: + raise ValueError(error_messages.SIGFIGS_AND_DECIMAL_PLACES_AT_SAME_TIME) + if sigfigs is not None and isinstance(value, str): + raise ValueError(error_messages.SIGFIGS_AND_EXACT_VALUE_AT_SAME_TIME) -@overload -def res( - name: str, - value: Union[float, str], - sys: float, - stat: float, - unit: str = "", - sigfigs: Union[int, None] = None, - decimal_places: Union[int, None] = None, -) -> PrintableResult: - return res(name, value, [(sys, "sys"), (stat, "stat")], unit, sigfigs, decimal_places) + if decimal_places is not None and isinstance(value, str): + raise ValueError(error_messages.DECIMAL_PLACES_AND_EXACT_VALUE_AT_SAME_TIME) + sys_or_stat_specified = sys is not None or stat is not None + if uncerts is not None and sys_or_stat_specified: + raise ValueError(error_messages.UNCERT_AND_SYS_STAT_AT_SAME_TIME) -@overload -def res( - name: str, - value: Union[float, str], - uncert: Union[ - float, - str, - Tuple[Union[float, str], str], - List[Union[float, str, Tuple[Union[float, str], str]]], - None, - ] = None, - unit: str = "", - sigfigs: Union[int, None] = None, - decimal_places: Union[int, None] = None, -) -> PrintableResult: - if uncert is None: - uncert = [] + if sys_or_stat_specified: + uncerts = [] + if sys is not None: + uncerts.append((sys, "sys")) + if stat is not None: + uncerts.append((stat, "stat")) + + if uncerts is None: + uncerts = [] # Parse user input name_res = parsers.parse_name(name) value_res = parsers.parse_value(value) - uncertainties_res = parsers.parse_uncertainties(uncert) + uncertainties_res = parsers.parse_uncertainties(uncerts) unit_res = parsers.parse_unit(unit) sigfigs_res = parsers.parse_sigfigs(sigfigs) decimal_places_res = parsers.parse_decimal_places(decimal_places) # Assemble the result - result = _Result( + result = Result( name_res, value_res, unit_res, uncertainties_res, sigfigs_res, decimal_places_res ) - _Rounder.round_result(result) - _res_cache.add_res(name, result) + Rounder.round_result(result, c.configuration.to_rounding_config()) + _res_cache.add(name_res, result) - return PrintableResult(result) + # Print automatically + printable_result = PrintableResult(result) + if c.configuration.print_auto: + printable_result.print() + # Export automatically + immediate_export_path = c.configuration.export_auto_to + if immediate_export_path != "": + _export(immediate_export_path, print_completed=False) -# Hack for method "overloading" in Python -# see https://beartype.github.io/plum/integration.html -# This is a good writeup: https://stackoverflow.com/a/29091980/ -@dispatch -def res(*args, **kwargs) -> object: - # This method only scans for all `overload`-decorated methods - # and properly adds them as Plum methods. - pass + return printable_result diff --git a/src/api/stringifier.py b/src/api/stringifier.py deleted file mode 100644 index 5b1d72b3..00000000 --- a/src/api/stringifier.py +++ /dev/null @@ -1,100 +0,0 @@ -from domain.result import _Result -from application.helpers import _Helpers - -# Config values: -min_exponent_for_non_scientific_notation = -2 -max_exponent_for_non_scientific_notation = 3 -identifier = "res" - - -class Stringifier: - @classmethod - def result_to_str(cls, result: _Result): - """ - Returns the result as human-readable string. - """ - - value = result.value - uncertainties = result.uncertainties - unit = result.unit - - string = f"{result.name} = " - use_scientific_notation = False - has_unit = unit != "" - - # Determine if scientific notation should be used: - if ( - value.get_exponent() < min_exponent_for_non_scientific_notation - or value.get_exponent() > max_exponent_for_non_scientific_notation - ): - use_scientific_notation = True - - if value.get_min_exponent() > 0: - use_scientific_notation = True - - for u in uncertainties: - if u.uncertainty.get_min_exponent() > 0: - use_scientific_notation = True - - # Create LaTeX string: - if value.get() < 0: - sign = "-" - else: - sign = "" - - if use_scientific_notation: - exponent = value.get_exponent() - factor = 10 ** (-exponent) - - if len(uncertainties) > 0: - string += "(" - - value_normalized = value.get_abs() * factor - decimal_places = value.get_sig_figs()-1 - string += sign - string += _Helpers.round_to_n_decimal_places(value_normalized, decimal_places) - - for u in uncertainties: - value_normalized = u.uncertainty.get_abs() * factor - decimal_places = exponent-u.uncertainty.get_min_exponent() - string += " ± " - string += _Helpers.round_to_n_decimal_places(value_normalized, decimal_places) - if len(uncertainties) > 1: - string += rf" ({u.name})" - - if len(uncertainties) > 0: - string += ")" - - string += rf"e{exponent}" - else: - if len(uncertainties) > 0 and unit != "": - string += "(" - - value_normalized = value.get_abs() - decimal_places = value.get_decimal_place() - string += sign - string += _Helpers.round_to_n_decimal_places(value_normalized, decimal_places) - - for u in uncertainties: - value_normalized = u.uncertainty.get_abs() - decimal_places = u.uncertainty.get_decimal_place() - string += " ± " - string += _Helpers.round_to_n_decimal_places(value_normalized, decimal_places) - if len(uncertainties) > 1: - string += rf" ({u.name})" - - if len(uncertainties) > 0 and unit != "": - string += ")" - - if has_unit: - unit = ( - unit.replace(r"\per", "/") - .replace(r"\squared", "^2") - .replace(r"\cubed", "^3") - .replace("\\", "") - ) - if unit[0] == "/": - unit = "1" + unit - string += rf" {unit}" - - return string diff --git a/src/application/cache.py b/src/application/cache.py index 8acca268..b4f89fd3 100644 --- a/src/application/cache.py +++ b/src/application/cache.py @@ -1,30 +1,27 @@ -from domain.result import _Result -from domain.tables.table import _Table +from application.error_messages import RESULT_SHADOWED +from domain.result import Result -class _ResultsCache: +class ResultsCache: """ A cache for all user-defined results. Results are hashed by their name. If the user tries to add a result with a name that already exists in the cache, the new result will replace the old one ("shadowing"). - # TODO: is this the wanted behavior? Maybe print a warning in this case. """ def __init__(self): - self.res_cache: dict[str, _Result] = {} - self.table_cache: dict[str, _Table] = {} + self.cache: dict[str, Result] = {} + self.issue_result_overwrite_warning = True - def add_res(self, name: str, result: _Result): - self.res_cache[name] = result + def configure(self, issue_result_overwrite_warning: bool): + self.issue_result_overwrite_warning = issue_result_overwrite_warning - def add_table(self, name: str, table: _Table): - self.table_cache[name] = table + def add(self, name, result: Result): - def get_all_results(self) -> list[_Result]: - return list(self.res_cache.values()) - - def get_all_tables(self) -> list[_Table]: - return list(self.table_cache.values()) + if self.issue_result_overwrite_warning and name in self.cache: + print(RESULT_SHADOWED.format(name=name)) + self.cache[name] = result -_res_cache = _ResultsCache() + def get_all_results(self) -> list[Result]: + return list(self.cache.values()) diff --git a/src/application/error_messages.py b/src/application/error_messages.py new file mode 100644 index 00000000..d4c019f4 --- /dev/null +++ b/src/application/error_messages.py @@ -0,0 +1,67 @@ +# Config and res error messages +SIGFIGS_AND_DECIMAL_PLACES_AT_SAME_TIME = ( + "You can't set both sigfigs and decimal places at the same time. " + "Please choose one or the other." +) +SIGFIGS_FALLBACK_AND_DECIMAL_PLACES_FALLBACK_AT_SAME_TIME = ( + "You can't set both sigfigs_fallback and decimal_places_fallback at the same time. " + "Please choose one or the other." +) +ONE_OF_SIGFIGS_FALLBACK_AND_DECIMAL_PLACES_FALLBACK_MUST_BE_SET = ( + "You need to set either sigfigs_fallback or decimal_places_fallback. Please choose one." +) +CONFIG_SIGFIGS_VALID_RANGE = "sigfigs must be greater than 0 (or -1)." +CONFIG_SIGFIGS_FALLBACK_VALID_RANGE = "sigfigs_fallback must be greater than 0 (or -1)." +SIGFIGS_AND_EXACT_VALUE_AT_SAME_TIME = ( + "You can't set sigfigs and supply an exact value. Please do one or the other." +) +DECIMAL_PLACES_AND_EXACT_VALUE_AT_SAME_TIME = ( + "You can't set decimal places and supply an exact value. Please do one or the other." +) +UNCERT_AND_SYS_STAT_AT_SAME_TIME = ( + "You can't set uncertainties and systematic/statistical uncertainties at the same time. " + "Please provide either the `uncert` param or the `sys`/`stat` params." +) + +# Parser error messages (generic) +STRING_MUST_BE_NUMBER = "String value must be a valid number, not {value}" +FIELD_MUST_BE_STRING = "{field} must be a string, not {type}" +FIELD_MUST_BE_INT = "{field} must be an int, not {type}" +FIELD_MUST_NOT_BE_EMPTY = "{field} must not be empty" +FIELD_MUST_BE_POSITIVE = "{field} must be positive" +FIELD_MUST_BE_NON_NEGATIVE = "{field} must be non-negative" + +# Parser error messages (specific) +STRING_EMPTY_AFTER_IGNORING_INVALID_CHARS = ( + "After ignoring invalid characters, the specified name is empty." +) +VALUE_TYPE = "{field} must be a float, int, Decimal or string, not {type}" +UNCERTAINTIES_MUST_BE_TUPLES_OR = ( + "Each uncertainty must be a tuple or a float/int/Decimal/str, not {type}" +) +UNIT_NOT_PASSED_AS_KEYWORD_ARGUMENT = ( + "Could it be the case you provided a unit but forgot `unit=` in front of it?" +) + +# Helpers +PRECISION_TOO_LOW = ( + "Your precision is set too low to be able to process the given value without any loss of " + "precision. Set a higher precision via: `wiz.config_init (precision=)`." +) +NUMBER_TO_WORD_TOO_HIGH = "For variable names, only use numbers between 0 and 999. Got {number}." + +# Runtime errors +SHORT_RESULT_IS_NONE = "Short result is None, but there should be at least two uncertainties." +INTERNAL_ROUNDER_HIERARCHY_ERROR = "Internal rounder hierarchy error. Please report this bug." +INTERNAL_MIN_EXPONENT_ERROR = "Internal min_exponent not set error. Please report this bug." +ROUND_TO_NEGATIVE_DECIMAL_PLACES = ( + "Internal rounding to negative decimal places. Please report this bug." +) + +# Warnings +INVALID_CHARS_IGNORED = "Invalid characters in name were ignored: {chars}" +NUM_OF_DECIMAL_PLACES_TOO_LOW = ( + "Warning: At least one of the specified values is out of range of the specified " + "number of decimal places. Thus, the exported value will be 0." +) +RESULT_SHADOWED = "Warning: A result with the name '{name}' already exists and will be overwritten." diff --git a/src/application/helpers.py b/src/application/helpers.py index 5e65a1d2..57998e7d 100644 --- a/src/application/helpers.py +++ b/src/application/helpers.py @@ -1,5 +1,8 @@ import math +import decimal +from decimal import Decimal +from application import error_messages _NUMBER_TO_WORD = { 0: "zero", @@ -33,34 +36,48 @@ } -class _Helpers: +class Helpers: @classmethod - def get_exponent(cls, value: float) -> int: + def get_exponent(cls, value: Decimal) -> int: + if value == 0: + return 0 return math.floor(math.log10(abs(value))) @classmethod - def get_first_digit(cls, value: float) -> int: + def get_first_digit(cls, value: Decimal) -> int: n = abs(value) * 10 ** (-cls.get_exponent(value)) return math.floor(n) @classmethod - def round_to_n_decimal_places(cls, value: float, n: int): - return f"{value:.{int(abs(n))}f}" + def round_to_n_decimal_places(cls, value: Decimal, n: int) -> str: + if n < 0: + raise RuntimeError(error_messages.ROUND_TO_NEGATIVE_DECIMAL_PLACES) + + try: + decimal_value = value.quantize(Decimal(f"1.{'0' * n}")) + return f"{decimal_value:.{n}f}" + except decimal.InvalidOperation as exc: + raise ValueError(error_messages.PRECISION_TOO_LOW) from exc @classmethod def number_to_word(cls, number: int) -> str: - if number >= 0 and number <= 19: + if 0 <= number <= 19: return _NUMBER_TO_WORD[number] - elif number >= 0 and number <= 99: + if 0 <= number <= 99: tens = number // 10 * 10 ones = number % 10 if ones == 0: return _NUMBER_TO_WORD[tens] - else: - return ( - _NUMBER_TO_WORD[tens] - + _NUMBER_TO_WORD[ones][0].upper() - + _NUMBER_TO_WORD[ones][1:] - ) - else: - raise RuntimeError("Runtime error.") + return _NUMBER_TO_WORD[tens] + cls.capitalize(_NUMBER_TO_WORD[ones]) + if 0 <= number <= 999: + hundreds = number // 100 + tens = number % 100 + if tens == 0: + return _NUMBER_TO_WORD[hundreds] + "Hundred" + return _NUMBER_TO_WORD[hundreds] + "Hundred" + cls.capitalize(cls.number_to_word(tens)) + + raise ValueError(error_messages.NUMBER_TO_WORD_TOO_HIGH.format(number=number)) + + @classmethod + def capitalize(cls, s: str) -> str: + return s[0].upper() + s[1:] diff --git a/src/application/latex_better_siunitx_stringifier.py b/src/application/latex_better_siunitx_stringifier.py new file mode 100644 index 00000000..46ee91b9 --- /dev/null +++ b/src/application/latex_better_siunitx_stringifier.py @@ -0,0 +1,59 @@ +from typing import List + +from application.helpers import Helpers +from application.stringifier import Stringifier + + +class LatexBetterSiunitxStringifier(Stringifier): + """ + Provides methods to convert results to LaTeX commands. + + We assume the result to already be correctly rounded at this point. + """ + + # pylint: disable=duplicate-code + plus_minus = r"\pm" + negative_sign = "-" + positive_sign = "" + + left_parenthesis = r"\left(" + right_parenthesis = r"\right)" + + value_prefix = "" + value_suffix = "" + + uncertainty_name_prefix = r"\Uncert" + uncertainty_name_suffix = "" + + scientific_notation_prefix = "e" + scientific_notation_suffix = "" + + unit_prefix = "" + unit_suffix = "" + # pylint: enable=duplicate-code + + def _modify_uncertainty_name(self, name) -> str: + return Helpers.capitalize(name) + + # pylint: disable-next=too-many-arguments + def _assemble_str_parts( + self, + sign: str, + value_rounded: str, + uncertainties_rounded: List[str], + should_use_parentheses: bool, + use_scientific_notation: bool, + exponent: int, + unit: str, + ): + num_part = f"{sign}{value_rounded}{''.join(uncertainties_rounded)}" + + if use_scientific_notation: + num_part += f" e{str(exponent)}" + + if unit != "": + string = rf"\qty{{{num_part}}}{{{unit}}}" + else: + string = rf"\num{{{num_part}}}" + + return string diff --git a/src/application/latex_commandifier.py b/src/application/latex_commandifier.py new file mode 100644 index 00000000..8012f41a --- /dev/null +++ b/src/application/latex_commandifier.py @@ -0,0 +1,95 @@ +from application.stringifier import Stringifier +from application.helpers import Helpers +from application.latex_ifelse import LatexIfElseBuilder +from application import error_messages +from domain.result import Result + + +class LatexCommandifier: + """Makes use of a LaTeX stringifier to embed a result (e.g. \\qty{1.23}{\\m}) + into a LaTeX command (e.g. \\newcommand{\\resultImportant}{\\qty{1.23}{\\m}}). + """ + + def __init__(self, stringifier: Stringifier): + self.s = stringifier + + def result_to_latex_cmd(self, result: Result) -> str: + """ + Returns the result as LaTeX command to be used in a .tex file. + """ + builder = LatexIfElseBuilder() + + cmd_name = f"{self.s.config.identifier}{Helpers.capitalize(result.name)}" + latex_str = rf"\newcommand*{{\{cmd_name}}}[1][]{{" + "\n" + + # Default case (full result) & value + builder.add_branch("", self.result_to_latex_str(result)) + builder.add_branch("value", self.result_to_latex_str_value(result)) + + # Without uncertainty + if len(result.uncertainties) > 0: + builder.add_branch("withoutUncert", self.result_to_latex_str_without_uncert(result)) + + # Single uncertainties + for i, u in enumerate(result.uncertainties): + if len(result.uncertainties) == 1: + uncertainty_name = "uncert" + else: + uncertainty_name = u.name if u.name != "" else Helpers.number_to_word(i + 1) + uncertainty_name = f"uncert{Helpers.capitalize(uncertainty_name)}" + uncertainty_latex_str = self.s.create_str(u.uncertainty, [], result.unit) + builder.add_branch(uncertainty_name, uncertainty_latex_str) + + # Total uncertainty and short result + if len(result.uncertainties) >= 2: + short_result = result.get_short_result() + if short_result is None: + raise RuntimeError(error_messages.SHORT_RESULT_IS_NONE) + uncertainty_latex_str = self.s.create_str( + short_result.uncertainties[0].uncertainty, [], result.unit + ) + builder.add_branch("uncertTotal", uncertainty_latex_str) + builder.add_branch("short", self.result_to_latex_str(short_result)) + + # Unit + if result.unit != "": + builder.add_branch("unit", rf"\unit{{{result.unit}}}") + builder.add_branch("withoutUnit", self.result_to_latex_str_without_unit(result)) + + # Error message + keywords = builder.keywords + if len(keywords) > 0: + error_message = "Use one of these keywords (or no keyword at all): " + error_message += ", ".join([rf"\texttt{{{k}}}" for k in keywords]) + else: + error_message = "This variable can only be used without keywords." + builder.add_else(rf"\scriptsize{{\textbf{{{error_message}}}}}") + + latex_str += builder.build() + latex_str += "\n}" + + return latex_str + + def result_to_latex_str(self, result: Result) -> str: + """ + Returns the result as LaTeX string making use of the siunitx package. + """ + return self.s.create_str(result.value, result.uncertainties, result.unit) + + def result_to_latex_str_value(self, result: Result) -> str: + """ + Returns only the value as LaTeX string making use of the siunitx package. + """ + return self.s.create_str(result.value, [], "") + + def result_to_latex_str_without_uncert(self, result: Result) -> str: + """ + Returns the result without uncertainty as LaTeX string making use of the siunitx package. + """ + return self.s.create_str(result.value, [], result.unit) + + def result_to_latex_str_without_unit(self, result: Result) -> str: + """ + Returns the result without unit as LaTeX string making use of the siunitx package. + """ + return self.s.create_str(result.value, result.uncertainties, "") diff --git a/src/application/latex_ifelse.py b/src/application/latex_ifelse.py new file mode 100644 index 00000000..3dc85289 --- /dev/null +++ b/src/application/latex_ifelse.py @@ -0,0 +1,33 @@ +class LatexIfElseBuilder: + def __init__(self): + self.latex: str = "" + self._num_parentheses_to_close: int = 0 + self.keywords: list[str] = [] + + def add_branch(self, keyword: str, body: str): + # Condition + if self.latex == "": + self.latex += rf" \ifthenelse{{\equal{{#1}}{{{keyword}}}}}{{" + else: + self.latex += "\n" + self.latex += rf" }}{{\ifthenelse{{\equal{{#1}}{{{keyword}}}}}{{" + self._num_parentheses_to_close += 1 + + if keyword != "": + self.keywords.append(keyword) + + # Body + self.latex += "\n" + self.latex += rf" {body}" + + def add_else(self, body: str): + self.latex += "\n" + self.latex += r" }{" + self.latex += rf"{body}" + self._num_parentheses_to_close += 1 + + def build(self) -> str: + for _ in range(self._num_parentheses_to_close): + self.latex += "}" + + return self.latex diff --git a/src/application/latex_stringifier.py b/src/application/latex_stringifier.py new file mode 100644 index 00000000..08ae6f1b --- /dev/null +++ b/src/application/latex_stringifier.py @@ -0,0 +1,30 @@ +from application.stringifier import Stringifier + + +class LatexStringifier(Stringifier): + """ + Provides methods to convert results to LaTeX commands. + + We assume the result to already be correctly rounded at this point. + """ + + # pylint: disable=duplicate-code + plus_minus = r"\pm" + negative_sign = "-" + positive_sign = "" + + left_parenthesis = r"\left(" + right_parenthesis = r"\right)" + + value_prefix = r"\num{" + value_suffix = r"}" + + uncertainty_name_prefix = r"_{\text{" + uncertainty_name_suffix = r"}}" + + scientific_notation_prefix = r" \cdot 10^{" + scientific_notation_suffix = "}" + + unit_prefix = r" \, \unit{" + unit_suffix = "}" + # pylint: enable=duplicate-code diff --git a/src/application/latexer.py b/src/application/latexer.py deleted file mode 100644 index e8014f48..00000000 --- a/src/application/latexer.py +++ /dev/null @@ -1,211 +0,0 @@ -from typing import List - -import textwrap -from domain.result import _Result -from domain.value import _Value -from domain.uncertainty import _Uncertainty -from application.helpers import _Helpers -from application.rounder import _Rounder - - -# Config values: -min_exponent_for_non_scientific_notation = -2 -max_exponent_for_non_scientific_notation = 3 -identifier = "res" - - -class _LaTeXer: - @classmethod - def result_to_latex_cmd(cls, result: _Result) -> str: - """ - Returns the result as LaTeX command to be used in a .tex file. - """ - - cmd_name = identifier + result.name[0].upper() + result.name[1:] - - latex_str = rf""" - \newcommand*{{\{cmd_name}}}[1][]{{ - \ifthenelse{{\equal{{#1}}{{}}}}{{ - {cls.result_to_latex_str(result)} - """ - latex_str = textwrap.dedent(latex_str).strip() - - number_of_parentheses_to_close = 0 - keywords = [] - - # Value only: - if len(result.uncertainties) > 0: - latex_str += "\n" - latex_str += r" }{\ifthenelse{\equal{#1}{valueOnly}}{" - latex_str += "\n" - latex_str += rf" {cls.result_to_latex_str_value_only(result)}" - keywords.append("valueOnly") - - number_of_parentheses_to_close += 1 - - # Single uncertainties: - for i, u in enumerate(result.uncertainties): - if len(result.uncertainties) == 1: - uncertainty_name = "error" - else: - if u.name != "": - uncertainty_name = u.name - else: - uncertainty_name = _Helpers.number_to_word(i + 1) - uncertainty_name = "error" + uncertainty_name[0].upper() + uncertainty_name[1:] - error_latex_str = cls.create_latex_str(u.uncertainty, [], result.unit) - - latex_str += "\n" - latex_str += rf" }}{{\ifthenelse{{\equal{{#1}}{{{uncertainty_name}}}}}{{" - latex_str += "\n" - latex_str += rf" {error_latex_str}" - keywords.append(uncertainty_name) - - number_of_parentheses_to_close += 1 - - # Total uncertainty and short result: - if len(result.uncertainties) > 1: - short_result = result.get_short_result() - _Rounder.round_result(short_result) - - error_latex_str = cls.create_latex_str( - short_result.uncertainties[0].uncertainty, [], result.unit - ) - - latex_str += "\n" - latex_str += r" }{\ifthenelse{\equal{#1}{errorTotal}}{" - latex_str += "\n" - latex_str += rf" {error_latex_str}" - keywords.append("errorTotal") - - number_of_parentheses_to_close += 1 - - latex_str += "\n" - latex_str += r" }{\ifthenelse{\equal{#1}{short}}{" - latex_str += "\n" - latex_str += rf" {cls.result_to_latex_str(short_result)}" - keywords.append("short") - - number_of_parentheses_to_close += 1 - - # Unit: - if result.unit != "": - latex_str += "\n" - latex_str += r" }{\ifthenelse{\equal{#1}{unit}}{" - latex_str += "\n" - latex_str += rf" \unit{{{result.unit}}}" - keywords.append("unit") - - number_of_parentheses_to_close += 1 - - # Error message: - if len(keywords) > 0: - latex_str += ( - "\n" - + r" }{\tiny\textbf{Please specify one of the following keywords: " - + ", ".join([rf"\texttt{{{k}}}" for k in keywords]) - + r" or don't use any keyword at all.}\normalsize}" - ) - else: - latex_str += ( - "\n" - + r" }{\tiny\textbf{This variable can only be used without keyword.}\normalsize}" - ) - - for _ in range(number_of_parentheses_to_close): - latex_str += "}" - latex_str += "\n}" - - return latex_str - - @classmethod - def result_to_latex_str(cls, result: _Result) -> str: - """ - Returns the result as LaTeX string making use of the siunitx package. - """ - return cls.create_latex_str(result.value, result.uncertainties, result.unit) - - @classmethod - def result_to_latex_str_value_only(cls, result: _Result) -> str: - """ - Returns only the value as LaTeX string making use of the siunitx package. - """ - return cls.create_latex_str(result.value, [], result.unit) - - @classmethod - def create_latex_str(cls, value: _Value, uncertainties: List[_Uncertainty], unit: str) -> str: - """ - Returns the result as LaTeX string making use of the siunitx package. - - This string does not yet contain "\newcommand*{}". - """ - - latex_str = "" - use_scientific_notation = False - has_unit = unit != "" - - # Determine if scientific notation should be used: - if ( - value.get_exponent() < min_exponent_for_non_scientific_notation - or value.get_exponent() > max_exponent_for_non_scientific_notation - ): - use_scientific_notation = True - - if value.get_min_exponent() > 0: - use_scientific_notation = True - - for u in uncertainties: - if u.uncertainty.get_min_exponent() > 0: - use_scientific_notation = True - - # Create LaTeX string: - if value.get() < 0: - sign = "-" - else: - sign = "" - - if use_scientific_notation: - exponent = value.get_exponent() - factor = 10 ** (-exponent) - - if len(uncertainties) > 0: - latex_str += "(" - - value_normalized = value.get_abs() * factor - decimal_places = value.get_sig_figs() - 1 - latex_str += sign - latex_str += _Helpers.round_to_n_decimal_places(value_normalized, decimal_places) - - for u in uncertainties: - value_normalized = u.uncertainty.get_abs() * factor - decimal_places = exponent - u.uncertainty.get_min_exponent() - latex_str += r" \pm " - latex_str += _Helpers.round_to_n_decimal_places(value_normalized, decimal_places) - if len(uncertainties) > 1: - latex_str += rf"_{{\text{{{u.name}}}}}" - - if len(uncertainties) > 0: - latex_str += ")" - - latex_str += rf" \cdot 10^{{{exponent}}}" - else: - if len(uncertainties) > 0 and unit != "": - latex_str += "(" - - value_normalized = value.get_abs() - decimal_places = value.get_decimal_place() - latex_str += sign - latex_str += _Helpers.round_to_n_decimal_places(value_normalized, decimal_places) - - for u in uncertainties: - latex_str += rf" \pm {_Helpers.round_to_n_decimal_places(u.uncertainty.get_abs(), u.uncertainty.get_decimal_place())}" - if len(uncertainties) > 1: - latex_str += rf"_{{\text{{{u.name}}}}}" - - if len(uncertainties) > 0 and unit != "": - latex_str += ")" - - if has_unit: - latex_str += rf"\ \unit{{{unit}}}" - - return latex_str diff --git a/src/application/rounder.py b/src/application/rounder.py index d866bd46..1eef8d2c 100644 --- a/src/application/rounder.py +++ b/src/application/rounder.py @@ -1,84 +1,176 @@ from typing import List +from decimal import Decimal -from domain.result import _Result -from domain.uncertainty import _Uncertainty -from application.helpers import _Helpers +from dataclasses import dataclass +from domain.result import Result +from domain.uncertainty import Uncertainty +from domain.value import Value, DecimalPlacesError +from application.helpers import Helpers +from application import error_messages -# Config values: -standard_sigfigs = 2 +@dataclass +class RoundingConfig: + sigfigs: int + decimal_places: int + sigfigs_fallback: int + decimal_places_fallback: int -class _Rounder: +class Rounder: + @classmethod - def round_result(cls, result: _Result) -> None: + def round_result(cls, result: Result, config: RoundingConfig) -> None: """ In-place rounds all numerical fields of a result to the correct number of significant figures. - Rounding hierarchy for uncertainties: + # Rounding hierarchy: + + 1. Is uncertainty exact? Do not round. + 2. Round uncertainty according to the hierarchy below. + + + # Rounding hierarchy for inexact uncertainty: + + 1. Is result value exact? + Round uncertainty according to result value. + + 2. Is number of sigfigs of result given? + Round value according to number of sigfigs. + Round uncertainties according to value. + + 3. Is number of decimal places of result given? + Round value according to number of decimal places. + Round uncertainties according to value. - 1. Is uncertainty exact? Do not round! - 2. Round uncertainty according to the hierarchy below! + 4. Is default for sigfigs given (not -1) (see config)? + Round value according to number of sigfigs. + Round uncertainties according to value. - Rounding hierarchy: + 5. Is default for decimal places given (not -1) (see config)? + Round value according to number of decimal places. + Round uncertainties according to value. - 1. Is value exact? Do not round! Round uncertainty according to value! - 2. Is number of sigfigs given? Round value according to number of sigfigs! Round - uncertainties according to value. - 3. Is number of decimal places given? Round value according to number of decimal places! - Round uncertainties according to value. - 4. Is at least one uncertainty given? Round each uncertainty according to standard rules! - Round value according to uncertainty with lowest min exponent! - 5. Round value to 2 sigfigs. + 6. Is at least one uncertainty given? + Round each uncertainty according to standard rules. + Round value according to uncertainty with lowest min exponent. - TODO: Warning message if user specifies exact value and sigfigs etc. + 7. Is fallback for sigfigs given (not -1) (see config)? + Round value according to number of sigfigs. + + 8. Is fallback for decimal places given (not -1) (see config)? + Round value according to number of decimal places. """ + + print_decimal_places_error = cls._round_result(result, config) + + short = result.get_short_result() + if short: + print_decimal_places_error = ( + cls._round_result(short, config) or print_decimal_places_error + ) + + if print_decimal_places_error: + print(error_messages.NUM_OF_DECIMAL_PLACES_TOO_LOW) + + @classmethod + # pylint: disable-next=too-many-branches + def _round_result(cls, result: Result, config: RoundingConfig) -> bool: + """See the docstring of the public `round_result` for details.""" + value = result.value uncertainties = result.uncertainties + print_decimal_places_error = False + # Rounding hierarchy 1: if value.is_exact(): - cls._uncertainties_set_min_exponents(uncertainties, value.get_min_exponent()) + print_decimal_places_error = cls._uncertainties_set_min_exponents( + uncertainties, value.get_min_exponent() + ) # Rounding hierarchy 2: elif result.sigfigs is not None: value.set_sigfigs(result.sigfigs) - cls._uncertainties_set_min_exponents(uncertainties, value.get_min_exponent()) + print_decimal_places_error = cls._uncertainties_set_min_exponents( + uncertainties, value.get_min_exponent() + ) # Rounding hierarchy 3: elif result.decimal_places is not None: - value.set_min_exponent(-result.decimal_places) - cls._uncertainties_set_min_exponents(uncertainties, value.get_min_exponent()) + print_decimal_places_error = cls._value_set_min_exponent(value, -result.decimal_places) + print_decimal_places_error = cls._uncertainties_set_min_exponents( + uncertainties, value.get_min_exponent() + ) # Rounding hierarchy 4: + elif config.sigfigs > -1: + value.set_sigfigs(config.sigfigs) + print_decimal_places_error = cls._uncertainties_set_min_exponents( + uncertainties, value.get_min_exponent() + ) + + # Rounding hierarchy 5: + elif config.decimal_places > -1: + min_exponent = -config.decimal_places + print_decimal_places_error = cls._value_set_min_exponent(value, min_exponent) + print_decimal_places_error = cls._uncertainties_set_min_exponents( + uncertainties, min_exponent + ) + + # Rounding hierarchy 6: elif len(uncertainties) > 0: for u in uncertainties: if u.uncertainty.is_exact(): continue - normalized_value = abs(u.uncertainty.get()) * 10 ** ( - -_Helpers.get_exponent(u.uncertainty.get()) - ) + shift = Decimal(f"1e{-Helpers.get_exponent(u.uncertainty.get())}") + normalized_value = abs(u.uncertainty.get()) * shift if round(normalized_value, 1) >= 3.0: u.uncertainty.set_sigfigs(1) else: u.uncertainty.set_sigfigs(2) - min_exponent = min([u.uncertainty.get_min_exponent() for u in uncertainties]) - value.set_min_exponent(min_exponent) + min_exponent = min(u.uncertainty.get_min_exponent() for u in uncertainties) + print_decimal_places_error = cls._value_set_min_exponent(value, min_exponent) + + # Rounding hierarchy 7: + elif config.sigfigs_fallback > -1: + value.set_sigfigs(config.sigfigs_fallback) + + # Rounding hierarchy 8: + elif config.decimal_places_fallback > -1: + min_exponent = -config.decimal_places_fallback + print_decimal_places_error = cls._value_set_min_exponent(value, min_exponent) - # Rounding hierarchy 5: else: - value.set_sigfigs(standard_sigfigs) - cls._uncertainties_set_min_exponents(uncertainties, value.get_min_exponent()) + # This branch cannot be reached, because the config makes sure that + # either`sigfigs_fallback` or `decimal_places_fallback` is set. + raise RuntimeError(error_messages.INTERNAL_ROUNDER_HIERARCHY_ERROR) + + return print_decimal_places_error + + @classmethod + def _value_set_min_exponent(cls, value: Value, min_exponent: int) -> bool: + try: + value.set_min_exponent(min_exponent) + return False + except DecimalPlacesError as _: + return True @classmethod def _uncertainties_set_min_exponents( - cls, uncertainties: List[_Uncertainty], min_exponent: int - ) -> None: + cls, uncertainties: List[Uncertainty], min_exponent: int + ) -> bool: + print_decimal_places_error = False + for u in uncertainties: if not u.uncertainty.is_exact(): - u.uncertainty.set_min_exponent(min_exponent) - return + try: + u.uncertainty.set_min_exponent(min_exponent) + except DecimalPlacesError as _: + print_decimal_places_error = True + + return print_decimal_places_error diff --git a/src/application/stringifier.py b/src/application/stringifier.py new file mode 100644 index 00000000..35d70406 --- /dev/null +++ b/src/application/stringifier.py @@ -0,0 +1,171 @@ +from dataclasses import dataclass +from typing import List, Tuple +from typing import Protocol, ClassVar +from decimal import Decimal + +# for why we use a Protocol instead of a ABC class, see +# https://github.com/microsoft/pyright/issues/2601#issuecomment-977053380 + +from domain.value import Value +from domain.uncertainty import Uncertainty +from application.helpers import Helpers + + +@dataclass +class StringifierConfig: + min_exponent_for_non_scientific_notation: int + max_exponent_for_non_scientific_notation: int + identifier: str + + +class Stringifier(Protocol): + """ + Provides methods to convert results to strings of customizable pattern. + + We assume the result to already be correctly rounded at this point. + """ + + config: StringifierConfig + + # pylint: disable=duplicate-code + plus_minus: ClassVar[str] + negative_sign: ClassVar[str] + positive_sign: ClassVar[str] + + left_parenthesis: ClassVar[str] + right_parenthesis: ClassVar[str] + + value_prefix: ClassVar[str] + value_suffix: ClassVar[str] + + uncertainty_name_prefix: ClassVar[str] + uncertainty_name_suffix: ClassVar[str] + + scientific_notation_prefix: ClassVar[str] + scientific_notation_suffix: ClassVar[str] + + unit_prefix: ClassVar[str] + unit_suffix: ClassVar[str] + # pylint: enable=duplicate-code + + def __init__(self, config: StringifierConfig): + self.config = config + + def create_str(self, value: Value, uncertainties: List[Uncertainty], unit: str) -> str: + """ + Returns the result as LaTeX string making use of the siunitx package. + + This string does not yet contain "\newcommand*{}". + """ + use_scientific_notation = self._should_use_scientific_notation(value, uncertainties) + should_use_parentheses = len(uncertainties) > 0 and (use_scientific_notation or unit != "") + + sign = self._value_to_sign_str(value) + value_rounded, exponent, factor = self._value_to_str(value, use_scientific_notation) + + uncertainties_rounded = [] + for u in uncertainties: + u_rounded = self._uncertainty_to_str(u, use_scientific_notation, exponent, factor) + u_rounded = f" {self.plus_minus} {self.value_prefix}{u_rounded}{self.value_suffix}" + if u.name != "": + u_rounded += self.uncertainty_name_prefix + u_rounded += self._modify_uncertainty_name(u.name) + u_rounded += self.uncertainty_name_suffix + uncertainties_rounded.append(u_rounded) + + # Assemble everything together + return self._assemble_str_parts( + sign, + value_rounded, + uncertainties_rounded, + should_use_parentheses, + use_scientific_notation, + exponent, + unit, + ) + + # pylint: disable-next=too-many-arguments + def _assemble_str_parts( + self, + sign: str, + value_rounded: str, + uncertainties_rounded: List[str], + should_use_parentheses: bool, + use_scientific_notation: bool, + exponent: int, + unit: str, + ): + string = f"{sign}{value_rounded}{''.join(uncertainties_rounded)}" + + if should_use_parentheses: + string = f"{self.left_parenthesis}{string}{self.right_parenthesis}" + + if use_scientific_notation: + e = f"{self.scientific_notation_prefix}{str(exponent)}{self.scientific_notation_suffix}" + string += e + + if unit != "": + string += f"{self.unit_prefix}{self._modify_unit(unit)}{self.unit_suffix}" + + return string + + def _value_to_sign_str(self, value: Value) -> str: + return self.negative_sign if value.get() < 0 else self.positive_sign + + def _value_to_str( + self, value: Value, use_scientific_notation: bool + ) -> Tuple[str, int, Decimal]: + exponent = value.get_exponent() + factor = Decimal(f"1e{-exponent}") if use_scientific_notation else Decimal("1.0") + + value_normalized = value.get_abs() * factor + decimal_places = ( + value.get_sig_figs() - 1 if use_scientific_notation else value.get_decimal_place() + ) + + return Helpers.round_to_n_decimal_places(value_normalized, decimal_places), exponent, factor + + def _uncertainty_to_str( + self, u: Uncertainty, use_scientific_notation: bool, exponent: int, factor: Decimal + ) -> str: + uncertainty_normalized = u.uncertainty.get_abs() * factor + decimal_places = ( + exponent - u.uncertainty.get_min_exponent() + if use_scientific_notation + else u.uncertainty.get_decimal_place() + ) + return Helpers.round_to_n_decimal_places(uncertainty_normalized, decimal_places) + + def _should_use_scientific_notation( + self, value: Value, uncertainties: List[Uncertainty] + ) -> bool: + """ + Returns whether scientific notation should be used for the given value and uncertainties. + """ + exponent = value.get_exponent() + if ( + exponent < self.config.min_exponent_for_non_scientific_notation + or exponent > self.config.max_exponent_for_non_scientific_notation + ): + return True + + if value.get_min_exponent() > 0: + return True + + for u in uncertainties: + if u.uncertainty.get_min_exponent() > 0: + return True + + return False + + def _modify_unit(self, unit: str) -> str: + """ + Returns the modified unit. + """ + return unit + + def _modify_uncertainty_name(self, name: str) -> str: + """ + Returns the modified value (as string). + """ + return name diff --git a/src/domain/result.py b/src/domain/result.py index d34b5a65..1fce9e50 100644 --- a/src/domain/result.py +++ b/src/domain/result.py @@ -1,36 +1,49 @@ -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import Union +from copy import copy +from decimal import Decimal -from domain.uncertainty import _Uncertainty -from domain.value import _Value +from domain.uncertainty import Uncertainty +from domain.value import Value @dataclass -class _Result: +class Result: """ A general-purpose result, i.e. a value that was somehow measured or calculated, along with a unit and optional uncertainties (list might be empty). """ name: str - value: _Value + value: Value unit: str - uncertainties: list[_Uncertainty] + uncertainties: list[Uncertainty] sigfigs: Union[int, None] decimal_places: Union[int, None] - def get_total_uncertainty(self) -> _Uncertainty: - s = 0 + total_uncertainty: Union[Uncertainty, None] = field(init=False) + + def __post_init__(self): + if len(self.uncertainties) >= 2: + self.total_uncertainty = self._calculate_total_uncertainty() + else: + self.total_uncertainty = None + + def _calculate_total_uncertainty(self) -> Uncertainty: + total = Decimal("0") for u in self.uncertainties: - s += u.uncertainty.get() ** 2 - return _Uncertainty(s**0.5) + total += u.uncertainty.get() ** 2 + return Uncertainty(Value(total.sqrt())) + + def get_short_result(self) -> Union["Result", None]: + if self.total_uncertainty is None: + return None - def get_short_result(self) -> "_Result": - return _Result( + return Result( self.name, - self.value, + copy(self.value), self.unit, - [self.get_total_uncertainty()], + [copy(self.total_uncertainty)], self.sigfigs, self.decimal_places, ) diff --git a/src/domain/uncertainty.py b/src/domain/uncertainty.py index 85ccb8ed..57bcbf7b 100644 --- a/src/domain/uncertainty.py +++ b/src/domain/uncertainty.py @@ -1,9 +1,7 @@ -from typing import Union +from domain.value import Value -from domain.value import _Value - -class _Uncertainty: +class Uncertainty: """ A named uncertainty value, e.g. a systematic uncertainty of ±0.1cm when measuring a length. In this case the uncertainty would be 0.1 and the name @@ -15,9 +13,6 @@ class _Uncertainty: interchangeably. """ - def __init__(self, uncertainty: Union[float, str], name: str = ""): - self.uncertainty = _Value(uncertainty) + def __init__(self, uncertainty: Value, name: str = ""): + self.uncertainty = uncertainty self.name = name - - def value(self) -> _Value: - return self.uncertainty diff --git a/src/domain/value.py b/src/domain/value.py index 6c8fa12c..1a454d18 100644 --- a/src/domain/value.py +++ b/src/domain/value.py @@ -1,52 +1,50 @@ from typing import Union +from decimal import Decimal -from application.helpers import _Helpers +from application.helpers import Helpers +from application import error_messages -class _Value: +class DecimalPlacesError(Exception): + pass + + +class Value: """ - A floating-point value represented as string that is either treated as exact - (does not have any uncertainties) or as inexact (has uncertainties). - Values that are exact will be exempt from significant figures rounding. + A decimal value. - Note that is_exact signifies if the value is to be taken as a *literal* value, - i.e. "3.14000" will be output as "3.14000" and not "3.14" if is_exact is True. - TODO: maybe find a better word for "exact"? + It is either exact or inexact. Values that are set as exact + will be exempt form any rounding. If the value is set as exact, it will be + treated as a *literal* value, i.e. "3.14000" will be output as "3.14000" + and not "3.14". """ - _value: float + _value: Decimal _is_exact: bool _max_exponent: int _min_exponent: int - # "3400.0" -> 3400, -1, 3 - # "3400" -> 3400, 0, 3 - # "3.4e3" -> 3400, 2, 3 + def __init__(self, value: Decimal, min_exponent: Union[int, None] = None): + self._value = value - def __init__(self, value: Union[float, str]): - if isinstance(value, str): - self._value = float(value) + if min_exponent is not None: + self._min_exponent = min_exponent self._is_exact = True - - # Determine min exponent: - value_str = value - exponent_offset = 0 - if "e" in value_str: - exponent_offset = int(value_str[value_str.index("e") + 1 :]) - value_str = value_str[0 : value_str.index("e")] - if "." in value_str: - decimal_places = len(value_str) - value_str.index(".") - 1 - self._min_exponent = -decimal_places + exponent_offset - else: - self._min_exponent = exponent_offset else: - self._value = value self._is_exact = False - self._max_exponent = _Helpers.get_exponent(self._value) + self._max_exponent = Helpers.get_exponent(self._value) def set_min_exponent(self, min_exponent: int): self._min_exponent = min_exponent + if min_exponent > self._max_exponent: + self._max_exponent = min_exponent + + # Check if the value is too small to be rounded to the specified number of decimal + # places: + rounded = Helpers.round_to_n_decimal_places(self._value, -min_exponent) + if Decimal(rounded) == 0 and self._value != 0: + raise DecimalPlacesError() def get_min_exponent(self) -> int: return self._min_exponent @@ -57,10 +55,10 @@ def set_sigfigs(self, sigfigs: int): def is_exact(self) -> bool: return self._is_exact - def get(self) -> float: + def get(self) -> Decimal: return self._value - def get_abs(self) -> float: + def get_abs(self) -> Decimal: return abs(self._value) def get_exponent(self) -> int: @@ -71,6 +69,7 @@ def get_sig_figs(self) -> int: def get_decimal_place(self) -> int: if self._min_exponent is None: - raise RuntimeError("An unexpected error occurred. Please report this bug.") - else: - return -self._min_exponent + # This should not happen as `_min_exponent` should be set + # by the time this method is called. + raise RuntimeError(error_messages.INTERNAL_MIN_EXPONENT_ERROR) + return -self._min_exponent diff --git a/src/resultwizard/__init__.py b/src/resultwizard/__init__.py index cb258914..f00a02ca 100644 --- a/src/resultwizard/__init__.py +++ b/src/resultwizard/__init__.py @@ -1,6 +1,3 @@ +from api.config import config_init, config from api.res import res -from api.tables.table import table -from api.tables.column import column -from api.tables.table_res import table_res from api.export import export -from application.cache import _ResultsCache diff --git a/tests/number_word_test.py b/tests/number_word_test.py new file mode 100644 index 00000000..30541160 --- /dev/null +++ b/tests/number_word_test.py @@ -0,0 +1,31 @@ +import pytest + +from application.helpers import Helpers + + +class TestNumberWord: + @pytest.mark.parametrize( + "value, expected", + [ + (0, "zero"), + (1, "one"), + (2, "two"), + (3, "three"), + (10, "ten"), + (18, "eighteen"), + (76, "seventySix"), + (100, "oneHundred"), + (101, "oneHundredOne"), + (123, "oneHundredTwentyThree"), + (305, "threeHundredFive"), + (911, "nineHundredEleven"), + (999, "nineHundredNinetyNine"), + ], + ) + def test_number_to_word(self, value, expected): + assert Helpers.number_to_word(value) == expected + + @pytest.mark.parametrize("value", [1000, 1001, -1, -500]) + def test_number_to_word_raises(self, value): + with pytest.raises(ValueError, match="numbers between 0 and 999"): + Helpers.number_to_word(value) diff --git a/tests/parsers_test.py b/tests/parsers_test.py new file mode 100644 index 00000000..777a3b1e --- /dev/null +++ b/tests/parsers_test.py @@ -0,0 +1,95 @@ +from decimal import Decimal +import pytest + +from api import parsers +from domain.value import Value + + +class TestNameParser: + + @pytest.mark.parametrize( + "name, expected", + [ + ("a12", "aTwelve"), + ("a01", "aOne"), + ("042", "fortyTwo"), + ("a911bc13", "aNineHundredElevenBcThirteen"), + ("13_600", "thirteenSixHundred"), + ("a_5_6b7_$5", "aFiveSixBSevenFive"), + ], + ) + def test_substitutes_numbers(self, name: str, expected: str): + assert parsers.parse_name(name) == expected + + @pytest.mark.parametrize( + "name, expected", + [ + ("ä", "ae"), + ("Ä", "Ae"), + ("ü", "ue"), + ("Ü", "Ue"), + ("ö", "oe"), + ("Ö", "Oe"), + ("ß", "ss"), + ("ẞ", "SS"), + ("äh", "aeh"), + ("Füße", "Fuesse"), + ("GIEẞEN", "GIESSEN"), + ], + ) + def test_replaces_umlauts(self, name: str, expected: str): + assert parsers.parse_name(name) == expected + + @pytest.mark.parametrize( + "name, expected", + [ + ("!a$", "a"), + ("!a$b", "ab"), + ("!a$b", "ab"), + ("!%a&/(=*)s.,'@\"§d", "asd"), + ], + ) + def test_strips_invalid_chars(self, name: str, expected: str): + assert parsers.parse_name(name) == expected + + @pytest.mark.parametrize("name", ["", "!", " ", "_ ", "§ _ '*"]) + def test_empty_name_fails(self, name): + with pytest.raises(ValueError): + parsers.parse_name(name) + + +class TestValueParser: + + @pytest.mark.parametrize( + "value, expected", + [ + # plain numbers + ("012", Value(Decimal("12.00000000000"), min_exponent=0)), + ("12", Value(Decimal("12.0"), min_exponent=0)), + ("-42", Value(Decimal("-42"), min_exponent=0)), + ("10050", Value(Decimal("10050"), min_exponent=0)), + # plain numbers scientific + ("13e3", Value(Decimal("13000.0"), min_exponent=3)), + # plain decimal + ("3.1415", Value(Decimal("3.1415"), min_exponent=-4)), + ("0.005", Value(Decimal("0.005"), min_exponent=-3)), + ("0.010", Value(Decimal("0.01"), min_exponent=-3)), + # decimal & scientific + ("3.1415e3", Value(Decimal("3141.5"), min_exponent=-1)), + ("2.71828e2", Value(Decimal("271.828"), min_exponent=-3)), + ("2.5e-1", Value(Decimal("0.25"), min_exponent=-2)), + ("0.1e2", Value(Decimal("10.0"), min_exponent=1)), + ("1.2e5", Value(Decimal("120000.0"), min_exponent=4)), + ("1.20e5", Value(Decimal("120000.0"), min_exponent=3)), + ( + "103.1570e-30", + Value(Decimal("0.0000000000000000000000000001031570"), min_exponent=-34), + ), + ], + ) + def test_parse_exact_value(self, value: str, expected: Value): + v = parsers.parse_exact_value(value) + # pylint: disable=protected-access + assert v._value == expected._value + assert v._min_exponent == expected._min_exponent + # pylint: enable=protected-access diff --git a/tests/playground.py b/tests/playground.py index 6ed2a308..d5008782 100644 --- a/tests/playground.py +++ b/tests/playground.py @@ -6,8 +6,7 @@ # (e.g. the site-packages directory)." # From: https://setuptools.pypa.io/en/latest/userguide/quickstart.html#development-mode - -from random import random +from decimal import Decimal import resultwizard as wiz print("#############################") @@ -15,12 +14,14 @@ print("#############################") print() - -# wiz.config( -# standard_sigfigs = 2, -# ... -# ) - +wiz.config_init( + print_auto=True, + export_auto_to="results-immediate.tex", + siunitx_fallback=False, + ignore_result_overwrite=False, +) +# wiz.config(sigfigs=2) +# wiz.config(decimal_places=2) ############################# # EXAMPLES @@ -28,36 +29,38 @@ print("### RESULTS API") -wiz.res("a1", 1.0, r"\mm").print() -# a: 1.0 \mm +# wiz.res("", 42.0).print() +# -> Error: "name must not be empty" -wiz.res("1 b", 1.0, 0.01, r"\per\mm\cubed").print() -# b: (1.0 ± 0.01) \mm - -wiz.res("c big", 1.0, (0.01, "systematic"), r"\mm").print() -# c: (1.0 ± 0.01 systematic) \mm - -wiz.res( - "d", 1.0e10, [(0.01e10, "systematic"), (0.0294999999e10, "stat")], r"\mm\per\second\squared" -).print() -# d: (1.0 ± 0.01 systematic ± 0.02 stat) \mm +wiz.res("a911", 1.05, unit=r"\mm\s\per\N\kg") +# wiz.res("a911", "1.052", 0.25, r"\mm\s\per\N\kg") -# wiz.standard_sigfigs(4) +wiz.res("1 b", 1.0, 0.01, unit=r"\per\mm\cubed") -wiz.res("e", "1.0", r"\mm").print() -# e: 1.0 \mm +# wiz.config(decimal_places=-1, sigfigs_fallback=3) -# wiz.standard_sigfigs(3) +wiz.res("c big", 1.0, (0.01, "systematic"), r"\mm") +wiz.res("d", 1.0e10, [(0.01e10, "sysyeah"), (0.0294999e10, "statyeah")], r"\mm\per\second^2") +# wiz.res("e", "1.0", r"\mm") # -> except error message that maybe we have forgotten to put `unit=` -wiz.res("f", "1.0e1", 25e-1).print() -# f: 1.0 +wiz.res("f", "1.0e1", 25e-1) +wiz.res("g", 42) +wiz.res("h", 42, sys=13.0, stat=24.0) +wiz.res("h&", 42, sys=13.0, stat=24.0) -# wiz.res("g", 1.0, sys=0.01, stat=0.02, unit=r"\mm").print() -# g: (1.0 ± 0.01 sys ± 0.02 stat) \mm - -# The following wont' work as we can't have positional arguments (here: unit) -# after keyword arguments (here: uncert) -# wiz.res("d", 1.0, uncert=[(0.01, "systematic"), (0.02, "stat")], r"\mm").print() +wiz.res("i", Decimal("42.0e-30"), Decimal("0.1e-31"), unit=r"\m") +wiz.res( + "i", + Decimal("42.0e-30"), + sys=Decimal("0.1e-31"), + stat=Decimal("0.05e-31"), + unit=r"\m\per\s\squared", +) +wiz.res("j", 0.009, None, "", 2) # really bad, but this is valid +# wiz.res("k", 1.55, 0.0, unit=r"\tesla") # -> uncertainty must be positive +wiz.res("k", 3, 1, r"\tesla") # integers work as well, yeah +wiz.res("l", 1.0, sys=0.01, stat=0.02, unit=r"\mm").print() +wiz.res("m", 1.0, uncerts=[(0.01, "systematic"), (0.02, "stat")], unit=r"\mm").print() wiz.table( "name", @@ -82,16 +85,14 @@ ), ], "description", - resize_to_fit_page_=True + resize_to_fit_page_=True, ) wiz.table( "name horizontal", [ wiz.column("Num.", [f"{i+1}" for i in range(4)]), - wiz.column( - "Random 1", [wiz.table_res(random(), random() * 0.1, r"\mm") for i in range(4)] - ), + wiz.column("Random 1", [wiz.table_res(random(), random() * 0.1, r"\mm") for i in range(4)]), wiz.column( "Random 2", [wiz.table_res(random(), random() * 0.1, r"\electronvolt") for i in range(4)], @@ -113,6 +114,8 @@ label="tab:horizontal", ) +wiz.res("Tour Eiffel Height", "330.3141516", "0.5", r"\m") +wiz.res("g Another Test", 9.81, 0.78, unit=r"\m/\s^2") ############################# # Export diff --git a/tests/rounder_test.py b/tests/rounder_test.py index dbe48abd..9a78aa63 100644 --- a/tests/rounder_test.py +++ b/tests/rounder_test.py @@ -1,77 +1,221 @@ -from application.rounder import _Rounder -from domain.result import _Result -from domain.value import _Value -from domain.uncertainty import _Uncertainty +# pylint: disable=redefined-outer-name -class TestRounder: - def test_hierarchy_1(self): - res = _Result("", _Value("1.0000"), "", [], 2, 10) - _Rounder.round_result(res) - assert res.value.get_min_exponent() == -4 - - res = _Result("", _Value("1.0000"), "", [_Uncertainty(0.1)], None, None) - _Rounder.round_result(res) - assert res.value.get_min_exponent() == -4 - assert res.uncertainties[0].uncertainty.get_min_exponent() == -4 - - res = _Result("", _Value("1.0000"), "", [_Uncertainty("0.1")], None, None) - _Rounder.round_result(res) - assert res.value.get_min_exponent() == -4 - assert res.uncertainties[0].uncertainty.get_min_exponent() == -1 - - def test_hierarchy_2(self): - res = _Result("", _Value(1.0), "", [], 10, 2) - _Rounder.round_result(res) - assert res.value.get_min_exponent() == -9 - - res = _Result("", _Value(1.0), "", [_Uncertainty(0.1)], 10, None) - _Rounder.round_result(res) - assert res.value.get_min_exponent() == -9 - assert res.uncertainties[0].uncertainty.get_min_exponent() == -9 +from typing import List +from decimal import Decimal +import pytest - res = _Result("", _Value(1.0), "", [_Uncertainty("0.1")], 10, None) - _Rounder.round_result(res) - assert res.value.get_min_exponent() == -9 - assert res.uncertainties[0].uncertainty.get_min_exponent() == -1 +from application.rounder import Rounder, RoundingConfig +from domain.result import Result +from domain.value import Value +from domain.uncertainty import Uncertainty - def test_hierarchy_3(self): - res = _Result("", _Value(1.0), "", [], None, 2) - _Rounder.round_result(res) - assert res.value.get_min_exponent() == -2 - res = _Result("", _Value(1.0), "", [_Uncertainty(0.1)], None, 2) - _Rounder.round_result(res) - assert res.value.get_min_exponent() == -2 - assert res.uncertainties[0].uncertainty.get_min_exponent() == -2 +@pytest.fixture +def config_defaults(): + return RoundingConfig(-1, -1, 2, -1) - res = _Result("", _Value(1.0), "", [_Uncertainty("0.1e-5")], None, 2) - _Rounder.round_result(res) - assert res.value.get_min_exponent() == -2 - assert res.uncertainties[0].uncertainty.get_min_exponent() == -6 - def test_hierarchy_4(self): - res = _Result("", _Value(1.0), "", [_Uncertainty(0.11)], None, None) - _Rounder.round_result(res) - assert res.value.get_min_exponent() == -2 - assert res.uncertainties[0].uncertainty.get_min_exponent() == -2 - - res = _Result("", _Value(1.0), "", [_Uncertainty(0.294999)], None, None) - _Rounder.round_result(res) - assert res.value.get_min_exponent() == -2 - assert res.uncertainties[0].uncertainty.get_min_exponent() == -2 - - res = _Result("", _Value(1.0), "", [_Uncertainty(0.295001)], None, None) - _Rounder.round_result(res) - assert res.value.get_min_exponent() == -1 - assert res.uncertainties[0].uncertainty.get_min_exponent() == -1 - - res = _Result("", _Value(1.0), "", [_Uncertainty(0.4), _Uncertainty(0.04)], None, None) - _Rounder.round_result(res) - assert res.value.get_min_exponent() == -2 - assert res.uncertainties[0].uncertainty.get_min_exponent() == -1 - assert res.uncertainties[1].uncertainty.get_min_exponent() == -2 +class TestRounder: - def test_hierarchy_5(self): - res = _Result("", _Value(1.0), "", [], None, None) - _Rounder.round_result(res) - assert res.value.get_min_exponent() == -1 + @pytest.mark.parametrize( + "result, config, expected_value_min_exponent, expected_uncert_min_exponents", + [ + # Hierarchy 1: + ( + Result("", Value(Decimal(1.0), -1), "", [], None, None), + RoundingConfig(-1, -1, 2, -1), + -1, + [], + ), + ( + Result( + "", Value(Decimal(1.0), -1), "", [Uncertainty(Value(Decimal(1.0)))], None, None + ), + RoundingConfig(-1, -1, 2, -1), + -1, + [-1], + ), + ( + Result( + "", + Value(Decimal(1.0), -1), + "", + [Uncertainty(Value(Decimal(1.0), -2))], + None, + None, + ), + RoundingConfig(-1, -1, 2, -1), + -1, + [-2], + ), + # Hierarchy 2: + ( + Result( + "", + Value(Decimal(1.0)), + "", + [Uncertainty(Value(Decimal(1.0)))], + 3, + None, + ), + RoundingConfig(-1, -1, 2, -1), + -2, + [-2], + ), + ( + Result( + "", + Value(Decimal(1.0)), + "", + [Uncertainty(Value(Decimal(1.0), -3))], + 3, + None, + ), + RoundingConfig(-1, -1, 2, -1), + -2, + [-3], + ), + # Hierarchy 3: + ( + Result( + "", + Value(Decimal(1.0)), + "", + [Uncertainty(Value(Decimal(1.0)))], + None, + 2, + ), + RoundingConfig(-1, -1, 2, -1), + -2, + [-2], + ), + ( + Result( + "", + Value(Decimal(1.0)), + "", + [Uncertainty(Value(Decimal(1.0), -3))], + None, + 2, + ), + RoundingConfig(-1, -1, 2, -1), + -2, + [-3], + ), + # Hierarchy 4: + ( + Result( + "", + Value(Decimal(1.0)), + "", + [Uncertainty(Value(Decimal(1.0)))], + None, + None, + ), + RoundingConfig(5, -1, 2, -1), + -4, + [-4], + ), + ( + Result( + "", + Value(Decimal(1.0)), + "", + [Uncertainty(Value(Decimal(1.0), -3))], + None, + None, + ), + RoundingConfig(5, -1, 2, -1), + -4, + [-3], + ), + # Hierarchy 5: + ( + Result( + "", + Value(Decimal(1.0)), + "", + [Uncertainty(Value(Decimal(1.0)))], + None, + None, + ), + RoundingConfig(-1, 4, 2, -1), + -4, + [-4], + ), + ( + Result( + "", + Value(Decimal(1.0)), + "", + [Uncertainty(Value(Decimal(1.0), -3))], + None, + None, + ), + RoundingConfig(-1, 4, 2, -1), + -4, + [-3], + ), + # Hierarchy 6: + ( + Result( + "", + Value(Decimal(1.0)), + "", + [ + Uncertainty(Value(Decimal(1.0), -3)), + Uncertainty(Value(Decimal(1.0))), + Uncertainty(Value(Decimal(2.0))), + Uncertainty(Value(Decimal(2.9499))), + Uncertainty(Value(Decimal(2.9500))), + Uncertainty(Value(Decimal(0.007))), + ], + None, + None, + ), + RoundingConfig(-1, -1, 2, -1), + -3, + [-3, -1, -1, -1, 0, -3], + ), + # Hierarchy 7: + ( + Result( + "", + Value(Decimal(1.0)), + "", + [], + None, + None, + ), + RoundingConfig(-1, -1, 2, -1), + -1, + [], + ), + # Hierarchy 8: + ( + Result( + "", + Value(Decimal(1.0)), + "", + [], + None, + None, + ), + RoundingConfig(-1, -1, -1, 2), + -2, + [], + ), + ], + ) + def test_all_hierarchies( + self, + result: Result, + config: RoundingConfig, + expected_value_min_exponent: int, + expected_uncert_min_exponents: List[int], + ): + Rounder.round_result(result, config) + assert result.value.get_min_exponent() == expected_value_min_exponent + assert [ + u.uncertainty.get_min_exponent() for u in result.uncertainties + ] == expected_uncert_min_exponents From 4e070f961ff3a08e99838d4e628a211a83fa9a09 Mon Sep 17 00:00:00 2001 From: paul019 <39464035+paul019@users.noreply.github.com> Date: Mon, 9 Sep 2024 12:06:16 +0200 Subject: [PATCH 23/25] Implement new api --- src/api/res.py | 58 ++++++++++---- src/api/tables/column.py | 10 +-- src/api/tables/table.py | 17 ++++- src/api/tables/table_res.py | 106 +++++--------------------- src/application/cache.py | 23 +++++- src/application/parsers.py | 143 ----------------------------------- src/domain/tables/column.py | 11 ++- src/domain/tables/table.py | 6 +- src/resultwizard/__init__.py | 4 + tests/playground.py | 1 + 10 files changed, 112 insertions(+), 267 deletions(-) delete mode 100644 src/application/parsers.py diff --git a/src/api/res.py b/src/api/res.py index ba06ff7a..b630947f 100644 --- a/src/api/res.py +++ b/src/api/res.py @@ -3,21 +3,19 @@ from api.printable_result import PrintableResult from api import parsers -from application.cache import ResultsCache +from application.cache import _res_cache from application.rounder import Rounder from application import error_messages from domain.result import Result -_res_cache = ResultsCache() - # "Wrong" import position to avoid circular imports from api.export import _export # pylint: disable=wrong-import-position,ungrouped-imports import api.config as c # pylint: disable=wrong-import-position,ungrouped-imports # pylint: disable-next=too-many-arguments, too-many-locals -def res( - name: str, +def _res( + name: Union[None, str], value: Union[float, int, str, Decimal], uncerts: Union[ float, @@ -33,16 +31,7 @@ def res( stat: Union[float, int, str, Decimal, None] = None, sigfigs: Union[int, None] = None, decimal_places: Union[int, None] = None, -) -> PrintableResult: - """ - Declares your result. Give it a name and a value. You may also optionally provide - uncertainties (via `uncert` or `sys`/`stat`) and a unit in `siunitx` format. - - You may additionally specify the number of significant figures or decimal places - to round this specific result to, irrespective of your global configuration. - - TODO: provide a link to the docs for more information and examples. - """ +) -> [str, Result]: # Verify user input if sigfigs is not None and decimal_places is not None: raise ValueError(error_messages.SIGFIGS_AND_DECIMAL_PLACES_AT_SAME_TIME) @@ -68,7 +57,10 @@ def res( uncerts = [] # Parse user input - name_res = parsers.parse_name(name) + if name is not None: + name_res = parsers.parse_name(name) + else: + name_res = "" value_res = parsers.parse_value(value) uncertainties_res = parsers.parse_uncertainties(uncerts) unit_res = parsers.parse_unit(unit) @@ -80,6 +72,40 @@ def res( name_res, value_res, unit_res, uncertainties_res, sigfigs_res, decimal_places_res ) Rounder.round_result(result, c.configuration.to_rounding_config()) + + return name_res, result + + +def res( + name: str, + value: Union[float, int, str, Decimal], + uncerts: Union[ + float, + int, + str, + Decimal, + Tuple[Union[float, int, str, Decimal], str], + List[Union[float, int, str, Decimal, Tuple[Union[float, int, str, Decimal], str]]], + None, + ] = None, + unit: str = "", + sys: Union[float, int, str, Decimal, None] = None, + stat: Union[float, int, str, Decimal, None] = None, + sigfigs: Union[int, None] = None, + decimal_places: Union[int, None] = None, +) -> PrintableResult: + """ + Declares your result. Give it a name and a value. You may also optionally provide + uncertainties (via `uncert` or `sys`/`stat`) and a unit in `siunitx` format. + + You may additionally specify the number of significant figures or decimal places + to round this specific result to, irrespective of your global configuration. + + TODO: provide a link to the docs for more information and examples. + """ + name_res, result = _res(name, value, uncerts, unit, sys, stat, sigfigs, decimal_places) + + # Add to cache _res_cache.add(name_res, result) # Print automatically diff --git a/src/api/tables/column.py b/src/api/tables/column.py index d95bb74d..8c92f88a 100644 --- a/src/api/tables/column.py +++ b/src/api/tables/column.py @@ -1,12 +1,12 @@ from typing import List, Union -from domain.tables.column import _Column -from domain.result import _Result +from domain.tables.column import Column +from domain.result import Result def column( title: str, - cells: List[Union[_Result, str]], + cells: List[Union[Result, str]], concentrate_units_if_possible: Union[bool, None] = None, -) -> _Column: - return _Column(title, cells, concentrate_units_if_possible) +) -> Column: + return Column(title, cells, concentrate_units_if_possible) diff --git a/src/api/tables/table.py b/src/api/tables/table.py index 85d53e77..f89da08e 100644 --- a/src/api/tables/table.py +++ b/src/api/tables/table.py @@ -2,13 +2,17 @@ from application.cache import _res_cache import api.parsers as parsers -from domain.tables.column import _Column -from domain.tables.table import _Table +from domain.tables.column import Column +from domain.tables.table import Table + +# "Wrong" import position to avoid circular imports +from api.export import _export # pylint: disable=wrong-import-position,ungrouped-imports +import api.config as c # pylint: disable=wrong-import-position,ungrouped-imports def table( name: str, - columns: List[_Column], + columns: List[Column], caption: str, label: Union[str, None] = None, resize_to_fit_page_: bool = False, @@ -37,7 +41,7 @@ def table( column.concentrate_units(concentrate_units_if_possible) # Assemble the table - _table = _Table( + _table = Table( name_res, columns, caption, @@ -48,4 +52,9 @@ def table( ) _res_cache.add_table(name, _table) + # Export automatically + immediate_export_path = c.configuration.export_auto_to + if immediate_export_path != "": + _export(immediate_export_path, print_completed=False) + return diff --git a/src/api/tables/table_res.py b/src/api/tables/table_res.py index 1cbb10bd..770e4270 100644 --- a/src/api/tables/table_res.py +++ b/src/api/tables/table_res.py @@ -1,102 +1,36 @@ +from decimal import Decimal from typing import Union, List, Tuple -from plum import dispatch, overload -from application.rounder import _Rounder -import api.parsers as parsers -from domain.result import _Result +from domain.result import Result +from api.res import _res -# TODO: import types from typing to ensure backwards compatibility down to Python 3.8 - -# TODO: use pydantic instead of manual and ugly type checking -# see: https://docs.pydantic.dev/latest/ -# This way we can code as if the happy path is the only path, and let pydantic -# handle the error checking and reporting. - - -@overload -def table_res( - value: Union[float, str], - unit: str = "", - sigfigs: Union[int, None] = None, - decimal_places: Union[int, None] = None, -) -> _Result: - return table_res(value, [], unit, sigfigs, decimal_places) - - -@overload def table_res( - value: Union[float, str], - uncert: Union[ + value: Union[float, int, str, Decimal], + uncerts: Union[ float, + int, str, - Tuple[Union[float, str], str], - List[Union[float, str, Tuple[Union[float, str], str]]], + Decimal, + Tuple[Union[float, int, str, Decimal], str], + List[Union[float, int, str, Decimal, Tuple[Union[float, int, str, Decimal], str]]], None, ] = None, - sigfigs: Union[int, None] = None, - decimal_places: Union[int, None] = None, -) -> _Result: - return table_res(value, uncert, "", sigfigs, decimal_places) - - -@overload -def table_res( - value: Union[float, str], - sigfigs: Union[int, None] = None, - decimal_places: Union[int, None] = None, -) -> _Result: - return table_res(value, [], "", sigfigs, decimal_places) - - -@overload -def table_res( - value: Union[float, str], - sys: float, - stat: float, unit: str = "", + sys: Union[float, int, str, Decimal, None] = None, + stat: Union[float, int, str, Decimal, None] = None, sigfigs: Union[int, None] = None, decimal_places: Union[int, None] = None, -) -> _Result: - return table_res(value, [(sys, "sys"), (stat, "stat")], unit, sigfigs, decimal_places) +) -> Result: + """ + Declares your result. Give it a name and a value. You may also optionally provide + uncertainties (via `uncert` or `sys`/`stat`) and a unit in `siunitx` format. + You may additionally specify the number of significant figures or decimal places + to round this specific result to, irrespective of your global configuration. -@overload -def table_res( - value: Union[float, str], - uncert: Union[ - float, - str, - Tuple[Union[float, str], str], - List[Union[float, str, Tuple[Union[float, str], str]]], - None, - ] = None, - unit: str = "", - sigfigs: Union[int, None] = None, - decimal_places: Union[int, None] = None, -) -> _Result: - if uncert is None: - uncert = [] - - # Parse user input - value_res = parsers.parse_value(value) - uncertainties_res = parsers.parse_uncertainties(uncert) - unit_res = parsers.parse_unit(unit) - sigfigs_res = parsers.parse_sigfigs(sigfigs) - decimal_places_res = parsers.parse_decimal_places(decimal_places) - - # Assemble the result - result = _Result("", value_res, unit_res, uncertainties_res, sigfigs_res, decimal_places_res) - _Rounder.round_result(result) + TODO: provide a link to the docs for more information and examples. + """ + _, result = _res(None, value, uncerts, unit, sys, stat, sigfigs, decimal_places) return result - - -# Hack for method "overloading" in Python -# see https://beartype.github.io/plum/integration.html -# This is a good writeup: https://stackoverflow.com/a/29091980/ -@dispatch -def table_res(*args, **kwargs) -> object: - # This method only scans for all `overload`-decorated methods - # and properly adds them as Plum methods. - pass diff --git a/src/application/cache.py b/src/application/cache.py index b4f89fd3..bb796017 100644 --- a/src/application/cache.py +++ b/src/application/cache.py @@ -1,5 +1,6 @@ from application.error_messages import RESULT_SHADOWED from domain.result import Result +from domain.tables.table import Table class ResultsCache: @@ -10,7 +11,8 @@ class ResultsCache: """ def __init__(self): - self.cache: dict[str, Result] = {} + self.results: dict[str, Result] = {} + self.tables: dict[str, Table] = {} self.issue_result_overwrite_warning = True def configure(self, issue_result_overwrite_warning: bool): @@ -18,10 +20,23 @@ def configure(self, issue_result_overwrite_warning: bool): def add(self, name, result: Result): - if self.issue_result_overwrite_warning and name in self.cache: + if self.issue_result_overwrite_warning and name in self.results: print(RESULT_SHADOWED.format(name=name)) - self.cache[name] = result + self.results[name] = result + + def add_table(self, name, table: Table): + + if self.issue_result_overwrite_warning and name in self.tables: + print(RESULT_SHADOWED.format(name=name)) + + self.tables[name] = table def get_all_results(self) -> list[Result]: - return list(self.cache.values()) + return list(self.results.values()) + + def get_all_tables(self) -> list[Table]: + return list(self.tables.values()) + + +_res_cache = ResultsCache() diff --git a/src/application/parsers.py b/src/application/parsers.py deleted file mode 100644 index be221b66..00000000 --- a/src/application/parsers.py +++ /dev/null @@ -1,143 +0,0 @@ -from typing import Union, List, Tuple - -from application.helpers import _Helpers -from domain.value import _Value -from domain.uncertainty import _Uncertainty - -def check_if_number_string(value: str) -> None: - """Raises a ValueError if the string is not a valid number.""" - try: - float(value) - except ValueError as exc: - raise ValueError(f"String value must be a valid number, not {value}") from exc - - - -def parse_name(name: str) -> str: - """Parses the name.""" - if not isinstance(name, str): - raise TypeError(f"`name` must be a string, not {type(name)}") - - name = ( - name.replace("ä", "ae") - .replace("ö", "oe") - .replace("ü", "ue") - .replace("Ä", "Ae") - .replace("Ö", "Oe") - .replace("Ü", "Ue") - .replace("ß", "ss") - ) - - parsed_name = "" - next_chat_upper = False - - for char in name: - if char.isalpha(): - if next_chat_upper: - parsed_name += char.upper() - next_chat_upper = False - else: - parsed_name += char - elif char.isdigit(): - digit = _Helpers.number_to_word(int(char)) - if parsed_name == "": - parsed_name += digit - else: - parsed_name += digit[0].upper() + digit[1:] - next_chat_upper = True - elif char in [" ", "_", "-"]: - next_chat_upper = True - - return parsed_name - - -def parse_unit(unit: str) -> str: - """Parses the unit.""" - if not isinstance(unit, str): - raise TypeError(f"`unit` must be a string, not {type(unit)}") - - # TODO: maybe add some basic checks to catch siunitx errors, e.g. - # unsupported symbols etc. But maybe leave this to LaTeX and just return - # the LaTeX later on. But catching it here would be more user-friendly, - # as the user would get immediate feedback and not only once they try to - # export the results. - return unit - - -def parse_sigfigs(sigfigs: Union[int, None]) -> Union[int, None]: - """Parses the number of sigfigs.""" - if sigfigs is None: - return None - - if not isinstance(sigfigs, int): - raise TypeError(f"`sigfigs` must be an int, not {type(sigfigs)}") - - if sigfigs < 1: - raise ValueError("`sigfigs` must be positive") - - return sigfigs - - -def parse_decimal_places(decimal_places: Union[int, None]) -> Union[int, None]: - """Parses the number of sigfigs.""" - if decimal_places is None: - return None - - if not isinstance(decimal_places, int): - raise TypeError(f"`decimal_places` must be an int, not {type(decimal_places)}") - - if decimal_places < 0: - raise ValueError("`decimal_places` must be non-negative") - - return decimal_places - - -def parse_value(value: Union[float, str]) -> _Value: - """Converts the value to a _Value object.""" - if not isinstance(value, (float, str)): - raise TypeError(f"`value` must be a float or string, not {type(value)}") - - if isinstance(value, str): - check_if_number_string(value) - - return _Value(value) - - -def parse_uncertainties( - uncertainties: Union[ - float, - str, - Tuple[Union[float, str], str], - List[Union[float, str, Tuple[Union[float, str], str]]], - ] -) -> List[_Uncertainty]: - """Converts the uncertainties to a list of _Uncertainty objects.""" - uncertainties_res = [] - - # no list, but a single value was given - if isinstance(uncertainties, (float, str, Tuple)): - uncertainties = [uncertainties] - - assert isinstance(uncertainties, List) - - for uncert in uncertainties: - if isinstance(uncert, (float, str)): - if isinstance(uncert, str): - check_if_number_string(uncert) - if float(uncert) <= 0: - raise ValueError("Uncertainty must be positive.") - uncertainties_res.append(_Uncertainty(uncert)) - - elif isinstance(uncert, Tuple): - if not isinstance(uncert[0], (float, str)): - raise TypeError( - f"First argument of uncertainty-tuple must be a float or a string, not {type(uncert[0])}" - ) - if isinstance(uncert[0], str): - check_if_number_string(uncert[0]) - uncertainties_res.append(_Uncertainty(uncert[0], parse_name(uncert[1]))) - - else: - raise TypeError(f"Each uncertainty must be a tuple or a float/str, not {type(uncert)}") - - return uncertainties_res diff --git a/src/domain/tables/column.py b/src/domain/tables/column.py index 26dbaaca..7a8f9d12 100644 --- a/src/domain/tables/column.py +++ b/src/domain/tables/column.py @@ -1,24 +1,24 @@ from dataclasses import dataclass from typing import Union, List -from domain.result import _Result +from domain.result import Result @dataclass -class _Column: +class Column: """ A table column. """ title: str - cells: List[Union[_Result, str]] + cells: List[Union[Result, str]] unit: str concentrate_units_if_possible: Union[bool, None] def __init__( self, title: str, - cells: List[Union[_Result, str]], + cells: List[Union[Result, str]], concentrate_units_if_possible: Union[bool, None] = None, ): """ @@ -46,7 +46,7 @@ def concentrate_units(self, concentrate_units_if_possible_master: bool): # Check if concentration of units is possible given the cell values: unit = None for cell in self.cells: - if isinstance(cell, _Result): + if isinstance(cell, Result): if unit is None: unit = cell.unit elif unit != cell.unit: @@ -56,6 +56,5 @@ def concentrate_units(self, concentrate_units_if_possible_master: bool): should_concentrate_units = False break - if should_concentrate_units and unit is not None: self.unit = unit diff --git a/src/domain/tables/table.py b/src/domain/tables/table.py index 208947a5..9f7d09b2 100644 --- a/src/domain/tables/table.py +++ b/src/domain/tables/table.py @@ -1,17 +1,17 @@ from dataclasses import dataclass from typing import List, Union -from domain.tables.column import _Column +from domain.tables.column import Column @dataclass -class _Table: +class Table: """ A table. """ name: str - columns: List[_Column] + columns: List[Column] caption: str label: Union[str, None] resize_to_fit_page: bool diff --git a/src/resultwizard/__init__.py b/src/resultwizard/__init__.py index f00a02ca..386f44b3 100644 --- a/src/resultwizard/__init__.py +++ b/src/resultwizard/__init__.py @@ -1,3 +1,7 @@ from api.config import config_init, config from api.res import res from api.export import export + +from api.tables.table import table +from api.tables.table_res import table_res +from api.tables.column import column diff --git a/tests/playground.py b/tests/playground.py index d5008782..ef533e19 100644 --- a/tests/playground.py +++ b/tests/playground.py @@ -6,6 +6,7 @@ # (e.g. the site-packages directory)." # From: https://setuptools.pypa.io/en/latest/userguide/quickstart.html#development-mode +from random import random from decimal import Decimal import resultwizard as wiz From 2fb35afa8741f6f99719cafee1fe6e5e227dad39 Mon Sep 17 00:00:00 2001 From: paul019 <39464035+paul019@users.noreply.github.com> Date: Mon, 9 Sep 2024 12:12:02 +0200 Subject: [PATCH 24/25] Add table_identifier to config --- src/api/config.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/api/config.py b/src/api/config.py index 2df28270..92cf8e4f 100644 --- a/src/api/config.py +++ b/src/api/config.py @@ -53,6 +53,7 @@ class Config: min_exponent_for_non_scientific_notation: int max_exponent_for_non_scientific_notation: int identifier: str + table_identifier: str sigfigs_fallback: int decimal_places_fallback: int siunitx_fallback: bool @@ -103,6 +104,7 @@ def config_init( min_exponent_for_non_scientific_notation: int = -2, max_exponent_for_non_scientific_notation: int = 3, identifier: str = "result", + table_identifier: str = "table", sigfigs_fallback: int = 2, decimal_places_fallback: int = -1, # -1: "per default use sigfigs as fallback instead" siunitx_fallback: bool = False, @@ -122,6 +124,7 @@ def config_init( min_exponent_for_non_scientific_notation, max_exponent_for_non_scientific_notation, identifier, + table_identifier, sigfigs_fallback, decimal_places_fallback, siunitx_fallback, From f9b4c88e0a2b771b7edaaa630542c5ff7c83fddd Mon Sep 17 00:00:00 2001 From: paul019 <39464035+paul019@users.noreply.github.com> Date: Mon, 9 Sep 2024 12:28:09 +0200 Subject: [PATCH 25/25] Rework the TableLatexCommandifier --- src/api/config.py | 1 + src/api/export.py | 14 +++++- src/api/latexer.py | 5 ++ src/application/stringifier.py | 1 + ...latexer.py => table_latex_commandifier.py} | 48 +++++++++---------- 5 files changed, 42 insertions(+), 27 deletions(-) rename src/application/tables/{latexer.py => table_latex_commandifier.py} (75%) diff --git a/src/api/config.py b/src/api/config.py index 92cf8e4f..78648bc0 100644 --- a/src/api/config.py +++ b/src/api/config.py @@ -65,6 +65,7 @@ def to_stringifier_config(self) -> StringifierConfig: self.min_exponent_for_non_scientific_notation, self.max_exponent_for_non_scientific_notation, self.identifier, + self.table_identifier, ) def to_rounding_config(self) -> RoundingConfig: diff --git a/src/api/export.py b/src/api/export.py index d21f9cd5..8e673ec3 100644 --- a/src/api/export.py +++ b/src/api/export.py @@ -1,5 +1,5 @@ from typing import Set -from api.latexer import get_latexer +from api.latexer import get_latexer, get_table_latexer from api.res import _res_cache import api.config as c from application.helpers import Helpers @@ -15,6 +15,7 @@ def export(filepath: str): def _export(filepath: str, print_completed: bool): results = _res_cache.get_all_results() + tables = _res_cache.get_all_tables() if print_completed: print(f"Processing {len(results)} result(s)") @@ -33,6 +34,7 @@ def _export(filepath: str, print_completed: bool): ] latexer = get_latexer() + table_latexer = get_table_latexer() uncertainty_names = set() result_lines = [] @@ -41,6 +43,11 @@ def _export(filepath: str, print_completed: bool): result_str = latexer.result_to_latex_cmd(result) result_lines.append(result_str) + table_lines = [] + for table in tables: + table_str = table_latexer.table_to_latex_cmd(table) + table_lines.append(table_str) + if not c.configuration.siunitx_fallback: siunitx_setup = _uncertainty_names_to_siunitx_setup(uncertainty_names) if siunitx_setup != "": @@ -51,6 +58,11 @@ def _export(filepath: str, print_completed: bool): lines.append("% Commands to print the results. Use them in your document.") lines.extend(result_lines) + lines.append("") + + lines.append("% Commands to print the tables. Use them in your document.") + lines.extend(table_lines) + # Write to file with open(filepath, "w", encoding="utf-8") as f: f.write("\n".join(lines)) diff --git a/src/api/latexer.py b/src/api/latexer.py index 8bf3cadc..3ad57870 100644 --- a/src/api/latexer.py +++ b/src/api/latexer.py @@ -3,12 +3,17 @@ from application.latex_commandifier import LatexCommandifier from application.latex_stringifier import LatexStringifier from application.stringifier import Stringifier +from application.tables.table_latex_commandifier import TableLatexCommandifier def get_latexer() -> LatexCommandifier: return LatexCommandifier(_choose_latex_stringifier()) +def get_table_latexer() -> TableLatexCommandifier: + return TableLatexCommandifier(_choose_latex_stringifier()) + + def _choose_latex_stringifier() -> Stringifier: use_fallback = c.configuration.siunitx_fallback stringifier_config = c.configuration.to_stringifier_config() diff --git a/src/application/stringifier.py b/src/application/stringifier.py index 35d70406..db6da6dc 100644 --- a/src/application/stringifier.py +++ b/src/application/stringifier.py @@ -16,6 +16,7 @@ class StringifierConfig: min_exponent_for_non_scientific_notation: int max_exponent_for_non_scientific_notation: int identifier: str + table_identifier: str class Stringifier(Protocol): diff --git a/src/application/tables/latexer.py b/src/application/tables/table_latex_commandifier.py similarity index 75% rename from src/application/tables/latexer.py rename to src/application/tables/table_latex_commandifier.py index 2b65fa6f..17a029c8 100644 --- a/src/application/tables/latexer.py +++ b/src/application/tables/table_latex_commandifier.py @@ -1,22 +1,21 @@ -from domain.tables.table import _Table -from domain.result import _Result -from application.latexer import _LaTeXer +from application.stringifier import Stringifier +from application.helpers import Helpers +from domain.result import Result +from domain.tables.table import Table -# Config values: -min_exponent_for_non_scientific_notation = -2 -max_exponent_for_non_scientific_notation = 3 -table_identifier = "table" +class TableLatexCommandifier: + """Makes use of a LaTeX stringifier to embed a table into a LaTeX command.""" + def __init__(self, stringifier: Stringifier): + self.s = stringifier -class _TableLaTeXer: - @classmethod - def table_to_latex_cmd(cls, table: _Table) -> str: + def table_to_latex_cmd(self, table: Table) -> str: """ Returns the table as LaTeX command to be used in a .tex file. """ - cmd_name = table_identifier + table.name[0].upper() + table.name[1:] + cmd_name = f"{self.s.config.table_identifier}{Helpers.capitalize(table.name)}" # New command: latex_str = rf"\newcommand*{{\{cmd_name}}}[1][]{{" + "\n" @@ -28,9 +27,9 @@ def table_to_latex_cmd(cls, table: _Table) -> str: latex_str += r"\resizebox{\textwidth}{!}{" if table.horizontal: - latex_str += cls._table_to_latex_tabular_horizontal(table) + latex_str += self._table_to_latex_tabular_horizontal(table) else: - latex_str += cls._table_to_latex_tabular_vertical(table) + latex_str += self._table_to_latex_tabular_vertical(table) # Table footer: if table.resize_to_fit_page: @@ -44,8 +43,7 @@ def table_to_latex_cmd(cls, table: _Table) -> str: return latex_str - @classmethod - def _table_to_latex_tabular_vertical(cls, table: _Table) -> str: + def _table_to_latex_tabular_vertical(self, table: Table) -> str: latex_str = r"\begin{tabular}{|" for _ in range(len(table.columns)): latex_str += r"c|" @@ -62,7 +60,7 @@ def _table_to_latex_tabular_vertical(cls, table: _Table) -> str: latex_str += rf"\textbf{{{column.title}}}" # Unit row: - if cls._exist_units(table): + if self._exist_units(table): latex_str += "\\\\\n" is_first_column = True for column in table.columns: @@ -84,8 +82,8 @@ def _table_to_latex_tabular_vertical(cls, table: _Table) -> str: cell = column.cells[i] - if isinstance(cell, _Result): - value_str = _LaTeXer.create_latex_str( + if isinstance(cell, Result): + value_str = self.s.create_str( cell.value, cell.uncertainties, cell.unit if column.unit == "" else "" ) latex_str += f"${value_str}$" @@ -99,10 +97,9 @@ def _table_to_latex_tabular_vertical(cls, table: _Table) -> str: return latex_str - @classmethod - def _table_to_latex_tabular_horizontal(cls, table: _Table) -> str: + def _table_to_latex_tabular_horizontal(self, table: Table) -> str: latex_str = r"\begin{tabular}{|l" - if cls._exist_units(table): + if self._exist_units(table): latex_str += r"c||" else: latex_str += r"||" @@ -117,7 +114,7 @@ def _table_to_latex_tabular_horizontal(cls, table: _Table) -> str: latex_str += rf"\textbf{{{row.title}}}" # Unit column: - if cls._exist_units(table): + if self._exist_units(table): if row.unit != "": latex_str += rf" & $[\unit{{{row.unit}}}]$" else: @@ -125,8 +122,8 @@ def _table_to_latex_tabular_horizontal(cls, table: _Table) -> str: # Value columns: for cell in row.cells: - if isinstance(cell, _Result): - value_str = _LaTeXer.create_latex_str( + if isinstance(cell, Result): + value_str = self.s.create_str( cell.value, cell.uncertainties, cell.unit if row.unit == "" else "" ) latex_str += f" & ${value_str}$" @@ -138,8 +135,7 @@ def _table_to_latex_tabular_horizontal(cls, table: _Table) -> str: return latex_str - @classmethod - def _exist_units(cls, table: _Table) -> bool: + def _exist_units(self, table: Table) -> bool: for column in table.columns: if column.unit != "": return True