Coverage for an_website/currency_converter/converter.py: 85.321%

109 statements  

« prev     ^ index     » next       coverage.py v7.6.4, created at 2024-11-16 19:56 +0000

1# This program is free software: you can redistribute it and/or modify 

2# it under the terms of the GNU Affero General Public License as 

3# published by the Free Software Foundation, either version 3 of the 

4# License, or (at your option) any later version. 

5# 

6# This program is distributed in the hope that it will be useful, 

7# but WITHOUT ANY WARRANTY; without even the implied warranty of 

8# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

9# GNU Affero General Public License for more details. 

10# 

11# You should have received a copy of the GNU Affero General Public License 

12# along with this program. If not, see <https://www.gnu.org/licenses/>. 

13 

14"""A page that converts german currencies.""" 

15 

16from __future__ import annotations 

17 

18import random 

19from typing import Final, TypeAlias, cast 

20 

21import regex 

22 

23from ..utils.request_handler import APIRequestHandler, HTMLRequestHandler 

24from ..utils.utils import ModuleInfo 

25 

26ValueDict: TypeAlias = dict[str, str | int | bool] 

27ValuesTuple: TypeAlias = tuple[int, int, int, int] 

28 

29KEYS: Final = ("euro", "mark", "ost", "schwarz") 

30MULTIPLIERS: Final = (1, 2, 4, 20) 

31 

32 

33def get_module_info() -> ModuleInfo: 

34 """Create and return the ModuleInfo for this module.""" 

35 return ModuleInfo( 

36 handlers=( 

37 (r"/waehrungs-rechner", CurrencyConverter), 

38 (r"/api/waehrungs-rechner", CurrencyConverterAPI), 

39 ), 

40 name="Währungsrechner", 

41 description=( 

42 "Ein Währungsrechner für teilweise veraltete deutsche Währungen" 

43 ), 

44 path="/waehrungs-rechner", 

45 keywords=( 

46 "Währungsrechner", 

47 "converter", 

48 "Euro", 

49 "D-Mark", 

50 "Ost-Mark", 

51 "Känguru", 

52 ), 

53 aliases=( 

54 "/rechner", 

55 "/w%C3%A4hrungs-rechner", 

56 "/währungs-rechner", 

57 "/currency-converter", 

58 ), 

59 ) 

60 

61 

62def string_to_num(num: str, divide_by: int = 1) -> int: 

63 """Convert a string to a number and divide it by divide_by.""" 

64 num = regex.sub(r"[^\d,]", "", num.replace(".", ",")) 

65 

66 if not num: 

67 return 0 

68 

69 if "," not in num: 

70 return (int(num) * 100) // divide_by 

71 

72 split = [spam for spam in num.split(",") if spam] 

73 if not split: 

74 return 0 

75 

76 if len(split) == 1: 

77 return (int(split[0]) * 100) // divide_by 

78 

79 dec = split[-1] 

80 pre = "".join(split[:-1]) 

81 

82 return (int(pre or "0") * 100 + int((dec + "00")[0:2])) // divide_by 

83 

84 

85def num_to_string(num: int) -> str: 

86 """ 

87 Convert a float to the german representation of a number. 

88 

89 The number has 2 or 0 digits after the comma. 

90 """ 

91 if not num: 

92 return "0" 

93 string = f"00{num}" 

94 string = (string[0:-2].lstrip("0") or "0") + "," + string[-2:] 

95 return string.removesuffix(",00") 

96 

97 

98async def conversion_string(value_dict: ValueDict) -> str: 

99 """Generate a text that complains how expensive everything is.""" 

100 return ( 

101 f"{value_dict.get('euro_str')} Euro, " 

102 f"das sind ja {value_dict.get('mark_str')} Mark; " 

103 f"{value_dict.get('ost_str')} Ostmark und " 

104 f"{value_dict.get('schwarz_str')} Ostmark auf dem Schwarzmarkt!" 

105 ) 

106 

107 

108async def continuation_string( 

109 values: ValuesTuple, 

110 ticket_price: None | int = None, 

111 ins_kino_gehen: None | str = None, 

112) -> str: 

113 """Generate a second text that complains how expensive everything is.""" 

114 price: int = ticket_price or values[0] 

115 if not ins_kino_gehen: 

116 ins_kino_gehen = "ins Kino gehen" 

117 price_ostmark: int = ( 

118 100 

119 if ins_kino_gehen == "ins Kino gehen" 

120 else int(max(round(price // 8) // 2, 50)) 

121 ) 

122 rand = random.Random(f"{price}|{values}|{ins_kino_gehen}") 

123 kino_count: int = values[-1] // price_ostmark 

124 output = [ 

125 "Und ich weiß nicht, ob ihr das noch wisst, aber man konnte locker für", 

126 "eine" if price_ostmark == 100 else num_to_string(price_ostmark), 

127 ( 

128 f"Ostmark {ins_kino_gehen}! Das heißt man konnte von " 

129 f"{num_to_string(values[-1])} Ostmark " 

130 f"{kino_count}-mal {ins_kino_gehen}." 

131 ), 

132 ] 

133 while True: # pylint: disable=while-used 

134 euro, mark, ost, schwarz = convert(int(price * kino_count)) 

135 no_time = ( 

136 " — dafür habt ihr ja keine Zeit im Kapitalismus, " 

137 "aber wenn ihr die Zeit hättet —" 

138 if mark > 20_300_000_000 * 100 

139 else "" 

140 ) 

141 output.append( 

142 f"Wenn ihr aber heute {kino_count}-mal " 

143 f"{ins_kino_gehen} wollt{no_time}, müsst ihr" 

144 ) 

145 if euro > 1_000_000 * 100: 

146 output.append("...äh...") 

147 output.append(f"{num_to_string(euro)} Euro bezahlen.") 

148 output.append( 

149 rand.choice( 

150 ( 

151 "Ja! Ja! Ich weiß, was ihr denkt!", 

152 "Ja, ja, ich weiß, was ihr denkt!", 

153 "Ich weiß, was ihr denkt!", 

154 ) 

155 ) 

156 ) 

157 if mark > 20_300_000_000 * 100: # Staatsschulden der DDR 

158 output.append( # the end of the text 

159 "Davon hätte man die DDR entschulden können! " 

160 f"Von einmal {ins_kino_gehen}. " 

161 "So teuer ist das alles geworden." 

162 ) 

163 break 

164 new_kino_count = int((schwarz * price_ostmark) // 100) 

165 # TODO: add random chance to get approximation 

166 output.append( 

167 f"{num_to_string(mark)} Mark, {num_to_string(ost)} Ostmark, " 

168 f"{num_to_string(schwarz)} Ostmark auf dem Schwarzmarkt, " 

169 f"davon konnte man {new_kino_count}-mal {ins_kino_gehen}." 

170 ) 

171 if new_kino_count <= kino_count: 

172 # it doesn't grow, exit to avoid infinite loop 

173 break 

174 kino_count = new_kino_count 

175 

176 return " ".join(output) 

177 

178 

179def convert(euro: int) -> ValuesTuple: 

180 """Convert a number to the german representation of a number.""" 

181 return cast(ValuesTuple, tuple(euro * _m for _m in MULTIPLIERS)) 

182 

183 

184async def get_value_dict( 

185 euro: int, 

186 ins_kino_gehen: None | str = None, 

187) -> ValueDict: 

188 """Create the value dict base on the euro.""" 

189 values = convert(euro) 

190 value_dict: ValueDict = {} 

191 for key, val in zip(KEYS, values, strict=True): 

192 value_dict[key] = val 

193 value_dict[key + "_str"] = num_to_string(val) 

194 

195 value_dict["text"] = await conversion_string(value_dict) 

196 value_dict["text2"] = await continuation_string( 

197 values, ins_kino_gehen=ins_kino_gehen 

198 ) 

199 return value_dict 

200 

201 

202class CurrencyConverter(HTMLRequestHandler): 

203 """Request handler for the currency converter page.""" 

204 

205 async def create_value_dict(self) -> None | ValueDict: 

206 """ 

207 Parse the arguments to get the value dict and return it. 

208 

209 Return None if no argument is given. 

210 """ 

211 arg_list: list[tuple[int, str, str]] = [] 

212 

213 for i, key in enumerate(KEYS): 

214 num_str = self.get_argument(key, None) 

215 if num_str is not None: 

216 arg_list.append((i, key, num_str)) 

217 

218 too_many_params = len(arg_list) > 1 

219 

220 for i, key, num_str in arg_list: 

221 euro = string_to_num(num_str, MULTIPLIERS[i]) 

222 if euro is not None: 

223 value_dict = await get_value_dict( 

224 euro, ins_kino_gehen=self.get_argument("text", None) 

225 ) 

226 if too_many_params: 

227 value_dict["too_many_params"] = True 

228 value_dict["key_used"] = key 

229 return value_dict 

230 

231 return None 

232 

233 async def get(self, *, head: bool = False) -> None: 

234 """Handle GET requests to the currency converter page.""" 

235 # pylint: disable=unused-argument 

236 value_dict = await self.create_value_dict() 

237 if value_dict is None: 

238 value_dict = await get_value_dict(0) 

239 description = self.description 

240 else: 

241 description = cast(str, value_dict["text"]) 

242 

243 if value_dict.get("too_many_params", False): 

244 used_key = value_dict.get("key_used") 

245 replace_url_with = self.fix_url( 

246 self.request.full_url(), 

247 **{ # type: ignore[arg-type] 

248 key: ( 

249 value_dict.get(f"{used_key}_str") 

250 if key == used_key 

251 else None 

252 ) 

253 for key in KEYS 

254 }, 

255 ) 

256 if replace_url_with != self.request.full_url(): 

257 return self.redirect(replace_url_with) 

258 

259 await self.render( 

260 "pages/converter.html", 

261 **value_dict, 

262 description=description, 

263 ) 

264 

265 

266class CurrencyConverterAPI(APIRequestHandler, CurrencyConverter): 

267 """Request handler for the currency converter API.""" 

268 

269 async def get(self, *, head: bool = False) -> None: 

270 """ 

271 Handle GET requests to the currency converter API. 

272 

273 If no arguments are given the potential arguments are shown. 

274 """ 

275 value_dict = await self.create_value_dict() 

276 

277 if head: 

278 return 

279 

280 if value_dict is None: 

281 return await self.finish( 

282 dict(zip(KEYS, [None] * len(KEYS), strict=True)) 

283 ) 

284 

285 for key in KEYS: 

286 value_dict[key] = value_dict.pop(f"{key}_str") 

287 

288 return await self.finish(value_dict)