Skip to content

Commit

Permalink
Feature/more unit tests (#161)
Browse files Browse the repository at this point in the history
  • Loading branch information
jagerber48 authored Mar 7, 2024
2 parents d4c6b4c + b7266cd commit 20a7b1a
Show file tree
Hide file tree
Showing 25 changed files with 2,566 additions and 267 deletions.
22 changes: 16 additions & 6 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,19 @@ Unreleased
documentation
`changelog <https://sciform.readthedocs.io/en/latest/project.html#changelog>`_.
Added
^^^^^

* Added many unit test to supplement the feature existing feature tests.
[`#102 <https://github.com/jagerber48/sciform/issues/102>`_]
* Added much more input validation and corresponding messaging.

* Checks on input types and values.
* Extra translations dictionaries are now checked so that keys must be
integers and values must consist of only English alphabetic
characters.
[`#157 <https://github.com/jagerber48/sciform/issues/157>`_]

Changed
^^^^^^^

Expand All @@ -21,16 +34,13 @@ Changed

* Move modules containing public interfaces into an ``api``
sub-package.
* Break a bulky ``format_utils`` module into multiple modules, now in
a ``format_utils`` sub-package.
* Break the bulky ``format_utils`` module into multiple modules, now
in a ``format_utils`` sub-package.
* Collect the main formatting algorithms into a ``formatting``
sub-package.
* Sort tests into feature and unit tests.

* Extra translations dictionaries are now checked so that keys must be
integers and values must consist of only English alphabetic
characters.
[`#157 <https://github.com/jagerber48/sciform/issues/157>`_]
* Some utility code refactoring.

----

Expand Down
1 change: 1 addition & 0 deletions src/sciform/format_utils/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Various formatting utilities."""

from decimal import Decimal
from typing import Union

Expand Down
4 changes: 3 additions & 1 deletion src/sciform/format_utils/exponents.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
if TYPE_CHECKING: # pragma: no cover
from decimal import Decimal

from sciform.options.option_types import AutoExpVal


def get_translation_dict(
exp_format: ExpFormatEnum,
Expand Down Expand Up @@ -118,7 +120,7 @@ def get_val_unc_exp(
val: Decimal,
unc: Decimal,
exp_mode: ExpModeEnum,
input_exp: int,
input_exp: int | AutoExpVal,
) -> int:
"""Get exponent for value/uncertainty formatting."""
if val.is_finite() and unc.is_finite():
Expand Down
113 changes: 58 additions & 55 deletions src/sciform/format_utils/make_strings.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,14 @@

from __future__ import annotations

import decimal
from typing import TYPE_CHECKING

from sciform.format_utils.exponents import get_exp_str
from sciform.format_utils.numbers import (
get_top_digit,
get_top_dec_place,
)
from sciform.options.option_types import (
DecimalSeparatorEnums,
ExpFormatEnum,
ExpModeEnum,
SeparatorEnum,
SignModeEnum,
)
Expand All @@ -22,56 +20,79 @@

def get_sign_str(num: Decimal, sign_mode: SignModeEnum) -> str:
"""Get the format sign string."""
if num < 0:
# Always return "-" for negative numbers.
sign_str = "-"
elif num > 0:
# Return "+", " ", or "" for positive numbers.
if sign_mode is SignModeEnum.ALWAYS:
sign_str = "+"
elif sign_mode is SignModeEnum.SPACE:
with decimal.localcontext() as ctx:
# TODO: Consider wrapping all the formatting in this context.
ctx.traps[decimal.InvalidOperation] = False

if num < 0:
# Always return "-" for negative numbers.
sign_str = "-"
elif num > 0:
# Return "+", " ", or "" for positive numbers.
if sign_mode is SignModeEnum.ALWAYS:
sign_str = "+"
elif sign_mode is SignModeEnum.SPACE:
sign_str = " "
elif sign_mode is SignModeEnum.NEGATIVE:
sign_str = ""
else:
msg = f"Invalid sign mode {sign_mode}."
raise ValueError(msg)
elif sign_mode is SignModeEnum.ALWAYS or sign_mode is SignModeEnum.SPACE:
"""
For anything else (typically 0, possibly nan) return " " in "+" and " "
modes.
"""
sign_str = " "
elif sign_mode is SignModeEnum.NEGATIVE:
sign_str = ""
else:
msg = f"Invalid sign mode {sign_mode}."
raise ValueError(msg)
elif sign_mode is SignModeEnum.ALWAYS or sign_mode is SignModeEnum.SPACE:
# For anything else (typically 0, possibly nan) return " " in "+" and " " modes
sign_str = " "
else:
# Otherwise return the empty string.
sign_str = ""
# Otherwise return the empty string.
sign_str = ""

return sign_str


def get_pad_str(left_pad_char: str, top_digit: int, top_padded_digit: int) -> str:
"""Get the string padding from top_digit place to top_padded_digit place."""
if top_padded_digit > top_digit:
pad_len = top_padded_digit - max(top_digit, 0)
def get_pad_str(
left_pad_char: str,
top_dec_place: int,
top_padded_dec_place: int,
) -> str:
"""Get the string padding from top_dec_place place to top_padded_dec_place place."""
if top_padded_dec_place > top_dec_place:
pad_len = top_padded_dec_place - max(top_dec_place, 0)
pad_str = left_pad_char * pad_len
else:
pad_str = ""
return pad_str


def format_num_by_top_bottom_dig(
def get_abs_num_str_by_bottom_dec_place(
num: Decimal,
target_top_digit: int,
target_bottom_digit: int,
target_bottom_dec_place: int,
) -> str:
"""Format a number according to specified bottom decimal places."""
prec = max(0, -target_bottom_dec_place)
abs_mantissa_str = f"{abs(num):.{prec}f}"
return abs_mantissa_str


def construct_num_str(
num: Decimal,
target_top_dec_place: int,
target_bottom_dec_place: int,
sign_mode: SignModeEnum,
left_pad_char: str,
) -> str:
"""Format a number according to specified top and bottom digit places."""
print_prec = max(0, -target_bottom_digit)
abs_mantissa_str = f"{abs(num):.{print_prec}f}"
"""Format a number to a specified decimal place, with left padding and a sign symbol.""" # noqa: E501
abs_num_str = get_abs_num_str_by_bottom_dec_place(
num,
target_bottom_dec_place,
)

sign_str = get_sign_str(num, sign_mode)

num_top_digit = get_top_digit(num)
pad_str = get_pad_str(left_pad_char, num_top_digit, target_top_digit)
return f"{sign_str}{pad_str}{abs_mantissa_str}"
num_top_dec_place = get_top_dec_place(num)
pad_str = get_pad_str(left_pad_char, num_top_dec_place, target_top_dec_place)
return f"{sign_str}{pad_str}{abs_num_str}"


def construct_val_unc_str( # noqa: PLR0913
Expand Down Expand Up @@ -112,31 +133,13 @@ def construct_val_unc_str( # noqa: PLR0913
return val_unc_str


def construct_val_unc_exp_str( # noqa: PLR0913
def construct_val_unc_exp_str(
*,
val_unc_str: str,
exp_val: int,
exp_mode: ExpModeEnum,
exp_format: ExpFormatEnum,
extra_si_prefixes: dict[int, str | None],
extra_iec_prefixes: dict[int, str | None],
extra_parts_per_forms: dict[int, str | None],
capitalize: bool,
superscript: bool,
exp_str: str,
paren_uncertainty: bool,
) -> str:
"""Combine the val_unc_str into the final val_unc_exp_str."""
exp_str = get_exp_str(
exp_val=exp_val,
exp_mode=exp_mode,
exp_format=exp_format,
capitalize=capitalize,
superscript=superscript,
extra_si_prefixes=extra_si_prefixes,
extra_iec_prefixes=extra_iec_prefixes,
extra_parts_per_forms=extra_parts_per_forms,
)

if exp_str == "":
val_unc_exp_str = val_unc_str
elif paren_uncertainty:
Expand Down
76 changes: 44 additions & 32 deletions src/sciform/format_utils/numbers.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,56 +7,61 @@
from math import floor, log2
from typing import Literal

from sciform.formatting.parser import any_val_pattern
from sciform.formatting.parser import (
ascii_exp_pattern,
finite_val_pattern,
non_finite_val_pattern,
)
from sciform.options.option_types import (
AutoDigits,
AutoExpVal,
ExpModeEnum,
)


def get_top_digit(num: Decimal) -> int:
def get_top_dec_place(num: Decimal) -> int:
"""Get the decimal place of a decimal's most significant digit."""
if not num.is_finite() or num == 0:
return 0
_, digits, exp = num.as_tuple()
_, digits, exp = num.normalize().as_tuple()
return len(digits) + exp - 1


def get_top_digit_binary(num: Decimal) -> int:
def get_top_dec_place_binary(num: Decimal) -> int:
"""Get the decimal place of a decimal's most significant digit."""
if not num.is_finite() or num == 0:
return 0
return floor(log2(abs(num)))


def get_bottom_digit(num: Decimal) -> int:
def get_bottom_dec_place(num: Decimal) -> int:
"""Get the decimal place of a decimal's least significant digit."""
if not num.is_finite():
return 0
_, _, exp = num.as_tuple()
_, _, exp = num.normalize().as_tuple()
return exp


def get_val_unc_top_digit(
def get_val_unc_top_dec_place(
val_mantissa: Decimal,
unc_mantissa: Decimal,
input_top_digit: int | AutoDigits,
input_top_dec_place: int,
*,
left_pad_matching: bool,
) -> int | AutoDigits:
"""Get top digit place for value/uncertainty formatting."""
"""Get top decimal place for value/uncertainty formatting."""
if left_pad_matching:
val_top_digit = get_top_digit(val_mantissa)
unc_top_digit = get_top_digit(unc_mantissa)
new_top_digit = max(
input_top_digit,
val_top_digit,
unc_top_digit,
val_top_dec_place = get_top_dec_place(val_mantissa)
unc_top_dec_place = get_top_dec_place(unc_mantissa)
new_top_dec_place = max(
input_top_dec_place,
val_top_dec_place,
unc_top_dec_place,
)
else:
new_top_digit = input_top_digit
return new_top_digit
new_top_dec_place = input_top_dec_place
new_top_dec_place = max(0, new_top_dec_place)
return new_top_dec_place


def get_fixed_exp(
Expand All @@ -74,7 +79,7 @@ def get_scientific_exp(
input_exp: int | type(AutoExpVal),
) -> int:
"""Get the exponent for scientific formatting mode."""
return get_top_digit(num) if input_exp is AutoExpVal else input_exp
return get_top_dec_place(num) if input_exp is AutoExpVal else input_exp


def get_engineering_exp(
Expand All @@ -85,7 +90,7 @@ def get_engineering_exp(
) -> int:
"""Get the exponent for engineering formatting modes."""
if input_exp is AutoExpVal:
exp_val = get_top_digit(num)
exp_val = get_top_dec_place(num)
exp_val = exp_val // 3 * 3 if not shifted else (exp_val + 1) // 3 * 3
else:
if input_exp % 3 != 0:
Expand All @@ -106,7 +111,7 @@ def get_binary_exp(
) -> int:
"""Get the exponent for binary formatting modes."""
if input_exp is AutoExpVal:
exp_val = get_top_digit_binary(num)
exp_val = get_top_dec_place_binary(num)
if iec:
exp_val = (exp_val // 10) * 10
else:
Expand Down Expand Up @@ -155,21 +160,28 @@ def get_mantissa_exp_base(
return mantissa, exp, base


# Optional parentheses needed to handle (nan)e+00 case
mantissa_exp_pattern = re.compile(
rf"""
^
\(?(?P<mantissa_str>{any_val_pattern})\)?
(?P<exp_str>[eEbB].*?)?
$
""",
re.VERBOSE,
)
# language=pythonverboseregexp noqa: ERA001
no_exp_pattern = rf"^(?P<mantissa>{non_finite_val_pattern})$"
# language=pythonverboseregexp noqa: ERA001
optional_exp_pattern = rf"""
^(?P<mantissa>{finite_val_pattern})(?P<exp>{ascii_exp_pattern})?$
"""
# language=pythonverboseregexp noqa: ERA001
always_exp_pattern = rf"""
^
\((?P<mantissa>{non_finite_val_pattern})\)
(?P<exp>{ascii_exp_pattern})
$
"""


def parse_mantissa_from_ascii_exp_str(number_str: str) -> str:
"""Break val/unc mantissa/exp strings into mantissa strings and an exp string."""
if match := mantissa_exp_pattern.match(number_str):
return match.group("mantissa_str")
if match := re.match(no_exp_pattern, number_str, re.VERBOSE):
return match.group("mantissa")
if match := re.match(optional_exp_pattern, number_str, re.VERBOSE):
return match.group("mantissa")
if match := re.match(always_exp_pattern, number_str, re.VERBOSE):
return match.group("mantissa")
msg = f'Invalid number string "{number_str}".'
raise ValueError(msg)
Loading

0 comments on commit 20a7b1a

Please sign in to comment.