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

1"""Strings that carry format information.""" 

2 

3from math import inf, isnan, nan 

4from typing import Final 

5 

6from pycommons.types import check_int_range, type_error 

7 

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 

20 

21 

22class FormattedStr(str): # noqa: SLOT000 

23 """ 

24 A subclass of `str` capable of holding formatting information. 

25 

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. 

32 

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. 

44 

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 """ 

50 

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 

60 

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. 

65 

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 

88 

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. 

94 

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? 

99 

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 

131 

132 @staticmethod 

133 def number(number: int | float | str | None) -> "FormattedStr": 

134 """ 

135 Create a formatted string representing a number. 

136 

137 :param number: the original number or numeric string 

138 :return: the formatted string representing it 

139 

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) 

176 

177 @staticmethod 

178 def special(text: str) -> "FormattedStr": 

179 r""" 

180 Create a special string. 

181 

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. 

185 

186 :param text: the text 

187 

188 >>> FormattedStr.special("bla") 

189 'bla' 

190 >>> FormattedStr.special("bla").mode 

191 5 

192 """ 

193 return FormattedStr(text, False, False, False, SPECIAL) 

194 

195 

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)