Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/more unit tests #161

Merged
merged 37 commits into from
Mar 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
753c905
test decimal place and rename digit_place -> decimal_place throughout
jagerber48 Feb 18, 2024
7f76fe3
ruff
jagerber48 Feb 18, 2024
b882896
tests for get_bottom_dec_place. Do normalization in the function. Les…
jagerber48 Feb 18, 2024
761a606
test get_val_unc_top_dec_place
jagerber48 Feb 18, 2024
fca9c3a
more tests and runtime validation
jagerber48 Feb 18, 2024
eed6cc0
ruff
jagerber48 Feb 18, 2024
f325ddc
add binary tests and mantissa factors for more tests
jagerber48 Feb 19, 2024
4c15076
mantissa_exp_base test cases
jagerber48 Feb 19, 2024
27be4a2
more mantissa_exp_base tests
jagerber48 Feb 20, 2024
8fe4593
finish number utils tests.
jagerber48 Feb 20, 2024
f8bbb39
more nan coverage
jagerber48 Feb 20, 2024
2b8bccf
docstring tests
jagerber48 Feb 20, 2024
079dcde
ndigits must be int
jagerber48 Feb 21, 2024
76e8b2a
test_rounding_utils
jagerber48 Feb 21, 2024
43aef4d
ruff
jagerber48 Feb 21, 2024
f2104b2
more rounding tests and NanTestCase
jagerber48 Feb 24, 2024
ffa63af
Handle infinite cases better
jagerber48 Feb 24, 2024
0726f49
start exp_utils tests
jagerber48 Feb 24, 2024
f27c035
test exp utils
jagerber48 Feb 25, 2024
f237e8b
more exp_utils tests
jagerber48 Feb 25, 2024
cd52b96
test exp translations
jagerber48 Feb 25, 2024
f889f54
Grouping tests
jagerber48 Feb 26, 2024
459a676
refacotor out construct_num_str
jagerber48 Feb 26, 2024
f4ad767
test_make_strings
jagerber48 Feb 26, 2024
d8ea45d
Tests
jagerber48 Feb 26, 2024
c5fae04
abs_num_str_by_bottom_dec_place test
jagerber48 Feb 26, 2024
97fcf98
test_construct_num_str
jagerber48 Feb 26, 2024
9fac59a
refactor get_exp_str call out of construct_val_unc_exp_str
jagerber48 Feb 26, 2024
f293d49
test construct_val_unc_exp_str
jagerber48 Feb 26, 2024
6cfec4c
move format_utils tests into dedicated directory, add some invalid mo…
jagerber48 Feb 26, 2024
7241444
remove redundant tests from test_invalid_options and migrate into for…
jagerber48 Feb 26, 2024
9e5a1ce
write validation tests
jagerber48 Feb 26, 2024
efe630f
multiple invalid keys
jagerber48 Feb 26, 2024
f84d449
add lots of validation code
jagerber48 Mar 7, 2024
77a269e
ruff
jagerber48 Mar 7, 2024
7cc83d2
ruff new version
jagerber48 Mar 7, 2024
b7266cd
changelog
jagerber48 Mar 7, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading