diff --git a/doc b/doc index 92e168a..5335524 160000 --- a/doc +++ b/doc @@ -1 +1 @@ -Subproject commit 92e168a288b396a7a97d200b80affdf8690bd03d +Subproject commit 5335524d3e57c7551f31c7e21fc04c464b23429a diff --git a/matemat/util/__init__.py b/matemat/util/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/matemat/util/currency_format.py b/matemat/util/currency_format.py new file mode 100644 index 0000000..a163238 --- /dev/null +++ b/matemat/util/currency_format.py @@ -0,0 +1,56 @@ + + +def format_chf(value: int, with_currencysign: bool = True) -> str: + """ + Formats a centime value into a commonly understood representation ("CHF -13.37"). + + :param value: The value to format, in centimes. + :param with_currencysign: Whether to include the currency prefix ("CHF ") in the output. + :return: A human-readable string representation. + """ + sign: str = '' + if value < 0: + # As // and % round towards -Inf, convert into a positive value and prepend the negative sign + sign = '-' + value = -value + # Split into full francs and fractions (centimes) + full: int = value // 100 + frac: int = value % 100 + csign: str = 'CHF ' if with_currencysign else '' + # Put it all together; centimes are always padded with 2 zeros + return f'{csign}{sign}{full}.{frac:02}' + + +def parse_chf(value: str) -> int: + """ + Parse a currency value into machine-readable format (integer centimes). The prefix "CHF", the decimal point, and + digits after the decimal point are optional. + + :param value: The value to parse. + :return: An integer representation of the value. + :raises: Value error: If more than two digits after the decimal point are present. + """ + # Remove optional leading "CHF" and strip whitespace + value = value.strip() + if value.startswith('CHF'): + value = value[3:] + value = value.strip() + if '.' not in value: + # already is an integer; parse and turn into centimes + return int(value, 10) * 100 + # Split at the decimal point + full, frac = value.split('.', 1) + if len(frac) > 2: + raise ValueError('Needs max. 2 digits after decimal point') + elif len(frac) < 2: + # Right-pad fraction with zeros ("x." -> "x.00", "x.x" -> "x.x0") + frac = frac + '0' * (2 - len(frac)) + # Parse both parts + ifrac: int = int(frac, 10) + ifull: int = int(full, 10) + if ifrac < 0: + raise ValueError('Fraction part must not be negative.') + if full.startswith('-'): + ifrac = -ifrac + # Combine into centime integer + return ifull * 100 + ifrac diff --git a/matemat/util/test/__init__.py b/matemat/util/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/matemat/webserver/test/test_currency_format.py b/matemat/util/test/test_currency_format.py similarity index 98% rename from matemat/webserver/test/test_currency_format.py rename to matemat/util/test/test_currency_format.py index 6385852..882ddac 100644 --- a/matemat/webserver/test/test_currency_format.py +++ b/matemat/util/test/test_currency_format.py @@ -1,7 +1,7 @@ import unittest -from matemat.webserver.util import format_chf, parse_chf +from matemat.util.currency_format import format_chf, parse_chf class TestCurrencyFormat(unittest.TestCase): diff --git a/matemat/webserver/httpd.py b/matemat/webserver/httpd.py index 4d6d87c..e69c859 100644 --- a/matemat/webserver/httpd.py +++ b/matemat/webserver/httpd.py @@ -17,7 +17,8 @@ import jinja2 from matemat import __version__ as matemat_version from matemat.exceptions import HttpException from matemat.webserver import RequestArguments, PageletResponse, RedirectResponse, TemplateResponse -from matemat.webserver.util import parse_args, format_chf +from matemat.webserver.util import parse_args +from matemat.util.currency_format import format_chf # # Python internal class hacks diff --git a/matemat/webserver/pagelets/modproduct.py b/matemat/webserver/pagelets/modproduct.py index e0e6814..51da39f 100644 --- a/matemat/webserver/pagelets/modproduct.py +++ b/matemat/webserver/pagelets/modproduct.py @@ -8,7 +8,7 @@ from matemat.webserver import pagelet, RequestArguments, PageletResponse, Redire from matemat.db import MatematDatabase from matemat.primitives import Product from matemat.exceptions import DatabaseConsistencyError, HttpException -from matemat.webserver.util import parse_chf +from matemat.util.currency_format import parse_chf @pagelet('/modproduct') diff --git a/matemat/webserver/pagelets/moduser.py b/matemat/webserver/pagelets/moduser.py index 4dda3f0..c51a05d 100644 --- a/matemat/webserver/pagelets/moduser.py +++ b/matemat/webserver/pagelets/moduser.py @@ -8,7 +8,7 @@ from matemat.webserver import pagelet, RequestArguments, PageletResponse, Redire from matemat.db import MatematDatabase from matemat.primitives import User from matemat.exceptions import DatabaseConsistencyError, HttpException -from matemat.webserver.util import parse_chf +from matemat.util.currency_format import parse_chf @pagelet('/moduser') diff --git a/matemat/webserver/util.py b/matemat/webserver/util.py index cd0d2ef..c95d303 100644 --- a/matemat/webserver/util.py +++ b/matemat/webserver/util.py @@ -136,59 +136,3 @@ def parse_args(request: str, postbody: Optional[bytes] = None, enctype: str = 't raise ValueError(f'Unsupported Content-Type: {enctype}') # Return the path and the parsed arguments return tokens.path, args - - -def format_chf(value: int, with_currencysign: bool = True) -> str: - """ - Formats a centime value into a commonly understood representation ("CHF -13.37"). - - :param value: The value to format, in centimes. - :param with_currencysign: Whether to include the currency prefix ("CHF ") in the output. - :return: A human-readable string representation. - """ - sign: str = '' - if value < 0: - # As // and % round towards -Inf, convert into a positive value and prepend the negative sign - sign = '-' - value = -value - # Split into full francs and fractions (centimes) - full: int = value // 100 - frac: int = value % 100 - csign: str = 'CHF ' if with_currencysign else '' - # Put it all together; centimes are always padded with 2 zeros - return f'{csign}{sign}{full}.{frac:02}' - - -def parse_chf(value: str) -> int: - """ - Parse a currency value into machine-readable format (integer centimes). The prefix "CHF", the decimal point, and - digits after the decimal point are optional. - - :param value: The value to parse. - :return: An integer representation of the value. - :raises: Value error: If more than two digits after the decimal point are present. - """ - # Remove optional leading "CHF" and strip whitespace - value = value.strip() - if value.startswith('CHF'): - value = value[3:] - value = value.strip() - if '.' not in value: - # already is an integer; parse and turn into centimes - return int(value, 10) * 100 - # Split at the decimal point - full, frac = value.split('.', 1) - if len(frac) > 2: - raise ValueError('Needs max. 2 digits after decimal point') - elif len(frac) < 2: - # Right-pad fraction with zeros ("x." -> "x.00", "x.x" -> "x.x0") - frac = frac + '0' * (2 - len(frac)) - # Parse both parts - ifrac: int = int(frac, 10) - ifull: int = int(full, 10) - if ifrac < 0: - raise ValueError('Fraction part must not be negative.') - if full.startswith('-'): - ifrac = -ifrac - # Combine into centime integer - return ifull * 100 + ifrac