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.
@@ -1,23 +1,23 @@
1
1
  """
2
2
  Rendered format options used in sciform backend formatting algorithm.
3
3
 
4
- :class:`UserOptions` are converted into :class:`RenderedOptions`
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 asdict, dataclass
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 RenderedOptions:
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
- fill_char: modes.FillCharEnum
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 __str__(self: RenderedOptions) -> str:
48
- options_dict = asdict(self)
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.rendered_options import RenderedOptions
4
+ from sciform.options.populated_options import PopulatedOptions
5
5
 
6
- PKG_DEFAULT_OPTIONS = RenderedOptions(
7
- exp_mode=modes.ExpModeEnum.FIXEDPOINT,
6
+ PKG_DEFAULT_OPTIONS = PopulatedOptions(
7
+ exp_mode="fixed_point",
8
8
  exp_val=modes.AutoExpVal,
9
- round_mode=modes.RoundModeEnum.SIG_FIG,
9
+ round_mode="sig_fig",
10
10
  ndigits=modes.AutoDigits,
11
- upper_separator=modes.SeparatorEnum.NONE,
12
- decimal_separator=modes.SeparatorEnum.POINT,
13
- lower_separator=modes.SeparatorEnum.NONE,
14
- sign_mode=modes.SignModeEnum.NEGATIVE,
15
- fill_char=modes.FillCharEnum.SPACE,
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=modes.ExpFormatEnum.STANDARD,
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 format_num, format_val_unc
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
- >>> snum = SciNum(12345.54321)
27
- >>> print(f"{snum:!3f}")
26
+ >>> num = SciNum(12345.54321)
27
+ >>> print(f"{num:!3f}")
28
28
  12300
29
- >>> print(f"{snum:+2.3R}")
29
+ >>> print(f"{num:+2.3R}")
30
30
  + 12.346E+03
31
- >>> snum = SciNum(123456.654321, 0.0234)
32
- >>> print(f"{snum:#!2r()}")
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) -> str:
49
- user_options = format_options_from_fmt_spec(fmt)
50
- rendered_options = user_options.render()
51
- if self.uncertainty is not None:
52
- return format_val_unc(self.value, self.uncertainty, rendered_options)
53
- return format_num(self.value, rendered_options)
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: