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
« 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/>.
14"""A page that converts german currencies."""
16from __future__ import annotations
18import random
19from typing import Final, TypeAlias, cast
21import regex
23from ..utils.request_handler import APIRequestHandler, HTMLRequestHandler
24from ..utils.utils import ModuleInfo
26ValueDict: TypeAlias = dict[str, str | int | bool]
27ValuesTuple: TypeAlias = tuple[int, int, int, int]
29KEYS: Final = ("euro", "mark", "ost", "schwarz")
30MULTIPLIERS: Final = (1, 2, 4, 20)
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 )
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(".", ","))
66 if not num:
67 return 0
69 if "," not in num:
70 return (int(num) * 100) // divide_by
72 split = [spam for spam in num.split(",") if spam]
73 if not split:
74 return 0
76 if len(split) == 1:
77 return (int(split[0]) * 100) // divide_by
79 dec = split[-1]
80 pre = "".join(split[:-1])
82 return (int(pre or "0") * 100 + int((dec + "00")[0:2])) // divide_by
85def num_to_string(num: int) -> str:
86 """
87 Convert a float to the german representation of a number.
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")
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 )
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
176 return " ".join(output)
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))
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)
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
202class CurrencyConverter(HTMLRequestHandler):
203 """Request handler for the currency converter page."""
205 async def create_value_dict(self) -> None | ValueDict:
206 """
207 Parse the arguments to get the value dict and return it.
209 Return None if no argument is given.
210 """
211 arg_list: list[tuple[int, str, str]] = []
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))
218 too_many_params = len(arg_list) > 1
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
231 return None
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"])
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)
259 await self.render(
260 "pages/converter.html",
261 **value_dict,
262 description=description,
263 )
266class CurrencyConverterAPI(APIRequestHandler, CurrencyConverter):
267 """Request handler for the currency converter API."""
269 async def get(self, *, head: bool = False) -> None:
270 """
271 Handle GET requests to the currency converter API.
273 If no arguments are given the potential arguments are shown.
274 """
275 value_dict = await self.create_value_dict()
277 if head:
278 return
280 if value_dict is None:
281 return await self.finish(
282 dict(zip(KEYS, [None] * len(KEYS), strict=True))
283 )
285 for key in KEYS:
286 value_dict[key] = value_dict.pop(f"{key}_str")
288 return await self.finish(value_dict)