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
« 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/>.
14"""A page that converts german currencies."""
16import random
17from typing import Final, TypeAlias, cast
19import regex
21from ..utils.request_handler import APIRequestHandler, HTMLRequestHandler
22from ..utils.utils import ModuleInfo
24ValueDict: TypeAlias = dict[str, str | int | bool]
25ValuesTuple: TypeAlias = tuple[int, int, int, int]
27KEYS: Final = ("euro", "mark", "ost", "schwarz")
28MULTIPLIERS: Final = (1, 2, 4, 20)
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 )
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(".", ","))
64 if not num:
65 return 0
67 if "," not in num:
68 return (int(num) * 100) // divide_by
70 split = [spam for spam in num.split(",") if spam]
71 if not split:
72 return 0
74 if len(split) == 1:
75 return (int(split[0]) * 100) // divide_by
77 dec = split[-1]
78 pre = "".join(split[:-1])
80 return (int(pre or "0") * 100 + int((dec + "00")[0:2])) // divide_by
83def num_to_string(num: int) -> str:
84 """
85 Convert a float to the german representation of a number.
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")
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 )
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
174 return " ".join(output)
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))
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)
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
200class CurrencyConverter(HTMLRequestHandler):
201 """Request handler for the currency converter page."""
203 async def create_value_dict(self) -> None | ValueDict:
204 """
205 Parse the arguments to get the value dict and return it.
207 Return None if no argument is given.
208 """
209 arg_list: list[tuple[int, str, str]] = []
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))
216 too_many_params = len(arg_list) > 1
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
229 return None
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"])
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)
257 await self.render(
258 "pages/converter.html",
259 **value_dict,
260 description=description,
261 )
264class CurrencyConverterAPI(APIRequestHandler, CurrencyConverter):
265 """Request handler for the currency converter API."""
267 async def get(self, *, head: bool = False) -> None:
268 """
269 Handle GET requests to the currency converter API.
271 If no arguments are given the potential arguments are shown.
272 """
273 value_dict = await self.create_value_dict()
275 if head:
276 return
278 if value_dict is None:
279 return await self.finish(
280 dict(zip(KEYS, [None] * len(KEYS), strict=True))
281 )
283 for key in KEYS:
284 value_dict[key] = value_dict.pop(f"{key}_str")
286 return await self.finish(value_dict)