sciform 0.32.3__py3-none-any.whl → 0.34.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sciform/__init__.py +16 -8
- sciform/format_utils.py +26 -94
- sciform/formatter.py +154 -56
- sciform/formatting.py +99 -30
- sciform/fsml.py +7 -7
- sciform/global_configuration.py +39 -36
- sciform/modes.py +3 -10
- sciform/options/__init__.py +1 -0
- sciform/options/conversion.py +120 -0
- sciform/{rendered_options.py → options/finalized_options.py} +8 -13
- sciform/{global_options.py → options/global_options.py} +10 -11
- sciform/options/input_options.py +104 -0
- sciform/options/populated_options.py +136 -0
- sciform/options/validation.py +101 -0
- sciform/output_conversion.py +168 -0
- sciform/scinum.py +13 -12
- {sciform-0.32.3.dist-info → sciform-0.34.0.dist-info}/METADATA +20 -12
- sciform-0.34.0.dist-info/RECORD +23 -0
- sciform/user_options.py +0 -189
- sciform-0.32.3.dist-info/RECORD +0 -18
- {sciform-0.32.3.dist-info → sciform-0.34.0.dist-info}/LICENSE +0 -0
- {sciform-0.32.3.dist-info → sciform-0.34.0.dist-info}/WHEEL +0 -0
- {sciform-0.32.3.dist-info → sciform-0.34.0.dist-info}/top_level.txt +0 -0
@@ -1,23 +1,23 @@
|
|
1
1
|
"""
|
2
2
|
Rendered format options used in sciform backend formatting algorithm.
|
3
3
|
|
4
|
-
:class:`
|
4
|
+
:class:`InputOptions` are converted into :class:`FinalizedOptions`
|
5
5
|
internally at format time.
|
6
6
|
"""
|
7
7
|
|
8
8
|
from __future__ import annotations
|
9
9
|
|
10
|
-
from dataclasses import
|
11
|
-
from enum import Enum
|
12
|
-
from pprint import pformat
|
10
|
+
from dataclasses import dataclass
|
13
11
|
from typing import TYPE_CHECKING
|
14
12
|
|
13
|
+
from sciform.options.validation import validate_options
|
14
|
+
|
15
15
|
if TYPE_CHECKING: # pragma: no cover
|
16
16
|
from sciform import modes
|
17
17
|
|
18
18
|
|
19
19
|
@dataclass(frozen=True)
|
20
|
-
class
|
20
|
+
class FinalizedOptions:
|
21
21
|
"""Rendered options: All options populated and using Enum instead of Literal."""
|
22
22
|
|
23
23
|
exp_mode: modes.ExpModeEnum
|
@@ -28,7 +28,7 @@ class RenderedOptions:
|
|
28
28
|
decimal_separator: modes.DecimalSeparatorEnums
|
29
29
|
lower_separator: modes.LowerSeparatorEnums
|
30
30
|
sign_mode: modes.SignModeEnum
|
31
|
-
|
31
|
+
left_pad_char: modes.LeftPadCharEnum
|
32
32
|
left_pad_dec_place: int
|
33
33
|
exp_format: modes.ExpFormatEnum
|
34
34
|
extra_si_prefixes: dict[int, str]
|
@@ -36,7 +36,6 @@ class RenderedOptions:
|
|
36
36
|
extra_parts_per_forms: dict[int, str]
|
37
37
|
capitalize: bool
|
38
38
|
superscript: bool
|
39
|
-
latex: bool
|
40
39
|
nan_inf_exp: bool
|
41
40
|
paren_uncertainty: bool
|
42
41
|
pdg_sig_figs: bool
|
@@ -44,9 +43,5 @@ class RenderedOptions:
|
|
44
43
|
paren_uncertainty_separators: bool
|
45
44
|
pm_whitespace: bool
|
46
45
|
|
47
|
-
def
|
48
|
-
|
49
|
-
for key, value in options_dict.items():
|
50
|
-
if isinstance(value, Enum):
|
51
|
-
options_dict[key] = value.value
|
52
|
-
return pformat(options_dict, sort_dicts=False)
|
46
|
+
def __post_init__(self: FinalizedOptions) -> None:
|
47
|
+
validate_options(self)
|
@@ -1,26 +1,25 @@
|
|
1
1
|
"""Global Options."""
|
2
2
|
|
3
3
|
from sciform import modes
|
4
|
-
from sciform.
|
4
|
+
from sciform.options.populated_options import PopulatedOptions
|
5
5
|
|
6
|
-
PKG_DEFAULT_OPTIONS =
|
7
|
-
exp_mode=
|
6
|
+
PKG_DEFAULT_OPTIONS = PopulatedOptions(
|
7
|
+
exp_mode="fixed_point",
|
8
8
|
exp_val=modes.AutoExpVal,
|
9
|
-
round_mode=
|
9
|
+
round_mode="sig_fig",
|
10
10
|
ndigits=modes.AutoDigits,
|
11
|
-
upper_separator=
|
12
|
-
decimal_separator=
|
13
|
-
lower_separator=
|
14
|
-
sign_mode=
|
15
|
-
|
11
|
+
upper_separator="",
|
12
|
+
decimal_separator=".",
|
13
|
+
lower_separator="",
|
14
|
+
sign_mode="-",
|
15
|
+
left_pad_char=" ",
|
16
16
|
left_pad_dec_place=0,
|
17
|
-
exp_format=
|
17
|
+
exp_format="standard",
|
18
18
|
extra_si_prefixes={},
|
19
19
|
extra_iec_prefixes={},
|
20
20
|
extra_parts_per_forms={},
|
21
21
|
capitalize=False,
|
22
22
|
superscript=False,
|
23
|
-
latex=False,
|
24
23
|
nan_inf_exp=False,
|
25
24
|
paren_uncertainty=False,
|
26
25
|
pdg_sig_figs=False,
|
@@ -0,0 +1,104 @@
|
|
1
|
+
"""InputOptions Dataclass which stores user input."""
|
2
|
+
|
3
|
+
|
4
|
+
from __future__ import annotations
|
5
|
+
|
6
|
+
from dataclasses import asdict, dataclass
|
7
|
+
from pprint import pformat
|
8
|
+
from typing import TYPE_CHECKING, Any, Literal
|
9
|
+
|
10
|
+
from sciform.options.validation import validate_options
|
11
|
+
|
12
|
+
if TYPE_CHECKING: # pragma: no cover
|
13
|
+
from sciform import modes
|
14
|
+
|
15
|
+
|
16
|
+
@dataclass(frozen=True)
|
17
|
+
class InputOptions:
|
18
|
+
"""
|
19
|
+
Dataclass storing user input.
|
20
|
+
|
21
|
+
Stores the user input to :class:`Formatter`, so any keyword
|
22
|
+
arguments that can be passed into :class:`Formatter` are stored in
|
23
|
+
:class:`InputOptions`. Any unpopulated options retain the ``None``
|
24
|
+
value. At format time the :class:`InputOptions` are populated and
|
25
|
+
replaced by a :class:`PopulatedOptions` instance which necessarily
|
26
|
+
has all attributes populated with meaningful values.
|
27
|
+
|
28
|
+
:class:`InputOptions` instances should only be accessed via the
|
29
|
+
:class:`Formatter.input_options()` property. They should not be
|
30
|
+
instantiated directly.
|
31
|
+
|
32
|
+
>>> from sciform import Formatter
|
33
|
+
>>> formatter = Formatter(
|
34
|
+
... exp_mode="engineering",
|
35
|
+
... round_mode="sig_fig",
|
36
|
+
... ndigits=2,
|
37
|
+
... superscript=True,
|
38
|
+
... )
|
39
|
+
>>> print(formatter.input_options.round_mode)
|
40
|
+
sig_fig
|
41
|
+
>>> print(formatter.input_options.exp_format)
|
42
|
+
None
|
43
|
+
>>> print(formatter.input_options)
|
44
|
+
InputOptions(
|
45
|
+
'exp_mode': 'engineering',
|
46
|
+
'round_mode': 'sig_fig',
|
47
|
+
'ndigits': 2,
|
48
|
+
'superscript': True,
|
49
|
+
)
|
50
|
+
>>> print(formatter.input_options.as_dict())
|
51
|
+
{'exp_mode': 'engineering', 'round_mode': 'sig_fig', 'ndigits': 2, 'superscript': True}
|
52
|
+
""" # noqa: E501
|
53
|
+
|
54
|
+
exp_mode: modes.ExpMode | None = None
|
55
|
+
exp_val: int | type(modes.AutoExpVal) | None = None
|
56
|
+
round_mode: modes.RoundMode | None = None
|
57
|
+
ndigits: int | type(modes.AutoDigits) | None = None
|
58
|
+
upper_separator: modes.UpperSeparators | None = None
|
59
|
+
decimal_separator: modes.DecimalSeparators | None = None
|
60
|
+
lower_separator: modes.LowerSeparators | None = None
|
61
|
+
sign_mode: modes.SignMode | None = None
|
62
|
+
left_pad_char: modes.LeftPadChar | Literal[0] | None = None
|
63
|
+
left_pad_dec_place: int | None = None
|
64
|
+
exp_format: modes.ExpFormat | None = None
|
65
|
+
extra_si_prefixes: dict[int, str] | None = None
|
66
|
+
extra_iec_prefixes: dict[int, str] | None = None
|
67
|
+
extra_parts_per_forms: dict[int, str] | None = None
|
68
|
+
capitalize: bool | None = None
|
69
|
+
superscript: bool | None = None
|
70
|
+
nan_inf_exp: bool | None = None
|
71
|
+
paren_uncertainty: bool | None = None
|
72
|
+
pdg_sig_figs: bool | None = None
|
73
|
+
left_pad_matching: bool | None = None
|
74
|
+
paren_uncertainty_separators: bool | None = None
|
75
|
+
pm_whitespace: bool | None = None
|
76
|
+
|
77
|
+
add_c_prefix: bool = None
|
78
|
+
add_small_si_prefixes: bool = None
|
79
|
+
add_ppth_form: bool = None
|
80
|
+
|
81
|
+
def __post_init__(self: InputOptions) -> None:
|
82
|
+
validate_options(self)
|
83
|
+
|
84
|
+
def as_dict(self: InputOptions) -> dict[str, Any]:
|
85
|
+
"""
|
86
|
+
Return a dict representation of the InputOptions.
|
87
|
+
|
88
|
+
This dict can be passed into :class:`Formatter` as ``**kwargs``,
|
89
|
+
possibly after modification. This allows for the possibility of
|
90
|
+
constructing new :class:`Formatter` instances based on old ones.
|
91
|
+
Only explicitly populated attributes are included in the
|
92
|
+
returned dictionary.
|
93
|
+
"""
|
94
|
+
options_dict = asdict(self)
|
95
|
+
for key in list(options_dict.keys()):
|
96
|
+
if options_dict[key] is None:
|
97
|
+
del options_dict[key]
|
98
|
+
return options_dict
|
99
|
+
|
100
|
+
def __str__(self: InputOptions) -> str:
|
101
|
+
options_str = pformat(self.as_dict(), width=-1, sort_dicts=False)
|
102
|
+
options_str = options_str.lstrip("{").rstrip("}")
|
103
|
+
options_str = f"InputOptions(\n {options_str},\n)"
|
104
|
+
return options_str
|
@@ -0,0 +1,136 @@
|
|
1
|
+
"""InputOptions Dataclass which stores user input."""
|
2
|
+
|
3
|
+
|
4
|
+
from __future__ import annotations
|
5
|
+
|
6
|
+
from dataclasses import asdict, dataclass
|
7
|
+
from pprint import pformat
|
8
|
+
from typing import TYPE_CHECKING, Any
|
9
|
+
|
10
|
+
from sciform.options.validation import validate_options
|
11
|
+
|
12
|
+
if TYPE_CHECKING: # pragma: no cover
|
13
|
+
from sciform import modes
|
14
|
+
|
15
|
+
|
16
|
+
@dataclass(frozen=True)
|
17
|
+
class PopulatedOptions:
|
18
|
+
"""
|
19
|
+
Dataclass storing fully populated formatting options.
|
20
|
+
|
21
|
+
User input options during :class:`Formatter` initialization are
|
22
|
+
stored in :class:`InputOptions` instances. But
|
23
|
+
:class:`InputOptions` instances don't necessarily have all options
|
24
|
+
populated as required for the formatting algorithm. At formatting
|
25
|
+
time the unpopulated options are populated from the global options.
|
26
|
+
The new resulting options object with all options populated is a
|
27
|
+
:class:`PopulatedOptions` instances. Note that the global options
|
28
|
+
are stored as a :class:`PopulatedOptions` instance.
|
29
|
+
|
30
|
+
:class:`PopulatedOptions` instances should only be accessed via the
|
31
|
+
:class:`Formatter.populated_options()` property. They should not be
|
32
|
+
instantiated directly.
|
33
|
+
|
34
|
+
>>> from sciform import Formatter
|
35
|
+
>>> formatter = Formatter(
|
36
|
+
... exp_mode="engineering",
|
37
|
+
... round_mode="sig_fig",
|
38
|
+
... ndigits=2,
|
39
|
+
... superscript=True,
|
40
|
+
... )
|
41
|
+
>>> print(formatter.populated_options.round_mode)
|
42
|
+
sig_fig
|
43
|
+
>>> print(formatter.populated_options.exp_format)
|
44
|
+
standard
|
45
|
+
>>> print(formatter.populated_options)
|
46
|
+
PopulatedOptions(
|
47
|
+
'exp_mode': 'engineering',
|
48
|
+
'exp_val': AutoExpVal,
|
49
|
+
'round_mode': 'sig_fig',
|
50
|
+
'ndigits': 2,
|
51
|
+
'upper_separator': '',
|
52
|
+
'decimal_separator': '.',
|
53
|
+
'lower_separator': '',
|
54
|
+
'sign_mode': '-',
|
55
|
+
'left_pad_char': ' ',
|
56
|
+
'left_pad_dec_place': 0,
|
57
|
+
'exp_format': 'standard',
|
58
|
+
'extra_si_prefixes': {},
|
59
|
+
'extra_iec_prefixes': {},
|
60
|
+
'extra_parts_per_forms': {},
|
61
|
+
'capitalize': False,
|
62
|
+
'superscript': True,
|
63
|
+
'nan_inf_exp': False,
|
64
|
+
'paren_uncertainty': False,
|
65
|
+
'pdg_sig_figs': False,
|
66
|
+
'left_pad_matching': False,
|
67
|
+
'paren_uncertainty_separators': True,
|
68
|
+
'pm_whitespace': True,
|
69
|
+
)
|
70
|
+
>>> print(formatter.populated_options.as_dict())
|
71
|
+
{'exp_mode': 'engineering', 'exp_val': AutoExpVal, 'round_mode': 'sig_fig', 'ndigits': 2, 'upper_separator': '', 'decimal_separator': '.', 'lower_separator': '', 'sign_mode': '-', 'left_pad_char': ' ', 'left_pad_dec_place': 0, 'exp_format': 'standard', 'extra_si_prefixes': {}, 'extra_iec_prefixes': {}, 'extra_parts_per_forms': {}, 'capitalize': False, 'superscript': True, 'nan_inf_exp': False, 'paren_uncertainty': False, 'pdg_sig_figs': False, 'left_pad_matching': False, 'paren_uncertainty_separators': True, 'pm_whitespace': True}
|
72
|
+
|
73
|
+
Note that :class:`PopulatedOptions` lacks the ``add_c_prefix``,
|
74
|
+
``add_small_si_prefixes`` and ``add_ppth_form`` options present
|
75
|
+
in :class:`InputOptions`. These options are helper functions which
|
76
|
+
modify the corresponding exponent replacement dictionaries.
|
77
|
+
|
78
|
+
>>> formatter = Formatter(
|
79
|
+
... exp_mode="engineering",
|
80
|
+
... exp_format="prefix",
|
81
|
+
... add_c_prefix=True,
|
82
|
+
... )
|
83
|
+
>>> print(formatter.input_options)
|
84
|
+
InputOptions(
|
85
|
+
'exp_mode': 'engineering',
|
86
|
+
'exp_format': 'prefix',
|
87
|
+
'add_c_prefix': True,
|
88
|
+
)
|
89
|
+
>>> print(formatter.input_options.extra_si_prefixes)
|
90
|
+
None
|
91
|
+
>>> print(formatter.populated_options.extra_si_prefixes)
|
92
|
+
{-2: 'c'}
|
93
|
+
|
94
|
+
""" # noqa: E501
|
95
|
+
|
96
|
+
exp_mode: modes.ExpMode
|
97
|
+
exp_val: int | type(modes.AutoExpVal)
|
98
|
+
round_mode: modes.RoundMode
|
99
|
+
ndigits: int | type(modes.AutoDigits)
|
100
|
+
upper_separator: modes.UpperSeparators
|
101
|
+
decimal_separator: modes.DecimalSeparators
|
102
|
+
lower_separator: modes.LowerSeparators
|
103
|
+
sign_mode: modes.SignMode
|
104
|
+
left_pad_char: modes.LeftPadChar
|
105
|
+
left_pad_dec_place: int
|
106
|
+
exp_format: modes.ExpFormat
|
107
|
+
extra_si_prefixes: dict[int, str]
|
108
|
+
extra_iec_prefixes: dict[int, str]
|
109
|
+
extra_parts_per_forms: dict[int, str]
|
110
|
+
capitalize: bool
|
111
|
+
superscript: bool
|
112
|
+
nan_inf_exp: bool
|
113
|
+
paren_uncertainty: bool
|
114
|
+
pdg_sig_figs: bool
|
115
|
+
left_pad_matching: bool
|
116
|
+
paren_uncertainty_separators: bool
|
117
|
+
pm_whitespace: bool
|
118
|
+
|
119
|
+
def __post_init__(self: PopulatedOptions) -> None:
|
120
|
+
validate_options(self)
|
121
|
+
|
122
|
+
def as_dict(self: PopulatedOptions) -> dict[str, Any]:
|
123
|
+
"""
|
124
|
+
Return a dict representation of the PopulatedOptions.
|
125
|
+
|
126
|
+
This dict can be passed into :class:`Formatter` as ``**kwargs``,
|
127
|
+
possibly after modification. This allows for the possibility of
|
128
|
+
constructing new :class:`Formatter` instances based on old ones.
|
129
|
+
"""
|
130
|
+
return asdict(self)
|
131
|
+
|
132
|
+
def __str__(self: PopulatedOptions) -> str:
|
133
|
+
options_str = pformat(self.as_dict(), width=-1, sort_dicts=False)
|
134
|
+
options_str = options_str.lstrip("{").rstrip("}")
|
135
|
+
options_str = f"PopulatedOptions(\n {options_str},\n)"
|
136
|
+
return options_str
|
@@ -0,0 +1,101 @@
|
|
1
|
+
"""Options validation."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
from typing import TYPE_CHECKING, get_args
|
6
|
+
|
7
|
+
from sciform import modes
|
8
|
+
|
9
|
+
if TYPE_CHECKING: # pragma: no cover
|
10
|
+
from sciform.options.finalized_options import FinalizedOptions
|
11
|
+
from sciform.options.input_options import InputOptions
|
12
|
+
from sciform.options.populated_options import PopulatedOptions
|
13
|
+
|
14
|
+
|
15
|
+
def validate_options(
|
16
|
+
options: InputOptions | PopulatedOptions | FinalizedOptions,
|
17
|
+
) -> None:
|
18
|
+
"""Validate user inputs."""
|
19
|
+
validate_sig_fig_round_mode(options)
|
20
|
+
validate_exp_val(options)
|
21
|
+
validate_separator_options(options)
|
22
|
+
|
23
|
+
|
24
|
+
def validate_sig_fig_round_mode(
|
25
|
+
options: InputOptions | PopulatedOptions | FinalizedOptions,
|
26
|
+
) -> None:
|
27
|
+
r"""Validate ndigits if round_mode == "sig_fig"."""
|
28
|
+
if (
|
29
|
+
options.round_mode == "sig_fig"
|
30
|
+
and isinstance(options.ndigits, int)
|
31
|
+
and options.ndigits < 1
|
32
|
+
):
|
33
|
+
msg = f"ndigits must be >= 1 for sig fig rounding, not {options.ndigits}."
|
34
|
+
raise ValueError(msg)
|
35
|
+
|
36
|
+
|
37
|
+
def validate_exp_val(
|
38
|
+
options: InputOptions | PopulatedOptions | FinalizedOptions,
|
39
|
+
) -> None:
|
40
|
+
"""Validate exp_val."""
|
41
|
+
if options.exp_val is not modes.AutoExpVal and options.exp_val is not None:
|
42
|
+
if options.exp_mode in ["fixed_point", "percent"] and options.exp_val != 0:
|
43
|
+
msg = (
|
44
|
+
f"Exponent must must be 0, not exp_val={options.exp_val}, for "
|
45
|
+
f"fixed point and percent exponent modes."
|
46
|
+
)
|
47
|
+
raise ValueError(msg)
|
48
|
+
if (
|
49
|
+
options.exp_mode in ["engineering", "engineering_shifted"]
|
50
|
+
and options.exp_val % 3 != 0
|
51
|
+
):
|
52
|
+
msg = (
|
53
|
+
f"Exponent must be a multiple of 3, not exp_val={options.exp_val}, "
|
54
|
+
f"for engineering exponent modes."
|
55
|
+
)
|
56
|
+
raise ValueError(msg)
|
57
|
+
if options.exp_mode == "binary_iec" and options.exp_val % 10 != 0:
|
58
|
+
msg = (
|
59
|
+
f"Exponent must be a multiple of 10, not "
|
60
|
+
f"exp_val={options.exp_val}, for binary IEC exponent mode."
|
61
|
+
)
|
62
|
+
raise ValueError(msg)
|
63
|
+
|
64
|
+
|
65
|
+
def validate_separator_options(
|
66
|
+
options: InputOptions | PopulatedOptions | FinalizedOptions,
|
67
|
+
) -> None:
|
68
|
+
"""Validate separator user input."""
|
69
|
+
if options.upper_separator is not None:
|
70
|
+
if options.upper_separator not in get_args(modes.UpperSeparators):
|
71
|
+
msg = (
|
72
|
+
f"upper_separator must be in "
|
73
|
+
f"{get_args(modes.UpperSeparators)}, not "
|
74
|
+
f"{options.upper_separator}."
|
75
|
+
)
|
76
|
+
raise ValueError(msg)
|
77
|
+
if options.upper_separator == options.decimal_separator:
|
78
|
+
msg = (
|
79
|
+
f"upper_separator and decimal_separator "
|
80
|
+
f"({options.upper_separator}) cannot be equal."
|
81
|
+
)
|
82
|
+
raise ValueError(msg)
|
83
|
+
|
84
|
+
if options.decimal_separator is not None and (
|
85
|
+
options.decimal_separator not in get_args(modes.DecimalSeparators)
|
86
|
+
):
|
87
|
+
msg = (
|
88
|
+
f"decimal_separator must be in "
|
89
|
+
f"{get_args(modes.DecimalSeparators)}, not "
|
90
|
+
f"{options.decimal_separator}."
|
91
|
+
)
|
92
|
+
raise ValueError(msg)
|
93
|
+
|
94
|
+
if options.lower_separator is not None and (
|
95
|
+
options.lower_separator not in get_args(modes.LowerSeparators)
|
96
|
+
):
|
97
|
+
msg = (
|
98
|
+
f"lower_separator must be in {get_args(modes.LowerSeparators)}, "
|
99
|
+
f"not {options.lower_separator}."
|
100
|
+
)
|
101
|
+
raise ValueError(msg)
|
@@ -0,0 +1,168 @@
|
|
1
|
+
"""Convert sciform outputs into latex commands."""
|
2
|
+
from __future__ import annotations
|
3
|
+
|
4
|
+
import re
|
5
|
+
from typing import Literal, get_args
|
6
|
+
|
7
|
+
ascii_exp_pattern = re.compile(
|
8
|
+
r"^(?P<mantissa>.*)(?P<ascii_base>[eEbB])(?P<exp>[+-]\d+)$",
|
9
|
+
)
|
10
|
+
ascii_base_dict = {"e": 10, "E": 10, "b": 2, "B": 2}
|
11
|
+
|
12
|
+
unicode_exp_pattern = re.compile(
|
13
|
+
r"^(?P<mantissa>.*)×(?P<base>10|2)(?P<super_exp>[⁺⁻]?[⁰¹²³⁴⁵⁶⁷⁸⁹]+)$",
|
14
|
+
)
|
15
|
+
superscript_translation = str.maketrans("⁺⁻⁰¹²³⁴⁵⁶⁷⁸⁹", "+-0123456789")
|
16
|
+
|
17
|
+
output_formats = Literal["latex", "html", "ascii"]
|
18
|
+
|
19
|
+
|
20
|
+
def _make_exp_str(
|
21
|
+
base: int,
|
22
|
+
exp: int,
|
23
|
+
output_format: output_formats,
|
24
|
+
*,
|
25
|
+
capitalize: bool = False,
|
26
|
+
) -> str:
|
27
|
+
if output_format == "latex":
|
28
|
+
return rf"\times{base}^{{{exp}}}"
|
29
|
+
if output_format == "html":
|
30
|
+
return f"×{base}<sup>{exp}</sup>"
|
31
|
+
if output_format == "ascii":
|
32
|
+
if base == 10:
|
33
|
+
exp_str = f"e{exp:+03d}"
|
34
|
+
elif base == 2:
|
35
|
+
exp_str = f"b{exp:+03d}"
|
36
|
+
else:
|
37
|
+
msg = f"base must be 10 or 2, not {base}"
|
38
|
+
raise ValueError(msg)
|
39
|
+
if capitalize:
|
40
|
+
exp_str = exp_str.upper()
|
41
|
+
return exp_str
|
42
|
+
msg = f"output_format must be in {get_args(output_formats)}, not {output_format}"
|
43
|
+
raise ValueError(msg)
|
44
|
+
|
45
|
+
|
46
|
+
def _string_replacements(input_str: str, replacements: list[tuple[str, str]]) -> str:
|
47
|
+
result_str = input_str
|
48
|
+
for old_chars, new_chars in replacements:
|
49
|
+
result_str = result_str.replace(old_chars, new_chars)
|
50
|
+
return result_str
|
51
|
+
|
52
|
+
|
53
|
+
def convert_sciform_format(
|
54
|
+
formatted_str: str,
|
55
|
+
output_format: output_formats,
|
56
|
+
) -> str:
|
57
|
+
r"""
|
58
|
+
Convert sciform output to new format for different output contexts.
|
59
|
+
|
60
|
+
convert_sciform_format() is used to convert a sciform output string
|
61
|
+
into different formats for presentation in different contexts.
|
62
|
+
Currently, LaTeX, HTML, and ASCII outputs are supported.
|
63
|
+
|
64
|
+
LaTeX
|
65
|
+
=====
|
66
|
+
|
67
|
+
For LaTeX conversion the resulting string is a valid LaTeX command
|
68
|
+
bracketed in "$" symbols to indicate it is in LaTeX math
|
69
|
+
environment. The following transformations are applied.
|
70
|
+
|
71
|
+
* The exponent is displayed using the LaTeX math superscript
|
72
|
+
construction, e.g. "10^{-3}"
|
73
|
+
* Any strings of alphabetic characters (plus ``"μ"``) are wrapped in
|
74
|
+
the LaTeX math-mode text environment, e.g.
|
75
|
+
``"nan"`` -> ``r"\text{nan}"`` or ``"k"`` -> ``r"\text{k}"``.
|
76
|
+
* The following character replacments are made:
|
77
|
+
|
78
|
+
* ``"%"`` -> ``r"\%"``
|
79
|
+
* ``"_"`` -> ``r"\_"``
|
80
|
+
* ``" "`` -> ``r"\:"``
|
81
|
+
* ``"±"`` -> ``r"\pm"``
|
82
|
+
* ``"×"`` -> ```r"\times"``
|
83
|
+
* ``"μ"`` -> ``r"\textmu"``
|
84
|
+
|
85
|
+
>>> from sciform.output_conversion import convert_sciform_format
|
86
|
+
>>> print(convert_sciform_format("(7.8900 ± 0.0001)×10²", "latex"))
|
87
|
+
$(7.8900\:\pm\:0.0001)\times10^{2}$
|
88
|
+
>>> print(convert_sciform_format("16.18033E+03", "latex"))
|
89
|
+
$16.18033\times10^{3}$
|
90
|
+
|
91
|
+
HTML
|
92
|
+
====
|
93
|
+
|
94
|
+
In HTML mode superscripts are representing using e.g.
|
95
|
+
"<sup>-3</sup>".
|
96
|
+
|
97
|
+
>>> from sciform.output_conversion import convert_sciform_format
|
98
|
+
>>> print(convert_sciform_format("(7.8900 ± 0.0001)×10²", "html"))
|
99
|
+
(7.8900 ± 0.0001)×10<sup>2</sup>
|
100
|
+
>>> print(convert_sciform_format("16.18033E+03", "html"))
|
101
|
+
16.18033×10<sup>3</sup>
|
102
|
+
|
103
|
+
ASCII
|
104
|
+
=====
|
105
|
+
|
106
|
+
In the ASCII mode exponents are always represented as e.g. "e-03".
|
107
|
+
Also, "±" is replaced by "+/-" and "μ" is replaced by "u".
|
108
|
+
|
109
|
+
>>> from sciform.output_conversion import convert_sciform_format
|
110
|
+
>>> print(convert_sciform_format("(7.8900 ± 0.0001)×10²", "ascii"))
|
111
|
+
(7.8900 +/- 0.0001)e+02
|
112
|
+
>>> print(convert_sciform_format("16.18033E+03", "ascii"))
|
113
|
+
16.18033E+03
|
114
|
+
"""
|
115
|
+
if match := re.match(ascii_exp_pattern, formatted_str):
|
116
|
+
mantissa = match.group("mantissa")
|
117
|
+
ascii_base = match.group("ascii_base")
|
118
|
+
base = ascii_base_dict[ascii_base]
|
119
|
+
exp = int(match.group("exp"))
|
120
|
+
exp_str = _make_exp_str(
|
121
|
+
base,
|
122
|
+
exp,
|
123
|
+
output_format,
|
124
|
+
capitalize=ascii_base.isupper(),
|
125
|
+
)
|
126
|
+
main_str = mantissa
|
127
|
+
suffix_str = exp_str
|
128
|
+
elif match := re.match(unicode_exp_pattern, formatted_str):
|
129
|
+
mantissa = match.group("mantissa")
|
130
|
+
base = int(match.group("base"))
|
131
|
+
super_exp = match.group("super_exp")
|
132
|
+
exp = int(super_exp.translate(superscript_translation))
|
133
|
+
exp_str = _make_exp_str(base, exp, output_format)
|
134
|
+
main_str = mantissa
|
135
|
+
suffix_str = exp_str
|
136
|
+
else:
|
137
|
+
main_str = formatted_str
|
138
|
+
suffix_str = ""
|
139
|
+
|
140
|
+
if output_format == "latex":
|
141
|
+
main_str = re.sub(
|
142
|
+
r"([a-zA-Zμ]+)",
|
143
|
+
r"\\text{\1}",
|
144
|
+
main_str,
|
145
|
+
)
|
146
|
+
|
147
|
+
replacements = [
|
148
|
+
("%", r"\%"),
|
149
|
+
("_", r"\_"),
|
150
|
+
(" ", r"\:"),
|
151
|
+
("±", r"\pm"),
|
152
|
+
("×", r"\times"),
|
153
|
+
("μ", r"\textmu"),
|
154
|
+
]
|
155
|
+
main_str = _string_replacements(main_str, replacements)
|
156
|
+
return f"${main_str}{suffix_str}$"
|
157
|
+
|
158
|
+
if output_format == "html":
|
159
|
+
return f"{main_str}{suffix_str}"
|
160
|
+
if output_format == "ascii":
|
161
|
+
replacements = [
|
162
|
+
("±", "+/-"),
|
163
|
+
("μ", "u"),
|
164
|
+
]
|
165
|
+
main_str = _string_replacements(main_str, replacements)
|
166
|
+
return f"{main_str}{suffix_str}"
|
167
|
+
msg = f"output_format must be in {get_args(output_formats)}, not {output_format}"
|
168
|
+
raise ValueError(msg)
|
sciform/scinum.py
CHANGED
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|
5
5
|
from decimal import Decimal
|
6
6
|
from typing import TYPE_CHECKING
|
7
7
|
|
8
|
-
from sciform.formatting import
|
8
|
+
from sciform.formatting import FormattedNumber, format_from_options
|
9
9
|
from sciform.fsml import format_options_from_fmt_spec
|
10
10
|
|
11
11
|
if TYPE_CHECKING: # pragma: no cover
|
@@ -23,13 +23,13 @@ class SciNum:
|
|
23
23
|
be populated with global default settings at format time.
|
24
24
|
|
25
25
|
>>> from sciform import SciNum
|
26
|
-
>>>
|
27
|
-
>>> print(f"{
|
26
|
+
>>> num = SciNum(12345.54321)
|
27
|
+
>>> print(f"{num:!3f}")
|
28
28
|
12300
|
29
|
-
>>> print(f"{
|
29
|
+
>>> print(f"{num:+2.3R}")
|
30
30
|
+ 12.346E+03
|
31
|
-
>>>
|
32
|
-
>>> print(f"{
|
31
|
+
>>> num = SciNum(123456.654321, 0.0234)
|
32
|
+
>>> print(f"{num:#!2r()}")
|
33
33
|
(0.123456654(23))e+06
|
34
34
|
"""
|
35
35
|
|
@@ -45,12 +45,13 @@ class SciNum:
|
|
45
45
|
else:
|
46
46
|
self.uncertainty = Decimal(str(uncertainty))
|
47
47
|
|
48
|
-
def __format__(self: SciNum, fmt: str) ->
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
48
|
+
def __format__(self: SciNum, fmt: str) -> FormattedNumber:
|
49
|
+
input_options = format_options_from_fmt_spec(fmt)
|
50
|
+
return format_from_options(
|
51
|
+
self.value,
|
52
|
+
self.uncertainty,
|
53
|
+
input_options=input_options,
|
54
|
+
)
|
54
55
|
|
55
56
|
def __repr__(self: SciNum) -> str:
|
56
57
|
if self.uncertainty is not None:
|