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

108 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-03-15 14:36 +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 

16 

17import random 

18from typing import Final, TypeAlias, cast 

19 

20import regex 

21 

22from ..utils.request_handler import APIRequestHandler, HTMLRequestHandler 

23from ..utils.utils import ModuleInfo 

24 

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

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

27 

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

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

30 

31 

32def get_module_info() -> ModuleInfo: 

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

34 return ModuleInfo( 

35 handlers=( 

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

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

38 ), 

39 name="Währungsrechner", 

40 description=( 

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

42 ), 

43 path="/waehrungs-rechner", 

44 keywords=( 

45 "Währungsrechner", 

46 "converter", 

47 "Euro", 

48 "D-Mark", 

49 "Ost-Mark", 

50 "Känguru", 

51 ), 

52 aliases=( 

53 "/rechner", 

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

55 "/währungs-rechner", 

56 "/currency-converter", 

57 ), 

58 ) 

59 

60 

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

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

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

64 

65 if not num: 

66 return 0 

67 

68 if "," not in num: 

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

70 

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

72 if not split: 

73 return 0 

74 

75 if len(split) == 1: 

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

77 

78 dec = split[-1] 

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

80 

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

82 

83 

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

85 """ 

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

87 

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

89 """ 

90 if not num: 

91 return "0" 

92 string = f"00{num}" 

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

94 return string.removesuffix(",00") 

95 

96 

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

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

99 return ( 

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

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

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

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

104 ) 

105 

106 

107async def continuation_string( 

108 values: ValuesTuple, 

109 ticket_price: None | int = None, 

110 ins_kino_gehen: None | str = None, 

111) -> str: 

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

113 price: int = ticket_price or values[0] 

114 if not ins_kino_gehen: 

115 ins_kino_gehen = "ins Kino gehen" 

116 price_ostmark: int = ( 

117 100 

118 if ins_kino_gehen == "ins Kino gehen" 

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

120 ) 

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

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

123 output = [ 

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

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

126 ( 

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

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

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

130 ), 

131 ] 

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

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

134 no_time = ( 

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

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

137 if mark > 20_300_000_000 * 100 

138 else "" 

139 ) 

140 output.append( 

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

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

143 ) 

144 if euro > 1_000_000 * 100: 

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

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

147 output.append( 

148 rand.choice( 

149 ( 

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

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

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

153 ) 

154 ) 

155 ) 

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

157 output.append( # the end of the text 

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

159 f"Von einmal {ins_kino_gehen}. " 

160 "So teuer ist das alles geworden." 

161 ) 

162 break 

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

164 # TODO: add random chance to get approximation 

165 output.append( 

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

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

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

169 ) 

170 if new_kino_count <= kino_count: 

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

172 break 

173 kino_count = new_kino_count 

174 

175 return " ".join(output) 

176 

177 

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

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

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

181 

182 

183async def get_value_dict( 

184 euro: int, 

185 ins_kino_gehen: None | str = None, 

186) -> ValueDict: 

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

188 values = convert(euro) 

189 value_dict: ValueDict = {} 

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

191 value_dict[key] = val 

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

193 

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

195 value_dict["text2"] = await continuation_string( 

196 values, ins_kino_gehen=ins_kino_gehen 

197 ) 

198 return value_dict 

199 

200 

201class CurrencyConverter(HTMLRequestHandler): 

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

203 

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

205 """ 

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

207 

208 Return None if no argument is given. 

209 """ 

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

211 

212 for i, key in enumerate(KEYS): 

213 num_str = self.get_argument(key, None) 

214 if num_str: 

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

216 

217 too_many_params = len(arg_list) > 1 

218 

219 for i, key, num_str in arg_list: 

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

221 if euro is not None: 

222 value_dict = await get_value_dict( 

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

224 ) 

225 if too_many_params: 

226 value_dict["too_many_params"] = True 

227 value_dict["key_used"] = key 

228 return value_dict 

229 

230 return None 

231 

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

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

234 # pylint: disable=unused-argument 

235 value_dict = await self.create_value_dict() 

236 if value_dict is None: 

237 value_dict = await get_value_dict(0) 

238 description = self.description 

239 else: 

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

241 

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

243 used_key = value_dict.get("key_used") 

244 replace_url_with = self.fix_url( 

245 self.request.full_url(), 

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

247 key: ( 

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

249 if key == used_key 

250 else None 

251 ) 

252 for key in KEYS 

253 }, 

254 ) 

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

256 return self.redirect(replace_url_with) 

257 

258 await self.render( 

259 "pages/converter.html", 

260 **value_dict, 

261 description=description, 

262 ) 

263 

264 

265class CurrencyConverterAPI(APIRequestHandler, CurrencyConverter): 

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

267 

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

269 """ 

270 Handle GET requests to the currency converter API. 

271 

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

273 """ 

274 value_dict = await self.create_value_dict() 

275 

276 if head: 

277 return 

278 

279 if value_dict is None: 

280 return await self.finish( 

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

282 ) 

283 

284 for key in KEYS: 

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

286 

287 return await self.finish(value_dict)