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

108 statements  

« prev     ^ index     » next       coverage.py v7.14.1, created at 2026-06-10 18: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 

16import random 

17from typing import Final, TypeAlias, cast 

18 

19import regex 

20 

21from ..utils.request_handler import APIRequestHandler, HTMLRequestHandler 

22from ..utils.utils import ModuleInfo 

23 

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

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

26 

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

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

29 

30 

31def get_module_info() -> ModuleInfo: 

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

33 return ModuleInfo( 

34 handlers=( 

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

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

37 ), 

38 name="Währungsrechner", 

39 description=( 

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

41 ), 

42 path="/waehrungs-rechner", 

43 keywords=( 

44 "Währungsrechner", 

45 "converter", 

46 "Euro", 

47 "D-Mark", 

48 "Ost-Mark", 

49 "Känguru", 

50 ), 

51 aliases=( 

52 "/rechner", 

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

54 "/währungs-rechner", 

55 "/currency-converter", 

56 ), 

57 ) 

58 

59 

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

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

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

63 

64 if not num: 

65 return 0 

66 

67 if "," not in num: 

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

69 

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

71 if not split: 

72 return 0 

73 

74 if len(split) == 1: 

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

76 

77 dec = split[-1] 

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

79 

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

81 

82 

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

84 """ 

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

86 

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

88 """ 

89 if not num: 

90 return "0" 

91 string = f"00{num}" 

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

93 return string.removesuffix(",00") 

94 

95 

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

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

98 return ( 

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

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

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

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

103 ) 

104 

105 

106async def continuation_string( 

107 values: ValuesTuple, 

108 ticket_price: None | int = None, 

109 ins_kino_gehen: None | str = None, 

110) -> str: 

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

112 price: int = ticket_price or values[0] 

113 if not ins_kino_gehen: 

114 ins_kino_gehen = "ins Kino gehen" 

115 price_ostmark: int = ( 

116 100 

117 if ins_kino_gehen == "ins Kino gehen" 

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

119 ) 

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

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

122 output = [ 

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

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

125 ( 

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

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

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

129 ), 

130 ] 

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

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

133 no_time = ( 

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

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

136 if mark > 20_300_000_000 * 100 

137 else "" 

138 ) 

139 output.append( 

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

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

142 ) 

143 if euro > 1_000_000 * 100: 

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

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

146 output.append( 

147 rand.choice( 

148 ( 

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

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

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

152 ) 

153 ) 

154 ) 

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

156 output.append( # the end of the text 

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

158 f"Von einmal {ins_kino_gehen}. " 

159 "So teuer ist das alles geworden." 

160 ) 

161 break 

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

163 # TODO: add random chance to get approximation 

164 output.append( 

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

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

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

168 ) 

169 if new_kino_count <= kino_count: 

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

171 break 

172 kino_count = new_kino_count 

173 

174 return " ".join(output) 

175 

176 

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

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

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

180 

181 

182async def get_value_dict( 

183 euro: int, 

184 ins_kino_gehen: None | str = None, 

185) -> ValueDict: 

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

187 values = convert(euro) 

188 value_dict: ValueDict = {} 

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

190 value_dict[key] = val 

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

192 

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

194 value_dict["text2"] = await continuation_string( 

195 values, ins_kino_gehen=ins_kino_gehen 

196 ) 

197 return value_dict 

198 

199 

200class CurrencyConverter(HTMLRequestHandler): 

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

202 

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

204 """ 

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

206 

207 Return None if no argument is given. 

208 """ 

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

210 

211 for i, key in enumerate(KEYS): 

212 num_str = self.get_argument(key, None) 

213 if num_str: 

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

215 

216 too_many_params = len(arg_list) > 1 

217 

218 for i, key, num_str in arg_list: 

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

220 if euro is not None: 

221 value_dict = await get_value_dict( 

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

223 ) 

224 if too_many_params: 

225 value_dict["too_many_params"] = True 

226 value_dict["key_used"] = key 

227 return value_dict 

228 

229 return None 

230 

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

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

233 # pylint: disable=unused-argument 

234 value_dict = await self.create_value_dict() 

235 if value_dict is None: 

236 value_dict = await get_value_dict(0) 

237 description = self.description 

238 else: 

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

240 

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

242 used_key = value_dict.get("key_used") 

243 replace_url_with = self.fix_url( 

244 self.request.full_url(), 

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

246 key: ( 

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

248 if key == used_key 

249 else None 

250 ) 

251 for key in KEYS 

252 }, 

253 ) 

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

255 return self.redirect(replace_url_with) 

256 

257 await self.render( 

258 "pages/converter.html", 

259 **value_dict, 

260 description=description, 

261 ) 

262 

263 

264class CurrencyConverterAPI(APIRequestHandler, CurrencyConverter): 

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

266 

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

268 """ 

269 Handle GET requests to the currency converter API. 

270 

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

272 """ 

273 value_dict = await self.create_value_dict() 

274 

275 if head: 

276 return 

277 

278 if value_dict is None: 

279 return await self.finish( 

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

281 ) 

282 

283 for key in KEYS: 

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

285 

286 return await self.finish(value_dict)