diff --git a/.vscode/settings.json b/.vscode/settings.json index 5fb1b1a6..97e171c4 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -62,6 +62,7 @@ ////////////////////////////////////// "cSpell.words": [ "Commandifier", + "getcontext", "github", "ifthen", "ifthenelse", @@ -70,12 +71,14 @@ "newcommand", "normalsize", "pipenv", + "prec", "pydantic", "pylint", "pytest", "resultwizard", "scriptsize", "scriptstyle", + "setcontext", "sigfigs", "siunitx", "Stringifier", diff --git a/src/api/config.py b/src/api/config.py index df105b40..560b49f1 100644 --- a/src/api/config.py +++ b/src/api/config.py @@ -1,3 +1,4 @@ +import decimal from typing import Union, cast from dataclasses import dataclass @@ -33,6 +34,9 @@ class Config: 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". """ sigfigs: int @@ -45,6 +49,7 @@ class Config: sigfigs_fallback: int decimal_places_fallback: int siunitx_fallback: bool + precision: int def to_stringifier_config(self) -> StringifierConfig: return StringifierConfig( @@ -100,9 +105,13 @@ def config_init( sigfigs_fallback: int = 2, decimal_places_fallback: int = -1, # -1: "per default use sigfigs as fallback instead" siunitx_fallback: bool = False, + precision: int = 100, ) -> None: global configuration # pylint: disable=global-statement + decimal.DefaultContext.prec = precision + decimal.setcontext(decimal.DefaultContext) + configuration = Config( sigfigs, decimal_places, @@ -114,6 +123,7 @@ def config_init( sigfigs_fallback, decimal_places_fallback, siunitx_fallback, + precision, ) _check_config() diff --git a/src/api/parsers.py b/src/api/parsers.py index 2c96f268..d165dee7 100644 --- a/src/api/parsers.py +++ b/src/api/parsers.py @@ -125,18 +125,15 @@ def parse_decimal_places(decimal_places: Union[int, None]) -> Union[int, None]: return decimal_places -def parse_value(value: Union[float, int, str]) -> Value: +def parse_value(value: Union[float, int, str, Decimal]) -> Value: """Converts the value to a _Value object.""" - if not isinstance(value, (float, int, str)): - raise TypeError(f"`value` must be a float, int or string, not {type(value)}") + if not isinstance(value, (float, int, str, Decimal)): + raise TypeError(f"`value` must be a float, int, Decimal or string, not {type(value)}") if isinstance(value, str): check_if_number_string(value) return parse_exact_value(value) - if isinstance(value, int): - value = float(value) - return Value(Decimal(value)) @@ -161,28 +158,27 @@ def parse_uncertainties( float, int, str, - Tuple[Union[float, int, str], str], - List[Union[float, str, Tuple[Union[float, int, 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]: """Converts the uncertainties to a list of _Uncertainty objects.""" uncertainties_res = [] # no list, but a single value was given - if isinstance(uncertainties, (float, int, str, Tuple)): + if isinstance(uncertainties, (float, int, str, Decimal, Tuple)): uncertainties = [uncertainties] - assert isinstance(uncertainties, List) - for uncert in uncertainties: - if isinstance(uncert, (float, int, str)): + 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, int, str)): + if not isinstance(uncert[0], (float, int, str, Decimal)): raise TypeError( "First argument of uncertainty-tuple must be a float," - + f" int or a string, not {type(uncert[0])}" + + f" int, Decimal or a string, not {type(uncert[0])}" ) uncertainties_res.append( Uncertainty(_parse_uncertainty_value(uncert[0]), parse_name(uncert[1])) @@ -196,7 +192,7 @@ def parse_uncertainties( return uncertainties_res -def _parse_uncertainty_value(value: Union[float, int, str]) -> Value: +def _parse_uncertainty_value(value: Union[float, int, str, Decimal]) -> Value: """Parses the value of an uncertainty.""" if isinstance(value, str): diff --git a/src/api/res.py b/src/api/res.py index d00f43d8..d3d1ddea 100644 --- a/src/api/res.py +++ b/src/api/res.py @@ -1,3 +1,4 @@ +from decimal import Decimal from typing import Union, List, Tuple from plum import dispatch, overload @@ -17,7 +18,7 @@ @overload def res( name: str, - value: Union[float, int, str], + value: Union[float, int, str, Decimal], unit: str = "", sigfigs: Union[int, None] = None, decimal_places: Union[int, None] = None, @@ -28,12 +29,13 @@ def res( @overload def res( name: str, - value: Union[float, int, str], + value: Union[float, int, str, Decimal], uncert: Union[ float, str, - Tuple[Union[float, int, str], str], - List[Union[float, int, str, Tuple[Union[float, int, 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, @@ -45,7 +47,7 @@ def res( @overload def res( name: str, - value: Union[float, int, str], + value: Union[float, int, str, Decimal], sigfigs: Union[int, None] = None, decimal_places: Union[int, None] = None, ) -> PrintableResult: @@ -56,7 +58,7 @@ def res( # pylint: disable=too-many-arguments def res( name: str, - value: Union[float, int, str], + value: Union[float, int, str, Decimal], sys: float, stat: float, unit: str = "", @@ -70,12 +72,13 @@ def res( # pylint: disable=too-many-arguments def res( name: str, - value: Union[float, int, str], + value: Union[float, int, str, Decimal], uncert: Union[ float, str, - Tuple[Union[float, int, str], str], - List[Union[float, int, str, Tuple[Union[float, int, 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 = "", diff --git a/src/application/helpers.py b/src/application/helpers.py index bf251a86..0f2b6a78 100644 --- a/src/application/helpers.py +++ b/src/application/helpers.py @@ -1,4 +1,5 @@ import math +import decimal from decimal import Decimal _NUMBER_TO_WORD = { @@ -45,9 +46,13 @@ def get_first_digit(cls, value: Decimal) -> int: @classmethod def round_to_n_decimal_places(cls, value: Decimal, n: int) -> str: - decimal_compare = f"1.{'0' * abs(n)}" - decimal = value.quantize(Decimal(decimal_compare)) - return str(decimal) + try: + decimal_value = value.quantize(Decimal(f"1.{'0' * abs(n)}")) + return f"{decimal_value:.{abs(n)}f}" + except decimal.InvalidOperation as exc: + raise ValueError( + "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=)`." + ) from exc @classmethod def number_to_word(cls, number: int) -> str: diff --git a/tests/playground.py b/tests/playground.py index 794e70d0..46525723 100644 --- a/tests/playground.py +++ b/tests/playground.py @@ -6,7 +6,7 @@ # (e.g. the site-packages directory)." # From: https://setuptools.pypa.io/en/latest/userguide/quickstart.html#development-mode - +from decimal import Decimal import resultwizard as wiz print("#############################") @@ -17,7 +17,6 @@ wiz.config_init( print_auto=True, export_auto_to="results-immediate.tex", - decimal_places=3, siunitx_fallback=False, ) # wiz.config(sigfigs=2) @@ -32,33 +31,21 @@ # wiz.res("", 42.0).print() # -> Error: "name must not be empty" -wiz.res("a911", 1.0, r"\mm\s\per\N\kg") -# a: 1.0 \mm - -wiz.res("1 b", 1.0, 0.01, r"\per\mm\cubed").print() -# b: (1.0 ± 0.01) \mm - -wiz.config(decimal_places=-1, sigfigs_fallback=3) - -wiz.res("c big", 1.0, (0.01, "systematic"), r"\mm").print() -# c: (1.0 ± 0.01 systematic) \mm +wiz.res("a911", 1.05, r"\mm\s\per\N\kg") +# wiz.res("a911", "1.052", 0.25, r"\mm\s\per\N\kg") -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("1 b", 1.0, 0.01, r"\per\mm\cubed") -wiz.res("e", "1.0", r"\mm").print() -# e: 1.0 \mm - -wiz.res("f", "1.0e1", 25e-1).print() -# f: 1.0 - -wiz.res("g", 42).print() +# wiz.config(decimal_places=-1, sigfigs_fallback=3) +wiz.res("c big", 1.0, (0.01, "systematic"), r"\mm") +wiz.res("d", 1.0e10, [(0.01e10, "systematic"), (0.0294999e10, "stat")], r"\mm\per\second\squared") +wiz.res("e", "1.0", r"\mm") +wiz.res("f", "1.0e1", 25e-1) +wiz.res("g", 42) wiz.res("h", 42, 13.0, 24.0) wiz.res("h&", 42, 13.0, 24.0) - +wiz.res("i", Decimal("42.0e-30"), Decimal("0.1e-31"), r"\m") # 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