Coverage for moptipy / utils / formatted_string.py: 87%
62 statements
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-24 08:49 +0000
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-24 08:49 +0000
1"""Strings that carry format information."""
3from math import inf, isnan, nan
4from typing import Final
6from pycommons.types import check_int_range, type_error
8#: the formatted string represents normal text
9TEXT: Final[int] = 0
10#: the formatted string represents a number
11NUMBER: Final[int] = 1
12#: the formatted string represents NaN, i.e., "not a number"
13NAN: Final[int] = 2
14#: the formatted string represents positive infinity
15POSITIVE_INFINITY: Final[int] = 3
16#: the formatted string represents negative infinity
17NEGATIVE_INFINITY: Final[int] = 4
18#: the formatted string represents a special character
19SPECIAL: Final[int] = 5
22class FormattedStr(str): # noqa: SLOT000
23 """
24 A subclass of `str` capable of holding formatting information.
26 This is a version of string that also stores format information that can,
27 for example, be used by a text format driver when typesetting text.
28 Instances of this class can also be used as normal strings and be printed
29 to the console without any issue. However, they also hold information
30 about whether the text should be :attr:`bold`, :attr:`italic`, or rendered
31 in a monospace :attr:`code` font.
33 Furthermore, if a number or numerical string is represented as a formatted
34 string, the field :attr:`mode` will be non-zero. If it is `TEXT=1`, the
35 string is a normal number, if it is `NAN=2`, the string is "nan", if it is
36 `POSITIVE_INFINITY=3`, then the string is `inf`, and if it is
37 `NEGATIVE_INFINITY=4`, the string is `-inf`. These values permit a text
38 driver, for example, to replace the special numeric values with unicode
39 constants or certain commands. It may also choose to replace floating
40 point number of the form `1.5E23` with something like `1.5*10^23`. It can
41 do so because a non-zero :attr:`mode` indicates that the string is
42 definitely representing a number and that numbers in the string did not
43 just occur for whatever other reason.
45 If the field :attr:`mode` has value `SPECIAL=5`, then the text contains a
46 special sequence, e.g., a unicode character outside of the normal range.
47 Then, the text format driver can render this character as a special
48 entity.
49 """
51 #: should this string be formatted in bold face?
52 bold: bool
53 #: should this string be formatted in italic face?
54 italic: bool
55 #: should this string be formatted in code face?
56 code: bool
57 #: the special mode: `TEXT`, `NUMBER`, `NAN`, `POSITIVE_INFINITY`,
58 #: `NEGATIVE_INFINITY`, or `SPECIAL`
59 mode: int
61 def __new__(cls, value, bold: bool = False, italic: bool = False, # noqa
62 code: bool = False, mode: int = TEXT): # noqa
63 """
64 Construct the formatted string.
66 :param value: the string value
67 :param bold: should the format be bold face?
68 :param italic: should the format be italic face?
69 :param code: should the format be code face?
70 :param mode: the mode of the formatted string
71 :returns: an instance of `FormattedStr`
72 """
73 if not isinstance(value, str):
74 raise type_error(value, "value", str)
75 if not isinstance(bold, bool):
76 raise type_error(bold, "bold", bool)
77 if not isinstance(italic, bool):
78 raise type_error(italic, "italic", bool)
79 if not isinstance(code, bool):
80 raise type_error(code, "code", bool)
81 check_int_range(mode, "mode", TEXT, SPECIAL)
82 ret = super().__new__(cls, value)
83 ret.bold = bold
84 ret.italic = italic
85 ret.code = code
86 ret.mode = mode
87 return ret
89 @staticmethod
90 def add_format(s: str, bold: bool = False, italic: bool = False,
91 code: bool = False) -> str:
92 """
93 Add the given format to the specified string.
95 :param s: the string
96 :param bold: should the format be bold face?
97 :param italic: should the format be italic face?
98 :param code: should the format be code face?
100 >>> from typing import cast
101 >>> st = "abc"
102 >>> type(st)
103 <class 'str'>
104 >>> fs = cast("FormattedStr", FormattedStr.add_format(st, bold=True))
105 >>> type(fs)
106 <class 'moptipy.utils.formatted_string.FormattedStr'>
107 >>> fs.bold
108 True
109 >>> fs.italic
110 False
111 >>> fs = cast("FormattedStr", FormattedStr.add_format(fs, italic=True))
112 >>> fs.bold
113 True
114 >>> fs.italic
115 True
116 >>> fs.mode
117 0
118 """
119 if isinstance(s, FormattedStr):
120 bold = bold or s.bold
121 italic = italic or s.italic
122 code = code or s.code
123 if (bold != s.bold) or (italic != s.italic) or (code != s.code):
124 return FormattedStr(s, bold, italic, code, s.mode)
125 return s
126 if not isinstance(s, str):
127 raise type_error(s, "s", str)
128 if bold or italic or code:
129 return FormattedStr(s, bold, italic, code, TEXT)
130 return s
132 @staticmethod
133 def number(number: int | float | str | None) -> "FormattedStr":
134 """
135 Create a formatted string representing a number.
137 :param number: the original number or numeric string
138 :return: the formatted string representing it
140 >>> FormattedStr.number(inf)
141 'inf'
142 >>> FormattedStr.number(inf) is _PINF
143 True
144 >>> FormattedStr.number(inf).mode
145 3
146 >>> FormattedStr.number(-inf)
147 '-inf'
148 >>> FormattedStr.number(-inf) is _NINF
149 True
150 >>> FormattedStr.number(-inf).mode
151 4
152 >>> FormattedStr.number(nan)
153 'nan'
154 >>> FormattedStr.number(nan) is _NAN
155 True
156 >>> FormattedStr.number(nan).mode
157 2
158 >>> FormattedStr.number(123)
159 '123'
160 >>> FormattedStr.number(123e3)
161 '123000.0'
162 >>> FormattedStr.number(123e3).mode
163 1
164 """
165 if not isinstance(number, int | str):
166 if isinstance(number, float):
167 if isnan(number):
168 return _NAN
169 if number >= inf:
170 return _PINF
171 if number <= -inf:
172 return _NINF
173 else:
174 raise type_error(number, "number", (float, int, str))
175 return FormattedStr(str(number), False, False, False, NUMBER)
177 @staticmethod
178 def special(text: str) -> "FormattedStr":
179 r"""
180 Create a special string.
182 A special string has mode `SPECIAL` set and notifies the text format
183 that special formatting conversion, e.g., a translation from the
184 character "alpha" (\u03b1) to `\alpha` is required.
186 :param text: the text
188 >>> FormattedStr.special("bla")
189 'bla'
190 >>> FormattedStr.special("bla").mode
191 5
192 """
193 return FormattedStr(text, False, False, False, SPECIAL)
196#: the constant for not-a-number
197_NAN: Final[FormattedStr] = FormattedStr(str(nan), False, False, False, NAN)
198#: the constant for positive infinity
199_PINF: Final[FormattedStr] = FormattedStr(str(inf), False, False, False,
200 POSITIVE_INFINITY)
201#: the constant for negative infinity
202_NINF: Final[FormattedStr] = FormattedStr(str(-inf), False, False, False,
203 NEGATIVE_INFINITY)